PI / src /components /Chat.tsx
noodcon's picture
Upload 64 files
9b9eafc verified
Raw History Blame Contribute Delete
4.99 kB
import { useEffect, useRef, type KeyboardEvent } from 'react';
import type { AgentMessage, AssistantMessage, ToolCall, ToolResultMessage } from '../shared/types';
import { PiLogo } from './TopBar';
function ToolCallView({ call }: { call: ToolCall }) {
const entries = Object.entries(call.arguments);
return (
<div className="tool">
<div className="tool-head">{call.name}</div>
{call.name === 'bash' && typeof call.arguments.command === 'string' ? (
<pre>$ {call.arguments.command}</pre>
) : (
entries.map(([k, v]) => (
<div className="kv" key={k}>
<b>{k}</b>
<pre>{String(v)}</pre>
</div>
))
)}
</div>
);
}
function ResultView({ m }: { m: ToolResultMessage }) {
const text = m.content.map((c) => c.text).join('\n');
return (
<details className={`result${m.isError ? ' bad' : ''}`} open={text.length < 400}>
<summary>
{m.toolName} result{m.details.exitCode !== undefined ? ` · exit ${m.details.exitCode}` : ''}
{m.isError ? ' · error' : ''}
</summary>
<pre>{text}</pre>
</details>
);
}
function AssistantView({ m, live }: { m: AssistantMessage; live?: boolean }) {
return (
<div className="msg assistant">
{m.content.map((c, i) =>
c.type === 'thinking' ? (
<details key={i} className="thinking" open={live}>
<summary>Thinking</summary>
<pre>{c.thinking}</pre>
</details>
) : c.type === 'text' ? (
<p key={i} className="text">
{c.text}
</p>
) : (
<ToolCallView key={i} call={c} />
),
)}
{live && m.content.length === 0 && <span className="typing" aria-label="Working"><i /><i /><i /></span>}
{m.stopReason === 'error' && <p className="err">{m.errorMessage}</p>}
{m.stopReason === 'aborted' && <p className="muted">Stopped.</p>}
{m.stopReason === 'length' && <p className="muted">Stopped at the output limit.</p>}
</div>
);
}
interface Props {
messages: AgentMessage[];
streaming: AssistantMessage | null;
busy: string | null;
draft: string;
contextTokens: number;
onDraft: (v: string) => void;
onSend: () => void;
onStop: () => void;
onNewChat: () => void;
}
export function ChatPanel({ messages, streaming, busy, draft, contextTokens, onDraft, onSend, onStop, onNewChat }: Props) {
const logRef = useRef<HTMLDivElement>(null);
const working = busy === 'prompt';
const last = messages[messages.length - 1];
useEffect(() => {
const el = logRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [messages, streaming]);
const onKey = (e: KeyboardEvent<HTMLTextAreaElement>) => {
// Không gửi khi đang gõ dấu bằng bộ gõ (tiếng Việt, tiếng Trung…).
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault();
onSend();
}
};
return (
<section className="chat" aria-label="Agent conversation">
<div className="log" ref={logRef} role="log" aria-live="polite">
{messages.length === 0 && !working && (
<div className="empty">
<PiLogo />
<h2>Ask Pi to work in your workspace</h2>
<p>Pi can read, write and edit files and run shell commands. Everything stays in this browser.</p>
<button onClick={() => onDraft("What's our total sales revenue?")}>What&apos;s our total sales revenue?</button>
</div>
)}
{messages.map((m, i) =>
m.role === 'user' ? (
<div key={i} className="msg user">{m.content}</div>
) : m.role === 'assistant' ? (
<AssistantView key={i} m={m} />
) : (
<ResultView key={i} m={m} />
),
)}
{working && streaming && <AssistantView m={streaming} live />}
{working && !streaming && last && last.role !== 'assistant' && (
<div className="msg assistant"><span className="typing" aria-label="Working"><i /><i /><i /></span></div>
)}
</div>
<div className="composer">
<label className="sr" htmlFor="prompt">Message Pi</label>
<textarea
id="prompt"
value={draft}
onChange={(e) => onDraft(e.target.value)}
onKeyDown={onKey}
placeholder="Message Pi… (Enter to send, Shift+Enter for a new line)"
rows={2}
/>
<div className="controls">
<button onClick={onNewChat} disabled={busy !== null || messages.length === 0}>New chat</button>
<span className="grow">{contextTokens > 0 ? `${contextTokens.toLocaleString()} / 8,192 tokens` : 'Model and tools run in your browser'}</span>
{busy !== null && <button onClick={onStop}>Stop</button>}
<button className="send" onClick={onSend} disabled={busy !== null || !draft.trim()}>Send ↑</button>
</div>
</div>
</section>
);
}