Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openbrowserclaw",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"type": "module",
"description": "Browser-native personal AI assistant. Zero infrastructure — the browser is the server.",
Expand All @@ -23,6 +23,7 @@
"vite-plugin-pwa": "^0.21.0"
},
"dependencies": {
"@huggingface/transformers": "next",
"lucide-react": "^0.575.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
Expand Down
68 changes: 55 additions & 13 deletions src/components/chat/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// ---------------------------------------------------------------------------

import { useEffect, useRef } from 'react';
import { X, MessageSquare, Globe, FileText, MapPin } from 'lucide-react';
import { X, MessageSquare, Globe, FileText, MessageCircle, HelpCircle } from 'lucide-react';
import { useOrchestratorStore } from '../../stores/orchestrator-store.js';
import { MessageList } from './MessageList.js';
import { ChatInput } from './ChatInput.js';
Expand All @@ -29,7 +29,8 @@ const LineGraphIcon = ({ className }: { className?: string }) => (
</svg>
);

const PROMPT_STARTERS = [
// Prompt starters for Claude (with tool access)
const CLAUDE_PROMPT_STARTERS = [
{
icon: Globe,
title: 'Latest news',
Expand All @@ -41,48 +42,82 @@ const PROMPT_STARTERS = [
prompt: 'Show me a graph with the Ethereum price over the last 6 months.',
},
{
icon: MapPin,
title: 'Map viewer',
prompt: 'Generate an interactive map viewer with the top locations to visit in Seattle.',
icon: FileText,
title: 'Create a file',
prompt: 'Create a simple HTML file with a hello world page.',
},
];

// Prompt starters for local model (chat only, no tools)
const LOCAL_PROMPT_STARTERS = [
{
icon: MessageCircle,
title: 'Have a conversation',
prompt: 'Tell me about yourself and what you can help with.',
},
{
icon: HelpCircle,
title: 'Ask a question',
prompt: 'What are the main differences between JavaScript and TypeScript?',
},
{
icon: FileText,
title: 'Get help with code',
prompt: 'Explain how async/await works in JavaScript with a simple example.',
},
];

export function ChatPage() {
const messages = useOrchestratorStore((s) => s.messages);
const isTyping = useOrchestratorStore((s) => s.isTyping);
const isStreaming = useOrchestratorStore((s) => s.isStreaming);
const toolActivity = useOrchestratorStore((s) => s.toolActivity);
const activityLog = useOrchestratorStore((s) => s.activityLog);
const orchState = useOrchestratorStore((s) => s.state);
const tokenUsage = useOrchestratorStore((s) => s.tokenUsage);
const error = useOrchestratorStore((s) => s.error);
const providerType = useOrchestratorStore((s) => s.providerType);
const sendMessage = useOrchestratorStore((s) => s.sendMessage);
const loadHistory = useOrchestratorStore((s) => s.loadHistory);

const bottomRef = useRef<HTMLDivElement>(null);

// Scroll to bottom on new messages
// Choose prompt starters based on provider type
const promptStarters = providerType === 'onnx' ? LOCAL_PROMPT_STARTERS : CLAUDE_PROMPT_STARTERS;
const starterTitle = providerType === 'onnx'
? 'Chat with the local AI'
: 'Start a conversation';
const starterDesc = providerType === 'onnx'
? 'This is a lightweight local model. It can chat and answer questions, but cannot execute commands or access files.'
: 'Try one of these to get started';

// Scroll to bottom on new messages or streaming content
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isTyping]);
}, [messages, isStreaming]);

// Load history on mount
useEffect(() => {
loadHistory();
}, [loadHistory]);

// Determine if we should show typing indicator
// Show for: Claude thinking, or local model loading (before streaming starts)
const showTyping = isTyping && !isStreaming;

return (
<div className="flex flex-col h-full">
{/* Messages area */}
<div className="flex-1 overflow-y-auto p-4 space-y-1">
{messages.length === 0 && !isTyping && (
{messages.length === 0 && !isTyping && !isStreaming && (
<div className="hero min-h-full">
<div className="hero-content text-center">
<div className="max-w-md">
<MessageSquare className="w-12 h-12 mx-auto mb-4 opacity-30" />
<h2 className="text-2xl font-bold">Start a conversation</h2>
<p className="mt-2 opacity-60 mb-6">Try one of these to get started</p>
<h2 className="text-2xl font-bold">{starterTitle}</h2>
<p className="mt-2 opacity-60 mb-6">{starterDesc}</p>
<div className="grid gap-3">
{PROMPT_STARTERS.map(({ icon: Icon, title, prompt }) => (
{promptStarters.map(({ icon: Icon, title, prompt }) => (
<button
key={title}
className="card card-bordered bg-base-200 hover:bg-base-300 transition-colors cursor-pointer text-left"
Expand All @@ -98,14 +133,21 @@ export function ChatPage() {
</button>
))}
</div>
{providerType === 'onnx' && (
<p className="mt-4 text-xs opacity-40">
💡 Switch to Claude mode in Settings for file operations, web access, and command execution.
</p>
)}
</div>
</div>
</div>
)}

<MessageList messages={messages} />

{isTyping && <TypingIndicator />}
{/* Show typing indicator during thinking/loading phase */}
{showTyping && <TypingIndicator />}

{toolActivity && (
<ToolActivity tool={toolActivity.tool} status={toolActivity.status} />
)}
Expand Down Expand Up @@ -147,4 +189,4 @@ export function ChatPage() {
</div>
</div>
);
}
}
21 changes: 18 additions & 3 deletions src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
// ---------------------------------------------------------------------------
// OpenBrowserClaw — Message list
// OpenBrowserClaw — Message list with streaming support
// ---------------------------------------------------------------------------

import type { StoredMessage } from '../../types.js';
import { MessageBubble } from './MessageBubble.js';
import { StreamingMessage } from './StreamingMessage.js';
import { useOrchestratorStore } from '../../stores/orchestrator-store.js';

interface Props {
messages: StoredMessage[];
}

export function MessageList({ messages }: Props) {
const streamingContent = useOrchestratorStore((s) => s.streamingContent);
const isStreaming = useOrchestratorStore((s) => s.isStreaming);
const orchState = useOrchestratorStore((s) => s.state);
const providerType = useOrchestratorStore((s) => s.providerType);

// Filter out any streaming placeholder messages from the array
const realMessages = messages.filter(m => !String(m.id).startsWith('streaming-'));

// Show streaming only for local model when in responding state
const showStreaming = isStreaming && providerType === 'onnx' && orchState === 'responding';

return (
<>
{messages.map((msg) => (
{realMessages.map((msg) => (
<MessageBubble key={msg.id} message={msg} />
))}
{/* Show streaming message for local model */}
{showStreaming && <StreamingMessage content={streamingContent} />}
</>
);
}
}
Loading