Push
This commit is contained in:
191
src/components/ChatInterface.tsx
Normal file
191
src/components/ChatInterface.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Message } from '@/types';
|
||||
import { streamChatResponse } from '@/services/geminiService';
|
||||
|
||||
const ChatInterface: React.FC = () => {
|
||||
const [input, setInput] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: 'welcome',
|
||||
role: 'model',
|
||||
text: "Sistem aktif. ZeroSixLab AI arayüzüne hoş geldiniz. Araştırmanız veya projeniz için nasıl yardımcı olabilirim?",
|
||||
timestamp: new Date(),
|
||||
}
|
||||
]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
// Only scroll inside the messages container, not the whole page
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Skip auto-scroll on initial load
|
||||
if (isInitialLoad) {
|
||||
setIsInitialLoad(false);
|
||||
return;
|
||||
}
|
||||
scrollToBottom();
|
||||
}, [messages, isInitialLoad]);
|
||||
|
||||
// Focus input on load (without scrolling page)
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus({ preventScroll: true });
|
||||
}, []);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMsg: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
text: input,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
// Prepare history for API
|
||||
const history = messages.map(m => ({
|
||||
role: m.role,
|
||||
parts: [{ text: m.text }]
|
||||
}));
|
||||
|
||||
try {
|
||||
const stream = await streamChatResponse(userMsg.text, history);
|
||||
|
||||
const modelMsgId = (Date.now() + 1).toString();
|
||||
const modelMsg: Message = {
|
||||
id: modelMsgId,
|
||||
role: 'model',
|
||||
text: '',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, modelMsg]);
|
||||
|
||||
let fullText = '';
|
||||
for await (const chunk of stream) {
|
||||
fullText += chunk;
|
||||
setMessages(prev =>
|
||||
prev.map(msg => msg.id === modelMsgId ? { ...msg, text: fullText } : msg)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setMessages(prev => [...prev, {
|
||||
id: Date.now().toString(),
|
||||
role: 'model',
|
||||
text: "Hata: Nöral bağlantı kararsız. Lütfen API anahtarını kontrol edin.",
|
||||
timestamp: new Date(),
|
||||
isError: true
|
||||
}]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-162.5 w-full max-w-5xl mx-auto glass-panel rounded-xl shadow-2xl overflow-hidden relative border border-zsl-primary/20 group">
|
||||
{/* Decorative Corner Lines */}
|
||||
<div className="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-zsl-primary/50 rounded-tl-xl z-20"></div>
|
||||
<div className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-zsl-primary/50 rounded-tr-xl z-20"></div>
|
||||
<div className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-zsl-primary/50 rounded-bl-xl z-20"></div>
|
||||
<div className="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-zsl-primary/50 rounded-br-xl z-20"></div>
|
||||
|
||||
{/* HUD Header */}
|
||||
<div className="bg-black/40 p-3 border-b border-zsl-primary/20 flex justify-between items-center backdrop-blur-md z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="w-2.5 h-2.5 bg-zsl-accent rounded-full animate-pulse shadow-[0_0_10px_#FFAA00]"></div>
|
||||
<div className="absolute inset-0 bg-zsl-accent rounded-full animate-ping opacity-20"></div>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-zsl-primary tracking-widest font-bold">CANLI_BAĞLANTI // GEMINI-2.5-FLASH</span>
|
||||
</div>
|
||||
<div className="font-mono text-[10px] text-zsl-muted flex gap-4 uppercase tracking-wider">
|
||||
<span className="hidden sm:inline">MEM: <span className="text-zsl-primary">32GB</span></span>
|
||||
<span className="hidden sm:inline">NET: <span className="text-green-400">SECURE</span></span>
|
||||
<span>LATENCY: <span className="text-zsl-primary">12ms</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Effect inside Chat */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(10,25,47,0.8)_2px,transparent_2px)] bg-size-[100%_4px] pointer-events-none opacity-20"></div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto p-6 space-y-6 scrollbar-thin scrollbar-thumb-zsl-card relative z-10">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[85%] rounded-xl p-5 relative transition-all duration-300 ${
|
||||
msg.role === 'user'
|
||||
? 'bg-zsl-primary/10 border border-zsl-primary/30 text-white rounded-tr-none shadow-[0_0_15px_rgba(0,212,255,0.05)]'
|
||||
: 'bg-[#0f1d35] border border-slate-700/50 text-zsl-text rounded-tl-none shadow-lg'
|
||||
} ${msg.isError ? 'border-red-500/50 text-red-200 bg-red-900/10' : ''}`}>
|
||||
|
||||
<div className="font-mono text-[10px] uppercase opacity-60 mb-2 flex justify-between gap-4 border-b border-white/5 pb-1 select-none">
|
||||
<span className={`font-bold ${msg.role === 'user' ? 'text-zsl-primary' : 'text-zsl-accent'}`}>
|
||||
{msg.role === 'user' ? '>> OPERATOR' : '>> ZERO_AI'}
|
||||
</span>
|
||||
<span>{msg.timestamp.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
|
||||
<div className="whitespace-pre-wrap leading-relaxed text-sm md:text-base font-light tracking-wide">
|
||||
{msg.text}
|
||||
{msg.role === 'model' && msg.id === messages[messages.length - 1].id && isLoading && (
|
||||
<span className="inline-block w-2 h-4 bg-zsl-accent ml-1 animate-pulse align-middle"></span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decorative side accent for model */}
|
||||
{msg.role === 'model' && (
|
||||
<div className="absolute left-0 top-6 w-0.5 h-8 bg-zsl-accent shadow-[0_0_10px_#FFAA00]"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Loading Indicator */}
|
||||
{isLoading && messages[messages.length - 1].role === 'user' && (
|
||||
<div className="flex justify-start animate-in fade-in duration-300">
|
||||
<div className="bg-[#0f1d35] p-3 rounded-lg rounded-tl-none border border-slate-700/50 flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-zsl-accent animate-pulse">PROCESSING...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="p-4 bg-black/40 border-t border-zsl-primary/20 backdrop-blur relative z-10">
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zsl-primary font-mono text-lg animate-pulse">{'>'}</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
placeholder="Komut girişi yapın..."
|
||||
className="flex-1 bg-zsl-bg/50 border border-slate-700 focus:border-zsl-primary text-white pl-8 pr-5 py-4 rounded-lg font-mono text-sm outline-none transition-all placeholder-slate-600 focus:shadow-[0_0_15px_rgba(0,212,255,0.1)] focus:bg-zsl-bg/80"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="bg-zsl-primary hover:bg-white text-black px-6 py-4 rounded-lg font-mono text-sm font-bold uppercase transition-all hover:shadow-[0_0_20px_rgba(0,212,255,0.4)] disabled:opacity-50 disabled:cursor-not-allowed group relative overflow-hidden active:scale-95"
|
||||
>
|
||||
<span className="relative z-10">GÖNDER</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInterface;
|
||||
Reference in New Issue
Block a user