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

@@ -18,6 +18,9 @@ API_PORT="3002"
# When set, send Authorization: Bearer <token> or x-api-key: <token>. # When set, send Authorization: Bearer <token> or x-api-key: <token>.
API_AUTH_TOKEN="" API_AUTH_TOKEN=""
# API callers can also send a temporary Gemini key per request with:
# x-gemini-api-key: <Gemini API Key>
# Gemini model defaults used by the API service. # Gemini model defaults used by the API service.
GEMINI_IMAGE_MODEL="gemini-3.1-flash-image-preview" GEMINI_IMAGE_MODEL="gemini-3.1-flash-image-preview"
GEMINI_TEXT_MODEL="gemini-2.5-flash" GEMINI_TEXT_MODEL="gemini-2.5-flash"

208
API图片修改-Agent.md Normal file
View File

@@ -0,0 +1,208 @@
# API 图片修改 Agent 调用说明
本服务提供一个本地 HTTP API用于让其他 Agent、脚本或自动化系统上传已有图片/文档,并按文字指令生成或修改图片。
默认地址:
- 本机:`http://localhost:3002`
- 局域网:`http://192.168.31.204:3002`
## 认证与 API Key
Gemini API Key 有三种传入方式,按优先级从高到低:
1. 单次请求 Header`x-gemini-api-key: YOUR_GEMINI_API_KEY`
2. 单次请求 JSON/Form 字段:`apiKey=YOUR_GEMINI_API_KEY`
3. 服务端环境变量:`.env.local` 中的 `GEMINI_API_KEY`
运行中更新服务端 Key
```bash
curl -X POST http://localhost:3002/api/config/api-key \
-H "Content-Type: application/json" \
-d "{\"apiKey\":\"YOUR_GEMINI_API_KEY\",\"persist\":true}"
```
`persist:true` 会写入本项目的 `.env.local`,该文件不会提交到 git。
如果服务设置了 `API_AUTH_TOKEN`,还需要在每次请求中加入:
```txt
Authorization: Bearer YOUR_API_AUTH_TOKEN
```
或:
```txt
x-api-key: YOUR_API_AUTH_TOKEN
```
## 健康检查
```bash
curl http://localhost:3002/api/health
```
返回示例:
```json
{
"ok": true,
"apiPort": 3002,
"hasGeminiApiKey": true,
"authEnabled": false,
"acceptsPerRequestApiKey": true
}
```
## 修改已有图片
使用 `multipart/form-data` 上传图片,字段名可以叫 `files``image` 或任意名称;服务会读取所有上传文件。
```bash
curl -X POST http://localhost:3002/api/edit-image \
-H "x-gemini-api-key: YOUR_GEMINI_API_KEY" \
-F "prompt=保留主体不变,把背景改成干净的白色摄影棚,增强产品质感" \
-F "imageSize=1K" \
-F "aspectRatio=1:1" \
-F "files=@input.png"
```
支持的图片常见格式:
- `image/png`
- `image/jpeg`
- `image/webp`
可选参数:
- `prompt``instruction`:修改指令,必填
- `imageSize``1K``2K``4K`,默认 `1K`
- `aspectRatio``1:1``4:3``3:4``16:9``9:16`,默认 `1:1`
- `model`:默认 `gemini-3.1-flash-image-preview`
- `apiKey`:单次调用使用的 Gemini API Key
成功响应示例:
```json
{
"ok": true,
"model": "gemini-3.1-flash-image-preview",
"images": [
{
"mimeType": "image/png",
"data": "BASE64_IMAGE_DATA",
"dataUrl": "data:image/png;base64,BASE64_IMAGE_DATA"
}
],
"texts": []
}
```
调用方可以直接使用 `images[0].dataUrl` 预览图片,或把 `images[0].data` 解码保存为 PNG 文件。
## JSON/Base64 方式修改图片
如果 Agent 已经拿到了图片 base64可以不用 multipart
```bash
curl -X POST http://localhost:3002/api/generate \
-H "Content-Type: application/json" \
-H "x-gemini-api-key: YOUR_GEMINI_API_KEY" \
-d "{
\"prompt\":\"把这张图改成赛博朋克夜景风格,但保留人物脸部特征\",
\"imageSize\":\"1K\",
\"aspectRatio\":\"1:1\",
\"images\":[
{
\"mimeType\":\"image/png\",
\"base64\":\"BASE64_IMAGE_DATA\"
}
]
}"
```
也可以传 `dataUrl`
```json
{
"prompt": "把背景改成浅灰色",
"images": [
{
"dataUrl": "data:image/png;base64,BASE64_IMAGE_DATA"
}
]
}
```
或传公开可访问 URL
```json
{
"prompt": "生成一张更适合电商首图的版本",
"images": [
{
"url": "https://example.com/input.png"
}
]
}
```
## 上传文档分析
用于 PDF、Word 转出的文本文件、图片文档等分析任务。这个接口默认使用文本模型。
```bash
curl -X POST http://localhost:3002/api/analyze-document \
-H "x-gemini-api-key: YOUR_GEMINI_API_KEY" \
-F "prompt=用中文总结这份文档,提取关键结论和待办事项" \
-F "files=@report.pdf"
```
响应中的 `texts` 是模型返回的文字结果:
```json
{
"ok": true,
"model": "gemini-2.5-flash",
"texts": ["文档总结内容..."],
"images": []
}
```
## 多文件输入
一个请求可以同时上传多张图片或多个文档:
```bash
curl -X POST http://localhost:3002/api/edit-image \
-H "x-gemini-api-key: YOUR_GEMINI_API_KEY" \
-F "prompt=参考第二张图的色调,修改第一张图" \
-F "files=@source.png" \
-F "files=@style-reference.jpg"
```
## 错误格式
失败时统一返回:
```json
{
"ok": false,
"error": "错误原因"
}
```
常见错误:
- `prompt or instruction is required.`:缺少修改指令
- `Gemini API key is required.`:没有配置或传入 Gemini API Key
- `Remote file is too large.`URL 文件超过大小限制
## Agent 调用建议
1. 优先使用 `POST /api/edit-image` + multipart 上传真实图片文件。
2. 如果图片已经是 base64使用 `POST /api/generate``images[].base64`
3. 每次调用都带 `x-gemini-api-key`,可避免依赖服务端环境。
4. 取返回的 `images[0].dataUrl` 给用户预览;保存文件时解码 `images[0].data`
5. 修改指令尽量包含保留内容、需要改变的内容、输出用途和画幅比例。

View File

@@ -29,6 +29,23 @@ The UI runs on port `3000`. The HTTP API runs separately on port `3002`.
3. Check the API: 3. Check the API:
`http://localhost:3002/api/health` `http://localhost:3002/api/health`
You can change the server API key without restarting:
```bash
curl -X POST http://localhost:3002/api/config/api-key \
-H "Content-Type: application/json" \
-d "{\"apiKey\":\"YOUR_GEMINI_API_KEY\",\"persist\":true}"
```
You can also pass a temporary Gemini key for one call:
```bash
curl -X POST http://localhost:3002/api/generate \
-H "Content-Type: application/json" \
-H "x-gemini-api-key: YOUR_GEMINI_API_KEY" \
-d "{\"prompt\":\"Create a clean product poster\"}"
```
### API examples ### API examples
Generate or edit with JSON/base64: Generate or edit with JSON/base64:
@@ -58,3 +75,5 @@ curl -X POST http://localhost:3002/api/analyze-document \
``` ```
Optional API auth: set `API_AUTH_TOKEN`, then send either `Authorization: Bearer <token>` or `x-api-key: <token>`. Optional API auth: set `API_AUTH_TOKEN`, then send either `Authorization: Bearer <token>` or `x-api-key: <token>`.
For Agent-facing image editing instructions, see `API图片修改-Agent.md`.

View File

@@ -1,8 +1,13 @@
import 'dotenv/config';
import express from 'express'; import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import dotenv from 'dotenv';
import multer from 'multer'; import multer from 'multer';
import { GoogleGenAI } from '@google/genai'; import { GoogleGenAI } from '@google/genai';
dotenv.config({ path: '.env.local' });
dotenv.config();
type InlineInput = { type InlineInput = {
name?: string; name?: string;
mimeType?: string; mimeType?: string;
@@ -13,6 +18,7 @@ type InlineInput = {
}; };
type GenerateRequest = { type GenerateRequest = {
apiKey?: string;
prompt?: string; prompt?: string;
instruction?: string; instruction?: string;
model?: string; model?: string;
@@ -40,22 +46,20 @@ const upload = multer({
}, },
}); });
const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY; let runtimeApiKey = process.env.GEMINI_API_KEY || process.env.API_KEY || '';
const apiPort = Number(process.env.API_PORT || 3002); const apiPort = Number(process.env.API_PORT || 3002);
const defaultImageModel = process.env.GEMINI_IMAGE_MODEL || 'gemini-3.1-flash-image-preview'; const defaultImageModel = process.env.GEMINI_IMAGE_MODEL || 'gemini-3.1-flash-image-preview';
const defaultTextModel = process.env.GEMINI_TEXT_MODEL || 'gemini-2.5-flash'; const defaultTextModel = process.env.GEMINI_TEXT_MODEL || 'gemini-2.5-flash';
const apiAuthToken = process.env.API_AUTH_TOKEN || ''; const apiAuthToken = process.env.API_AUTH_TOKEN || '';
if (!apiKey) { if (!runtimeApiKey) {
console.warn('GEMINI_API_KEY/API_KEY is not set. API calls will fail until it is configured.'); console.warn('GEMINI_API_KEY/API_KEY is not set. API calls will fail until it is configured.');
} }
const ai = new GoogleGenAI({ apiKey });
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', process.env.API_CORS_ORIGIN || '*'); res.setHeader('Access-Control-Allow-Origin', process.env.API_CORS_ORIGIN || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,x-api-key'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,x-api-key,x-gemini-api-key');
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
res.status(204).end(); res.status(204).end();
return; return;
@@ -86,6 +90,29 @@ function requireAuth(req: express.Request, res: express.Response, next: express.
}); });
} }
function getRequestApiKey(req: express.Request, body?: GenerateRequest) {
const headerKey = req.header('x-gemini-api-key') || '';
return body?.apiKey || headerKey || runtimeApiKey;
}
function persistApiKeyToEnvLocal(apiKey: string) {
const envPath = path.resolve(process.cwd(), '.env.local');
let content = '';
if (fs.existsSync(envPath)) {
content = fs.readFileSync(envPath, 'utf8');
}
const escaped = apiKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const line = `GEMINI_API_KEY="${escaped}"`;
if (/^GEMINI_API_KEY=.*$/m.test(content)) {
content = content.replace(/^GEMINI_API_KEY=.*$/m, line);
} else {
content = content ? `${content.trimEnd()}\n${line}\n` : `${line}\n`;
}
fs.writeFileSync(envPath, content, 'utf8');
}
function stripDataPrefix(value: string) { function stripDataPrefix(value: string) {
const match = value.match(/^data:([^;]+);base64,(.+)$/); const match = value.match(/^data:([^;]+);base64,(.+)$/);
if (!match) { if (!match) {
@@ -194,6 +221,7 @@ async function buildParts(body: GenerateRequest, files: Express.Multer.File[] =
function normalizeMultipartBody(req: express.Request): GenerateRequest { function normalizeMultipartBody(req: express.Request): GenerateRequest {
return { return {
apiKey: req.body.apiKey,
prompt: String(req.body.prompt || req.body.instruction || ''), prompt: String(req.body.prompt || req.body.instruction || ''),
model: req.body.model, model: req.body.model,
imageSize: req.body.imageSize, imageSize: req.body.imageSize,
@@ -224,8 +252,14 @@ function collectResponseParts(response: any) {
return { images, texts }; return { images, texts };
} }
async function runGemini(body: GenerateRequest, files: Express.Multer.File[] = [], textOnly = false) { async function runGemini(req: express.Request, body: GenerateRequest, files: Express.Multer.File[] = [], textOnly = false) {
const parts = await buildParts(body, files); const parts = await buildParts(body, files);
const requestApiKey = getRequestApiKey(req, body);
if (!requestApiKey) {
throw new Error('Gemini API key is required. Set GEMINI_API_KEY, call POST /api/config/api-key, or send x-gemini-api-key.');
}
const ai = new GoogleGenAI({ apiKey: requestApiKey });
const model = body.model || (textOnly ? defaultTextModel : defaultImageModel); const model = body.model || (textOnly ? defaultTextModel : defaultImageModel);
const config: Record<string, unknown> = {}; const config: Record<string, unknown> = {};
@@ -247,6 +281,7 @@ async function runGemini(body: GenerateRequest, files: Express.Multer.File[] = [
return { return {
ok: true, ok: true,
model, model,
usedRequestApiKey: Boolean(body.apiKey || req.header('x-gemini-api-key')),
input: { input: {
fileCount: files.length + (body.images?.length || 0) + (body.documents?.length || 0) + (body.files?.length || 0), fileCount: files.length + (body.images?.length || 0) + (body.documents?.length || 0) + (body.files?.length || 0),
partCount: parts.length, partCount: parts.length,
@@ -273,6 +308,8 @@ app.get('/api', (_req, res) => {
name: 'Gemini Draw API', name: 'Gemini Draw API',
endpoints: [ endpoints: [
'GET /api/health', 'GET /api/health',
'GET /api/config',
'POST /api/config/api-key',
'POST /api/generate', 'POST /api/generate',
'POST /api/generate/upload', 'POST /api/generate/upload',
'POST /api/edit-image', 'POST /api/edit-image',
@@ -285,16 +322,53 @@ app.get('/api/health', (_req, res) => {
res.json({ res.json({
ok: true, ok: true,
apiPort, apiPort,
hasGeminiApiKey: Boolean(apiKey), hasGeminiApiKey: Boolean(runtimeApiKey),
authEnabled: Boolean(apiAuthToken),
acceptsPerRequestApiKey: true,
defaultImageModel,
defaultTextModel,
});
});
app.get('/api/config', requireAuth, (_req, res) => {
res.json({
ok: true,
apiPort,
hasGeminiApiKey: Boolean(runtimeApiKey),
apiKeyPreview: runtimeApiKey ? `${runtimeApiKey.slice(0, 6)}...${runtimeApiKey.slice(-4)}` : '',
authEnabled: Boolean(apiAuthToken), authEnabled: Boolean(apiAuthToken),
defaultImageModel, defaultImageModel,
defaultTextModel, defaultTextModel,
}); });
}); });
app.post('/api/config/api-key', requireAuth, async (req, res) => {
try {
const nextApiKey = String(req.body.apiKey || '').trim();
const persist = Boolean(req.body.persist);
if (!nextApiKey) {
throw new Error('apiKey is required.');
}
runtimeApiKey = nextApiKey;
process.env.GEMINI_API_KEY = nextApiKey;
if (persist) {
persistApiKeyToEnvLocal(nextApiKey);
}
res.json({
ok: true,
persisted: persist,
apiKeyPreview: `${runtimeApiKey.slice(0, 6)}...${runtimeApiKey.slice(-4)}`,
});
} catch (error) {
handleError(res, error);
}
});
app.post('/api/generate', requireAuth, async (req, res) => { app.post('/api/generate', requireAuth, async (req, res) => {
try { try {
res.json(await runGemini(req.body)); res.json(await runGemini(req, req.body));
} catch (error) { } catch (error) {
handleError(res, error); handleError(res, error);
} }
@@ -302,7 +376,7 @@ app.post('/api/generate', requireAuth, async (req, res) => {
app.post('/api/generate/upload', requireAuth, upload.any(), async (req, res) => { app.post('/api/generate/upload', requireAuth, upload.any(), async (req, res) => {
try { try {
res.json(await runGemini(normalizeMultipartBody(req), (req.files as Express.Multer.File[]) || [])); res.json(await runGemini(req, normalizeMultipartBody(req), (req.files as Express.Multer.File[]) || []));
} catch (error) { } catch (error) {
handleError(res, error); handleError(res, error);
} }
@@ -312,7 +386,7 @@ app.post('/api/edit-image', requireAuth, upload.any(), async (req, res) => {
try { try {
const files = (req.files as Express.Multer.File[]) || []; const files = (req.files as Express.Multer.File[]) || [];
const body = req.is('multipart/form-data') ? normalizeMultipartBody(req) : req.body; const body = req.is('multipart/form-data') ? normalizeMultipartBody(req) : req.body;
res.json(await runGemini(body, files)); res.json(await runGemini(req, body, files));
} catch (error) { } catch (error) {
handleError(res, error); handleError(res, error);
} }
@@ -322,7 +396,7 @@ app.post('/api/analyze-document', requireAuth, upload.any(), async (req, res) =>
try { try {
const files = (req.files as Express.Multer.File[]) || []; const files = (req.files as Express.Multer.File[]) || [];
const body = req.is('multipart/form-data') ? normalizeMultipartBody(req) : req.body; const body = req.is('multipart/form-data') ? normalizeMultipartBody(req) : req.body;
res.json(await runGemini(body, files, true)); res.json(await runGemini(req, body, files, true));
} catch (error) { } catch (error) {
handleError(res, error); handleError(res, error);
} }

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; 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'; import { GoogleGenAI } from '@google/genai';
declare global { declare global {
@@ -14,6 +14,7 @@ declare global {
function useApiKey() { function useApiKey() {
const [hasKey, setHasKey] = useState(false); const [hasKey, setHasKey] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [apiKey, setApiKey] = useState('');
useEffect(() => { useEffect(() => {
checkKey(); checkKey();
@@ -21,14 +22,20 @@ function useApiKey() {
const checkKey = async () => { const checkKey = async () => {
try { 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) { if (window.aistudio?.hasSelectedApiKey) {
const result = await window.aistudio.hasSelectedApiKey(); const result = await window.aistudio.hasSelectedApiKey();
setHasKey(result); setHasKey(result || Boolean(nextApiKey));
} else { } else {
setHasKey(true); setHasKey(Boolean(nextApiKey));
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setHasKey(Boolean(localStorage.getItem('geminiApiKey') || process.env.API_KEY || process.env.GEMINI_API_KEY));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -39,6 +46,8 @@ function useApiKey() {
if (window.aistudio?.openSelectKey) { if (window.aistudio?.openSelectKey) {
await window.aistudio.openSelectKey(); await window.aistudio.openSelectKey();
setHasKey(true); setHasKey(true);
} else {
checkKey();
} }
} catch (e) { } catch (e) {
console.error(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() { 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() { export default function App() {
const { isLoggedIn, isLoading: isAuthLoading, login, logout, changePassword } = useAuth(); 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 [showChangePassword, setShowChangePassword] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
if (isAuthLoading || isKeyLoading) { 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>; 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) { if (!hasKey) {
return ( 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="min-h-screen flex flex-col items-center justify-center bg-zinc-950 text-zinc-200 p-4">
<div className="w-16 h-16 bg-zinc-800 rounded-full flex items-center justify-center mx-auto"> <div className="max-w-md w-full bg-zinc-900 border border-zinc-800 rounded-2xl p-8 text-center space-y-6">
<Wand2 className="w-8 h-8 text-emerald-400" /> <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> </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>
</div> {showApiKey && (
<ApiKeyModal
currentKey={apiKey}
onClose={() => setShowApiKey(false)}
onSave={saveKey}
/>
)}
</>
); );
} }
return ( return (
<> <>
<ImageEditor <ImageEditor
onSelectKey={selectKey} apiKey={apiKey}
onOpenApiKey={() => setShowApiKey(true)}
onLogout={logout} onLogout={logout}
onChangePassword={() => setShowChangePassword(true)} onChangePassword={() => setShowChangePassword(true)}
/> />
@@ -282,11 +364,18 @@ export default function App() {
onChange={changePassword} 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 [image, setImage] = useState<string | null>(null);
const [mimeType, setMimeType] = useState<string>(''); const [mimeType, setMimeType] = useState<string>('');
const [prompt, setPrompt] = useState(''); const [prompt, setPrompt] = useState('');
@@ -347,7 +436,11 @@ function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey:
setError(null); setError(null);
try { 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 ai = new GoogleGenAI({ apiKey });
const parts: any[] = []; const parts: any[] = [];
@@ -423,10 +516,13 @@ function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey:
<LogOut className="w-5 h-5" /> <LogOut className="w-5 h-5" />
</button> </button>
<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" 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> </button>
</div> </div>
</header> </header>
@@ -545,10 +641,10 @@ function ImageEditor({ onSelectKey, onLogout, onChangePassword }: { onSelectKey:
</div> </div>
{(error.includes("API Key") || error.includes("permission") || error.includes("403")) && ( {(error.includes("API Key") || error.includes("permission") || error.includes("403")) && (
<button <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" 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> </button>
)} )}
</div> </div>