Przejdź do głównej zawartości

AI Chat

Chat AI z obsługą streamingu, draftów i trybów kontekstowych (visit/patient/specialist).

Na tej stronie

AI Chat to interaktywny interfejs do rozmów z AI w kontekście wizyty lub ogólnych konsultacji weterynaryjnych.


flowchart TB
subgraph UI["Chat UI"]
Input[Message Input]
Messages[Message List]
Streaming[Streaming Display]
end
subgraph Hooks
useChatHistory[useChatHistory]
useChat[useChat]
end
subgraph Client["UnifiedAIClient"]
chat[chat]
chatStream[chatStream]
cancelStream[cancelStream]
end
subgraph Backend
unified_ai_chat
unified_ai_chat_stream
end
UI --> Hooks
Hooks --> Client
Client --> Backend

┌─────────────────────────────────────┐
│ AI Assistant [Detach] [X] │
├─────────────────────────────────────┤
│ │
│ 🐾 Witaj! Jak mogę pomóc? │
│ │
│ 👤 Pies ma wymioty od 2 dni │
│ │
│ 🐾 Rozumiem. Kilka pytań: │
│ - Czy pies je normalnie? │
│ - Czy są inne objawy? │
│ - Jaki jest wiek psa? │
│ │
│ 👤 ...typing... │
│ │
├─────────────────────────────────────┤
│ [Type a message...] [Send] 🎤 │
└─────────────────────────────────────┘

Plik: src/features/ai-suite/hooks/useChatHistory.ts

const {
messages,
isLoading,
addMessage,
clearMessages,
loadDraft,
persistDraft,
} = useChatHistory(storageKey);
// Auto-save every second (debounced)
useEffect(() => {
const timer = setTimeout(() => {
persistDraft(messages);
}, 1000);
return () => clearTimeout(timer);
}, [messages]);
// Save on unmount
useEffect(() => {
return () => persistDraft(messagesRef.current);
}, []);

sequenceDiagram
participant User
participant UI as Chat Panel
participant Hook as useChat
participant Client as UnifiedAIClient
participant BE as Backend
User->>UI: Type message
User->>UI: Press Send
UI->>Hook: sendMessage(text)
Hook->>Hook: Add user message to history
alt Streaming enabled
Hook->>Client: chatStream(message, options)
Client->>BE: unified_ai_chat_stream
loop Token streaming
BE-->>Client: token
Client-->>Hook: onToken(token)
Hook->>UI: Update display
end
BE-->>Client: complete
Client-->>Hook: onComplete(response)
else Non-streaming
Hook->>Client: chat(message, options)
Client->>BE: unified_ai_chat
BE-->>Client: Full response
Client-->>Hook: response
end
Hook->>Hook: Add AI message to history
Hook->>UI: Update messages

interface ChatOptions {
canonicalModel?: string; // 'chat' | 'ai-suggestions'
visitId?: string; // Link to visit context
patientId?: string; // Patient context
systemPrompt?: string; // Custom system prompt
temperature?: number; // 0-1, creativity level
maxTokens?: number; // Response length limit
}

Chat może działać w różnych kontekstach:

ModeContextUse Case
GeneralNoneOgólne pytania
VisitvisitIdPytania o konkretną wizytę
PatientpatientIdHistoria pacjenta
SpecialistCustom promptAI Specjalista

interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
metadata?: {
visitId?: string;
sources?: string[]; // References
confidence?: number;
};
}

Drafty są zapisywane lokalnie:

// Save draft
await safeInvoke('ai_chat_draft_save', {
context_key: storageKey,
messages: messages.map(m => ({
role: m.role,
content: m.content,
timestamp: m.timestamp,
})),
});
// Load draft
const draft = await safeInvoke('ai_chat_draft_load', {
context_key: storageKey,
});

Trwałe sesje dla historii:

// Save session
await chatSessionsApi.save({
visitId: currentVisitId,
messages: messages,
});
// List sessions
const sessions = await chatSessionsApi.list({
visitId: currentVisitId,
limit: 20,
});

// Start streaming
const requestId = UnifiedAIClient.chatStream(
message,
options,
onToken,
onComplete,
onError
);
// Cancel streaming
UnifiedAIClient.cancelStream(requestId);

Chat można odłączyć do osobnego okna:

useAssistantDetachedWindow.ts
if (isTauriEnv) {
// Create new window
await createWindow(DETACHED_WINDOW_LABEL, {
width: 400,
height: 600,
title: 'AI Assistant',
});
// Apply glass effect
await safeInvoke('vista_apply_glass', {
label: DETACHED_WINDOW_LABEL,
});
}

// Handle errors gracefully
try {
await sendMessage(text);
} catch (error) {
if (error.code === 'STREAM_CANCELLED') {
// User cancelled - no error message
} else if (error.code === 'RATE_LIMIT') {
addMessage({
role: 'system',
content: 'Zbyt wiele wiadomości. Odczekaj chwilę.',
});
} else {
addMessage({
role: 'system',
content: 'Wystąpił błąd. Spróbuj ponownie.',
});
}
}