feat: Initialize Craze Analytix project structure

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.
This commit is contained in:
Christian
2025-12-11 11:25:26 +01:00
parent 563e6110ce
commit 9ba63ab8f8
23 changed files with 4481 additions and 8 deletions
+247
View File
@@ -0,0 +1,247 @@
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;