1 Commits

Author SHA1 Message Date
Roberto Musso
a16e1cc42a feat: add custom hook useAIChat for managing AI chat interactions
- Implemented useAIChat hook to handle chat messages, input state, and streaming responses.
- Added types for ChatMessage and ChatContext to ensure type safety.
- Integrated TRPC mutation for sending messages and handling responses.
- Included functionality for clearing messages and managing streaming content.
2026-02-26 23:31:41 +01:00
2 changed files with 1360 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,130 @@
import { useState, useCallback, useRef } from 'react';
import { trpc } from '@/lib/trpc';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
error?: boolean;
}
export interface ChatContext {
type: 'global' | 'project';
projectId?: string;
uiContext?: string;
}
export interface UseAIChatReturn {
messages: ChatMessage[];
input: string;
setInput: (v: string) => void;
isStreaming: boolean;
streamingContent: string;
handleSend: (overrideMessage?: string, overrideContext?: ChatContext) => void;
clearMessages: () => void;
}
export function useAIChat(defaultContext: ChatContext): UseAIChatReturn {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [streamingContent, setStreamingContent] = useState('');
const streamingContentRef = useRef('');
const chatMutation = trpc.ai.chat.useMutation();
const clearMessages = useCallback(() => {
setMessages([]);
setStreamingContent('');
streamingContentRef.current = '';
}, []);
const handleSend = useCallback(
(overrideMessage?: string, overrideContext?: ChatContext) => {
const trimmed = (overrideMessage ?? input).trim();
if (!trimmed || isStreaming) return;
const userMsg: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content: trimmed,
};
setMessages((prev) => [...prev, userMsg]);
if (!overrideMessage) setInput('');
setIsStreaming(true);
setStreamingContent('');
streamingContentRef.current = '';
const unsubscribe = window.electronAI.onStreamChunk(({ token, done }) => {
if (done) {
const finalContent = streamingContentRef.current;
setMessages((prev) => [
...prev,
{ id: crypto.randomUUID(), role: 'assistant', content: finalContent },
]);
setStreamingContent('');
streamingContentRef.current = '';
setIsStreaming(false);
unsubscribe();
return;
}
streamingContentRef.current += token;
setStreamingContent(streamingContentRef.current);
});
const ctx = overrideContext ?? defaultContext;
chatMutation.mutate(
{
message: trimmed,
context: {
type: ctx.type,
...(ctx.type === 'project' && ctx.projectId ? { projectId: ctx.projectId } : {}),
...(ctx.uiContext ? { uiContext: ctx.uiContext } : {}),
},
},
{
onSuccess: (data) => {
if (data.error) {
unsubscribe();
setMessages((prev) => [
...prev,
{ id: crypto.randomUUID(), role: 'assistant', content: data.error!, error: true },
]);
setStreamingContent('');
streamingContentRef.current = '';
setIsStreaming(false);
}
},
onError: (err) => {
unsubscribe();
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
role: 'assistant',
content: err.message || 'An unexpected error occurred.',
error: true,
},
]);
setStreamingContent('');
streamingContentRef.current = '';
setIsStreaming(false);
},
},
);
},
[input, isStreaming, defaultContext, chatMutation],
);
return {
messages,
input,
setInput,
isStreaming,
streamingContent,
handleSend,
clearMessages,
};
}