diff --git a/frontend/src/pages/scenario/PlaygroundPage.tsx b/frontend/src/pages/scenario/PlaygroundPage.tsx index ac19de875..b2ead83d8 100644 --- a/frontend/src/pages/scenario/PlaygroundPage.tsx +++ b/frontend/src/pages/scenario/PlaygroundPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { Box, Button, @@ -25,6 +25,15 @@ const IMAGE_SCENARIO = 'imagegen'; type Quality = 'auto' | 'high' | 'medium' | 'low' | 'standard'; + +const fileToDataURL = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : ''); + reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`)); + reader.readAsDataURL(file); + }); + const extractModelsFromRules = (rules: any[] | undefined | null): string[] => { if (!Array.isArray(rules)) return []; const seen = new Set(); @@ -45,6 +54,8 @@ const PlaygroundPage: React.FC = () => { const [models, setModels] = useState([]); const [model, setModel] = useState(''); const [prompt, setPrompt] = useState(''); + const [imageRefs, setImageRefs] = useState(''); + const [uploadRefs, setUploadRefs] = useState([]); const [size, setSize] = useState('1024x1024'); const [quality, setQuality] = useState('auto'); const [count, setCount] = useState(1); @@ -68,19 +79,38 @@ const PlaygroundPage: React.FC = () => { return () => { cancelled = true; }; }, []); + + const handleRefUpload = useCallback(async (e: ChangeEvent) => { + const files = Array.from(e.target.files ?? []); + if (files.length === 0) return; + + try { + const encoded = await Promise.all(files.map((f) => fileToDataURL(f))); + const valid = encoded.map((v) => v.trim()).filter(Boolean); + setUploadRefs((prev) => Array.from(new Set([...prev, ...valid]))); + } catch (err: any) { + showNotification(err?.message || 'Failed to load reference image', 'error'); + } finally { + e.target.value = ''; + } + }, [showNotification]); + const handleGenerate = useCallback(async () => { if (!prompt.trim() || !model) return; setSending(true); setResults([]); try { const client = await getOpenAIClient(IMAGE_SCENARIO); + const refs = imageRefs.split('\n').map((v) => v.trim()).filter(Boolean); + const mergedRefs = [...refs, ...uploadRefs]; const resp = await client.images.generate({ model, prompt: prompt.trim(), n: count, size: size as any, quality, - }); + ...(mergedRefs.length > 0 ? ({ extra_body: { input_image_refs: mergedRefs } } as any) : {}), + } as any); setResults(resp.data ?? []); } catch (err: any) { const status = err?.status ? `${err.status}: ` : ''; @@ -89,7 +119,7 @@ const PlaygroundPage: React.FC = () => { } finally { setSending(false); } - }, [prompt, model, count, size, quality, showNotification]); + }, [prompt, imageRefs, uploadRefs, model, count, size, quality, showNotification]); const noModels = useMemo(() => models.length === 0, [models]); @@ -186,6 +216,57 @@ const PlaygroundPage: React.FC = () => { disabled={noModels} /> + + setImageRefs(e.target.value)} + disabled={noModels} + /> + + + + + {uploadRefs.length > 0 && ( + + {t('playground.uploadedCount', { defaultValue: "{{count}} uploaded reference image(s)", count: uploadRefs.length })} + + )} + {uploadRefs.length > 0 && ( + + )} + + + {uploadRefs.length > 0 && ( + + {uploadRefs.slice(0, 8).map((src, idx) => ( + + ))} + + )} +