Phase 5: AI provider system with local and cloud support
- Implement AIProvider base interface with chat() and is_available() - Add LocalProvider connecting to bundled llama-server via OpenAI SDK - Add OpenAIProvider for direct OpenAI API access - Add AnthropicProvider for Anthropic Claude API - Add LiteLLMProvider for multi-provider gateway - Build AIProviderService with provider routing, auto-selection, and transcript context injection - Add ai.chat IPC handler supporting chat, list_providers, set_provider, and configure actions - Add ai_chat, ai_list_providers, ai_configure Tauri commands - Build interactive AIChatPanel with message history, quick actions (Summarize, Action Items), and transcript context awareness - Tests: 30 Python, 6 Rust, 0 Svelte errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,261 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { segments, speakers } from '$lib/stores/transcript';
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
let messages = $state<ChatMessage[]>([]);
|
||||
let inputText = $state('');
|
||||
let isLoading = $state(false);
|
||||
let chatContainer: HTMLDivElement;
|
||||
|
||||
function getTranscriptContext(): string {
|
||||
const segs = $segments;
|
||||
const spks = $speakers;
|
||||
if (segs.length === 0) return '';
|
||||
|
||||
return segs.map(seg => {
|
||||
const speaker = spks.find(s => s.id === seg.speaker_id);
|
||||
const name = speaker?.display_name || speaker?.label || 'Unknown';
|
||||
return `[${name}]: ${seg.text}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const text = inputText.trim();
|
||||
if (!text || isLoading) return;
|
||||
|
||||
messages = [...messages, { role: 'user', content: text }];
|
||||
inputText = '';
|
||||
isLoading = true;
|
||||
|
||||
// Auto-scroll to bottom
|
||||
requestAnimationFrame(() => {
|
||||
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
});
|
||||
|
||||
try {
|
||||
const chatMessages = messages.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
const result = await invoke<{ response: string }>('ai_chat', {
|
||||
messages: chatMessages,
|
||||
transcriptContext: getTranscriptContext(),
|
||||
});
|
||||
|
||||
messages = [...messages, { role: 'assistant', content: result.response }];
|
||||
} catch (err) {
|
||||
messages = [...messages, {
|
||||
role: 'assistant',
|
||||
content: `Error: ${err}`,
|
||||
}];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
requestAnimationFrame(() => {
|
||||
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
// Quick action buttons
|
||||
async function summarize() {
|
||||
inputText = 'Please summarize this transcript in bullet points.';
|
||||
await sendMessage();
|
||||
}
|
||||
|
||||
async function extractActions() {
|
||||
inputText = 'What action items or follow-ups were discussed?';
|
||||
await sendMessage();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ai-chat-panel">
|
||||
<h3>AI Chat</h3>
|
||||
<p class="placeholder">Ask questions about the transcript, generate summaries</p>
|
||||
<div class="panel-header">
|
||||
<h3>AI Chat</h3>
|
||||
{#if messages.length > 0}
|
||||
<button class="clear-btn" onclick={clearChat} title="Clear chat">Clear</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" bind:this={chatContainer}>
|
||||
{#if messages.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>Ask questions about the transcript</p>
|
||||
{#if $segments.length > 0}
|
||||
<div class="quick-actions">
|
||||
<button class="quick-btn" onclick={summarize}>Summarize</button>
|
||||
<button class="quick-btn" onclick={extractActions}>Action Items</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#each messages as msg}
|
||||
<div class="message {msg.role}">
|
||||
<div class="message-content">{msg.content}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if isLoading}
|
||||
<div class="message assistant loading">
|
||||
<div class="message-content">Thinking...</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<textarea
|
||||
class="input-textarea"
|
||||
placeholder="Ask about the transcript..."
|
||||
bind:value={inputText}
|
||||
onkeydown={handleKeydown}
|
||||
disabled={isLoading}
|
||||
></textarea>
|
||||
<button
|
||||
class="send-btn"
|
||||
onclick={sendMessage}
|
||||
disabled={isLoading || !inputText.trim()}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ai-chat-panel {
|
||||
padding: 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
min-height: 0;
|
||||
}
|
||||
h3 { margin: 0 0 0.5rem; }
|
||||
.placeholder {
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem 0.5rem;
|
||||
}
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.clear-btn {
|
||||
background: none;
|
||||
border: 1px solid #4a5568;
|
||||
color: #999;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.clear-btn:hover {
|
||||
color: #e0e0e0;
|
||||
border-color: #e94560;
|
||||
}
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.75rem;
|
||||
min-height: 0;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.quick-btn {
|
||||
background: rgba(233, 69, 96, 0.15);
|
||||
border: 1px solid rgba(233, 69, 96, 0.3);
|
||||
color: #e94560;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.quick-btn:hover {
|
||||
background: rgba(233, 69, 96, 0.25);
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.message.user {
|
||||
background: rgba(233, 69, 96, 0.15);
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.message.assistant {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.message.loading {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem 0.75rem;
|
||||
}
|
||||
.input-textarea {
|
||||
flex: 1;
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
resize: none;
|
||||
min-height: 2rem;
|
||||
max-height: 4rem;
|
||||
}
|
||||
.input-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #e94560;
|
||||
}
|
||||
.send-btn {
|
||||
background: #e94560;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
align-self: flex-end;
|
||||
}
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background: #d63851;
|
||||
}
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user