From 71be2a2b4e975e63c8a1fef6d307f1d43368dfb9 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 23 Jun 2026 04:43:41 +0800 Subject: [PATCH] feat: save full generation params in image history with info popover --- .../components/image-results.tsx | 167 +++++++++++++++--- .../src/features/image-playground/index.tsx | 37 +++- .../src/features/image-playground/types.ts | 12 +- web/default/src/i18n/locales/zh.json | 5 +- 4 files changed, 197 insertions(+), 24 deletions(-) diff --git a/web/default/src/features/image-playground/components/image-results.tsx b/web/default/src/features/image-playground/components/image-results.tsx index 522e9cdb9..3a10f086d 100644 --- a/web/default/src/features/image-playground/components/image-results.tsx +++ b/web/default/src/features/image-playground/components/image-results.tsx @@ -1,13 +1,19 @@ -import { Download, RotateCw, ZoomIn, Trash2, X, Clock, AlertCircle } from 'lucide-react' +import { Download, RotateCw, ZoomIn, Trash2, X, Clock, AlertCircle, Info, Copy, ArrowRight } from 'lucide-react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' import { useState } from 'react' -import type { GeneratedImage } from '../types' +import type { GeneratedImage, GeneratedImageParams } from '../types' type ImageResultsProps = { images: GeneratedImage[] @@ -17,6 +23,7 @@ type ImageResultsProps = { onDeleteImage?: (id: string) => void onClearAll?: () => void onClearFailed?: () => void + onFillParams?: (params: GeneratedImageParams) => void } function formatDuration(ms: number): string { @@ -24,6 +31,126 @@ function formatDuration(ms: number): string { return `${(ms / 1000).toFixed(1)}s` } +function formatParamsText(params: GeneratedImageParams): string { + const lines = [ + `Prompt: ${params.prompt}`, + `Model: ${params.model}`, + `Group: ${params.group}`, + `Size: ${params.size}`, + `Quality: ${params.quality}`, + `Style: ${params.style}`, + `Count: ${params.n}`, + ] + return lines.join('\n') +} + +function ImageParamsPopover({ image, onFillParams }: { image: GeneratedImage; onFillParams?: (params: GeneratedImageParams) => void }) { + const { t } = useTranslation() + const params = image.params + if (!params) return null + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(formatParamsText(params)) + toast.success(t('Copied')) + } catch { + toast.error(t('Copy failed')) + } + } + + const handleFill = () => { + onFillParams?.(params) + } + + return ( + + + + + e.preventDefault()} + > +
+
+ {t('Generation Parameters')} +
+ + {onFillParams && ( + + )} +
+
+
+
+ {t('Prompt')}: + {params.prompt} +
+
+
+ {t('Model')}: + {params.model} +
+
+ {t('Group')}: + {params.group} +
+
+ {t('Size')}: + {params.size} +
+
+ {t('Quality')}: + {params.quality} +
+
+ {t('Style')}: + {params.style} +
+
+ {t('Count')}: + {params.n} +
+
+ {image.revisedPrompt && ( +
+ {t('Revised Prompt')}: + {image.revisedPrompt} +
+ )} +
+
+
+
+ ) +} + export function ImageResults({ images, isGenerating, @@ -32,6 +159,7 @@ export function ImageResults({ onDeleteImage, onClearAll, onClearFailed, + onFillParams, }: ImageResultsProps) { const { t } = useTranslation() const [previewImage, setPreviewImage] = useState(null) @@ -178,7 +306,7 @@ export function ImageResults({ <> {image.revisedPrompt {/* Duration badge */} @@ -188,23 +316,22 @@ export function ImageResults({ {formatDuration(image.durationMs)} )} - {/* Prompt badge */} - {image.prompt && ( - - {image.prompt} - - )} - {/* Delete button */} - {onDeleteImage && ( - - )} + {/* Top-right buttons: info + delete */} +
+ {image.params && ( + + )} + {onDeleteImage && ( + + )} +
{/* Hover actions */}
diff --git a/web/default/src/features/image-playground/index.tsx b/web/default/src/features/image-playground/index.tsx index 2496c61e1..40fa6a5bd 100644 --- a/web/default/src/features/image-playground/index.tsx +++ b/web/default/src/features/image-playground/index.tsx @@ -33,6 +33,7 @@ import { } from './types' import type { GeneratedImage, + GeneratedImageParams, GroupOption, ImagePlaygroundConfig, ModelOption, @@ -295,6 +296,15 @@ export function ImagePlayground() { } const durationMs = performance.now() - startTimeRef.current + const generationParams = { + prompt: prompt.trim(), + model: config.model, + group: config.group, + size: config.size, + quality: config.quality, + style: config.style, + n: generationCount, + } const newImages: GeneratedImage[] = (response.data || []).map( (item, index) => ({ id: pendingImages[index]?.id || `${Date.now()}-${index}`, @@ -303,7 +313,7 @@ export function ImagePlayground() { (item.b64_json ? `data:image/png;base64,${item.b64_json}` : ''), - prompt: prompt.trim(), + params: generationParams, revisedPrompt: item.revised_prompt, status: 'success' as const, durationMs, @@ -390,6 +400,15 @@ export function ImagePlayground() { if (response.data && response.data.length > 0) { const item = response.data[0] + const retryParams = { + prompt: prompt.trim(), + model: config.model, + group: config.group, + size: config.size, + quality: config.quality, + style: config.style, + n: 1, + } setImages((prev) => prev.map((img) => img.id === id @@ -400,7 +419,7 @@ export function ImagePlayground() { (item.b64_json ? `data:image/png;base64,${item.b64_json}` : ''), - prompt: prompt.trim(), + params: retryParams, revisedPrompt: item.revised_prompt, status: 'success' as const, durationMs, @@ -441,6 +460,19 @@ export function ImagePlayground() { setImages((prev) => prev.filter((img) => img.status !== 'failed')) }, []) + const handleFillParams = useCallback((params: GeneratedImageParams) => { + setPrompt(params.prompt) + setConfig((prev) => ({ + ...prev, + model: params.model, + group: params.group, + size: params.size, + quality: params.quality, + style: params.style, + n: params.n, + })) + }, []) + return (
{/* Header */} @@ -573,6 +605,7 @@ export function ImagePlayground() { onDeleteImage={handleDeleteImage} onClearAll={handleClearAll} onClearFailed={handleClearFailed} + onFillParams={handleFillParams} />
diff --git a/web/default/src/features/image-playground/types.ts b/web/default/src/features/image-playground/types.ts index db9d99de5..92de39bc0 100644 --- a/web/default/src/features/image-playground/types.ts +++ b/web/default/src/features/image-playground/types.ts @@ -29,10 +29,20 @@ export interface ImageGenerationResponse { export type GeneratedImageStatus = 'pending' | 'success' | 'failed' +export interface GeneratedImageParams { + prompt: string + model: string + group: string + size: string + quality: string + style: string + n: number +} + export interface GeneratedImage { id: string url: string - prompt?: string + params?: GeneratedImageParams revisedPrompt?: string width?: number height?: number diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 133c4b4d9..16e04db64 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -4900,6 +4900,9 @@ "Upload": "上传", "Ref": "参考", "Remove": "移除", - "Click or drag to upload reference images": "点击或拖拽上传参考图" + "Click or drag to upload reference images": "点击或拖拽上传参考图", + "Generation Parameters": "生成参数", + "Reuse parameters": "填入参数", + "Revised Prompt": "修订提示词" } }