File size: 9,994 Bytes
8af739b
 
 
 
a677ebd
 
 
8af739b
 
 
 
 
 
 
 
ad31128
 
a1cccea
 
8af739b
 
 
 
 
 
d2a09b6
8af739b
 
 
a1cccea
8af739b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2a09b6
8af739b
d2a09b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8af739b
 
d2a09b6
8af739b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2a09b6
8af739b
 
ad31128
 
8af739b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2a09b6
8af739b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a7ce2c
ad31128
 
 
8af739b
 
 
 
d2a09b6
 
 
 
 
a677ebd
 
 
 
 
d2a09b6
 
a677ebd
 
 
 
 
8af739b
 
 
 
d2a09b6
8af739b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a7ce2c
 
 
 
8af739b
2a7ce2c
8af739b
 
 
 
 
ad31128
 
 
 
8af739b
 
 
 
 
 
 
 
 
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
'use client'

import React, { useState, useRef, useEffect } from 'react'
import Window from './Window'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'
import {
  PaperPlaneRight,
  Sparkle,
  ArrowUp
} from '@phosphor-icons/react'

interface GeminiChatProps {
  onClose: () => void
  onMinimize?: () => void
  onMaximize?: () => void
  onFocus?: () => void
  zIndex?: number
}

interface Message {
  id: string
  role: 'user' | 'assistant'
  content: string
  thought?: string
  timestamp: number
}

export function GeminiChat({ onClose, onMinimize, onMaximize, onFocus, zIndex }: GeminiChatProps) {
  const [messages, setMessages] = useState<Message[]>([
    {
      id: '1',
      role: 'assistant',
      content: "Hello! I'm Gemini. How can I help you today?",
      timestamp: Date.now()
    }
  ])
  const [input, setInput] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const scrollRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // Load messages from localStorage
    const savedMessages = localStorage.getItem('gemini-chat-messages')
    if (savedMessages) {
      try {
        const parsed = JSON.parse(savedMessages)
        if (parsed.length > 0) {
          setMessages(parsed)
        }
      } catch (e) {
        console.error('Failed to load messages')
      }
    }
  }, [])

  useEffect(() => {
    // Save messages to localStorage
    if (messages.length > 1) {
      localStorage.setItem('gemini-chat-messages', JSON.stringify(messages.slice(-20))) // Keep last 20 messages
    }
  }, [messages])

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight
    }
  }, [messages])

  const handleSend = async () => {
    if (!input.trim() || isLoading) return

    const userMessage: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: input.trim(),
      timestamp: Date.now()
    }

    setMessages(prev => [...prev, userMessage])
    setInput('')
    setIsLoading(true)

    try {
      // Get conversation history (last 10 messages)
      const history = messages.slice(-10).map(msg => ({
        role: msg.role,
        content: msg.content
      }))

      const response = await fetch('/api/gemini/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          message: userMessage.content,
          history
        }),
      })

      if (!response.ok) throw new Error('Network response was not ok')

      const reader = response.body?.getReader()
      const decoder = new TextDecoder()

      // Create a placeholder message
      const assistantMessage: Message = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: '',
        thought: '',
        timestamp: Date.now()
      }
      setMessages(prev => [...prev, assistantMessage])

      if (reader) {
        while (true) {
          const { done, value } = await reader.read()
          if (done) break

          const chunk = decoder.decode(value, { stream: true })
          const lines = chunk.split('\n\n')

          for (const line of lines) {
            if (line.startsWith('data: ')) {
              try {
                const data = JSON.parse(line.slice(6))
                setMessages(prev => {
                  const newMessages = [...prev]
                  const lastMsg = newMessages[newMessages.length - 1]
                  if (lastMsg.id === assistantMessage.id) {
                    return [
                      ...newMessages.slice(0, -1),
                      {
                        ...lastMsg,
                        content: lastMsg.content + (data.text || ''),
                        thought: (lastMsg.thought || '') + (data.thought || '')
                      }
                    ]
                  }
                  return newMessages
                })
              } catch (e) {
                console.error('Error parsing chunk:', e)
              }
            }
          }
        }
      }

    } catch (error) {
      console.error('Error sending message:', error)
      const errorMessage: Message = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: "I'm currently running in demo mode. For full functionality, please ensure the API is configured.",
        timestamp: Date.now()
      }
      setMessages(prev => [...prev, errorMessage])
    }

    setIsLoading(false)
  }

  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSend()
    }
  }

  const clearChat = () => {
    setMessages([
      {
        id: Date.now().toString(),
        role: 'assistant',
        content: "Chat history cleared. How can I help you today?",
        timestamp: Date.now()
      }
    ])
    localStorage.removeItem('gemini-chat-messages')
  }

  return (
    <Window
      id="gemini"
      title="Gemini"
      isOpen={true}
      onClose={onClose}
      onMinimize={onMinimize}
      onMaximize={onMaximize}
      width={700}
      height={500}
      x={100}
      y={100}
      className="gemini-window"
      headerClassName="bg-white border-b border-gray-100"
    >
      <div className="flex flex-col h-full bg-white">
        {/* Chat Header */}
        <div className="px-4 py-2 border-b border-gray-100 flex items-center justify-between">
          <div className="flex items-center gap-2">
            <div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-pink-600 flex items-center justify-center">
              <Sparkle size={18} weight="fill" className="text-white" />
            </div>
            <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-pink-600 font-bold">
              Gemini
            </span>
          </div>
          <button
            onClick={clearChat}
            className="text-xs text-gray-500 hover:text-gray-700"
          >
            Clear chat
          </button>
        </div>

        {/* Messages Area */}
        <div
          ref={scrollRef}
          className="flex-1 overflow-y-auto p-4 space-y-4"
        >
          {messages.map(message => (
            <div
              key={message.id}
              className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
            >
              <div
                className={`max-w-[80%] select-text ${message.role === 'user'
                  ? 'bg-blue-600 text-white rounded-2xl rounded-tr-none'
                  : 'bg-gray-100 text-gray-800 rounded-2xl rounded-tl-none'
                  } px-4 py-2 text-sm`}
              >
                {message.role === 'assistant' && (
                  <p className="font-semibold text-blue-600 mb-1 text-xs">Gemini</p>
                )}
                {message.thought && (
                  <div className="mb-2 p-2 bg-white/50 rounded text-xs text-gray-600 border border-gray-200/50 italic">
                    <div className="font-semibold mb-1 not-italic flex items-center gap-1">
                      <Sparkle size={12} /> Thinking Process:
                    </div>
                    <div className="markdown-content">
                      <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
                        {message.thought}
                      </ReactMarkdown>
                    </div>
                  </div>
                )}
                <div className="markdown-content">
                  <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
                    {message.content}
                  </ReactMarkdown>
                </div>
              </div>
            </div>
          ))}

          {isLoading && messages[messages.length - 1].role === 'user' && (
            <div className="flex justify-start">
              <div className="bg-gray-100 rounded-2xl rounded-tl-none px-4 py-3 text-sm">
                <p className="font-semibold text-blue-600 mb-1 text-xs">Gemini</p>
                <div className="flex gap-1">
                  <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></span>
                  <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></span>
                  <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></span>
                </div>
              </div>
            </div>
          )}
        </div>

        {/* Input Area */}
        <div className="border-t border-gray-100 p-4">
          <div className="flex items-center gap-2">
            <input
              ref={inputRef}
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={handleKeyPress}
              onPaste={(e) => {
                // Allow paste - default behavior
                e.stopPropagation()
              }}
              placeholder="Ask Gemini..."
              className="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 transition-all select-text"
              disabled={isLoading}
            />
            <button
              onClick={handleSend}
              disabled={!input.trim() || isLoading}
              className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${input.trim() && !isLoading
                ? 'bg-blue-600 hover:bg-blue-700 text-white'
                : 'bg-gray-200 text-gray-400 cursor-not-allowed'
                }`}
            >
              <ArrowUp size={16} weight="bold" />
            </button>
          </div>
        </div>
      </div>
    </Window>
  )
}