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>
-42
View File
@@ -33,7 +33,6 @@ import {
type PlaygroundUpload,
} from './playground-upload';
import {
ApiKeySelect,
ModeSwitch,
PlaygroundGreeting,
apiKeyNoticeText,
@@ -175,12 +174,8 @@ export function AssistantChatPlayground(props: {
<ThreadPrimitive.Empty>
<div className="assistantEmptyStage">
<AssistantEmptyState
apiKeyNotice={apiKeyNotice}
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
canRun={canRun}
modelOptions={props.modelOptions}
selectedApiKeyId={activeApiKeyId}
selectedModel={props.selectedModel}
token={props.token}
activeApiKeySecret={activeApiKeySecret}
@@ -188,8 +183,6 @@ export function AssistantChatPlayground(props: {
uploadMessage={chatUploadMessage}
uploads={chatUploads}
uploading={chatUploading}
onApiKeyChange={props.onApiKeyChange}
onCreateApiKey={props.onCreateApiKey}
onModeChange={props.onModeChange}
onModelChange={props.onModelChange}
onRemoveUpload={(id) => setChatUploads((current) => current.filter((item) => item.id !== id))}
@@ -219,21 +212,15 @@ export function AssistantChatPlayground(props: {
</div>
<ThreadPrimitive.ViewportFooter className="assistantComposerDock">
<AssistantChatComposer
apiKeyNotice={apiKeyNotice}
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
canRun={canRun}
docked
modelOptions={props.modelOptions}
placeholder={assistantPlaceholder(props.token, props.selectedModel, activeApiKeySecret)}
selectedApiKeyId={activeApiKeyId}
selectedModel={props.selectedModel}
uploadAccept={sharedChatUploadAccept}
uploadMessage={chatUploadMessage}
uploads={chatUploads}
uploading={chatUploading}
onApiKeyChange={props.onApiKeyChange}
onCreateApiKey={props.onCreateApiKey}
onModeChange={props.onModeChange}
onModelChange={props.onModelChange}
onRemoveUpload={(id) => setChatUploads((current) => current.filter((item) => item.id !== id))}
@@ -265,20 +252,14 @@ function AssistantChatPersistenceBridge(props: { storedMessagesById: StoredOpenA
function AssistantEmptyState(props: {
activeApiKeySecret: string;
apiKeyNotice: string;
apiKeySecretsById: Record<string, string>;
apiKeys: GatewayApiKey[];
canRun: boolean;
modelOptions: ModelOption[];
selectedApiKeyId: string;
selectedModel: string;
token: string;
uploadAccept: string;
uploadMessage: string;
uploads: PlaygroundUpload[];
uploading: boolean;
onApiKeyChange: (apiKeyId: string) => void;
onCreateApiKey: () => void;
onModeChange: (mode: PlaygroundMode) => void;
onModelChange: (value: string) => void;
onRemoveUpload: (id: string) => void;
@@ -292,20 +273,14 @@ function AssistantEmptyState(props: {
<ModeSwitch activeMode="chat" onModeChange={props.onModeChange} />
<PlaygroundGreeting activeMode={activeMode} />
<AssistantChatComposer
apiKeyNotice={props.apiKeyNotice}
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
canRun={props.canRun}
modelOptions={props.modelOptions}
placeholder={placeholder}
selectedApiKeyId={props.selectedApiKeyId}
selectedModel={props.selectedModel}
uploadAccept={props.uploadAccept}
uploadMessage={props.uploadMessage}
uploads={props.uploads}
uploading={props.uploading}
onApiKeyChange={props.onApiKeyChange}
onCreateApiKey={props.onCreateApiKey}
onModeChange={props.onModeChange}
onModelChange={props.onModelChange}
onRemoveUpload={props.onRemoveUpload}
@@ -316,21 +291,15 @@ function AssistantEmptyState(props: {
}
function AssistantChatComposer(props: {
apiKeyNotice: string;
apiKeySecretsById: Record<string, string>;
apiKeys: GatewayApiKey[];
canRun: boolean;
docked?: boolean;
modelOptions: ModelOption[];
placeholder: string;
selectedApiKeyId: string;
selectedModel: string;
uploadAccept?: string;
uploadMessage?: string;
uploads?: PlaygroundUpload[];
uploading?: boolean;
onApiKeyChange: (apiKeyId: string) => void;
onCreateApiKey: () => void;
onModeChange: (mode: PlaygroundMode) => void;
onModelChange: (value: string) => void;
onRemoveUpload?: (id: string) => void;
@@ -375,17 +344,6 @@ function AssistantChatComposer(props: {
<option value={item.value} key={item.value}>{modelOptionLabel(item)}</option>
)) : <option value=""></option>}
</Select>
<ApiKeySelect
apiKeySecretsById={props.apiKeySecretsById}
apiKeys={props.apiKeys}
selectedApiKeyId={props.selectedApiKeyId}
onApiKeyChange={props.onApiKeyChange}
/>
{props.apiKeyNotice && (
<Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
API Key
</Button>
)}
<ComposerPrimitive.Send className="composerSendButton" disabled={!props.canRun} aria-label="发送消息">
<Send size={18} />
</ComposerPrimitive.Send>