Add openrouter
This commit is contained in:
parent
7c03c594c1
commit
36b686bbd4
8 changed files with 380 additions and 61 deletions
|
|
@ -2,12 +2,13 @@ import React, { useState, useEffect } from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
import { X, Key, Eye, EyeOff, Loader2 } from 'lucide-react';
|
||||
import { useBodyScrollLock } from '../hooks/useBodyScrollLock';
|
||||
import type { AIProvider, UserPreferences } from '../types';
|
||||
|
||||
interface ApiKeyModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
apiKey: string | undefined;
|
||||
onSave: (key: string | undefined) => Promise<void>;
|
||||
preferences: UserPreferences;
|
||||
onSave: (prefs: Partial<UserPreferences>) => Promise<void>;
|
||||
saving: boolean;
|
||||
hasAIAccess: boolean;
|
||||
}
|
||||
|
|
@ -15,25 +16,37 @@ interface ApiKeyModalProps {
|
|||
export const ApiKeyModal: React.FC<ApiKeyModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
apiKey,
|
||||
preferences,
|
||||
onSave,
|
||||
saving,
|
||||
hasAIAccess,
|
||||
}) => {
|
||||
useBodyScrollLock(isOpen);
|
||||
const [localApiKey, setLocalApiKey] = useState(apiKey || '');
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [localProvider, setLocalProvider] = useState<AIProvider>(preferences.aiProvider || 'gemini');
|
||||
const [localGeminiKey, setLocalGeminiKey] = useState(preferences.geminiApiKey || '');
|
||||
const [localOpenRouterKey, setLocalOpenRouterKey] = useState(preferences.openRouterApiKey || '');
|
||||
const [localOpenRouterModel, setLocalOpenRouterModel] = useState(preferences.openRouterModel || '');
|
||||
const [showGeminiKey, setShowGeminiKey] = useState(false);
|
||||
const [showOpenRouterKey, setShowOpenRouterKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLocalApiKey(apiKey || '');
|
||||
setLocalProvider(preferences.aiProvider || 'gemini');
|
||||
setLocalGeminiKey(preferences.geminiApiKey || '');
|
||||
setLocalOpenRouterKey(preferences.openRouterApiKey || '');
|
||||
setLocalOpenRouterModel(preferences.openRouterModel || '');
|
||||
}
|
||||
}, [isOpen, apiKey]);
|
||||
}, [isOpen, preferences]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(localApiKey || undefined);
|
||||
await onSave({
|
||||
aiProvider: localProvider,
|
||||
geminiApiKey: localGeminiKey || undefined,
|
||||
openRouterApiKey: localOpenRouterKey || undefined,
|
||||
openRouterModel: localOpenRouterModel || undefined,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
@ -58,8 +71,8 @@ export const ApiKeyModal: React.FC<ApiKeyModalProps> = ({
|
|||
<Key size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black">Account Settings</h2>
|
||||
<p className="text-sm opacity-80">Manage your API access</p>
|
||||
<h2 className="text-xl font-black">AI Settings</h2>
|
||||
<p className="text-sm opacity-80">Configure your AI provider</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -70,33 +83,114 @@ export const ApiKeyModal: React.FC<ApiKeyModalProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-50">
|
||||
<div className="p-6 bg-gray-50 space-y-4">
|
||||
{hasAIAccess ? (
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<Key size={18} />
|
||||
Custom Gemini API Key
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Use your own API key for quiz generation. Leave empty to use the system key.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
value={localApiKey}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
placeholder="AIza..."
|
||||
className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showApiKey ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-bold text-gray-800 mb-2">AI Provider</label>
|
||||
<div className="flex bg-gray-200 p-1 rounded-xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocalProvider('gemini')}
|
||||
className={`flex-1 py-2 px-3 rounded-lg font-bold text-sm transition-all ${
|
||||
localProvider === 'gemini'
|
||||
? 'bg-white shadow-sm text-gray-800'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Gemini
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocalProvider('openrouter')}
|
||||
className={`flex-1 py-2 px-3 rounded-lg font-bold text-sm transition-all ${
|
||||
localProvider === 'openrouter'
|
||||
? 'bg-white shadow-sm text-gray-800'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
OpenRouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{localProvider === 'gemini' ? (
|
||||
<div>
|
||||
<label className="block font-bold text-gray-800 mb-2">
|
||||
Gemini API Key
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Leave empty to use the system key.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showGeminiKey ? 'text' : 'password'}
|
||||
value={localGeminiKey}
|
||||
onChange={(e) => setLocalGeminiKey(e.target.value)}
|
||||
placeholder="AIza..."
|
||||
className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGeminiKey(!showGeminiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showGeminiKey ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-bold text-gray-800 mb-2">
|
||||
OpenRouter API Key
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Get your key from{' '}
|
||||
<a
|
||||
href="https://openrouter.ai/keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-theme-primary hover:underline"
|
||||
>
|
||||
openrouter.ai/keys
|
||||
</a>
|
||||
</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showOpenRouterKey ? 'text' : 'password'}
|
||||
value={localOpenRouterKey}
|
||||
onChange={(e) => setLocalOpenRouterKey(e.target.value)}
|
||||
placeholder="sk-or-..."
|
||||
className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOpenRouterKey(!showOpenRouterKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showOpenRouterKey ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-bold text-gray-800 mb-2">
|
||||
Model
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Default: google/gemini-3-flash-preview
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={localOpenRouterModel}
|
||||
onChange={(e) => setLocalOpenRouterModel(e.target.value)}
|
||||
placeholder="google/gemini-3-flash-preview"
|
||||
className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<Key size={32} className="mx-auto mb-3 text-gray-400" />
|
||||
|
|
|
|||
|
|
@ -15,8 +15,18 @@ import type { Quiz, GameConfig } from '../types';
|
|||
|
||||
type GenerateMode = 'topic' | 'document';
|
||||
|
||||
import type { AIProvider } from '../types';
|
||||
|
||||
interface LandingProps {
|
||||
onGenerate: (options: { topic?: string; questionCount?: number; files?: File[]; useOcr?: boolean }) => void;
|
||||
onGenerate: (options: {
|
||||
topic?: string;
|
||||
questionCount?: number;
|
||||
files?: File[];
|
||||
useOcr?: boolean;
|
||||
aiProvider?: AIProvider;
|
||||
apiKey?: string;
|
||||
openRouterModel?: string;
|
||||
}) => void;
|
||||
onCreateManual: () => void;
|
||||
onLoadQuiz: (quiz: Quiz, quizId?: string) => void;
|
||||
onJoin: (pin: string, name: string) => void;
|
||||
|
|
@ -103,7 +113,10 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
|||
|
||||
const { defaultConfig, saving: savingConfig, saveDefaultConfig } = useUserConfig();
|
||||
const { preferences, hasAIAccess, saving: savingPrefs, savePreferences, applyColorScheme } = useUserPreferences();
|
||||
const canUseAI = auth.isAuthenticated && (hasAIAccess || preferences.geminiApiKey);
|
||||
const hasValidApiKey = preferences.aiProvider === 'openrouter'
|
||||
? !!preferences.openRouterApiKey
|
||||
: (hasAIAccess || !!preferences.geminiApiKey);
|
||||
const canUseAI = auth.isAuthenticated && hasValidApiKey;
|
||||
|
||||
const {
|
||||
quizzes,
|
||||
|
|
@ -205,11 +218,19 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
|||
const handleHostSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (canGenerate && !isLoading) {
|
||||
const aiProvider = preferences.aiProvider || 'gemini';
|
||||
const apiKey = aiProvider === 'openrouter'
|
||||
? preferences.openRouterApiKey
|
||||
: preferences.geminiApiKey;
|
||||
|
||||
onGenerate({
|
||||
topic: generateMode === 'topic' ? topic.trim() : undefined,
|
||||
questionCount,
|
||||
files: generateMode === 'document' ? selectedFiles : undefined,
|
||||
useOcr: generateMode === 'document' && showOcrOption ? useOcr : undefined
|
||||
useOcr: generateMode === 'document' && showOcrOption ? useOcr : undefined,
|
||||
aiProvider,
|
||||
apiKey,
|
||||
openRouterModel: aiProvider === 'openrouter' ? preferences.openRouterModel : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -597,9 +618,9 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
|||
<ApiKeyModal
|
||||
isOpen={accountSettingsOpen}
|
||||
onClose={() => setAccountSettingsOpen(false)}
|
||||
apiKey={preferences.geminiApiKey}
|
||||
onSave={async (key) => {
|
||||
await savePreferences({ ...preferences, geminiApiKey: key });
|
||||
preferences={preferences}
|
||||
onSave={async (prefs) => {
|
||||
await savePreferences({ ...preferences, ...prefs });
|
||||
}}
|
||||
saving={savingPrefs}
|
||||
hasAIAccess={hasAIAccess}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue