Reuben_OS / app /components /GeminiChat.tsx
Reubencf's picture
fixing layout issue and rendering issues
2a7ce2c
'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>
)
}