mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 23:38:36 +00:00
656 lines
24 KiB
TypeScript
656 lines
24 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Send, Bot, User, Loader2, Trash2, AlertTriangle, Maximize2, Minimize2, ChevronDown, Settings } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { useSettings } from '@/contexts/SettingsContext';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import GlitchText from '@/components/GlitchText';
|
|
import MessageContent from '@/components/MessageContent';
|
|
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
interface Message {
|
|
id: string;
|
|
role: 'user' | 'assistant' | 'system';
|
|
content: string;
|
|
timestamp: Date;
|
|
}
|
|
|
|
interface CustomApiConfig {
|
|
endpoint: string;
|
|
apiKey: string;
|
|
model: string;
|
|
}
|
|
|
|
const STORAGE_KEY = 'ai-chat-history';
|
|
const PROVIDER_KEY = 'ai-chat-provider';
|
|
|
|
const AIChat = () => {
|
|
const [messages, setMessages] = useState<Message[]>(() => {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
try {
|
|
const parsed = JSON.parse(stored);
|
|
return parsed.map((msg: Message) => ({
|
|
...msg,
|
|
timestamp: new Date(msg.timestamp),
|
|
}));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
return [];
|
|
});
|
|
const [input, setInput] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
const [selectedProvider, setSelectedProvider] = useState<AIProvider>(() => {
|
|
const stored = localStorage.getItem(PROVIDER_KEY);
|
|
return (stored as AIProvider) || 'pollinations';
|
|
});
|
|
const [customApiConfig, setCustomApiConfig] = useState<CustomApiConfig>(() => {
|
|
const stored = localStorage.getItem(CUSTOM_API_STORAGE_KEY);
|
|
if (stored) {
|
|
try {
|
|
return JSON.parse(stored);
|
|
} catch {
|
|
return { endpoint: '', apiKey: '', model: '' };
|
|
}
|
|
}
|
|
return { endpoint: '', apiKey: '', model: '' };
|
|
});
|
|
const [showCustomApiDialog, setShowCustomApiDialog] = useState(false);
|
|
const [tempCustomConfig, setTempCustomConfig] = useState<CustomApiConfig>({ endpoint: '', apiKey: '', model: '' });
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const { playSound } = useSettings();
|
|
const { toast } = useToast();
|
|
|
|
// Persist messages to localStorage
|
|
useEffect(() => {
|
|
if (messages.length > 0) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
|
|
} else {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
}
|
|
}, [messages]);
|
|
|
|
// Persist provider selection
|
|
useEffect(() => {
|
|
localStorage.setItem(PROVIDER_KEY, selectedProvider);
|
|
}, [selectedProvider]);
|
|
|
|
// Persist custom API config
|
|
useEffect(() => {
|
|
if (customApiConfig.endpoint || customApiConfig.apiKey) {
|
|
localStorage.setItem(CUSTOM_API_STORAGE_KEY, JSON.stringify(customApiConfig));
|
|
}
|
|
}, [customApiConfig]);
|
|
|
|
// Exit fullscreen on Escape key
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && isFullscreen) {
|
|
setIsFullscreen(false);
|
|
playSound('click');
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleEscape);
|
|
return () => window.removeEventListener('keydown', handleEscape);
|
|
}, [isFullscreen, playSound]);
|
|
|
|
useEffect(() => {
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
}
|
|
}, [messages]);
|
|
|
|
const sendMessage = async () => {
|
|
if (!input.trim() || isLoading) return;
|
|
|
|
const userMessage: Message = {
|
|
id: Date.now().toString(),
|
|
role: 'user',
|
|
content: input.trim(),
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
|
setInput('');
|
|
setIsLoading(true);
|
|
playSound('click');
|
|
|
|
try {
|
|
// Build chat history, filtering out empty messages, system notices, and ensuring proper alternation
|
|
const validMessages = messages.filter(msg => msg.content.trim() !== '' && msg.role !== 'system');
|
|
let chatHistory: { role: 'user' | 'assistant'; content: string }[] = [];
|
|
|
|
for (const msg of validMessages) {
|
|
// Avoid consecutive same-role messages by merging or skipping
|
|
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === msg.role) {
|
|
// Merge consecutive same-role messages
|
|
chatHistory[chatHistory.length - 1].content += '\n' + msg.content;
|
|
} else if (msg.role === 'user' || msg.role === 'assistant') {
|
|
chatHistory.push({ role: msg.role, content: msg.content });
|
|
}
|
|
}
|
|
|
|
// Add the new user message
|
|
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === 'user') {
|
|
chatHistory[chatHistory.length - 1].content += '\n' + userMessage.content;
|
|
} else {
|
|
chatHistory.push({ role: 'user', content: userMessage.content });
|
|
}
|
|
|
|
// Truncate history to stay within API character limits (max ~5000 chars to be safe)
|
|
const MAX_CHARS = 5000;
|
|
const systemPromptLength = 100; // approximate system prompt length
|
|
let totalChars = systemPromptLength;
|
|
|
|
// Keep messages from the end (most recent) until we hit the limit
|
|
const truncatedHistory: typeof chatHistory = [];
|
|
const originalLength = chatHistory.length;
|
|
for (let i = chatHistory.length - 1; i >= 0; i--) {
|
|
const msgLength = chatHistory[i].content.length;
|
|
if (totalChars + msgLength > MAX_CHARS && truncatedHistory.length > 0) {
|
|
break;
|
|
}
|
|
totalChars += msgLength;
|
|
truncatedHistory.unshift(chatHistory[i]);
|
|
}
|
|
|
|
// Check if truncation occurred and notify user
|
|
const wasTruncated = truncatedHistory.length < originalLength;
|
|
if (wasTruncated) {
|
|
const systemNotice: Message = {
|
|
id: `system-${Date.now()}`,
|
|
role: 'system',
|
|
content: '⚠ Memory limit reached. Earlier conversation context has been cleared. The AI may not remember previous topics.',
|
|
timestamp: new Date(),
|
|
};
|
|
setMessages(prev => [...prev, systemNotice]);
|
|
}
|
|
|
|
chatHistory = truncatedHistory;
|
|
|
|
const assistantMessage: Message = {
|
|
id: (Date.now() + 1).toString(),
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
setMessages(prev => [...prev, assistantMessage]);
|
|
|
|
// Helper function to make API request with retry logic
|
|
const provider = getProvider(selectedProvider);
|
|
|
|
// For custom provider, use user's config
|
|
const apiEndpoint = selectedProvider === 'custom' ? customApiConfig.endpoint : provider.endpoint;
|
|
const apiKey = selectedProvider === 'custom' ? customApiConfig.apiKey : provider.apiKey;
|
|
const apiModel = selectedProvider === 'custom' ? customApiConfig.model : provider.model;
|
|
|
|
if (selectedProvider === 'custom' && (!customApiConfig.endpoint || !customApiConfig.apiKey)) {
|
|
toast({
|
|
title: 'Custom API not configured',
|
|
description: 'Please configure your custom API settings first.',
|
|
variant: 'destructive',
|
|
});
|
|
setMessages(prev => prev.filter(msg => msg.id !== assistantMessage.id));
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const makeRequest = async (retries = 3, delay = 1000): Promise<Response> => {
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if ((provider.requiresAuth || selectedProvider === 'custom') && apiKey) {
|
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
}
|
|
|
|
const response = await fetch(apiEndpoint, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
model: apiModel,
|
|
messages: [
|
|
{ role: 'system', content: 'You are a helpful AI assistant with a hacker/cyberpunk personality. Keep responses concise and engaging. IMPORTANT: When sharing code examples, ALWAYS wrap them in markdown code blocks with the language specified, like ```python\ncode here\n``` or ```javascript\ncode here\n```. Never show code without proper markdown code block formatting.' },
|
|
...chatHistory,
|
|
],
|
|
stream: true,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
return response;
|
|
}
|
|
|
|
// If it's a 500 error and we have retries left, wait and try again
|
|
if (response.status >= 500 && attempt < retries) {
|
|
console.log(`API returned ${response.status}, retrying in ${delay}ms (attempt ${attempt}/${retries})`);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
delay *= 2; // Exponential backoff
|
|
continue;
|
|
}
|
|
|
|
throw new Error(`API error: ${response.status}`);
|
|
} catch (error) {
|
|
// Network errors (failed to fetch) - retry if we have attempts left
|
|
if (attempt < retries && error instanceof TypeError) {
|
|
console.log(`Network error, retrying in ${delay}ms (attempt ${attempt}/${retries})`);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
delay *= 2;
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
throw new Error('Failed after all retries');
|
|
};
|
|
|
|
const response = await makeRequest();
|
|
|
|
const reader = response.body?.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
if (reader) {
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// Process SSE format
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
const data = line.slice(6).trim();
|
|
if (data === '[DONE]') continue;
|
|
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
const content = parsed.choices?.[0]?.delta?.content || '';
|
|
if (content) {
|
|
setMessages(prev =>
|
|
prev.map(msg =>
|
|
msg.id === assistantMessage.id
|
|
? { ...msg, content: msg.content + content }
|
|
: msg
|
|
)
|
|
);
|
|
}
|
|
} catch {
|
|
// Not valid JSON, might be plain text
|
|
if (data && data !== '[DONE]') {
|
|
setMessages(prev =>
|
|
prev.map(msg =>
|
|
msg.id === assistantMessage.id
|
|
? { ...msg, content: msg.content + data }
|
|
: msg
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle any remaining buffer content
|
|
if (buffer.trim()) {
|
|
setMessages(prev =>
|
|
prev.map(msg =>
|
|
msg.id === assistantMessage.id
|
|
? { ...msg, content: msg.content + buffer }
|
|
: msg
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
playSound('click');
|
|
} catch (error) {
|
|
console.error('AI Chat error:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: error instanceof Error ? error.message : 'Failed to get AI response',
|
|
variant: 'destructive',
|
|
});
|
|
// Remove the empty assistant message if error occurred
|
|
setMessages(prev => prev.filter(msg => msg.content !== ''));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const clearChat = () => {
|
|
setMessages([]);
|
|
playSound('click');
|
|
toast({
|
|
title: 'Chat Cleared',
|
|
description: 'Conversation history has been erased.',
|
|
});
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
};
|
|
|
|
const toggleFullscreen = () => {
|
|
setIsFullscreen(!isFullscreen);
|
|
playSound('click');
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className={`flex flex-col ${
|
|
isFullscreen
|
|
? 'fixed inset-0 z-50 bg-background p-4 md:p-8'
|
|
: 'h-full'
|
|
}`}
|
|
>
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-4">
|
|
<GlitchText
|
|
text="AI Terminal"
|
|
className="font-minecraft text-2xl md:text-3xl text-primary text-glow"
|
|
/>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={toggleFullscreen}
|
|
className="text-primary hover:bg-primary/20 order-first sm:order-last"
|
|
title={isFullscreen ? 'Exit fullscreen (Esc)' : 'Fullscreen'}
|
|
>
|
|
{isFullscreen ? (
|
|
<Minimize2 className="w-4 h-4" />
|
|
) : (
|
|
<Maximize2 className="w-4 h-4" />
|
|
)}
|
|
</Button>
|
|
{messages.length > 0 && (
|
|
<>
|
|
<div className="flex items-center gap-2 px-2 py-1 border border-primary/30 rounded text-xs font-pixel">
|
|
<span className="text-muted-foreground">Memory:</span>
|
|
<span className={`${
|
|
(() => {
|
|
const totalChars = messages.filter(m => m.role !== 'system').reduce((acc, m) => acc + m.content.length, 0);
|
|
const percent = (totalChars / 5000) * 100;
|
|
return percent > 80 ? 'text-red-400' : percent > 50 ? 'text-yellow-400' : 'text-green-400';
|
|
})()
|
|
}`}>
|
|
{Math.min(100, Math.round((messages.filter(m => m.role !== 'system').reduce((acc, m) => acc + m.content.length, 0) / 5000) * 100))}%
|
|
</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={clearChat}
|
|
className="text-primary hover:bg-primary/20"
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
Clear
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<span className="text-muted-foreground font-pixel text-sm">{'>'} Model:</span>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-primary hover:bg-primary/20 font-pixel text-sm gap-1"
|
|
>
|
|
{getProvider(selectedProvider).name}
|
|
<ChevronDown className="w-3 h-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="bg-background border-primary/50">
|
|
{AI_PROVIDERS.map((provider) => (
|
|
<DropdownMenuItem
|
|
key={provider.id}
|
|
onClick={() => {
|
|
if (provider.id === 'custom') {
|
|
setTempCustomConfig(customApiConfig);
|
|
setShowCustomApiDialog(true);
|
|
}
|
|
setSelectedProvider(provider.id);
|
|
playSound('click');
|
|
}}
|
|
className={`font-pixel text-xs cursor-pointer ${
|
|
selectedProvider === provider.id ? 'text-primary' : 'text-foreground'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2 w-full">
|
|
<div className="flex-1">
|
|
<div>{provider.name}</div>
|
|
<div className="text-muted-foreground text-[10px]">{provider.description}</div>
|
|
</div>
|
|
{provider.id === 'custom' && (
|
|
<Settings className="w-3 h-3 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
{selectedProvider === 'custom' && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setTempCustomConfig(customApiConfig);
|
|
setShowCustomApiDialog(true);
|
|
playSound('click');
|
|
}}
|
|
className="text-primary hover:bg-primary/20 font-pixel text-xs"
|
|
>
|
|
<Settings className="w-3 h-3 mr-1" />
|
|
Configure
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50" ref={scrollRef}>
|
|
{messages.length === 0 ? (
|
|
<div className="h-full flex items-center justify-center text-muted-foreground">
|
|
<div className="text-center">
|
|
<Bot className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
<p className="font-pixel text-sm">No messages yet.</p>
|
|
<p className="font-pixel text-xs mt-2">Start a conversation with the AI.</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{messages.map((message, index) => (
|
|
message.role === 'system' ? (
|
|
<motion.div
|
|
key={message.id}
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="flex justify-center"
|
|
>
|
|
<div className="flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
|
|
<AlertTriangle className="w-4 h-4 text-yellow-500" />
|
|
<p className="font-pixel text-xs text-yellow-500">{message.content}</p>
|
|
</div>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key={message.id}
|
|
initial={{ opacity: 0, x: message.role === 'user' ? 20 : -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.05 }}
|
|
className={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
{message.role === 'assistant' && (
|
|
<div className="w-8 h-8 rounded border border-primary/50 flex items-center justify-center bg-primary/10 flex-shrink-0">
|
|
<Bot className="w-4 h-4 text-primary" />
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`max-w-[80%] p-3 rounded-lg ${
|
|
message.role === 'user'
|
|
? 'bg-primary/20 border border-primary/50'
|
|
: 'bg-secondary/50 border border-primary/30'
|
|
}`}
|
|
>
|
|
<MessageContent
|
|
content={message.content}
|
|
isLoading={isLoading && message.role === 'assistant' && message.content === ''}
|
|
/>
|
|
</div>
|
|
{message.role === 'user' && (
|
|
<div className="w-8 h-8 rounded border border-primary/50 flex items-center justify-center bg-primary/10 flex-shrink-0">
|
|
<User className="w-4 h-4 text-primary" />
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
)
|
|
))}
|
|
{isLoading && messages[messages.length - 1]?.role === 'user' && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="flex gap-3 justify-start"
|
|
>
|
|
<div className="w-8 h-8 rounded border border-primary/50 flex items-center justify-center bg-primary/10">
|
|
<Bot className="w-4 h-4 text-primary" />
|
|
</div>
|
|
<div className="p-3 rounded-lg bg-secondary/50 border border-primary/30">
|
|
<Loader2 className="w-4 h-4 text-primary animate-spin" />
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
|
|
<div className="flex gap-2">
|
|
<Textarea
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Type your message..."
|
|
disabled={isLoading}
|
|
className="resize-none font-pixel text-sm bg-background/50 border-primary/50 text-primary placeholder:text-muted-foreground focus:border-primary"
|
|
rows={2}
|
|
/>
|
|
<Button
|
|
onClick={sendMessage}
|
|
disabled={!input.trim() || isLoading}
|
|
className="px-4 bg-primary/20 border border-primary hover:bg-primary/30 text-primary"
|
|
>
|
|
{isLoading ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<Send className="w-5 h-5" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Custom API Configuration Dialog */}
|
|
<Dialog open={showCustomApiDialog} onOpenChange={setShowCustomApiDialog}>
|
|
<DialogContent className="bg-background border-primary/50">
|
|
<DialogHeader>
|
|
<DialogTitle className="font-pixel text-primary">Custom API Configuration</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="endpoint" className="font-pixel text-sm">API Endpoint</Label>
|
|
<Input
|
|
id="endpoint"
|
|
placeholder="https://api.example.com/v1/chat/completions"
|
|
value={tempCustomConfig.endpoint}
|
|
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, endpoint: e.target.value }))}
|
|
className="font-pixel text-sm bg-background/50 border-primary/50"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="apiKey" className="font-pixel text-sm">API Key</Label>
|
|
<Input
|
|
id="apiKey"
|
|
type="password"
|
|
placeholder="sk-..."
|
|
value={tempCustomConfig.apiKey}
|
|
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, apiKey: e.target.value }))}
|
|
className="font-pixel text-sm bg-background/50 border-primary/50"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="model" className="font-pixel text-sm">Model Name</Label>
|
|
<Input
|
|
id="model"
|
|
placeholder="gpt-4, claude-3, etc."
|
|
value={tempCustomConfig.model}
|
|
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, model: e.target.value }))}
|
|
className="font-pixel text-sm bg-background/50 border-primary/50"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground font-pixel">
|
|
Your API key is stored locally in your browser and never sent to our servers.
|
|
</p>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => setShowCustomApiDialog(false)}
|
|
className="font-pixel text-sm"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setCustomApiConfig(tempCustomConfig);
|
|
setShowCustomApiDialog(false);
|
|
playSound('click');
|
|
toast({
|
|
title: 'Configuration Saved',
|
|
description: 'Custom API settings have been updated.',
|
|
});
|
|
}}
|
|
className="font-pixel text-sm bg-primary/20 border border-primary hover:bg-primary/30 text-primary"
|
|
>
|
|
Save
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
export default AIChat;
|