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>
  );
}