icoder/index.tsx

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/>);