This commit is contained in:
Simon Knott 2025-02-13 12:45:36 +01:00
parent 3cbd591bc6
commit c58566c82e
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
2 changed files with 26 additions and 21 deletions

View file

@ -7,11 +7,11 @@ import type { Conversation, LLMMessage } from './llm';
export function AIConversation({ history, conversation }: { history: LLMMessage[], conversation: Conversation }) { export function AIConversation({ history, conversation }: { history: LLMMessage[], conversation: Conversation }) {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const onSubmit = useCallback(async (event: React.FormEvent<HTMLFormElement>) => { const onSubmit = useCallback(() => {
event.preventDefault(); setInput(content => {
setInput(''); conversation.send(content);
const content = new FormData(event.target as any).get('content') as string; return '';
await conversation.send(content); });
}, [conversation]); }, [conversation]);
return ( return (
@ -34,29 +34,33 @@ export function AIConversation({ history, conversation }: { history: LLMMessage[
))} ))}
</div> </div>
<form onSubmit={onSubmit} className="input-form"> <div className="input-form">
<input <textarea
type="text"
name='content' name='content'
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSubmit();
}
}}
placeholder="Ask a question..." placeholder="Ask a question..."
className="message-input" className="message-input"
/> />
{conversation.isSending() ? ( {conversation.isSending() ? (
<button type="button" className="send-button" onClick={(evt) => { <button type="button" className="send-button" onClick={(evt) => {
evt.preventDefault() evt.preventDefault()
console.log("aborting")
conversation.abortSending(); conversation.abortSending();
}}> }}>
Cancel Cancel
</button> </button>
) : ( ) : (
<button type="submit" className="send-button" disabled={!input.trim()}> <button className="send-button" disabled={!input.trim()} onClick={onSubmit}>
Send Send
</button> </button>
)} )}
</form> </div>
</div> </div>
); );
} }

View file

@ -169,7 +169,7 @@ class LLMChat {
getConversation(id: string, systemPrompt: string) { getConversation(id: string, systemPrompt: string) {
if (!this.conversations.has(id)) { if (!this.conversations.has(id)) {
const conversation = new Conversation(this, systemPrompt); const conversation = new Conversation(this, systemPrompt);
this.conversations.set(id, conversation); this.conversations.set(id, conversation); // TODO: cleanup
} }
return this.conversations.get(id)!; return this.conversations.get(id)!;
} }
@ -178,36 +178,37 @@ class LLMChat {
export class Conversation { export class Conversation {
history: LLMMessage[]; history: LLMMessage[];
onChange = new EventEmitter<void>(); onChange = new EventEmitter<void>();
private _abortController: AbortController | undefined; private _abortControllers = new Set<AbortController>();
constructor(private chat: LLMChat, systemPrompt: string) { constructor(private chat: LLMChat, systemPrompt: string) {
this.history = [{ role: 'developer', content: systemPrompt }]; this.history = [{ role: 'developer', content: systemPrompt }];
} }
async send(content: string, displayContent?: string) { async send(content: string, displayContent?: string) {
if (this.isSending())
throw new Error('Already sending');
const response: LLMMessage = { role: 'assistant', content: '' }; const response: LLMMessage = { role: 'assistant', content: '' };
this.history.push({ role: 'user', content, displayContent }, response); this.history.push({ role: 'user', content, displayContent }, response);
const abortController = new AbortController();
this._abortControllers.add(abortController);
this.onChange.fire(); this.onChange.fire();
this._abortController = new AbortController();
try { try {
for await (const chunk of this.chat.api.chatCompletion(this.history, this._abortController.signal)) { for await (const chunk of this.chat.api.chatCompletion(this.history, abortController.signal)) {
response.content += chunk; response.content += chunk;
this.onChange.fire(); this.onChange.fire();
} }
} finally { } finally {
this._abortController = undefined; this._abortControllers.delete(abortController);
this.onChange.fire();
} }
} }
isSending(): boolean { isSending(): boolean {
return this._abortController !== undefined; return this._abortControllers.size > 0;
} }
abortSending() { abortSending() {
this._abortController!.abort(); for (const controller of this._abortControllers)
controller.abort();
this._abortControllers.clear();
this.onChange.fire(); this.onChange.fire();
} }