mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2025-12-06 21:36:57 +00:00
Compare commits
No commits in common. "16fdfa48b744b8c7361e70baa67767b5847ad5f9" and "ba145a3da0d65dae603c9297eba5c1d52a348b39" have entirely different histories.
16fdfa48b7
...
ba145a3da0
@ -1,47 +0,0 @@
|
|||||||
export type AIProvider = 'pollinations' | 'gpt-oss' | 'custom';
|
|
||||||
|
|
||||||
export interface AIProviderConfig {
|
|
||||||
id: AIProvider;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
endpoint: string;
|
|
||||||
requiresAuth: boolean;
|
|
||||||
apiKey?: string;
|
|
||||||
model?: string;
|
|
||||||
allowsCustomKey?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AI_PROVIDERS: AIProviderConfig[] = [
|
|
||||||
{
|
|
||||||
id: 'pollinations',
|
|
||||||
name: 'Pollinations.ai',
|
|
||||||
description: 'Free, no login required',
|
|
||||||
endpoint: 'https://text.pollinations.ai/openai',
|
|
||||||
requiresAuth: false,
|
|
||||||
model: 'openai',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpt-oss',
|
|
||||||
name: 'GPT-OSS-20B',
|
|
||||||
description: 'Alternative model (IP-locked)',
|
|
||||||
endpoint: 'https://api.pawan.krd/gpt-oss-20b/v1/chat/completions',
|
|
||||||
requiresAuth: true,
|
|
||||||
apiKey: 'pk-AvSqFEAjTobfSGEGTnqKVuSwcfcDDMeEZSkeFqnlrGFJNVAC',
|
|
||||||
model: 'gpt-oss-20b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'custom',
|
|
||||||
name: 'Custom API',
|
|
||||||
description: 'Use your own OpenAI-compatible API',
|
|
||||||
endpoint: '',
|
|
||||||
requiresAuth: true,
|
|
||||||
allowsCustomKey: true,
|
|
||||||
model: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getProvider = (id: AIProvider): AIProviderConfig => {
|
|
||||||
return AI_PROVIDERS.find(p => p.id === id) || AI_PROVIDERS[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CUSTOM_API_STORAGE_KEY = 'ai-chat-custom-api';
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Send, Bot, User, Loader2, Trash2, AlertTriangle, Maximize2, Minimize2, ChevronDown, Settings } from 'lucide-react';
|
import { Send, Bot, User, Loader2, Trash2, AlertTriangle, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
@ -8,22 +8,6 @@ import { useSettings } from '@/contexts/SettingsContext';
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import GlitchText from '@/components/GlitchText';
|
import GlitchText from '@/components/GlitchText';
|
||||||
import MessageContent from '@/components/MessageContent';
|
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 {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
@ -32,14 +16,7 @@ interface Message {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomApiConfig {
|
|
||||||
endpoint: string;
|
|
||||||
apiKey: string;
|
|
||||||
model: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'ai-chat-history';
|
const STORAGE_KEY = 'ai-chat-history';
|
||||||
const PROVIDER_KEY = 'ai-chat-provider';
|
|
||||||
|
|
||||||
const AIChat = () => {
|
const AIChat = () => {
|
||||||
const [messages, setMessages] = useState<Message[]>(() => {
|
const [messages, setMessages] = useState<Message[]>(() => {
|
||||||
@ -60,23 +37,6 @@ const AIChat = () => {
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = 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 scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -90,18 +50,6 @@ const AIChat = () => {
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [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
|
// Exit fullscreen on Escape key
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
@ -198,40 +146,16 @@ const AIChat = () => {
|
|||||||
setMessages(prev => [...prev, assistantMessage]);
|
setMessages(prev => [...prev, assistantMessage]);
|
||||||
|
|
||||||
// Helper function to make API request with retry logic
|
// 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> => {
|
const makeRequest = async (retries = 3, delay = 1000): Promise<Response> => {
|
||||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const headers: Record<string, string> = {
|
const response = await fetch('https://text.pollinations.ai/openai', {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if ((provider.requiresAuth || selectedProvider === 'custom') && apiKey) {
|
|
||||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(apiEndpoint, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: apiModel,
|
model: 'openai',
|
||||||
messages: [
|
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.' },
|
{ 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,
|
...chatHistory,
|
||||||
@ -424,64 +348,9 @@ const AIChat = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<p className="text-muted-foreground mb-4 font-pixel text-sm">
|
||||||
<span className="text-muted-foreground font-pixel text-sm">{'>'} Model:</span>
|
{'>'} Free AI chat powered by Pollinations.ai - no login required
|
||||||
<DropdownMenu>
|
</p>
|
||||||
<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}>
|
<ScrollArea className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50" ref={scrollRef}>
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
@ -580,74 +449,6 @@ const AIChat = () => {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user