From bdc9be63d539b1d39e22cb797fac4664baa21ade Mon Sep 17 00:00:00 2001 From: wangbo Date: Fri, 15 May 2026 01:08:54 +0800 Subject: [PATCH] Refine playground composer and settings layout --- apps/web/src/pages/PlaygroundPage.tsx | 133 ++++++++++++++------- apps/web/src/pages/playground-chat.tsx | 42 ------- apps/web/src/styles/playground.css | 158 ++++++++++++++++++++----- 3 files changed, 217 insertions(+), 116 deletions(-) diff --git a/apps/web/src/pages/PlaygroundPage.tsx b/apps/web/src/pages/PlaygroundPage.tsx index 9c06163..5a4b95e 100644 --- a/apps/web/src/pages/PlaygroundPage.tsx +++ b/apps/web/src/pages/PlaygroundPage.tsx @@ -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([]); const [mediaUploading, setMediaUploading] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); const isMountedRef = useRef(false); const pendingMediaModelRef = useRef(''); const resumedTaskIdsRef = useRef(new Set()); @@ -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 }; @@ -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 : ( 开启创作 Test - - + + + @@ -490,10 +492,70 @@ export function PlaygroundPage(props: { )} + setSettingsOpen(false)} + onCreateApiKey={() => { + setSettingsOpen(false); + props.onCreateApiKey(); + }} + /> ); } +function PlaygroundSettingsDialog(props: { + apiKeySecretsById: Record; + apiKeys: GatewayApiKey[]; + open: boolean; + selectedApiKeyId: string; + onApiKeyChange: (apiKeyId: string) => void; + onClose: () => void; + onCreateApiKey: () => void; +}) { + const notice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById); + return ( + 完成} + open={props.open} + title="设置" + onClose={props.onClose} + onSubmit={(event) => { + event.preventDefault(); + props.onClose(); + }} + > + + {notice && ( +
+ {notice} + +
+ )} +
+ ); +} + export function PlaygroundEntry(props: { onModeChange: (mode: PlaygroundMode) => void; }) { @@ -566,8 +628,6 @@ export function PublicWorksGallery() { } function Composer(props: { - apiKeySecretsById?: Record; - 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 && ( - - )} - {apiKeyNotice && props.onCreateApiKey && ( - - )} -
- {quickItems.map((item) => )} -
- diff --git a/apps/web/src/pages/playground-chat.tsx b/apps/web/src/pages/playground-chat.tsx index dba063d..43acd4c 100644 --- a/apps/web/src/pages/playground-chat.tsx +++ b/apps/web/src/pages/playground-chat.tsx @@ -33,7 +33,6 @@ import { type PlaygroundUpload, } from './playground-upload'; import { - ApiKeySelect, ModeSwitch, PlaygroundGreeting, apiKeyNoticeText, @@ -175,12 +174,8 @@ export function AssistantChatPlayground(props: {
setChatUploads((current) => current.filter((item) => item.id !== id))} @@ -219,21 +212,15 @@ export function AssistantChatPlayground(props: {
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; - 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: { ; - 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: { )) : } - - {props.apiKeyNotice && ( - - )} diff --git a/apps/web/src/styles/playground.css b/apps/web/src/styles/playground.css index 71f2cd3..db8bad2 100644 --- a/apps/web/src/styles/playground.css +++ b/apps/web/src/styles/playground.css @@ -613,23 +613,42 @@ width: min(260px, 26vw); } -.composerQuickPrompts { - display: flex; - min-width: 0; - flex: 1; +.composerEstimatedCharge { + display: inline-flex; + align-items: center; gap: 6px; - overflow: hidden; + margin-left: auto; + color: #526170; + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + white-space: nowrap; } -.composerQuickPrompts button { - min-height: 32px; - padding: 0 10px; - border: 1px solid var(--border); +.composerEstimatedCharge svg { + width: 15px; + height: 15px; + color: #526170; +} + +.composerMediaSendButton.shButton { + width: 40px; + height: 40px; + min-width: 40px; + border: 0; border-radius: 999px; - background: var(--surface); - color: var(--text-soft); - font-size: var(--font-size-xs); - white-space: nowrap; + background: #0d0f14; + color: #fff; + box-shadow: 0 10px 28px rgba(13, 15, 20, 0.18); +} + +.composerMediaSendButton.shButton:hover { + background: #1b1f27; +} + +.composerMediaSendButton.shButton svg { + width: 20px; + height: 20px; + stroke-width: 2.5; } .playgroundModeCards { @@ -801,8 +820,8 @@ } .playgroundSidebar { - display: grid; - align-content: start; + display: flex; + flex-direction: column; gap: 8px; padding: 22px 16px; border-right: 1px solid var(--border); @@ -816,6 +835,11 @@ margin-bottom: 10px; } +.playgroundSidebarNav { + display: grid; + gap: 8px; +} + .playgroundSideItem { display: flex; min-height: 38px; @@ -834,6 +858,75 @@ background: var(--surface-muted); } +.playgroundSettingsButton { + margin-top: auto; + border-top: 1px solid var(--border); + border-radius: 0; + padding-top: 12px; +} + +.playgroundSettingsButton:hover { + border-radius: var(--radius-md); +} + +.playgroundSettingsDialog { + width: min(640px, calc(100vw - 32px)); +} + +.playgroundSettingsDialogBody { + grid-template-columns: 1fr; + gap: 14px; + padding: 0; +} + +.playgroundSettingsField { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 360px); + gap: 24px; + align-items: center; + padding: 18px 24px; + border-bottom: 1px solid var(--border); +} + +.playgroundSettingsFieldCopy { + display: grid; + gap: 4px; + min-width: 0; +} + +.playgroundSettingsFieldCopy strong { + color: var(--text-strong); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); +} + +.playgroundSettingsFieldCopy small { + color: var(--muted-foreground); + font-size: var(--font-size-sm); + line-height: var(--line-height-normal); +} + +.playgroundSettingsField .playgroundApiKeySelect { + width: 100%; +} + +.playgroundSettingsNotice { + display: grid; + gap: 10px; + margin: 16px 24px; + padding: 10px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-muted); + color: var(--text-soft); + font-size: var(--font-size-sm); + line-height: var(--line-height-relaxed); +} + +.playgroundSettingsNotice .shButton { + justify-self: start; +} + .playgroundStage { display: grid; align-items: stretch; @@ -1424,7 +1517,7 @@ position: sticky; bottom: 0; margin: auto auto 0; - padding: 18px 0 24px; + padding: 12px 0 14px; background: linear-gradient(180deg, rgba(250, 250, 250, 0), var(--surface-subtle) 40%); } @@ -1824,6 +1917,8 @@ } .mediaTaskPage { + --media-task-max-width: 1120px; + --media-composer-max-width: 960px; display: grid; grid-template-rows: minmax(0, 1fr) auto; width: 100%; @@ -1843,18 +1938,18 @@ } .mediaTaskTimeline h1 { - width: min(1240px, 100%); + width: min(var(--media-task-max-width), 100%); font-size: 1.875rem; } .mediaTaskTimeline > .playgroundError { - width: min(1240px, 100%); + width: min(var(--media-task-max-width), 100%); } .mediaTaskItem { display: grid; gap: 14px; - width: min(1240px, 100%); + width: min(var(--media-task-max-width), 100%); } .mediaTaskHeader { @@ -1988,12 +2083,11 @@ height: var(--task-reference-height); margin: 2px 2px 2px 0; vertical-align: middle; - transition: width 180ms ease; + transition: z-index 120ms ease; } .mediaTaskReferenceStack:hover { z-index: 14; - width: min(calc(var(--reference-count) * 58px), calc(100vw - 96px)); } .mediaTaskReferenceCard { @@ -2285,12 +2379,12 @@ } .mediaComposerDock { - padding: 16px 40px 24px; + padding: 10px 40px 14px; background: linear-gradient(180deg, rgba(250, 250, 250, 0), var(--surface-subtle) 26%); } .mediaComposerDock .playgroundComposer { - width: min(1240px, 100%); + width: min(var(--media-composer-max-width), 100%); min-height: 190px; margin: 0 auto; border-radius: 26px; @@ -2450,15 +2544,11 @@ } .mediaComposerDock { - padding: 12px 18px 20px; + padding: 10px 18px 14px; } .assistantComposerDock { - padding-bottom: 20px; - } - - .composerQuickPrompts { - flex-wrap: wrap; + padding-bottom: 14px; } .publicWorksMasonry { @@ -2467,6 +2557,16 @@ } @media (max-width: 560px) { + .playgroundSettingsField { + grid-template-columns: 1fr; + gap: 10px; + padding: 16px; + } + + .playgroundSettingsNotice { + margin: 14px 16px; + } + .mediaAspectGrid, .mediaCountGrid { grid-template-columns: repeat(2, minmax(0, 1fr));