add api key settings and agent docs

This commit is contained in:
2026-05-09 17:34:24 +08:00
parent 2563ab8f70
commit 57287cbc67
5 changed files with 444 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { Upload, Image as ImageIcon, Wand2, Loader2, AlertCircle, X, Download, Lock, User, LogOut, Settings } from 'lucide-react';
import { Upload, Image as ImageIcon, Wand2, Loader2, AlertCircle, X, Download, Lock, User, LogOut, Settings, KeyRound } from 'lucide-react';
import { GoogleGenAI } from '@google/genai';
declare global {
@@ -14,6 +14,7 @@ declare global {
function useApiKey() {
const [hasKey, setHasKey] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [apiKey, setApiKey] = useState('');
useEffect(() => {
checkKey();
@@ -21,14 +22,20 @@ function useApiKey() {
const checkKey = async () => {
try {
const localApiKey = localStorage.getItem('geminiApiKey') || '';
const envApiKey = process.env.API_KEY || process.env.GEMINI_API_KEY || '';
const nextApiKey = localApiKey || envApiKey;
setApiKey(nextApiKey);
if (window.aistudio?.hasSelectedApiKey) {
const result = await window.aistudio.hasSelectedApiKey();
setHasKey(result);
setHasKey(result || Boolean(nextApiKey));
} else {
setHasKey(true);
setHasKey(Boolean(nextApiKey));
}
} catch (e) {
console.error(e);
setHasKey(Boolean(localStorage.getItem('geminiApiKey') || process.env.API_KEY || process.env.GEMINI_API_KEY));
} finally {
setIsLoading(false);
}
@@ -39,6 +46,8 @@ function useApiKey() {
if (window.aistudio?.openSelectKey) {
await window.aistudio.openSelectKey();
setHasKey(true);
} else {
checkKey();
}
} catch (e) {
console.error(e);
@@ -48,7 +57,18 @@ function useApiKey() {
}
};
return { hasKey, isLoading, selectKey };
const saveKey = (nextApiKey: string) => {
const trimmed = nextApiKey.trim();
if (trimmed) {
localStorage.setItem('geminiApiKey', trimmed);
} else {
localStorage.removeItem('geminiApiKey');
}
setApiKey(trimmed);
setHasKey(Boolean(trimmed || process.env.API_KEY || process.env.GEMINI_API_KEY));
};
return { hasKey, isLoading, apiKey, selectKey, saveKey };
}
function useAuth() {
@@ -229,10 +249,62 @@ function ChangePasswordModal({ onClose, onChange }: { onClose: () => void, onCha
);
}
function ApiKeyModal({ currentKey, onClose, onSave }: { currentKey: string, onClose: () => void, onSave: (apiKey: string) => void }) {
const [apiKey, setApiKey] = useState(currentKey);
const [saved, setSaved] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(apiKey);
setSaved(true);
setTimeout(onClose, 500);
};
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="max-w-lg w-full bg-zinc-900 border border-zinc-800 rounded-2xl p-8 space-y-6 shadow-2xl">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-white">Gemini API Key</h2>
<button onClick={onClose} className="text-zinc-500 hover:text-white transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-medium text-zinc-500 uppercase tracking-wider ml-1">API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl py-3 px-4 text-white focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all"
placeholder="AIza..."
/>
</div>
{saved && (
<div className="text-emerald-400 text-sm bg-emerald-400/10 p-3 rounded-xl border border-emerald-400/20">
Saved
</div>
)}
<button
type="submit"
className="w-full py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-bold transition-all"
>
Save API Key
</button>
</form>
</div>
</div>
);
}
export default function App() {
const { isLoggedIn, isLoading: isAuthLoading, login, logout, changePassword } = useAuth();
const { hasKey, isLoading: isKeyLoading, selectKey } = useApiKey();
const { hasKey, isLoading: isKeyLoading, apiKey, saveKey } = useApiKey();
const [showChangePassword, setShowChangePassword] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
if (isAuthLoading || isKeyLoading) {
return <div className="min-h-screen flex items-center justify-center bg-zinc-950 text-zinc-200"><Loader2 className="animate-spin" /></div>;
@@ -244,35 +316,45 @@ export default function App() {
if (!hasKey) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-950 text-zinc-200 p-4">
<div className="max-w-md w-full bg-zinc-900 border border-zinc-800 rounded-2xl p-8 text-center space-y-6">
<div className="w-16 h-16 bg-zinc-800 rounded-full flex items-center justify-center mx-auto">
<Wand2 className="w-8 h-8 text-emerald-400" />
<>
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-950 text-zinc-200 p-4">
<div className="max-w-md w-full bg-zinc-900 border border-zinc-800 rounded-2xl p-8 text-center space-y-6">
<div className="w-16 h-16 bg-zinc-800 rounded-full flex items-center justify-center mx-auto">
<Wand2 className="w-8 h-8 text-emerald-400" />
</div>
<h1 className="text-2xl font-semibold text-white">API Key Required</h1>
<p className="text-zinc-400">
To use the Gemini 3.1 Flash Image model, you need to set a Google AI Studio API key.
</p>
<button
onClick={() => setShowApiKey(true)}
className="w-full py-3 px-4 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-medium transition-colors"
>
Set API Key
</button>
<p className="text-xs text-zinc-500">
<a href="https://ai.google.dev/gemini-api/docs/billing" target="_blank" rel="noreferrer" className="underline hover:text-zinc-300">
Billing documentation
</a>
</p>
</div>
<h1 className="text-2xl font-semibold text-white">API Key Required</h1>
<p className="text-zinc-400">
To use the Gemini 3.1 Flash Image model, you need to select a paid Google Cloud API key.
</p>
<button
onClick={selectKey}
className="w-full py-3 px-4 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-medium transition-colors"
>
Select API Key
</button>
<p className="text-xs text-zinc-500">
<a href="https://ai.google.dev/gemini-api/docs/billing" target="_blank" rel="noreferrer" className="underline hover:text-zinc-300">
Billing documentation
</a>
</p>
</div>
</div>
{showApiKey && (
<ApiKeyModal
currentKey={apiKey}
onClose={() => setShowApiKey(false)}
onSave={saveKey}
/>
)}
</>
);
}
return (
<>
<ImageEditor
onSelectKey={selectKey}
apiKey={apiKey}
onOpenApiKey={() => setShowApiKey(true)}
onLogout={logout}
onChangePassword={() => setShowChangePassword(true)}
/>
@@ -282,11 +364,18 @@ export default function App() {
onChange={changePassword}
/>
)}
{showApiKey && (
<ApiKeyModal
currentKey={apiKey}
onClose={() => setShowApiKey(false)}
onSave={saveKey}
/>
)}
</>
);
}
function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey: () => Promise<void>, onLogout: () => void, onChangePassword: () => void }) {
function ImageEditor({ apiKey, onOpenApiKey, onLogout, onChangePassword }: { apiKey: string, onOpenApiKey: () => void, onLogout: () => void, onChangePassword: () => void }) {
const [image, setImage] = useState<string | null>(null);
const [mimeType, setMimeType] = useState<string>('');
const [prompt, setPrompt] = useState('');
@@ -347,7 +436,11 @@ function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey:
setError(null);
try {
const apiKey = process.env.API_KEY || process.env.GEMINI_API_KEY;
if (!apiKey) {
setError("Gemini API Key is missing. Please set it first.");
return;
}
const ai = new GoogleGenAI({ apiKey });
const parts: any[] = [];
@@ -423,10 +516,13 @@ function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey:
<LogOut className="w-5 h-5" />
</button>
<button
onClick={onSelectKey}
onClick={onOpenApiKey}
className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded-lg text-sm font-medium transition-colors border border-zinc-700"
>
Restart / Change API Key
<span className="inline-flex items-center gap-2">
<KeyRound className="w-4 h-4" />
API Key
</span>
</button>
</div>
</header>
@@ -545,10 +641,10 @@ function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey:
</div>
{(error.includes("API Key") || error.includes("permission") || error.includes("403")) && (
<button
onClick={onSelectKey}
onClick={onOpenApiKey}
className="self-start px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-300 rounded-lg text-sm font-medium transition-colors"
>
Reselect API Key
Set API Key
</button>
)}
</div>