Refine playground composer and settings layout
This commit is contained in:
parent
a4f731af88
commit
bdc9be63d5
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user