mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:05:23 +02:00
Sets up the project with Vite, React, Tailwind CSS, Gemini AI integration, and necessary dependencies for data analysis. Includes initial configuration for TypeScript, Tailwind, and project metadata.
248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import { ChatIcon, CloseIcon, SendIcon } from './Icons';
|
|
import { ChatMessage } from '../types';
|
|
|
|
interface AIChatProps {
|
|
onSendMessage: (text: string) => Promise<string>;
|
|
isOpen: boolean;
|
|
setIsOpen: (open: boolean) => void;
|
|
apiKey: string;
|
|
onApiKeyChange: (key: string) => void;
|
|
}
|
|
|
|
const ModelMessage: React.FC<{ text: string }> = ({ text }) => {
|
|
const elements: React.ReactNode[] = [];
|
|
let listItems: React.ReactNode[] = [];
|
|
|
|
const flushList = () => {
|
|
if (listItems.length > 0) {
|
|
elements.push(
|
|
<ul key={`ul-${elements.length}`} className="list-disc list-inside space-y-1 my-2 pl-2">
|
|
{listItems}
|
|
</ul>
|
|
);
|
|
listItems = [];
|
|
}
|
|
};
|
|
|
|
const parseBold = (content: string, keyPrefix: string) => {
|
|
const parts = content.split(/(\*\*.*?\*\*)/g);
|
|
return parts.map((part, i) => {
|
|
if (part.startsWith('**') && part.endsWith('**')) {
|
|
return <strong key={`${keyPrefix}-${i}`}>{part.slice(2, -2)}</strong>;
|
|
}
|
|
return part;
|
|
});
|
|
}
|
|
|
|
text.split('\n').forEach((line, index) => {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith('* ') || trimmedLine.startsWith('- ')) {
|
|
const content = trimmedLine.substring(2);
|
|
listItems.push(<li key={index}>{parseBold(content, `li-${index}`)}</li>);
|
|
} else {
|
|
flushList();
|
|
if (line.trim() !== '') {
|
|
elements.push(
|
|
<p key={`p-${index}`} className="my-1">
|
|
{parseBold(line, `p-${index}`)}
|
|
</p>
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
flushList(); // Flush any remaining list items at the end
|
|
|
|
return <>{elements.length > 0 ? elements : <p>{text}</p>}</>;
|
|
};
|
|
|
|
const SettingsIcon = () => (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 1 1 0-9h.75c.704 0 1.402-.03 2.09-.09a1.692 1.692 0 0 1 1.624 1.374c.11 1.054.547 2.028 1.218 2.822.67.793 1.644 1.23 2.697 1.34a1.694 1.694 0 0 1 1.374 1.625c.06.688.09 1.386.09 2.09v.75a4.5 4.5 0 1 1-9 0v-.75c0-.704-.03-1.402-.09-2.09a1.692 1.692 0 0 1-1.374-1.624 11.264 11.264 0 0 0-1.34-2.698 11.263 11.263 0 0 0-2.822-1.217A1.692 1.692 0 0 1 10.34 15.84Z" />
|
|
</svg>
|
|
);
|
|
|
|
|
|
const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen, apiKey, onApiKeyChange }) => {
|
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
{ role: 'model', text: 'Hello! I am your AI Data Analyst. I can answer questions about your data, analyze trends, and perform calculations.', timestamp: new Date() }
|
|
]);
|
|
const [input, setInput] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const [showConfig, setShowConfig] = useState(!apiKey);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
const scrollToBottom = () => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isOpen) scrollToBottom();
|
|
}, [messages, isOpen, showConfig]);
|
|
|
|
useEffect(() => {
|
|
// If no API key is present when opened, show config
|
|
if (!apiKey) setShowConfig(true);
|
|
}, [apiKey]);
|
|
|
|
const handleSend = async () => {
|
|
if (!input.trim() || !apiKey) return;
|
|
|
|
const userMsg: ChatMessage = { role: 'user', text: input, timestamp: new Date() };
|
|
setMessages(prev => [...prev, userMsg]);
|
|
setInput('');
|
|
setIsTyping(true);
|
|
|
|
const responseText = await onSendMessage(userMsg.text);
|
|
|
|
setIsTyping(false);
|
|
setMessages(prev => [...prev, { role: 'model', text: responseText, timestamp: new Date() }]);
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSend();
|
|
}
|
|
};
|
|
|
|
const handleSaveKey = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
// Input value is already bound to parent state via local var, but we use form submission to switch view
|
|
if (apiKey) setShowConfig(false);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Trigger Button */}
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-110
|
|
${isOpen ? 'bg-slate-700 rotate-90 opacity-0 pointer-events-none' : 'bg-primary text-white rotate-0 opacity-100'}`}
|
|
>
|
|
<ChatIcon />
|
|
</button>
|
|
|
|
{/* Chat Window */}
|
|
<div
|
|
className={`fixed z-50 bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden
|
|
${isOpen
|
|
? 'bottom-6 right-6 w-96 h-[600px] rounded-2xl opacity-100 translate-y-0'
|
|
: 'bottom-6 right-6 w-96 h-0 opacity-0 translate-y-10 pointer-events-none'}`}
|
|
>
|
|
{/* Header */}
|
|
<div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2 h-2 rounded-full ${apiKey ? 'bg-green-400 animate-pulse' : 'bg-red-500'}`}></div>
|
|
<h3 className="font-bold text-slate-100">AI Data Assistant</h3>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setShowConfig(!showConfig)}
|
|
className="text-slate-400 hover:text-white transition-colors p-1 rounded hover:bg-white/10"
|
|
title="API Settings"
|
|
>
|
|
<SettingsIcon />
|
|
</button>
|
|
<button onClick={() => setIsOpen(false)} className="text-slate-400 hover:text-white transition-colors p-1 rounded hover:bg-white/10">
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Configuration Screen */}
|
|
{showConfig ? (
|
|
<div className="flex-1 p-6 flex flex-col justify-center bg-slate-950">
|
|
<div className="mb-6 text-center">
|
|
<div className="w-12 h-12 bg-indigo-500/20 rounded-full flex items-center justify-center mx-auto mb-4 text-indigo-400">
|
|
<ChatIcon />
|
|
</div>
|
|
<h3 className="text-lg font-bold text-white mb-2">Connect Gemini AI</h3>
|
|
<p className="text-sm text-slate-400">
|
|
To enable the AI assistant, please enter your Google Gemini API Key.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveKey} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-500 uppercase mb-1">API Key</label>
|
|
<input
|
|
type="password"
|
|
value={apiKey}
|
|
onChange={(e) => onApiKeyChange(e.target.value)}
|
|
placeholder="AIzaSy..."
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-3 text-white focus:ring-2 focus:ring-indigo-500 outline-none"
|
|
required
|
|
/>
|
|
<p className="text-[10px] text-slate-500 mt-2">
|
|
Key is stored locally in your browser.
|
|
<a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer" className="text-indigo-400 hover:underline ml-1">Get a key here.</a>
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 rounded-lg transition-colors"
|
|
>
|
|
Save & Start Chatting
|
|
</button>
|
|
</form>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-950/50 custom-scrollbar">
|
|
{messages.map((msg, idx) => (
|
|
<div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
<div
|
|
className={`max-w-[85%] rounded-2xl p-3 text-sm leading-relaxed shadow-sm
|
|
${msg.role === 'user'
|
|
? 'bg-primary text-white rounded-br-none'
|
|
: 'bg-slate-800 text-slate-200 border border-border rounded-bl-none'}`}
|
|
>
|
|
{msg.role === 'model' ? <ModelMessage text={msg.text} /> : msg.text}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{isTyping && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-slate-800 border border-border rounded-2xl rounded-bl-none p-4 flex gap-1 items-center">
|
|
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce"></div>
|
|
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-100"></div>
|
|
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-200"></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Input */}
|
|
<div className="p-4 bg-slate-900 border-t border-border">
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Ask about revenue, growth, units..."
|
|
className="w-full bg-slate-950 border border-slate-700 text-slate-200 rounded-full py-3 pl-4 pr-12 focus:outline-none focus:border-primary transition-colors placeholder-slate-500 text-sm"
|
|
/>
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={!input.trim() || isTyping}
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-primary text-white rounded-full hover:bg-indigo-400 disabled:opacity-50 transition-colors"
|
|
>
|
|
<SendIcon />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default AIChat;
|