feat(web): add reusable admin form dialog

This commit is contained in:
2026-05-09 20:15:35 +08:00
parent c0335bd5d0
commit a5e66e79cd
49 changed files with 6013 additions and 1108 deletions
+171
View File
@@ -0,0 +1,171 @@
import type { FormEvent } from 'react';
import type { GatewayApiKey, GatewayTask } from '@easyai-ai-gateway/contracts';
import type { LoadState, TaskForm } from '../types';
const taskKindOptions = [
['chat.completions', 'Chat'],
['images.generations', '生图'],
['images.edits', '图像编辑'],
] as const;
export function CoreFlowPanel(props: {
apiKeyForm: { name: string };
apiKeys: GatewayApiKey[];
apiKeySecret: string;
coreMessage: string;
coreState: LoadState;
platformForm: { provider: string; platformKey: string; name: string; baseUrl: string };
taskForm: TaskForm;
taskResult: GatewayTask | null;
onAPIKeyFormChange: (value: { name: string }) => void;
onPlatformFormChange: (value: { provider: string; platformKey: string; name: string; baseUrl: string }) => void;
onSubmitAPIKey: (event: FormEvent<HTMLFormElement>) => void;
onSubmitPlatform: (event: FormEvent<HTMLFormElement>) => void;
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: TaskForm) => void;
}) {
return (
<section className="corePanel" aria-label="核心链路验证">
<div className="sectionHeader">
<div>
<p className="eyebrow">Smoke Flow</p>
<h2></h2>
</div>
<span>{props.coreState === 'loading' ? '运行中' : '本地闭环'}</span>
</div>
<div className="coreGrid">
<ApiKeyForm {...props} />
<PlatformForm {...props} />
<TaskSmokeForm {...props} />
</div>
{props.coreMessage && (
<p className="coreMessage" data-error={props.coreState === 'error'}>
{props.coreMessage}
</p>
)}
</section>
);
}
function ApiKeyForm(props: {
apiKeyForm: { name: string };
apiKeys: GatewayApiKey[];
apiKeySecret: string;
coreState: LoadState;
onAPIKeyFormChange: (value: { name: string }) => void;
onSubmitAPIKey: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="inlineForm" onSubmit={props.onSubmitAPIKey}>
<h3>1. API Key</h3>
<label>
<span></span>
<input value={props.apiKeyForm.name} onChange={(event) => props.onAPIKeyFormChange({ name: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
API Key
</button>
<p className="formHint"> {props.apiKeys.length} Key</p>
{props.apiKeySecret && <code className="secretBox">{props.apiKeySecret}</code>}
</form>
);
}
function PlatformForm(props: {
coreState: LoadState;
platformForm: { provider: string; platformKey: string; name: string; baseUrl: string };
onPlatformFormChange: (value: { provider: string; platformKey: string; name: string; baseUrl: string }) => void;
onSubmitPlatform: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="inlineForm" onSubmit={props.onSubmitPlatform}>
<h3>2. </h3>
<label>
<span>Provider</span>
<input value={props.platformForm.provider} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, provider: event.target.value })} />
</label>
<label>
<span> Key</span>
<input value={props.platformForm.platformKey} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, platformKey: event.target.value })} />
</label>
<label>
<span></span>
<input value={props.platformForm.name} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, name: event.target.value })} />
</label>
<label>
<span>Base URL</span>
<input value={props.platformForm.baseUrl} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, baseUrl: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
</button>
</form>
);
}
function TaskSmokeForm(props: {
coreState: LoadState;
taskForm: TaskForm;
taskResult: GatewayTask | null;
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: TaskForm) => void;
}) {
return (
<form className="inlineForm" onSubmit={props.onSubmitTask}>
<h3>3. Phase 1 </h3>
<label>
<span></span>
<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>
<span></span>
<input value={props.taskForm.model} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, model: event.target.value })} />
</label>
<label>
<span>Prompt</span>
<input value={props.taskForm.prompt} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, prompt: event.target.value })} />
</label>
{props.taskForm.kind === 'images.edits' && (
<>
<label>
<span> URL</span>
<input value={props.taskForm.image ?? ''} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, image: event.target.value })} />
</label>
<label>
<span>Mask URL</span>
<input value={props.taskForm.mask ?? ''} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, mask: event.target.value })} />
</label>
</>
)}
<button type="submit" disabled={props.coreState === 'loading'}>
</button>
{props.taskResult && (
<div className="resultBox">
<div>
<span className="statusPill">{props.taskResult.status}</span>
<strong>{props.taskResult.model}</strong>
</div>
<pre>{JSON.stringify(props.taskResult.result ?? {}, null, 2)}</pre>
</div>
)}
</form>
);
}
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, model: 'gpt-image-1', image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png' };
}
return { ...current, kind, model: 'gpt-image-1' };
}