Refine playground composer and settings layout

This commit is contained in:
wangbo 2026-05-15 01:08:54 +08:00
parent a4f731af88
commit bdc9be63d5
3 changed files with 217 additions and 116 deletions

View File

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

View File

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

View File

@ -613,23 +613,42 @@
width: min(260px, 26vw); width: min(260px, 26vw);
} }
.composerQuickPrompts { .composerEstimatedCharge {
display: flex; display: inline-flex;
min-width: 0; align-items: center;
flex: 1;
gap: 6px; 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 { .composerEstimatedCharge svg {
min-height: 32px; width: 15px;
padding: 0 10px; height: 15px;
border: 1px solid var(--border); color: #526170;
}
.composerMediaSendButton.shButton {
width: 40px;
height: 40px;
min-width: 40px;
border: 0;
border-radius: 999px; border-radius: 999px;
background: var(--surface); background: #0d0f14;
color: var(--text-soft); color: #fff;
font-size: var(--font-size-xs); box-shadow: 0 10px 28px rgba(13, 15, 20, 0.18);
white-space: nowrap; }
.composerMediaSendButton.shButton:hover {
background: #1b1f27;
}
.composerMediaSendButton.shButton svg {
width: 20px;
height: 20px;
stroke-width: 2.5;
} }
.playgroundModeCards { .playgroundModeCards {
@ -801,8 +820,8 @@
} }
.playgroundSidebar { .playgroundSidebar {
display: grid; display: flex;
align-content: start; flex-direction: column;
gap: 8px; gap: 8px;
padding: 22px 16px; padding: 22px 16px;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
@ -816,6 +835,11 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.playgroundSidebarNav {
display: grid;
gap: 8px;
}
.playgroundSideItem { .playgroundSideItem {
display: flex; display: flex;
min-height: 38px; min-height: 38px;
@ -834,6 +858,75 @@
background: var(--surface-muted); 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 { .playgroundStage {
display: grid; display: grid;
align-items: stretch; align-items: stretch;
@ -1424,7 +1517,7 @@
position: sticky; position: sticky;
bottom: 0; bottom: 0;
margin: auto auto 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%); background: linear-gradient(180deg, rgba(250, 250, 250, 0), var(--surface-subtle) 40%);
} }
@ -1824,6 +1917,8 @@
} }
.mediaTaskPage { .mediaTaskPage {
--media-task-max-width: 1120px;
--media-composer-max-width: 960px;
display: grid; display: grid;
grid-template-rows: minmax(0, 1fr) auto; grid-template-rows: minmax(0, 1fr) auto;
width: 100%; width: 100%;
@ -1843,18 +1938,18 @@
} }
.mediaTaskTimeline h1 { .mediaTaskTimeline h1 {
width: min(1240px, 100%); width: min(var(--media-task-max-width), 100%);
font-size: 1.875rem; font-size: 1.875rem;
} }
.mediaTaskTimeline > .playgroundError { .mediaTaskTimeline > .playgroundError {
width: min(1240px, 100%); width: min(var(--media-task-max-width), 100%);
} }
.mediaTaskItem { .mediaTaskItem {
display: grid; display: grid;
gap: 14px; gap: 14px;
width: min(1240px, 100%); width: min(var(--media-task-max-width), 100%);
} }
.mediaTaskHeader { .mediaTaskHeader {
@ -1988,12 +2083,11 @@
height: var(--task-reference-height); height: var(--task-reference-height);
margin: 2px 2px 2px 0; margin: 2px 2px 2px 0;
vertical-align: middle; vertical-align: middle;
transition: width 180ms ease; transition: z-index 120ms ease;
} }
.mediaTaskReferenceStack:hover { .mediaTaskReferenceStack:hover {
z-index: 14; z-index: 14;
width: min(calc(var(--reference-count) * 58px), calc(100vw - 96px));
} }
.mediaTaskReferenceCard { .mediaTaskReferenceCard {
@ -2285,12 +2379,12 @@
} }
.mediaComposerDock { .mediaComposerDock {
padding: 16px 40px 24px; padding: 10px 40px 14px;
background: linear-gradient(180deg, rgba(250, 250, 250, 0), var(--surface-subtle) 26%); background: linear-gradient(180deg, rgba(250, 250, 250, 0), var(--surface-subtle) 26%);
} }
.mediaComposerDock .playgroundComposer { .mediaComposerDock .playgroundComposer {
width: min(1240px, 100%); width: min(var(--media-composer-max-width), 100%);
min-height: 190px; min-height: 190px;
margin: 0 auto; margin: 0 auto;
border-radius: 26px; border-radius: 26px;
@ -2450,15 +2544,11 @@
} }
.mediaComposerDock { .mediaComposerDock {
padding: 12px 18px 20px; padding: 10px 18px 14px;
} }
.assistantComposerDock { .assistantComposerDock {
padding-bottom: 20px; padding-bottom: 14px;
}
.composerQuickPrompts {
flex-wrap: wrap;
} }
.publicWorksMasonry { .publicWorksMasonry {
@ -2467,6 +2557,16 @@
} }
@media (max-width: 560px) { @media (max-width: 560px) {
.playgroundSettingsField {
grid-template-columns: 1fr;
gap: 10px;
padding: 16px;
}
.playgroundSettingsNotice {
margin: 14px 16px;
}
.mediaAspectGrid, .mediaAspectGrid,
.mediaCountGrid { .mediaCountGrid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));