visual-journal / src /components /workbench-pro-dock-panels.tsx
Visual Journal deploy
Deploy 5554830 to Docker Space
e4e0afe
Raw
History Blame Contribute Delete
13.8 kB
'use client';
import { Checkbox } from '@/components/ui/checkbox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { WorkbenchProDockProps, OutputFormat, Quality, SizePreset } from '@/components/workbench-pro-dock';
import { WorkbenchProRoutePanel } from '@/components/workbench-pro-route-panel';
import type { GptImageModel } from '@/lib/cost-utils';
import { useI18n } from '@/lib/i18n';
import { getImageOutputFormatLabel } from '@/lib/image-display-labels';
import type { ImageStreamMode } from '@/lib/image-upstream-strategy';
import { getPresetDimensions } from '@/lib/size-utils';
import { resolveStreamingBatchToggleState } from '@/lib/streaming-batch';
import type * as React from 'react';
type Translation = ReturnType<typeof useI18n>['t'];
type ProPanelProps = Omit<WorkbenchProDockProps, 'defaultMode' | 'defaultProTab'>;
const modelOptions: GptImageModel[] = ['gpt-image-2', 'gpt-image-1.5', 'gpt-image-1', 'gpt-image-1-mini'];
const outputFormatOptions: OutputFormat[] = ['png', 'jpeg', 'webp'];
const qualityOptions: Quality[] = ['auto', 'low', 'medium', 'high'];
const streamModeOptions: ImageStreamMode[] = ['auto', 'stream', 'non_stream'];
function getQualityLabel(quality: Quality, t: Translation): string {
if (quality === 'auto') return t('common.auto');
if (quality === 'low') return t('common.low');
if (quality === 'medium') return t('common.medium');
return t('common.high');
}
function getStreamModeLabel(streamMode: ImageStreamMode, t: Translation): string {
if (streamMode === 'stream') return t('streaming.modeStream');
if (streamMode === 'non_stream') return t('streaming.modeNonStream');
return t('streaming.modeAuto');
}
function getStreamingStrategyLabel(strategy: ProPanelProps['streamingStrategy'], t: Translation): string {
if (strategy === 'auto') return t('upstream.strategyAuto');
if (strategy === 'off') return t('upstream.strategyOff');
if (strategy === 'openai-sse') return t('upstream.strategyOpenAiSse');
if (strategy === 'newapi-keepalive-sse') return t('upstream.strategyKeepaliveSse');
if (strategy === 'responses-sse') return t('upstream.strategyResponsesSse');
if (strategy === 'force-sse') return t('upstream.strategyForceSse');
return t('upstream.serverDefault');
}
function formatResolution(size: SizePreset, model: GptImageModel, t: Translation): string {
const resolution = getPresetDimensions(size, model);
if (resolution) return resolution;
if (size === 'custom') return t('common.custom');
return '1024 px';
}
function ReadonlyField({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className='space-y-1'>
<p className='text-muted-foreground'>{label}</p>
<div className='border-border bg-background/68 flex h-8 items-center rounded-md border px-3 font-medium'>
{children}
</div>
</div>
);
}
export function WorkbenchEasySummary({
model,
streamMode,
outputFormat,
size,
allowStreamingBatch,
enableParallelBatch,
parallelBatchTargetCount,
streamingStrategy,
defaultStreamingStrategy
}: ProPanelProps) {
const { t } = useI18n();
const resolution = formatResolution(size, model, t);
const effectiveStreamingStrategy =
streamingStrategy === 'server-default' ? defaultStreamingStrategy : streamingStrategy;
const parallelBatchVisible = resolveStreamingBatchToggleState({
allowStreamingBatch,
userEnabled: enableParallelBatch,
targetCount: parallelBatchTargetCount,
streamMode,
streamingStrategy: effectiveStreamingStrategy
}).checked;
return (
<div className='grid grid-cols-4 gap-3 text-xs'>
<ReadonlyField label={t('form.model')}>{model}</ReadonlyField>
<ReadonlyField label={t('ux.streaming')}>
<span className='min-w-0 truncate'>
{parallelBatchVisible
? `${getStreamModeLabel(streamMode, t)} / ${t('streaming.parallelBatchEnabled')}`
: getStreamModeLabel(streamMode, t)}
</span>
</ReadonlyField>
<ReadonlyField label={t('form.outputFormat')}>{getImageOutputFormatLabel(outputFormat, t)}</ReadonlyField>
<ReadonlyField label={t('ux.resolution')}>{resolution}</ReadonlyField>
</div>
);
}
function OutputPanel({
outputFormat,
onOutputFormatChange,
quality,
onQualityChange,
disabled,
resolution,
t
}: Pick<ProPanelProps, 'outputFormat' | 'onOutputFormatChange' | 'quality' | 'onQualityChange' | 'disabled'> & {
resolution: string;
t: Translation;
}) {
return (
<TabsContent value='output' className='mt-0 grid grid-cols-4 gap-3 text-xs'>
<div className='space-y-1'>
<p className='text-muted-foreground'>{t('form.outputFormat')}</p>
<Select
value={outputFormat}
onValueChange={(value) => onOutputFormatChange(value as OutputFormat)}
disabled={disabled}>
<SelectTrigger
id='pro-output-format-select'
aria-label={t('form.outputFormat')}
className='h-8 w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent>
{outputFormatOptions.map((format) => (
<SelectItem key={format} value={format}>
{getImageOutputFormatLabel(format, t)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='space-y-1'>
<p className='text-muted-foreground'>{t('form.quality')}</p>
<Select
value={quality}
onValueChange={(value) => onQualityChange(value as Quality)}
disabled={disabled}>
<SelectTrigger id='pro-quality-select' aria-label={t('form.quality')} className='h-8 w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent>
{qualityOptions.map((option) => (
<SelectItem key={option} value={option}>
{getQualityLabel(option, t)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ReadonlyField label={t('ux.colorSpace')}>sRGB</ReadonlyField>
<ReadonlyField label={t('ux.resolution')}>{resolution}</ReadonlyField>
</TabsContent>
);
}
function ModelPanel({
model,
onModelChange,
disabled,
resolution,
t
}: Pick<ProPanelProps, 'model' | 'onModelChange' | 'disabled'> & { resolution: string; t: Translation }) {
return (
<TabsContent value='model' className='mt-0 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-3 text-xs'>
<div className='space-y-1'>
<p className='text-muted-foreground'>{t('form.model')}</p>
<Select
value={model}
onValueChange={(value) => onModelChange(value as GptImageModel)}
disabled={disabled}>
<SelectTrigger id='pro-model-select' aria-label={t('form.model')} className='h-8 w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent>
{modelOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ReadonlyField label={t('ux.resolution')}>{resolution}</ReadonlyField>
</TabsContent>
);
}
function StreamPanel({
streamMode,
onStreamModeChange,
allowStreamingBatch,
enableParallelBatch,
onEnableParallelBatchChange,
parallelBatchTargetCount,
disabled,
strategyLabel,
streamingStrategy,
defaultStreamingStrategy,
t
}: Pick<
ProPanelProps,
| 'streamMode'
| 'onStreamModeChange'
| 'allowStreamingBatch'
| 'enableParallelBatch'
| 'onEnableParallelBatchChange'
| 'parallelBatchTargetCount'
| 'disabled'
| 'streamingStrategy'
| 'defaultStreamingStrategy'
> & {
strategyLabel: string;
t: Translation;
}) {
const effectiveStreamingStrategy =
streamingStrategy === 'server-default' ? defaultStreamingStrategy : streamingStrategy;
const streamingDisabledByStrategy = effectiveStreamingStrategy === 'off';
const parallelBatchToggle = resolveStreamingBatchToggleState({
allowStreamingBatch,
userEnabled: enableParallelBatch,
targetCount: parallelBatchTargetCount,
streamMode,
streamingStrategy: effectiveStreamingStrategy
});
const canEnableParallelBatch = parallelBatchToggle.canEnable;
const parallelBatchChecked = parallelBatchToggle.checked;
const parallelBatchUnavailableKey = parallelBatchToggle.unavailableReasonKey;
return (
<TabsContent value='stream' className='mt-0 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-3 text-xs'>
<div className='space-y-1'>
<p className='text-muted-foreground'>{t('streaming.mode')}</p>
<Select
value={streamMode}
onValueChange={(value) => onStreamModeChange(value as ImageStreamMode)}
disabled={disabled || streamingDisabledByStrategy}>
<SelectTrigger
id='pro-stream-mode-select'
aria-label={t('streaming.mode')}
disabled={disabled || streamingDisabledByStrategy}
className='h-8 w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent>
{streamModeOptions.map((option) => (
<SelectItem key={option} value={option}>
{getStreamModeLabel(option, t)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ReadonlyField label={t('upstream.streamingStrategy')}>{strategyLabel}</ReadonlyField>
<div className='border-border bg-muted/20 col-span-2 flex items-start gap-3 rounded-md border p-3'>
<label
htmlFor='pro-parallel-batch-enabled'
className='-m-1.5 flex min-h-8 min-w-8 cursor-pointer items-start justify-center p-1.5 has-disabled:cursor-not-allowed'>
<Checkbox
id='pro-parallel-batch-enabled'
checked={parallelBatchChecked}
onCheckedChange={(value) => onEnableParallelBatchChange(value === true)}
disabled={disabled || !canEnableParallelBatch}
aria-label={t('streaming.parallelBatch')}
className='mt-0.5'
/>
</label>
<div className='grid gap-1'>
<label
htmlFor='pro-parallel-batch-enabled'
className='text-foreground text-sm leading-none font-medium'>
{t('streaming.parallelBatch')}
</label>
<p className='text-muted-foreground text-xs leading-5'>
{canEnableParallelBatch
? t('streaming.parallelBatchDescription')
: t(parallelBatchUnavailableKey)}
</p>
</div>
</div>
</TabsContent>
);
}
export function WorkbenchProPanel({
defaultTab,
...props
}: ProPanelProps & { defaultTab: 'output' | 'model' | 'stream' | 'route' }) {
const { t } = useI18n();
const resolution = formatResolution(props.size, props.model, t);
const strategyLabel = getStreamingStrategyLabel(props.streamingStrategy, t);
return (
<Tabs defaultValue={defaultTab} className='gap-0'>
<TabsList className='border-border bg-background/45 mb-3 grid h-9 w-full grid-cols-4 rounded-md border p-0'>
<TabsTrigger value='output' className='rounded-md'>
{t('ux.output')}
</TabsTrigger>
<TabsTrigger value='model' className='rounded-md'>
{t('ux.modelRoute')}
</TabsTrigger>
<TabsTrigger value='stream' className='rounded-md'>
{t('ux.streaming')}
</TabsTrigger>
<TabsTrigger value='route' className='rounded-md'>
{t('ux.route')}
</TabsTrigger>
</TabsList>
<div className='px-4 py-3 text-xs'>
<OutputPanel {...props} resolution={resolution} t={t} />
<ModelPanel {...props} resolution={resolution} t={t} />
<StreamPanel {...props} strategyLabel={strategyLabel} t={t} />
<WorkbenchProRoutePanel {...props} />
</div>
</Tabs>
);
}