153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
import React, {useState} from 'react';
|
|
import {Box, render, Text, useInput} from 'ink';
|
|
import TextInput from 'ink-text-input';
|
|
import Spinner from 'ink-spinner';
|
|
|
|
import {addMessage, callTools, type ChatMessage, findMessages, getMessages, requestLLMStream} from "./src/llm.ts";
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
const App = () => {
|
|
const [input, setInput] = useState(''); // 当前输入框内容
|
|
const [isChatting, setIsChatting] = useState(false); // 是否正在对话
|
|
const [history, setHistory] = useState<ChatMessage[]>(getMessages()); // 对话历史
|
|
const [currentResponse, setCurrentResponse] = useState(''); // 当前流式输出的内容
|
|
const [currentReasoningResponse, setCurrentReasoningResponse] = useState(''); // 当前流式输出的内容
|
|
|
|
// 处理提交
|
|
const handleSubmit = async (value: string) => {
|
|
if (!value) return;
|
|
setIsChatting(true);
|
|
await addMessage("now is " + dayjs().format("YYYY-MM-DD HH:mm:ss"), "system");
|
|
await addMessage(value);
|
|
setHistory(getMessages());
|
|
setInput('');
|
|
|
|
await dealRequest();
|
|
};
|
|
|
|
const dealRequest = async () => {
|
|
let request_id = "";
|
|
let fullText = '';
|
|
let reasoningFullText = '';
|
|
for await (const token of requestLLMStream(false)) {
|
|
fullText += token.content || "";
|
|
reasoningFullText += token.reasoning_content || "";
|
|
setCurrentResponse(fullText);
|
|
setCurrentReasoningResponse(reasoningFullText);
|
|
if (token.id) {
|
|
request_id = token.id;
|
|
}
|
|
}
|
|
const lastMsg = findMessages(request_id);
|
|
|
|
if (lastMsg && lastMsg.finish_reason === "tool_calls") {
|
|
// 处理工具调用
|
|
if (lastMsg.tool_call_id && lastMsg.tool_call_name) {
|
|
await callTools(
|
|
lastMsg.tool_call_id,
|
|
lastMsg.tool_call_name,
|
|
lastMsg.tool_call_arguments
|
|
)
|
|
await dealRequest()
|
|
}
|
|
return;
|
|
} else {
|
|
// 流结束,归档到历史
|
|
setHistory(getMessages());
|
|
setCurrentResponse('');
|
|
setCurrentReasoningResponse('');
|
|
setIsChatting(false);
|
|
}
|
|
}
|
|
// 监听退出快捷键 (Ctrl+C)
|
|
useInput((input, key) => {
|
|
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
process.exit();
|
|
}
|
|
});
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
{/* 1. 标题栏 */}
|
|
<Box marginBottom={1}>
|
|
<Text color="black" bold>🤖 iCoder </Text>
|
|
</Box>
|
|
|
|
{/* 2. 渲染历史记录 */}
|
|
{history.map((msg) => (
|
|
<Box key={msg.id} flexDirection="column" marginBottom={msg.role == "assistant" ? 2 : 1}>
|
|
<Box marginTop={1}>
|
|
<Text color={"gray"}>{msg.reasoning_content ?? ""}</Text>
|
|
</Box>
|
|
<Box marginTop={1}>
|
|
<Text>{msg.content ?? ""}</Text>
|
|
|
|
<Text>
|
|
{msg.finish_reason}
|
|
{msg.tool_call_id}
|
|
{msg.tool_call_name}
|
|
{msg.tool_call_arguments}
|
|
</Text>
|
|
</Box>
|
|
{
|
|
msg.role === "user" ? (
|
|
<Box marginTop={1}>
|
|
<Text color={"gray"}>{msg.createdAt ?? ""}</Text>
|
|
</Box>
|
|
) : null
|
|
}
|
|
</Box>
|
|
))}
|
|
|
|
{/* 3. 渲染当前正在生成的流 */}
|
|
{isChatting && currentResponse && (
|
|
<Box flexDirection="column" marginBottom={1}>
|
|
<Box marginBottom={1}>
|
|
<Text color="green">
|
|
<Spinner type="dots"/> 正在工作...
|
|
</Text>
|
|
</Box>
|
|
<Box>
|
|
<Text color="gray">{currentReasoningResponse}</Text>
|
|
</Box>
|
|
<Box>
|
|
<Text>{currentResponse}</Text>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
{/* 4. 加载状态 */}
|
|
{isChatting && !currentResponse && (
|
|
<Box>
|
|
<Text color="yellow">
|
|
<Spinner type="dots"/> 正在思考...
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
|
|
{/* 5. 输入区 */}
|
|
{!isChatting && (
|
|
<Box>
|
|
<Box marginRight={2}>
|
|
<Text color="green">🎨</Text>
|
|
</Box>
|
|
<Box>
|
|
<TextInput
|
|
value={input}
|
|
onChange={setInput}
|
|
onSubmit={handleSubmit}
|
|
placeholder="输入问题并回车..."
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
<Box marginTop={1}>
|
|
<Text dimColor>按 Ctrl+C 退出</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
render(<App/>); |