easyai-ai-gateway/apps/web/src/pages/ApiDocsPage.tsx

224 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMemo, type FormEvent } from 'react';
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
import { BookOpen, Play, Search, Send } from 'lucide-react';
import { Badge, Button, Select, Textarea } from '../components/ui';
import type { ApiDocSection, LoadState, TaskForm } from '../types';
const docs: Array<{ key: ApiDocSection; group: string; method: string; path: string; title: string }> = [
{ key: 'chat', group: '聊天(Chat)', method: 'POST', path: '/v1/chat/completions', title: 'Chat(聊天)' },
{ key: 'imageGeneration', group: '图片', method: 'POST', path: '/v1/images/generations', title: '创建图片' },
{ key: 'imageEdit', group: '图片', method: 'POST', path: '/v1/images/edits', title: '编辑图片' },
{ key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估' },
{ key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件' },
];
const guideItems = ['获取 Base URL 和 API Key', '通知设置-WebHook 参数介绍', '错误码', '测试模式'];
const taskKindOptions = [
['chat.completions', 'Chat'],
['images.generations', '生图'],
['images.edits', '图像编辑'],
] as const;
export function ApiDocsPage(props: {
activeDocSection: ApiDocSection;
apiKeySecret: string;
canRun: boolean;
coreMessage: string;
coreState: LoadState;
taskForm: TaskForm;
taskResult: GatewayTask | null;
onLogin: () => void;
onDocSectionChange: (value: ApiDocSection) => void;
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: TaskForm) => void;
}) {
const current = docs.find((item) => item.key === props.activeDocSection) ?? docs[0];
const bodyExample = useMemo(() => requestBodyExample(props.taskForm), [props.taskForm]);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
if (!props.canRun) {
event.preventDefault();
props.onLogin();
return;
}
props.onSubmitTask(event);
}
return (
<div className="apiDocsShell">
<aside className="docsSidebar">
<div className="docsBrand">
<BookOpen size={19} />
<strong>API Docs</strong>
</div>
<label className="docsSearch">
<Search size={15} />
<input placeholder="搜索" />
</label>
<DocsGroup title="指南" items={guideItems.map((title) => ({ title }))} />
{groupDocs(docs).map((group) => (
<DocsGroup
key={group.title}
title={group.title}
items={group.items.map((item) => ({
active: item.key === props.activeDocSection,
method: item.method,
title: item.title,
onClick: () => props.onDocSectionChange(item.key),
}))}
/>
))}
</aside>
<main className="docsArticle">
<p className="eyebrow">{current.group}</p>
<h1>{current.title}</h1>
<div className="endpointBar">
<Badge variant="warning">{current.method}</Badge>
<code>{current.path}</code>
</div>
<p className="docsLead">
integration-platform / OpenAI API Key server-main token
</p>
<section className="paramCard">
<header>
<h2>Header </h2>
<Button type="button" variant="secondary" size="sm"></Button>
</header>
<ParamRow name="Content-Type" type="string" required value="application/json" />
<ParamRow name="Accept" type="string" required value="application/json" />
<ParamRow name="Authorization" type="string" value="Bearer {{YOUR_API_KEY}}" />
</section>
<section className="paramCard">
<header>
<h2>Body </h2>
<Badge variant="outline">application/json</Badge>
</header>
<ParamRow name="model" type="string" required value="模型 ID 或别名" />
<ParamRow name="messages / prompt" type="array|string" required value="对话消息或图片提示词" />
<ParamRow name="simulation" type="boolean" value="测试模式开关" />
<ParamRow name="stream" type="boolean" value="对话进度流式返回" />
</section>
</main>
<aside className="docsRunner">
<form onSubmit={handleSubmit}>
<header>
<strong>线</strong>
<Button type="submit" size="sm" disabled={props.canRun && props.coreState === 'loading'}>
<Send size={14} />
{props.canRun ? '发送' : '登录'}
</Button>
</header>
<div className="runnerEndpoint">
<Badge variant="warning">POST</Badge>
<span>{current.path}</span>
</div>
<label className="shLabel">
<Select value={props.taskForm.kind} onChange={(event) => props.onTaskFormChange(defaultTaskForKind(event.target.value as TaskForm['kind'], props.taskForm))}>
{taskKindOptions.map(([value, label]) => (
<option value={value} key={value}>{label}</option>
))}
</Select>
</label>
<label className="shLabel">
Body
<Textarea value={bodyExample} onChange={(event) => props.onTaskFormChange(parseBody(event.target.value, props.taskForm))} />
</label>
<Button type="submit" disabled={props.canRun && props.coreState === 'loading'}>
<Play size={15} />
{!props.canRun ? '登录后运行' : props.coreState === 'loading' ? '运行中' : '运行测试'}
</Button>
</form>
<section className="runnerResult">
<strong></strong>
{props.taskResult ? (
<pre>{JSON.stringify(props.taskResult.result ?? props.taskResult, null, 2)}</pre>
) : (
<div className="emptyState">
<span></span>
</div>
)}
{props.coreMessage && <p>{props.coreMessage}</p>}
</section>
</aside>
</div>
);
}
function DocsGroup(props: {
items: Array<{ active?: boolean; method?: string; onClick?: () => void; title: string }>;
title: string;
}) {
return (
<section className="docsGroup">
<h3>{props.title}</h3>
{props.items.map((item) => (
<button type="button" data-active={item.active} key={item.title} onClick={item.onClick}>
<span>{item.title}</span>
{item.method && <Badge variant="warning">{item.method}</Badge>}
</button>
))}
</section>
);
}
function ParamRow(props: { name: string; required?: boolean; type: string; value: string }) {
return (
<div className="paramRow">
<code>{props.name}</code>
<span>{props.type}</span>
<p>{props.value}</p>
<em>{props.required ? '必需' : '可选'}</em>
</div>
);
}
function groupDocs(items: typeof docs) {
const groups = new Map<string, typeof docs>();
for (const item of items) {
groups.set(item.group, [...(groups.get(item.group) ?? []), item]);
}
return Array.from(groups, ([title, groupItems]) => ({ title, items: groupItems }));
}
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
if (kind === 'images.edits') return { ...current, kind, image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png', model: 'gpt-image-1' };
return { ...current, kind, model: 'gpt-image-1' };
}
function requestBodyExample(task: TaskForm) {
const body = task.kind === 'chat.completions'
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], simulation: true, stream: true }
: task.kind === 'images.edits'
? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask, simulation: true }
: { model: task.model, prompt: task.prompt, quality: 'medium', simulation: true, size: '1024x1024' };
return JSON.stringify(body, null, 2);
}
function parseBody(value: string, current: TaskForm): TaskForm {
try {
const body = JSON.parse(value) as {
image?: string;
mask?: string;
messages?: Array<{ content?: string }>;
model?: string;
prompt?: string;
};
return {
...current,
image: body.image ?? current.image,
mask: body.mask ?? current.mask,
model: body.model ?? current.model,
prompt: body.prompt ?? body.messages?.[0]?.content ?? current.prompt,
};
} catch {
return current;
}
}