dd04edb420
Three-layer architecture built on @push.rocks/smartagent: - ts/ — ChatSession wrapping runAgent() with conversation state management - ts_cli/ — ink-based terminal chat TUI (React.createElement, no JSX) - ts_web/ — Lit web components (smartchat-window, smartchat-message, smartchat-input)
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { React, h, Box, Text, useInput, useApp, ChatSession } from './plugins.js';
|
|
import type { LanguageModelV3, ToolSet, IChatUsage } from './plugins.js';
|
|
import { MessageList } from './components.messagelist.js';
|
|
import { InputArea } from './components.inputarea.js';
|
|
import { StatusBar } from './components.statusbar.js';
|
|
import type { IChatMessage } from './components.message.js';
|
|
|
|
export interface IChatAppProps {
|
|
model: LanguageModelV3;
|
|
system?: string;
|
|
tools?: ToolSet;
|
|
maxSteps?: number;
|
|
modelName?: string;
|
|
}
|
|
|
|
const emptyUsage: IChatUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, turns: 0 };
|
|
|
|
export function ChatApp({ model, system, tools, maxSteps, modelName }: IChatAppProps): React.ReactElement {
|
|
const { exit } = useApp();
|
|
const [messages, setMessages] = React.useState<IChatMessage[]>([]);
|
|
const [streamingText, setStreamingText] = React.useState('');
|
|
const [busy, setBusy] = React.useState(false);
|
|
const [usage, setUsage] = React.useState<IChatUsage>({ ...emptyUsage });
|
|
|
|
const sessionRef = React.useRef<ChatSession | null>(null);
|
|
|
|
if (!sessionRef.current) {
|
|
sessionRef.current = new ChatSession(
|
|
{ model, system, tools, maxSteps: maxSteps ?? 20 },
|
|
{
|
|
onToken: (delta) => {
|
|
setStreamingText((prev) => prev + delta);
|
|
},
|
|
onToolCall: (name, input) => {
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
role: 'tool' as const,
|
|
content: '',
|
|
toolName: name,
|
|
toolInput: typeof input === 'string' ? input : JSON.stringify(input),
|
|
},
|
|
]);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
useInput((input, key) => {
|
|
if (input === 'c' && key.ctrl) {
|
|
exit();
|
|
}
|
|
if (input === 'l' && key.ctrl) {
|
|
setMessages([]);
|
|
setStreamingText('');
|
|
sessionRef.current?.clear();
|
|
setUsage({ ...emptyUsage });
|
|
}
|
|
});
|
|
|
|
const handleSubmit = async (text: string) => {
|
|
if (busy) return;
|
|
|
|
if (text === '/clear') {
|
|
setMessages([]);
|
|
setStreamingText('');
|
|
sessionRef.current?.clear();
|
|
setUsage({ ...emptyUsage });
|
|
return;
|
|
}
|
|
|
|
if (text === '/quit' || text === '/exit') {
|
|
exit();
|
|
return;
|
|
}
|
|
|
|
setMessages((prev) => [...prev, { role: 'user', content: text }]);
|
|
setBusy(true);
|
|
setStreamingText('');
|
|
|
|
try {
|
|
const result = await sessionRef.current!.send(text);
|
|
setStreamingText('');
|
|
setMessages((prev) => [...prev, { role: 'assistant', content: result.text }]);
|
|
setUsage(sessionRef.current!.getUsage());
|
|
} catch (error) {
|
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
setMessages((prev) => [...prev, { role: 'assistant', content: `Error: ${errMsg}` }]);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return h(Box, { flexDirection: 'column' as const },
|
|
h(Box, { paddingX: 1 },
|
|
h(Text, { bold: true, color: 'cyan' }, 'smartchat'),
|
|
modelName ? h(Text, { dimColor: true }, ` \u2014 ${modelName}`) : null,
|
|
),
|
|
h(MessageList, { messages, streamingText, busy }),
|
|
h(InputArea, { disabled: busy, onSubmit: handleSubmit }),
|
|
h(StatusBar, { modelName, usage, busy }),
|
|
);
|
|
}
|