mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2025-12-06 13:36:57 +00:00
Add multi AI providers support
Introduce a second AI provider (GPT-OSS-20B) and UI to switch between Pollinations.ai and GPT-OSS, including integration scaffolding and API key handling as a secret. This enables testing the new model without altering core UX. X-Lovable-Edit-ID: edt-0d2ba6be-027b-4029-9e71-17c272bde735
This commit is contained in:
commit
fa78070bac
35
src/lib/aiProviders.ts
Normal file
35
src/lib/aiProviders.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export type AIProvider = 'pollinations' | 'gpt-oss';
|
||||
|
||||
export interface AIProviderConfig {
|
||||
id: AIProvider;
|
||||
name: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
requiresAuth: boolean;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
export const getProvider = (id: AIProvider): AIProviderConfig => {
|
||||
return AI_PROVIDERS.find(p => p.id === id) || AI_PROVIDERS[0];
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Send, Bot, User, Loader2, Trash2, AlertTriangle, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { Send, Bot, User, Loader2, Trash2, AlertTriangle, Maximize2, Minimize2, ChevronDown } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
@ -8,6 +8,13 @@ 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 } from '@/lib/aiProviders';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
@ -17,6 +24,7 @@ interface Message {
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'ai-chat-history';
|
||||
const PROVIDER_KEY = 'ai-chat-provider';
|
||||
|
||||
const AIChat = () => {
|
||||
const [messages, setMessages] = useState<Message[]>(() => {
|
||||
@ -37,6 +45,10 @@ const AIChat = () => {
|
||||
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 scrollRef = useRef<HTMLDivElement>(null);
|
||||
const { playSound } = useSettings();
|
||||
const { toast } = useToast();
|
||||
@ -50,6 +62,11 @@ const AIChat = () => {
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// Persist provider selection
|
||||
useEffect(() => {
|
||||
localStorage.setItem(PROVIDER_KEY, selectedProvider);
|
||||
}, [selectedProvider]);
|
||||
|
||||
// Exit fullscreen on Escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
@ -146,16 +163,23 @@ const AIChat = () => {
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
|
||||
// Helper function to make API request with retry logic
|
||||
const provider = getProvider(selectedProvider);
|
||||
const makeRequest = async (retries = 3, delay = 1000): Promise<Response> => {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await fetch('https://text.pollinations.ai/openai', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
if (provider.requiresAuth && provider.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${provider.apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(provider.endpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: 'openai',
|
||||
model: provider.model,
|
||||
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,
|
||||
@ -348,9 +372,40 @@ const AIChat = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mb-4 font-pixel text-sm">
|
||||
{'>'} Free AI chat powered by Pollinations.ai - no login required
|
||||
</p>
|
||||
<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={() => {
|
||||
setSelectedProvider(provider.id);
|
||||
playSound('click');
|
||||
}}
|
||||
className={`font-pixel text-xs cursor-pointer ${
|
||||
selectedProvider === provider.id ? 'text-primary' : 'text-foreground'
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div>{provider.name}</div>
|
||||
<div className="text-muted-foreground text-[10px]">{provider.description}</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50" ref={scrollRef}>
|
||||
{messages.length === 0 ? (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user