Spaces:
Running
Running
File size: 4,885 Bytes
1f5b714 750639c 1f5b714 deb36e3 1f5b714 9527547 1f5b714 9527547 1f5b714 c449c99 1f5b714 c449c99 1f5b714 43ad437 1f5b714 9527547 2ec0e37 9527547 1f5b714 9c32828 1f5b714 2ec0e37 1f5b714 9ec186d 1f5b714 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
import { useState, useRef, useEffect } from 'react';
import { Send, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { ChatMessage } from './ChatMessage';
import { sendChat } from '@/services/rag';
import type { ChatMessage as ChatMessageType } from '@/types/rag';
import { useToast } from '@/hooks/useToast';
import { APIException } from '@/services/api';
interface ChatPanelProps {
onNavigateToNote: (path: string) => void;
onNotesChanged?: () => void;
}
export function ChatPanel({ onNavigateToNote, onNotesChanged }: ChatPanelProps) {
const [messages, setMessages] = useState<ChatMessageType[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const toast = useToast();
// Auto-scroll to bottom
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const handleSubmit = async () => {
if (!input.trim() || isLoading) return;
const userMsg: ChatMessageType = {
role: 'user',
content: input.trim(),
timestamp: new Date().toISOString()
};
// Construct new history immediately
const newHistory = [...messages, userMsg];
// Optimistically update UI
setMessages(newHistory);
setInput('');
setIsLoading(true);
try {
const response = await sendChat({ messages: newHistory });
const assistantMsg: ChatMessageType = {
role: 'assistant',
content: response.answer,
timestamp: new Date().toISOString(),
sources: response.sources,
notes_written: response.notes_written
};
setMessages(prev => [...prev, assistantMsg]);
// Trigger refresh if notes were created/updated
if (response.notes_written && response.notes_written.length > 0) {
console.log('[ChatPanel] Notes written:', response.notes_written);
if (onNotesChanged) {
console.log('[ChatPanel] Calling onNotesChanged()');
await onNotesChanged();
console.log('[ChatPanel] onNotesChanged() completed');
} else {
console.error('[ChatPanel] onNotesChanged is undefined!');
}
} else {
console.log('[ChatPanel] No notes written, skipping refresh');
}
} catch (err) {
console.error("Chat error:", err);
let errorMessage = "Failed to get response from agent";
if (err instanceof APIException) {
errorMessage = err.message || err.error;
} else if (err instanceof Error) {
errorMessage = err.message;
}
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
return (
<div className="flex flex-col h-full bg-background">
{/* Header */}
<div className="p-4 border-b border-border">
<h2 className="font-semibold">Gemini Planning Agent</h2>
<p className="text-xs text-muted-foreground">Ask questions about your vault</p>
</div>
{/* Message List */}
<div className="flex-1 overflow-y-auto" ref={scrollRef}>
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground p-8 text-center">
<p>π Hi! I can help you navigate this vault.</p>
<p className="text-sm mt-2">Try asking: "How does authentication work?"</p>
</div>
) : (
<div className="divide-y divide-border/50">
{messages.map((msg, i) => (
<ChatMessage
key={i}
message={msg}
onSourceClick={onNavigateToNote}
onRefreshNeeded={onNotesChanged}
/>
))}
{isLoading && (
<div className="p-4 flex items-center gap-2 text-muted-foreground text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
Thinking...
</div>
)}
</div>
)}
</div>
{/* Input Area */}
<div className="p-4 pb-14 border-t border-border">
<div className="flex gap-2">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask a question..."
className="min-h-[40px] max-h-[150px] resize-none"
rows={1}
/>
<Button onClick={handleSubmit} disabled={isLoading || !input.trim()} size="icon">
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}
|