Download src/components/Chat.tsx from noodcon/PI: direct link, hf CLI and curl.
- Browser
- Download file 4.99 kB
-
https://huggingface.co/spaces/noodcon/PI/resolve/main/src/components/Chat.tsx
- Command line
-
hf download hf://spaces/noodcon/PI/src/components/Chat.tsx
-
curl -L -o Chat.tsx https://huggingface.co/spaces/noodcon/PI/resolve/main/src/components/Chat.tsx
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'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> | |
| ); | |
| } | |