Refine playground composer and settings layout

This commit is contained in:
2026-05-15 01:08:54 +08:00
parent a4f731af88
commit bdc9be63d5
3 changed files with 217 additions and 116 deletions
+88 -45
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { GatewayApiKey, GatewayTask, PlatformModel } from '@easyai-ai-gateway/contracts';
import { ChevronDown, MessageSquarePlus, Send, Sparkles } from 'lucide-react';
import { Badge, Button, Select, Textarea } from '../components/ui';
import { ArrowUp, ChevronDown, MessageSquarePlus, Settings2, Sparkles } from 'lucide-react';
import { Badge, Button, FormDialog, Select, Textarea } from '../components/ui';
import { createImageEditTask, createImageGenerationTask, createVideoGenerationTask, pollTaskUntilSettled, resolveApiAssetUrl, taskIsPending } from '../api';
import type { PlaygroundMode } from '../types';
import {
@@ -48,7 +48,6 @@ import {
modeOptions,
modelOptionLabel,
placeholderByMode,
quickPrompts,
resolveSelectedApiKeyId,
videoModeOptions,
type ModelOption,
@@ -96,6 +95,7 @@ export function PlaygroundPage(props: {
const [mediaUploadMessage, setMediaUploadMessage] = useState('');
const [mediaUploads, setMediaUploads] = useState<PlaygroundUpload[]>([]);
const [mediaUploading, setMediaUploading] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const isMountedRef = useRef(false);
const pendingMediaModelRef = useRef('');
const resumedTaskIdsRef = useRef(new Set<string>());
@@ -288,6 +288,12 @@ export function PlaygroundPage(props: {
setMediaRuns((current) => [...current, run]);
setMediaMessage('');
if (!overrides) {
setPrompt('');
setMediaUploads([]);
setMediaUploadMessage('');
setImageHasReference(false);
}
try {
const requestPrompt = replacePlaygroundResourceTokens(trimmedPrompt, runUploads, runMode);
let response: { task: GatewayTask; next: Record<string, string> };
@@ -310,11 +316,6 @@ export function PlaygroundPage(props: {
: await createImageGenerationTask(credential, requestPayload);
}
setMediaRuns((current) => updateMediaRun(current, localId, { status: response.task.status, task: response.task }));
if (!overrides) {
setMediaUploads([]);
setMediaUploadMessage('');
setImageHasReference(false);
}
void pollMediaRunUntilSettled(credential, localId, response.task);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '生成任务提交失败';
@@ -397,12 +398,9 @@ export function PlaygroundPage(props: {
const mediaComposer = props.mode === 'chat' ? null : (
<Composer
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
mode={props.mode}
modelOptions={modelOptions}
prompt={prompt}
selectedApiKeyId={activeApiKeyId}
selectedModel={selectedModel}
imageHasReference={effectiveImageHasReference}
mediaSettings={mediaSettings}
@@ -412,8 +410,6 @@ export function PlaygroundPage(props: {
uploads={mediaUploads}
uploading={mediaUploading}
videoMode={videoMode}
onApiKeyChange={props.onApiKeyChange}
onCreateApiKey={props.onCreateApiKey}
onImageReferenceChange={setImageHasReference}
onMediaSettingsChange={setMediaSettings}
onModeChange={props.onModeChange}
@@ -445,13 +441,19 @@ export function PlaygroundPage(props: {
<strong></strong>
<Badge variant="secondary">Test</Badge>
</div>
<button type="button" className="playgroundSideItem active" onClick={startNewThread}>
<MessageSquarePlus size={15} />
</button>
<button type="button" className="playgroundSideItem">
<Sparkles size={15} />
<div className="playgroundSidebarNav">
<button type="button" className="playgroundSideItem active" onClick={startNewThread}>
<MessageSquarePlus size={15} />
</button>
<button type="button" className="playgroundSideItem">
<Sparkles size={15} />
</button>
</div>
<button type="button" className="playgroundSideItem playgroundSettingsButton" onClick={() => setSettingsOpen(true)}>
<Settings2 size={15} />
</button>
</aside>
@@ -490,10 +492,70 @@ export function PlaygroundPage(props: {
)}
</section>
</main>
<PlaygroundSettingsDialog
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
open={settingsOpen}
selectedApiKeyId={activeApiKeyId}
onApiKeyChange={props.onApiKeyChange}
onClose={() => setSettingsOpen(false)}
onCreateApiKey={() => {
setSettingsOpen(false);
props.onCreateApiKey();
}}
/>
</div>
);
}
function PlaygroundSettingsDialog(props: {
apiKeySecretsById: Record<string, string>;
apiKeys: GatewayApiKey[];
open: boolean;
selectedApiKeyId: string;
onApiKeyChange: (apiKeyId: string) => void;
onClose: () => void;
onCreateApiKey: () => void;
}) {
const notice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById);
return (
<FormDialog
bodyClassName="playgroundSettingsDialogBody"
className="playgroundSettingsDialog"
eyebrow="在线测试"
footer={<Button type="submit"></Button>}
open={props.open}
title="设置"
onClose={props.onClose}
onSubmit={(event) => {
event.preventDefault();
props.onClose();
}}
>
<label className="playgroundSettingsField">
<span className="playgroundSettingsFieldCopy">
<strong>API Key</strong>
<small>线</small>
</span>
<ApiKeySelect
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
selectedApiKeyId={props.selectedApiKeyId}
onApiKeyChange={props.onApiKeyChange}
/>
</label>
{notice && (
<div className="playgroundSettingsNotice">
<span>{notice}</span>
<Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
API Key
</Button>
</div>
)}
</FormDialog>
);
}
export function PlaygroundEntry(props: {
onModeChange: (mode: PlaygroundMode) => void;
}) {
@@ -566,8 +628,6 @@ export function PublicWorksGallery() {
}
function Composer(props: {
apiKeySecretsById?: Record<string, string>;
apiKeys?: GatewayApiKey[];
compact?: boolean;
imageHasReference?: boolean;
mediaCapabilities?: MediaModelCapabilities;
@@ -575,15 +635,12 @@ function Composer(props: {
mode: PlaygroundMode;
modelOptions: ModelOption[];
prompt: string;
selectedApiKeyId?: string;
selectedModel?: string;
uploadAccept?: string;
uploadMessage?: string;
uploads?: PlaygroundUpload[];
uploading?: boolean;
videoMode?: VideoCreateMode;
onApiKeyChange?: (apiKeyId: string) => void;
onCreateApiKey?: () => void;
onImageReferenceChange?: (value: boolean) => void;
onMediaSettingsChange?: (settings: MediaGenerationSettings) => void;
onModeChange: (mode: PlaygroundMode) => void;
@@ -595,8 +652,6 @@ function Composer(props: {
onUploadFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void;
onVideoModeChange?: (value: VideoCreateMode) => void;
}) {
const quickItems = quickPrompts[props.mode];
const apiKeyNotice = props.apiKeys && props.apiKeySecretsById ? apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById) : '';
const hasMediaReferencePicker = props.mode !== 'chat' && Boolean(props.onUploadFiles);
const mediaReferenceMessage = hasMediaReferencePicker
? props.uploadMessage || sharedMediaUploadSummaryMessage(props.uploads ?? [], props.mode, props.videoMode ?? 'text_to_video')
@@ -672,24 +727,12 @@ function Composer(props: {
onChange={props.onMediaSettingsChange}
/>
)}
{props.apiKeys && props.apiKeySecretsById && props.onApiKeyChange && (
<ApiKeySelect
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
selectedApiKeyId={props.selectedApiKeyId ?? ''}
onApiKeyChange={props.onApiKeyChange}
/>
)}
{apiKeyNotice && props.onCreateApiKey && (
<Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
API Key
</Button>
)}
<div className="composerQuickPrompts">
{quickItems.map((item) => <button type="button" key={item} onClick={() => props.onPromptChange(item)}>{item}</button>)}
</div>
<Button type="button" size="icon" aria-label="发送测试" onClick={props.onSubmit}>
<Send size={15} />
<span className="composerEstimatedCharge" aria-label="预计扣费 1 / 张">
<Sparkles size={14} />
<span>1 / </span>
</span>
<Button type="button" size="icon" className="composerMediaSendButton" aria-label="发送测试" onClick={props.onSubmit}>
<ArrowUp size={24} />
</Button>
</div>
</div>