feat: add Ink-based fullscreen TUI components
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, useApp, useInput } from 'ink';
|
||||
import { StatusBar } from './StatusBar.js';
|
||||
import { MessageList } from './MessageList.js';
|
||||
import { InputBar } from './InputBar.js';
|
||||
import type { Message, ModelClient } from '../../../models/types.js';
|
||||
import type { ManagedSession } from '../../../session/index.js';
|
||||
|
||||
export interface AppProps {
|
||||
session: ManagedSession;
|
||||
modelClient: ModelClient;
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
onExit?: () => void;
|
||||
}
|
||||
|
||||
export function App({
|
||||
session,
|
||||
modelClient,
|
||||
systemPrompt,
|
||||
model,
|
||||
onExit,
|
||||
}: AppProps): React.ReactElement {
|
||||
const { exit } = useApp();
|
||||
const [input, setInput] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>(session.getHistory());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useInput((inputChar, key) => {
|
||||
if (key.escape) {
|
||||
onExit?.();
|
||||
exit();
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(async (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || isLoading) return;
|
||||
|
||||
// Handle commands
|
||||
if (trimmed === '/quit' || trimmed === '/exit') {
|
||||
onExit?.();
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === '/reset' || trimmed === '/clear') {
|
||||
session.clear();
|
||||
setMessages([]);
|
||||
setInput('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular message
|
||||
const userMessage: Message = { role: 'user', content: trimmed };
|
||||
session.addMessage(userMessage);
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await modelClient.chat({
|
||||
messages: session.getHistory(),
|
||||
system: systemPrompt,
|
||||
});
|
||||
|
||||
const assistantMessage: Message = { role: 'assistant', content: response.content };
|
||||
session.addMessage(assistantMessage);
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
const errorMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, session, modelClient, systemPrompt, exit, onExit]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height="100%">
|
||||
<StatusBar
|
||||
sessionId={session.id}
|
||||
messageCount={messages.length}
|
||||
model={model}
|
||||
/>
|
||||
<MessageList messages={messages} />
|
||||
<InputBar
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
onSubmit={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
placeholder="Type a message... (Esc to exit)"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user