feat: update Makelore modules and conversations
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-07-31 10:08:41 +08:00
parent b8ca3f8eea
commit 80e8386fa6
175 changed files with 8036 additions and 10581 deletions

View File

@@ -8,10 +8,13 @@ import {
Plus,
RefreshCw,
Send,
Settings2,
Sparkles,
X,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { IMAGE_WORKSPACE_CREATE_PROJECT_EVENT } from '@/lib/image-workspace';
import { cn } from '@/lib/utils';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
@@ -65,22 +68,47 @@ function OptionSelect({
}) {
if (options.length === 0) return null;
return (
<label className="grid min-w-[116px] gap-1 text-[11px] font-black text-[#68788B]">
{label}
<select
<label className="grid min-w-0 gap-1.5 text-[11px] font-semibold text-muted-foreground">
<span>{label}</span>
<Select
aria-label={label}
value={value ?? ''}
onChange={(event) => onChange(event.target.value)}
className="h-9 rounded-md border-2 border-[#26384D] bg-white px-2 text-xs font-black text-[#26384D] outline-none focus:ring-2 focus:ring-[#DCE6EF]"
className="h-9 rounded-lg border-border/70 bg-surface-input px-2 text-xs font-medium text-foreground"
>
{options.map((option) => (
<option key={option.id} value={option.id}>{option.label}</option>
))}
</select>
</Select>
</label>
);
}
function MessageStatus({ message }: { message: ImageWorkspaceMessage }) {
const label = messageStatusLabel(message);
if (!label) return null;
return (
<span
role="status"
className={cn(
'inline-flex items-center gap-1.5 rounded-md bg-surface-tertiary px-2 py-0.5 text-[10px] font-semibold',
message.status === 'failed' ? 'text-destructive' : 'text-muted-foreground',
)}
>
<span
aria-hidden="true"
className={cn(
'h-1.5 w-1.5 rounded-full',
message.status === 'failed' ? 'bg-destructive' : 'bg-brand',
(message.status === 'queued' || message.status === 'running') && 'animate-pulse',
)}
/>
{label}
</span>
);
}
export function ImageCanvas() {
const status = useImageWorkspaceStore((state) => state.status);
const snapshot = useImageWorkspaceStore((state) => state.snapshot);
@@ -254,10 +282,10 @@ export function ImageCanvas() {
if (status === 'idle' || status === 'loading') {
return (
<div data-testid="image-canvas-page" className="-m-6 flex min-h-full items-center justify-center bg-[#FFFFFF] p-6 text-[#26384D]">
<div role="status" className="flex items-center gap-3 rounded-lg border-2 border-[#26384D] bg-white px-5 py-4 text-sm font-black shadow-[4px_4px_0_#26384D]">
<Loader2 className="h-5 w-5 animate-spin" />
<div data-testid="image-canvas-page" className="-m-5 flex min-h-full items-center justify-center bg-background p-6 text-foreground sm:-m-6">
<div role="status" className="surface-card flex items-center gap-3 rounded-2xl border border-border/70 bg-background px-5 py-4 text-sm font-semibold shadow-float">
<Loader2 className="h-5 w-5 animate-spin text-brand" />
<span></span>
</div>
</div>
);
@@ -267,21 +295,24 @@ export function ImageCanvas() {
return (
<div
data-testid="image-canvas-page"
className="-m-6 flex min-h-full items-center justify-center bg-[#FFFFFF] p-6 text-[#26384D]"
className="-m-5 flex min-h-full items-center justify-center bg-background p-6 text-foreground sm:-m-6"
>
<section
data-testid="image-workspace-unavailable"
className="w-full max-w-xl rounded-lg border-4 border-[#26384D] bg-[#F6F8FA] p-8 text-center shadow-[8px_8px_0_#26384D]"
className="surface-card w-full max-w-md rounded-2xl border border-border/70 bg-background p-8 text-center shadow-float"
>
<Cloud className="mx-auto h-10 w-10" />
<h1 className="mt-4 text-2xl font-black"></h1>
<p className="mt-3 text-sm font-bold leading-6 text-[#68788B]">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-surface-tertiary text-foreground">
<Cloud className="h-6 w-6" />
</div>
<h1 className="mt-5 text-2xl font-semibold tracking-[-0.025em]"></h1>
<p className="mx-auto mt-3 max-w-sm text-sm font-medium leading-6 text-muted-foreground">
</p>
{workspaceError ? <p role="alert" className="mt-3 text-xs font-bold text-[#8b3a3a]">{workspaceError}</p> : null}
{workspaceError ? <p role="alert" className="mt-3 rounded-xl bg-destructive/10 px-3 py-2 text-xs font-medium text-destructive">{workspaceError}</p> : null}
<Button
type="button"
className="mt-5 border-2 border-[#26384D] bg-[#DCE6EF] font-black text-[#26384D] shadow-[3px_3px_0_#26384D]"
variant="outline"
className="mt-5 border-brand/20 bg-brand-soft font-semibold text-foreground hover:bg-brand-soft/70"
onClick={() => void load()}
>
<RefreshCw className="mr-2 h-4 w-4" />
@@ -294,16 +325,18 @@ export function ImageCanvas() {
if (!activeProject) {
return (
<div data-testid="image-canvas-page" className="-m-6 flex min-h-full items-center justify-center bg-[#FFFFFF] p-6 text-[#26384D]">
<section className="w-full max-w-xl rounded-lg border-4 border-[#26384D] bg-[#F6F8FA] p-8 text-center shadow-[8px_8px_0_#26384D]">
<ImageIcon className="mx-auto h-10 w-10" />
<h1 className="mt-4 text-2xl font-black"></h1>
<p className="mt-3 text-sm font-bold leading-6 text-[#68788B]">
Agent
<div data-testid="image-canvas-page" className="-m-5 flex min-h-full items-center justify-center bg-background p-6 text-foreground sm:-m-6">
<section className="surface-card w-full max-w-md rounded-2xl border border-border/70 bg-background p-8 text-center shadow-float">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-brand-soft text-brand">
<ImageIcon className="h-6 w-6" />
</div>
<h1 className="mt-5 text-2xl font-semibold tracking-[-0.025em]"></h1>
<p className="mx-auto mt-3 max-w-sm text-sm font-medium leading-6 text-muted-foreground">
Agent
</p>
<Button
type="button"
className="mt-5 border-2 border-[#26384D] bg-[#DCE6EF] font-black text-[#26384D] shadow-[3px_3px_0_#26384D]"
className="mt-5 border border-brand/20 bg-brand font-semibold text-primary-foreground"
onClick={openCreateProject}
>
<Plus className="mr-2 h-4 w-4" />
@@ -323,96 +356,144 @@ export function ImageCanvas() {
return (
<div
data-testid="image-canvas-page"
className="-m-6 min-h-full bg-[#FFFFFF] p-4 text-[#26384D]"
className="-m-5 flex min-h-full min-w-0 flex-col overflow-hidden bg-background text-foreground sm:-m-6"
>
<div
data-testid="image-workspace-page"
className="mx-auto flex min-h-[calc(100vh-5rem)] max-w-6xl flex-col overflow-hidden rounded-lg border-4 border-[#26384D] bg-[#F6F8FA] shadow-[8px_8px_0_#26384D]"
<div data-testid="image-workspace-page" className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<header
data-testid="image-workspace-header"
className="glass-surface flex shrink-0 items-center justify-between gap-3 border-b border-border/70 bg-background/70 px-4 py-3 sm:px-6"
>
<header className="flex flex-wrap items-center justify-between gap-3 border-b-4 border-[#26384D] bg-[#FFFFFF] px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-brand-soft text-brand">
<ImageIcon className="h-4 w-4" />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<ImageIcon className="h-4 w-4 shrink-0" />
<h1 className="truncate text-lg font-black">{activeProject.name}</h1>
<div className="flex min-w-0 items-center gap-2">
<h1 className="truncate text-base font-semibold tracking-[-0.015em]">{activeProject.name}</h1>
<span className="hidden shrink-0 rounded-md bg-surface-tertiary px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground sm:inline-flex">
Canvas
</span>
</div>
<p className="mt-1 flex items-center gap-1.5 text-xs font-bold text-[#68788B]">
<Bot className="h-3.5 w-3.5" />
{activeAgent ? `当前 Agent${activeAgent.name}` : '当前项目还没有 Agent'}
<p className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs font-medium text-muted-foreground">
<Bot className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">
{activeAgent ? `当前 Agent${activeAgent.name}` : '当前项目还没有 Agent'}
</span>
</p>
</div>
<button
type="button"
aria-label="刷新创作空间"
className="flex h-9 w-9 items-center justify-center rounded-md border-2 border-[#26384D] bg-white shadow-[2px_2px_0_#26384D]"
onClick={() => void load()}
>
<RefreshCw className="h-4 w-4" />
</button>
</header>
</div>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="刷新创作空间"
title="刷新创作空间"
className="h-9 w-9 shrink-0 rounded-full text-muted-foreground hover:text-foreground"
onClick={() => void load()}
>
<RefreshCw className="h-4 w-4" />
</Button>
</header>
<main data-testid="image-workspace-conversation" className="min-h-0 flex-1 overflow-auto px-4 py-5 sm:px-8">
<main
data-testid="image-workspace-conversation"
className="relative min-h-0 flex-1 overflow-y-auto bg-background px-4 py-5 sm:px-6 sm:py-7"
>
<div className="mx-auto flex min-h-full w-full max-w-4xl flex-col pb-4 sm:pb-8">
{activeProject.messages.length === 0 ? (
<div className="mx-auto flex min-h-[260px] max-w-xl flex-col items-center justify-center text-center">
<Sparkles className="h-9 w-9" />
<h2 className="mt-3 text-xl font-black"></h2>
<p className="mt-2 text-sm font-bold leading-6 text-[#68788B]">
</p>
<div
data-testid="image-workspace-empty-state"
className="chat-empty-state flex flex-1 items-center justify-center px-1 py-10 sm:px-4"
>
<div className="w-full max-w-2xl text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-brand-soft text-brand">
<Sparkles className="h-6 w-6" />
</div>
<h2 className="mt-5 text-2xl font-medium tracking-[-0.025em] sm:text-[28px]">
</h2>
<p className="mx-auto mt-3 max-w-xl text-sm font-medium leading-6 text-muted-foreground">
</p>
<p className="mt-5 text-xs font-medium text-muted-foreground/75">
/Ctrl + Enter
</p>
</div>
</div>
) : (
<div className="mx-auto max-w-4xl space-y-5">
<div className="flex min-h-full flex-col gap-8">
{activeProject.messages.map((message) => {
const statusLabel = messageStatusLabel(message);
const assistant = message.role === 'assistant';
return (
<article
key={message.id}
className={cn('flex', assistant ? 'justify-start' : 'justify-end')}
className={cn('flex gap-3', assistant ? 'justify-start' : 'justify-end')}
>
<div className={cn(
'max-w-[92%] rounded-lg border-2 border-[#26384D] p-3 shadow-[3px_3px_0_#26384D] sm:max-w-[78%]',
assistant ? 'bg-white' : 'bg-[#DCE6EF]',
)}>
<div className="mb-2 flex items-center gap-2 text-[11px] font-black text-[#68788B]">
{assistant ? <Bot className="h-3.5 w-3.5" /> : null}
<span>{assistant ? 'AI 创作 Agent' : '你'}</span>
{statusLabel ? <span className="rounded-full bg-[#E9EFF5] px-2 py-0.5">{statusLabel}</span> : null}
{assistant ? (
<div className="mt-1 flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-md border border-border/70 bg-surface-subtle text-foreground">
<Bot className="h-4 w-4" />
</div>
) : null}
<div className={cn('min-w-0', assistant ? 'w-full' : 'max-w-[88%] sm:max-w-[78%]')}>
<div className={cn(
'mb-2 flex items-center gap-2 text-[11px] font-semibold text-muted-foreground',
!assistant && 'justify-end',
)}>
<span>{assistant ? 'AI 创作 Agent' : '你'}</span>
<MessageStatus message={message} />
</div>
<div className={cn(
assistant
? 'chat-assistant-message-surface'
: 'chat-user-message-surface rounded-2xl px-4 py-3',
)}>
{message.text ? <p className="whitespace-pre-wrap text-sm font-medium leading-6">{message.text}</p> : null}
{message.images.length > 0 ? (
<div className={cn('grid gap-3', message.text && 'mt-3', message.images.length > 1 && 'sm:grid-cols-2')}>
{message.images.map((image) => (
<figure key={image.id} className="overflow-hidden rounded-2xl border border-border/70 bg-surface-subtle shadow-soft">
<img
src={image.thumbnailUrl || image.url}
alt={image.alt || '生成图片'}
className="min-h-40 max-h-[min(62vh,40rem)] w-full bg-surface-subtle object-contain"
/>
{assistant ? (
<figcaption className="flex flex-wrap gap-2 border-t border-border/70 bg-background/70 p-2">
<Button
type="button"
variant="outline"
size="sm"
className="h-8 rounded-full border-border/70 bg-background px-3 text-xs font-semibold"
onClick={() => addReference(image)}
disabled={!canAddReference}
>
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 rounded-full px-3 text-xs font-semibold"
onClick={() => addReference(image)}
disabled={!canAddReference}
>
</Button>
</figcaption>
) : null}
</figure>
))}
</div>
) : null}
{!message.text && message.images.length === 0 && statusLabel ? (
<div className="flex items-center gap-2 rounded-xl bg-surface-subtle px-3 py-2 text-sm font-medium text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin text-brand" />
{statusLabel}
</div>
) : null}
{message.error ? <p role="alert" className="mt-3 rounded-xl bg-destructive/10 px-3 py-2 text-xs font-semibold text-destructive">{message.error}</p> : null}
</div>
{message.text ? <p className="whitespace-pre-wrap text-sm font-bold leading-6">{message.text}</p> : null}
{message.images.length > 0 ? (
<div className={cn('grid gap-3', message.images.length > 1 && 'mt-3 sm:grid-cols-2')}>
{message.images.map((image) => (
<figure key={image.id} className="overflow-hidden rounded-md border-2 border-[#26384D] bg-[#FFFFFF]">
<img
src={image.thumbnailUrl || image.url}
alt={image.alt || '生成图片'}
className="max-h-[520px] w-full bg-[#F6F8FA] object-contain"
/>
{assistant ? (
<figcaption className="grid grid-cols-2 gap-2 border-t-2 border-[#26384D] p-2">
<button
type="button"
className="rounded-md border-2 border-[#26384D] bg-[#E9EFF5] px-2 py-1.5 text-xs font-black disabled:opacity-50"
onClick={() => addReference(image)}
disabled={!canAddReference}
>
</button>
<button
type="button"
className="rounded-md border-2 border-[#26384D] bg-white px-2 py-1.5 text-xs font-black disabled:opacity-50"
onClick={() => addReference(image)}
disabled={!canAddReference}
>
</button>
</figcaption>
) : null}
</figure>
))}
</div>
) : null}
{message.error ? <p role="alert" className="mt-2 text-xs font-black text-[#a72222]">{message.error}</p> : null}
</div>
</article>
);
@@ -420,39 +501,54 @@ export function ImageCanvas() {
<div ref={conversationEndRef} aria-hidden="true" />
</div>
)}
</main>
</div>
</main>
<section data-testid="image-workspace-composer" className="border-t-4 border-[#26384D] bg-[#FFFFFF] p-3 sm:p-4">
<div className="mx-auto max-w-4xl rounded-lg border-2 border-[#26384D] bg-white p-3 shadow-[4px_4px_0_#26384D]">
{!activeAgent ? (
<div className="mb-3 flex flex-wrap items-center justify-between gap-2 rounded-md border-2 border-[#26384D] bg-[#E9EFF5] p-3 text-xs font-black">
<span> Agent Agent</span>
<Button type="button" size="sm" onClick={() => void handleAddAgent()} disabled={addingAgent}>
{addingAgent ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Plus className="mr-2 h-4 w-4" />}
Agent
</Button>
</div>
) : null}
<form
data-testid="image-workspace-composer"
className="shrink-0 bg-background px-4 pb-4 pt-2 sm:px-0 sm:pb-5"
onSubmit={(event) => {
event.preventDefault();
void handleSend();
}}
>
<div className="mx-auto w-full max-w-[46rem]">
{!activeAgent ? (
<div className="mb-2 flex flex-wrap items-center justify-between gap-2 rounded-xl border border-amber-400/30 bg-amber-50 px-3 py-2.5 text-xs font-semibold text-amber-900">
<span> Agent Agent</span>
<Button type="button" size="sm" variant="outline" className="h-8 border-amber-400/40 bg-background text-amber-900 hover:bg-amber-100" onClick={() => void handleAddAgent()} disabled={addingAgent}>
{addingAgent ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Plus className="mr-2 h-4 w-4" />}
Agent
</Button>
</div>
) : null}
<div
data-testid="image-workspace-dropzone"
className="chat-composer-surface relative flex min-w-0 flex-col gap-2 rounded-2xl border border-border/70 bg-background p-3 text-foreground shadow-soft focus-within:ring-1 focus-within:ring-foreground/10"
>
{selectedReferences.length > 0 ? (
<div data-testid="image-workspace-selected-references" className="mb-3 flex flex-wrap gap-2">
<div data-testid="image-workspace-selected-references" className="flex flex-wrap gap-2" aria-label="已添加到本次创作的参考图">
{selectedReferences.map((image) => (
<div key={image.id} className="flex items-center gap-2 rounded-md border-2 border-[#26384D] bg-[#F6F8FA] p-1.5 pr-2">
<img src={image.thumbnailUrl || image.url} alt="已选参考图" className="h-10 w-10 rounded object-cover" />
<span className="max-w-28 truncate text-[11px] font-black">{image.alt || '参考图'}</span>
<button
<div key={image.id} className="flex min-w-0 max-w-full items-center gap-2 rounded-xl border border-border/70 bg-surface-subtle p-1.5 pr-2">
<img src={image.thumbnailUrl || image.url} alt="已选参考图" className="h-9 w-9 rounded-lg object-cover" />
<span className="max-w-36 truncate text-[11px] font-semibold">{image.alt || '参考图'}</span>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="移除参考图"
className="h-6 w-6 rounded-full text-muted-foreground hover:text-foreground"
onClick={() => setSelectedReferences((current) => current.filter((item) => item.id !== image.id))}
>
<X className="h-3.5 w-3.5" />
</button>
</Button>
</div>
))}
</div>
) : null}
<textarea
<Textarea
aria-label="创作描述"
value={prompt}
onChange={(event) => {
@@ -460,49 +556,65 @@ export function ImageCanvas() {
setActionError(null);
}}
onKeyDown={handleComposerKeyDown}
rows={3}
placeholder="描述你想创作的画面,也可以添加参考图…"
className="min-h-24 w-full resize-none bg-transparent px-1 py-1 text-sm font-bold leading-6 outline-none placeholder:text-[#8A98A8]"
className="max-h-40 !min-h-[72px] resize-none border-0 bg-transparent p-0 text-sm font-medium leading-6 shadow-none placeholder:text-muted-foreground/70 focus-visible:border-transparent focus-visible:ring-0"
/>
{actionError ? <p role="alert" className="mt-2 rounded-md bg-[#ffd6d6] px-3 py-2 text-xs font-black">{actionError}</p> : null}
<div className="mt-3 flex flex-wrap items-end gap-2 border-t-2 border-[#26384D]/20 pt-3">
<input
ref={fileInputRef}
type="file"
className="hidden"
aria-label="选择参考图"
accept={capabilities?.referenceUpload.acceptedMimeTypes.join(',')}
onChange={(event) => void handleUploadReference(event)}
/>
<button
type="button"
className="flex h-9 items-center gap-1.5 rounded-md border-2 border-[#26384D] bg-[#F6F8FA] px-3 text-xs font-black disabled:opacity-50"
onClick={() => fileInputRef.current?.click()}
disabled={!canUploadReference || uploading}
>
{uploading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ImagePlus className="h-4 w-4" />}
</button>
<OptionSelect label="创作模式" value={settings.modeId} options={capabilities?.modes ?? []} onChange={(modeId) => setSettings((current) => ({ ...current, modeId }))} />
<OptionSelect label="模型" value={settings.modelId} options={capabilities?.models ?? []} onChange={(modelId) => setSettings((current) => ({ ...current, modelId }))} />
<OptionSelect label="画幅" value={settings.aspectRatioId} options={capabilities?.aspectRatios ?? []} onChange={(aspectRatioId) => setSettings((current) => ({ ...current, aspectRatioId }))} />
<OptionSelect label="分辨率" value={settings.resolutionId} options={capabilities?.resolutions ?? []} onChange={(resolutionId) => setSettings((current) => ({ ...current, resolutionId }))} />
<OptionSelect label="生成数量" value={settings.outputCountId} options={capabilities?.outputCounts ?? []} onChange={(outputCountId) => setSettings((current) => ({ ...current, outputCountId }))} />
{actionError ? <p role="alert" className="rounded-xl bg-destructive/10 px-3 py-2 text-xs font-semibold text-destructive">{actionError}</p> : null}
<div data-testid="image-workspace-composer-actions" className="flex flex-wrap items-center justify-between gap-2 border-t border-border/70 pt-2">
<div className="flex min-w-0 items-center gap-1">
<input
ref={fileInputRef}
type="file"
className="sr-only"
aria-label="选择参考图"
accept={capabilities?.referenceUpload.acceptedMimeTypes.join(',')}
onChange={(event) => void handleUploadReference(event)}
/>
<Button
type="button"
variant="ghost"
aria-label="添加参考图"
title="添加参考图"
className="h-8 rounded-full px-2.5 text-xs font-semibold"
onClick={() => fileInputRef.current?.click()}
disabled={!canUploadReference || uploading}
>
{uploading ? <Loader2 className="mr-1.5 h-4 w-4 animate-spin" /> : <ImagePlus className="mr-1.5 h-4 w-4" />}
</Button>
<span className="hidden px-1 text-[10px] font-medium text-muted-foreground/75 sm:inline">
/Ctrl + Enter
</span>
</div>
<Button
type="button"
className="ml-auto h-10 border-2 border-[#26384D] bg-[#DCE6EF] px-5 font-black text-[#26384D] shadow-[3px_3px_0_#26384D]"
onClick={() => void handleSend()}
type="submit"
className="h-9 rounded-full border border-brand/20 bg-brand px-4 font-semibold text-primary-foreground"
disabled={submitting || !activeAgent || !prompt.trim()}
>
{submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Send className="mr-2 h-4 w-4" />}
</Button>
</div>
<div data-testid="image-workspace-generation-controls" className="border-t border-border/70 pt-3">
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-semibold text-muted-foreground">
<Settings2 className="h-3.5 w-3.5" />
</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
<OptionSelect label="创作模式" value={settings.modeId} options={capabilities?.modes ?? []} onChange={(modeId) => setSettings((current) => ({ ...current, modeId }))} />
<OptionSelect label="模型" value={settings.modelId} options={capabilities?.models ?? []} onChange={(modelId) => setSettings((current) => ({ ...current, modelId }))} />
<OptionSelect label="画幅" value={settings.aspectRatioId} options={capabilities?.aspectRatios ?? []} onChange={(aspectRatioId) => setSettings((current) => ({ ...current, aspectRatioId }))} />
<OptionSelect label="分辨率" value={settings.resolutionId} options={capabilities?.resolutions ?? []} onChange={(resolutionId) => setSettings((current) => ({ ...current, resolutionId }))} />
<OptionSelect label="生成数量" value={settings.outputCountId} options={capabilities?.outputCounts ?? []} onChange={(outputCountId) => setSettings((current) => ({ ...current, outputCountId }))} />
</div>
</div>
</div>
</section>
</div>
</form>
</div>
</div>
);