Skip to content

Commit 1c32b8f

Browse files
committed
Function: Change Image Generation Page Layout
1 parent e51152e commit 1c32b8f

File tree

4 files changed

+366
-68
lines changed

4 files changed

+366
-68
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react';
2+
import { ImageGenerationResult } from '../../types/image';
3+
import { MessageContent } from '../../types/chat';
4+
5+
interface ImageGenerateHistoryItemProps {
6+
imageResult: ImageGenerationResult;
7+
}
8+
9+
const ImageGenerateHistoryItem: React.FC<ImageGenerateHistoryItemProps> = ({
10+
imageResult,
11+
}) => {
12+
13+
// Function to render image grid based on number of images and aspect ratio
14+
const renderImageGrid = (images: MessageContent[]) => {
15+
if (images.length === 0) return null;
16+
17+
// Convert aspect ratio string (e.g. "16:9") to calculate best layout
18+
const [widthRatio, heightRatio] = imageResult.aspectRatio.split(':').map(Number);
19+
const isWide = widthRatio > heightRatio;
20+
21+
// Grid layout classes based on number of images
22+
let gridClass = "grid-cols-1";
23+
24+
if (images.length === 2) {
25+
gridClass = isWide ? "grid-cols-2" : "grid-cols-1 grid-rows-2";
26+
} else if (images.length === 3) {
27+
gridClass = "grid-cols-3";
28+
} else if (images.length === 4) {
29+
gridClass = "grid-cols-2 grid-rows-2";
30+
}
31+
32+
return (
33+
<div className={`grid gap-2 ${gridClass}`}>
34+
{images.map((image, index) => (
35+
<div
36+
key={index}
37+
className="overflow-hidden rounded-lg"
38+
style={{
39+
aspectRatio: imageResult.aspectRatio.replace(':', '/'),
40+
}}
41+
>
42+
<img
43+
src={image.content}
44+
alt={`${imageResult.prompt} (${index + 1})`}
45+
className="object-cover w-full h-full"
46+
/>
47+
</div>
48+
))}
49+
</div>
50+
);
51+
};
52+
53+
return (
54+
<div className="mb-6 overflow-hidden border rounded-lg shadow-sm image-result-item">
55+
<div className="image-result-container">
56+
{renderImageGrid(imageResult.images)}
57+
</div>
58+
59+
<div className="p-4 border-t bg-gray-50">
60+
<div className="mb-2">
61+
<div className="flex items-start justify-between">
62+
<div className="flex-1 mr-4">
63+
<p className="mb-1 text-sm font-medium text-gray-900 truncate">{imageResult.prompt}</p>
64+
65+
<div className="flex flex-wrap gap-2 mt-2">
66+
<span className="px-2 py-1 text-xs bg-gray-100 rounded-full">
67+
{imageResult.model}
68+
</span>
69+
<span className="px-2 py-1 text-xs bg-gray-100 rounded-full">
70+
{imageResult.provider}
71+
</span>
72+
<span className="px-2 py-1 text-xs bg-gray-100 rounded-full">
73+
{imageResult.aspectRatio}
74+
</span>
75+
<span className="px-2 py-1 text-xs bg-gray-100 rounded-full">
76+
Seed: {imageResult.seed}
77+
</span>
78+
</div>
79+
</div>
80+
</div>
81+
</div>
82+
</div>
83+
</div>
84+
);
85+
};
86+
87+
export default ImageGenerateHistoryItem;

src/components/image/ImageGenerateResult.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/components/pages/ImageGenerationPage.tsx

Lines changed: 112 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,52 @@ import { ChevronDown, RefreshCw, Settings, Zap } from "lucide-react";
77
import { useTranslation } from "react-i18next";
88
import { AIService } from "../../services/ai-service";
99
import { OPENAI_PROVIDER_NAME } from "../../services/providers/openai-service";
10+
import { ImageGenerationManager, ImageGenerationStatus, ImageGenerationHandler } from "../../services/image-generation-handler";
11+
import { DatabaseIntegrationService } from "../../services/database-integration";
12+
import { ImageGenerationResult } from "../../types/image";
13+
import ImageGenerateHistoryItem from "../image/ImageGenerateHistoryItem";
1014

1115
export const ImageGenerationPage = () => {
1216
const { t } = useTranslation();
1317
const [prompt, setPrompt] = useState("");
14-
const [isGenerating, setIsGenerating] = useState(false);
15-
const [imageResult, setImageResult] = useState<string | null>(null);
1618
const [error, setError] = useState<Error | null>(null);
1719
const [isApiKeyMissing, setIsApiKeyMissing] = useState(true);
1820
const [aspectRatio, setAspectRatio] = useState("1:1");
1921
const [imageCount, setImageCount] = useState(1);
2022
const [randomSeed, setRandomSeed] = useState(
2123
Math.floor(Math.random() * 1000000).toString()
2224
);
25+
const [generationResults, setGenerationResults] = useState<Map<string, ImageGenerationHandler>>(
26+
new Map()
27+
);
28+
const [historyResults, setHistoryResults] = useState<ImageGenerationResult[]>([]);
29+
const [isLoadingHistory, setIsLoadingHistory] = useState(true);
30+
31+
// Initialize image generation manager
32+
useEffect(() => {
33+
const imageManager = ImageGenerationManager.getInstance();
34+
35+
// Register for updates on generation status changes
36+
imageManager.setUpdateCallback((handlers) => {
37+
setGenerationResults(new Map(handlers));
38+
});
39+
40+
// Load history from database
41+
const loadHistoryResults = async () => {
42+
try {
43+
setIsLoadingHistory(true);
44+
const dbService = DatabaseIntegrationService.getInstance();
45+
await dbService.initialize();
46+
// TODO: Implement loading image history from database when available
47+
setIsLoadingHistory(false);
48+
} catch (error) {
49+
console.error('Error loading image history:', error);
50+
setIsLoadingHistory(false);
51+
}
52+
};
53+
54+
loadHistoryResults();
55+
}, []);
2356

2457
// Check if API key is available
2558
useEffect(() => {
@@ -40,7 +73,6 @@ export const ImageGenerationPage = () => {
4073
const handleGenerateImage = async () => {
4174
if (!prompt.trim()) return;
4275

43-
setIsGenerating(true);
4476
setError(null);
4577

4678
try {
@@ -51,6 +83,20 @@ export const ImageGenerationPage = () => {
5183
throw new Error("OpenAI service not available");
5284
}
5385

86+
// Create a new generation handler
87+
const imageManager = ImageGenerationManager.getInstance();
88+
const handler = imageManager.createHandler({
89+
prompt: prompt,
90+
seed: randomSeed,
91+
number: imageCount,
92+
aspectRatio: aspectRatio,
93+
provider: OPENAI_PROVIDER_NAME,
94+
model: "dall-e-3",
95+
});
96+
97+
// Set status to generating
98+
handler.setGenerating();
99+
54100
// Map aspect ratio to size dimensions
55101
const sizeMap: Record<string, `${number}x${number}`> = {
56102
"1:1": "1024x1024",
@@ -68,23 +114,28 @@ export const ImageGenerationPage = () => {
68114
style: "vivid"
69115
});
70116

71-
// Set the result image
117+
// Process the result
72118
if (images && images.length > 0) {
73-
// Check if the image is already a full data URL
74-
const base64Data = images[0] as string;
75-
if (base64Data.startsWith('data:image')) {
76-
setImageResult(base64Data);
77-
} else {
78-
// If it's just a base64 string without the data URI prefix, add it
79-
setImageResult(`data:image/png;base64,${base64Data}`);
80-
}
119+
// Convert image data to full URLs if needed
120+
const processedImages = images.map(img => {
121+
const base64Data = img as string;
122+
if (base64Data.startsWith('data:image')) {
123+
return base64Data;
124+
} else {
125+
return `data:image/png;base64,${base64Data}`;
126+
}
127+
});
128+
129+
// Update the handler with successful results
130+
handler.setSuccess(processedImages);
131+
132+
// Generate new seed for next generation
133+
generateNewSeed();
81134
} else {
82135
throw new Error("No images generated");
83136
}
84137
} catch (err) {
85138
setError(err as Error);
86-
} finally {
87-
setIsGenerating(false);
88139
}
89140
};
90141

@@ -93,6 +144,25 @@ export const ImageGenerationPage = () => {
93144
setRandomSeed(Math.floor(Math.random() * 1000000).toString());
94145
};
95146

147+
// Get all results to display in order (active generations first, then history)
148+
const getAllResults = () => {
149+
const handlerResults = Array.from(generationResults.values())
150+
.map(handler => handler.getResult())
151+
.sort((a, b) => {
152+
// Prioritize active generations first
153+
if (a.status === ImageGenerationStatus.GENERATING && b.status !== ImageGenerationStatus.GENERATING) {
154+
return -1;
155+
}
156+
if (b.status === ImageGenerationStatus.GENERATING && a.status !== ImageGenerationStatus.GENERATING) {
157+
return 1;
158+
}
159+
// Then sort by most recent first (assuming imageResultId is a UUID with timestamp components)
160+
return b.imageResultId.localeCompare(a.imageResultId);
161+
});
162+
163+
return [...handlerResults, ...historyResults];
164+
};
165+
96166
return (
97167
<div className="flex flex-col w-full h-full bg-white">
98168
{isApiKeyMissing && (
@@ -121,19 +191,19 @@ export const ImageGenerationPage = () => {
121191
<div className="flex flex-row gap-2 p-2 border border-gray-300 rounded-lg shadow-sm">
122192
<button
123193
onClick={handleGenerateImage}
124-
disabled={isGenerating || !prompt.trim() || isApiKeyMissing}
194+
disabled={!prompt.trim() || isApiKeyMissing}
125195
className="px-4 py-2.5 text-nowrap flex flex-row gap-1 text-white text-center confirm-btn"
126196
>
127197
<Settings></Settings>
128198
{t("imageGeneration.settingsButton")}
129199
</button>
130200
<button
131201
onClick={handleGenerateImage}
132-
disabled={isGenerating || !prompt.trim() || isApiKeyMissing}
202+
disabled={!prompt.trim() || isApiKeyMissing}
133203
className="px-4 py-2.5 text-nowrap flex flex-row gap-1 text-white text-center confirm-btn"
134204
>
135205
<Zap></Zap>
136-
{isGenerating
206+
{Array.from(generationResults.values()).some(h => h.getStatus() === ImageGenerationStatus.GENERATING)
137207
? t("imageGeneration.generating")
138208
: t("imageGeneration.generateButton")}
139209
</button>
@@ -154,41 +224,34 @@ export const ImageGenerationPage = () => {
154224
</div>
155225
)}
156226

157-
{/* Loading indicator */}
158-
{isGenerating && (
159-
<div className="flex items-center justify-center h-64 rounded-lg bg-gray-50">
160-
<div className="flex flex-col items-center">
161-
<div className="w-10 h-10 border-4 rounded-full border-primary-300 border-t-primary-600 animate-spin"></div>
162-
<p className="mt-4 text-gray-600">
163-
{t("imageGeneration.generating")}
164-
</p>
165-
</div>
166-
</div>
167-
)}
168-
169227
{/* Results display */}
170-
{imageResult && !isGenerating && (
171-
<div className="grid grid-cols-1 gap-4">
172-
<div className="overflow-hidden border rounded-lg image-result-area">
173-
<img
174-
src={imageResult}
175-
alt="Generated from AI"
176-
className="object-contain w-full"
177-
/>
228+
<div className="grid grid-cols-1 gap-4">
229+
{/* Active generations and history */}
230+
{getAllResults().map(result => (
231+
<ImageGenerateHistoryItem
232+
key={result.imageResultId}
233+
imageResult={result}
234+
/>
235+
))}
236+
237+
{/* Loading indicator for history */}
238+
{isLoadingHistory && (
239+
<div className="flex items-center justify-center h-24 rounded-lg bg-gray-50">
240+
<div className="w-8 h-8 border-4 rounded-full border-primary-300 border-t-primary-600 animate-spin"></div>
178241
</div>
179-
</div>
180-
)}
181-
182-
{/* Placeholder when no results */}
183-
{!imageResult && !isGenerating && (
184-
<div className="flex items-center justify-center h-64 rounded-lg image-generation-result-placeholder">
185-
<div className="text-center">
186-
<p className="text-gray-500">
187-
{t("imageGeneration.placeholderText")}
188-
</p>
242+
)}
243+
244+
{/* Placeholder when no results */}
245+
{getAllResults().length === 0 && !isLoadingHistory && (
246+
<div className="flex items-center justify-center h-64 rounded-lg image-generation-result-placeholder">
247+
<div className="text-center">
248+
<p className="text-gray-500">
249+
{t("imageGeneration.placeholderText")}
250+
</p>
251+
</div>
189252
</div>
190-
</div>
191-
)}
253+
)}
254+
</div>
192255
</div>
193256

194257
{/* Right side - Results */}

0 commit comments

Comments
 (0)