Skip to content

Commit e50a233

Browse files
committed
added examples tab for ui
1 parent 1e1ee0c commit e50a233

File tree

5 files changed

+283
-15
lines changed

5 files changed

+283
-15
lines changed

api/llm/tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _generate_image_core(
3838
print("[TOOL] User exceeded the generation limit of 10 this week.")
3939
return "Failed as user exceeded the max generation limit of 10 this week."
4040

41-
use_sdxl = True
41+
use_sdxl = False # True for testing purposes
4242
if use_sdxl:
4343
input = {
4444
"width": 768,

src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function RootLayout({
88
}) {
99
return (
1010
<html lang="en">
11-
<body>
11+
<body className="bg-black">
1212
{children}
1313
<Analytics />
1414
</body>

src/app/page.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
import { useState, useEffect } from "react";
44
import ChatInterface from "@/ui/chat-interface";
55
import ImageCard from "@/ui/image-card";
6+
import ExamplesDropdown from "@/ui/examples-dropdown";
67
import { sendChatMessage, uploadImageToS3 } from "@/lib/actions";
78
import type { Message, ImageItem } from "@/lib/types";
89

910
const INITIAL_MESSAGE: Message = {
1011
id: "initial-message",
1112
content:
12-
"Hi there. I'm Pablo, your AI image editing assistant. I was named after Pablo Picasso!\
13-
Upload an image and ask how you want your images edited.\
14-
You may select multiple images for me to refer to.",
13+
"Hi there! I'm Pablo, your AI image-editing assistant.\
14+
Upload an image or choose one from above, then tell me how you'd like it edited.\
15+
You can even select multiple images for reference. 🎨✨",
1516
sender: "agent",
1617
timestamp: new Date(), // Use current time instead of static timestamp
1718
};
@@ -122,8 +123,17 @@ export default function Home() {
122123
selectedImageIds.includes(img.id),
123124
);
124125

125-
// Add user message (without image metadata)
126-
const userMessage = createMessage(message, "user");
126+
// Create message content with selected image references
127+
let messageContent = message;
128+
if (selectedImageObjects.length > 0) {
129+
const imageReferences = selectedImageObjects
130+
.map((img) => `📷 **${img.title}**`)
131+
.join("\n");
132+
messageContent = `${message}\n\n**Selected Images:**\n${imageReferences}`;
133+
}
134+
135+
// Add user message with image metadata
136+
const userMessage = createMessage(messageContent, "user");
127137
addMessage(userMessage);
128138

129139
// Set loading state
@@ -187,7 +197,7 @@ export default function Home() {
187197
setImages((prev) => [...prev, uploadedImage]);
188198

189199
// Auto-select the uploaded image
190-
setSelectedImages((prev) => new Set([imageId]));
200+
setSelectedImages(() => new Set([imageId]));
191201

192202
// Scroll to show the new image
193203
setTimeout(scrollToRight, 100);
@@ -277,7 +287,7 @@ export default function Home() {
277287
</h2>
278288
{selectedImages.size === 0 ? (
279289
<p className="text-sm text-gray-400 text-right">
280-
Select images for editing
290+
Select or Scroll
281291
</p>
282292
) : (
283293
<div className="bg-blue-600/20 backdrop-blur-xl rounded-xl px-3 border border-blue-500/30">
@@ -314,23 +324,27 @@ export default function Home() {
314324
<div className="flex justify-center mt-4 gap-16">
315325
<button
316326
onClick={scrollLeft}
317-
className="text-2xl text-gray-400 hover:text-white transition-colors duration-200 font-bold"
327+
className="text-2xl text-gray-100 hover:text-white transition-colors duration-200 font-bold"
318328
aria-label="Scroll left"
319329
>
320330
&lt;
321331
</button>
332+
322333
<button
323334
onClick={scrollRight}
324-
className="text-2xl text-gray-400 hover:text-white transition-colors duration-200 font-bold"
335+
className="text-2xl text-gray-100 hover:text-white transition-colors duration-200 font-bold"
325336
aria-label="Scroll right"
326337
>
327338
&gt;
328339
</button>
329340
</div>
330341
</div>
331342

343+
{/* Examples Dropdown */}
344+
<ExamplesDropdown />
345+
332346
{/* Chat Interface */}
333-
<div className="h-[600px] bg-gray-700/30 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/10 overflow-hidden">
347+
<div className="h-[600px] bg-gray-700/30 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 overflow-hidden">
334348
<ChatInterface
335349
messages={messages}
336350
onSendMessage={handleChatMessage}

src/ui/examples-dropdown.tsx

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
5+
// Data
6+
const examples = [
7+
{
8+
title: "Vintage Black & White",
9+
prompt:
10+
"Convert this photo to a vintage black and white style with film grain texture",
11+
beforeImage:
12+
"https://img-edit-agent-bucket.s3.us-east-1.amazonaws.com/public/picasso_woman.png",
13+
afterImage: "/examples/after-1.jpg",
14+
},
15+
{
16+
title: "Picasso Abstract Style",
17+
prompt:
18+
"Transform this into a Picasso-style abstract portrait with geometric shapes and bold colors",
19+
beforeImage: "/examples/before-2.jpg",
20+
afterImage: "/examples/after-2.jpg",
21+
},
22+
{
23+
title: "Dramatic Sunset Lighting",
24+
prompt:
25+
"Apply a dramatic sunset lighting effect with warm orange and purple tones",
26+
beforeImage: "/examples/before-3.jpg",
27+
afterImage: "/examples/after-3.jpg",
28+
},
29+
];
30+
31+
// Types
32+
type DisplayState = "loading" | "image" | "error";
33+
34+
// Reusable Components
35+
const Icon = ({ className, path }: { className: string; path: string }) => (
36+
<svg
37+
className={className}
38+
fill="none"
39+
stroke="currentColor"
40+
viewBox="0 0 24 24"
41+
>
42+
<path
43+
strokeLinecap="round"
44+
strokeLinejoin="round"
45+
strokeWidth={2}
46+
d={path}
47+
/>
48+
</svg>
49+
);
50+
51+
const ImagePlaceholder = ({
52+
color,
53+
label,
54+
imageUrl,
55+
}: {
56+
color: string;
57+
label: string;
58+
imageUrl?: string;
59+
}) => {
60+
const [displayState, setDisplayState] = useState<DisplayState>("loading");
61+
62+
useEffect(() => {
63+
if (!imageUrl) {
64+
setDisplayState("loading");
65+
return;
66+
}
67+
68+
const img = new Image();
69+
img.onload = () => setDisplayState("image");
70+
img.onerror = () => setDisplayState("error");
71+
img.src = imageUrl;
72+
}, [imageUrl]);
73+
74+
const showImage = displayState === "image";
75+
const showPlaceholder = !showImage;
76+
77+
return (
78+
<div className="space-y-2 sm:space-y-3">
79+
<div className="flex items-center gap-2">
80+
<div
81+
className={`w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-full ${color} flex-shrink-0`}
82+
/>
83+
<span className="text-xs sm:text-sm font-medium text-gray-300">
84+
{label}
85+
</span>
86+
</div>
87+
<div className="relative aspect-[4/3] sm:aspect-square rounded-xl overflow-hidden bg-gradient-to-br from-gray-800 to-gray-900 border border-white/20 min-h-[160px] sm:min-h-[180px]">
88+
{showImage && (
89+
// eslint-disable-next-line @next/next/no-img-element
90+
<img
91+
src={imageUrl}
92+
alt={label}
93+
className="w-full h-full object-cover"
94+
/>
95+
)}
96+
{showPlaceholder && (
97+
<div className="absolute inset-0 flex items-center justify-center p-4">
98+
<div className="text-center">
99+
<Icon
100+
className="w-8 h-8 sm:w-12 sm:h-12 text-gray-600 mx-auto mb-2"
101+
path="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
102+
/>
103+
<span className="text-gray-500 text-xs sm:text-sm block">
104+
{displayState === "error"
105+
? "Image failed to load"
106+
: "Image placeholder"}
107+
</span>
108+
</div>
109+
</div>
110+
)}
111+
</div>
112+
</div>
113+
);
114+
};
115+
116+
const NavigationButton = ({
117+
direction,
118+
onClick,
119+
}: {
120+
direction: "prev" | "next";
121+
onClick: () => void;
122+
}) => (
123+
<button
124+
onClick={onClick}
125+
className="p-2 rounded-full bg-white/10 hover:bg-white/20 border border-white/20 transition-all duration-200 hover:scale-105 flex-shrink-0 cursor-pointer"
126+
>
127+
<Icon
128+
className="w-4 h-4 text-gray-300"
129+
path={direction === "prev" ? "M15 19l-7-7 7-7" : "M9 5l7 7-7 7"}
130+
/>
131+
</button>
132+
);
133+
134+
const DotIndicator = ({
135+
index,
136+
current,
137+
onClick,
138+
}: {
139+
index: number;
140+
current: number;
141+
onClick: () => void;
142+
}) => (
143+
<button
144+
onClick={onClick}
145+
className={`w-2 h-2 rounded-full transition-all duration-200 cursor-pointer ${
146+
index === current
147+
? "bg-blue-500 scale-125"
148+
: "bg-gray-500 hover:bg-gray-400"
149+
}`}
150+
/>
151+
);
152+
153+
// Main Component
154+
export default function ExamplesDropdown() {
155+
const [isOpen, setIsOpen] = useState(false);
156+
const [currentExample, setCurrentExample] = useState(0);
157+
158+
const nextExample = () =>
159+
setCurrentExample((current) => (current + 1) % examples.length);
160+
const prevExample = () =>
161+
setCurrentExample((current) =>
162+
current === 0 ? examples.length - 1 : current - 1,
163+
);
164+
165+
const currentExampleData = examples[currentExample];
166+
167+
return (
168+
<div className="bg-white/5 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 overflow-hidden">
169+
{/* Header */}
170+
<button
171+
onClick={() => setIsOpen(!isOpen)}
172+
className="w-full p-6 flex items-center justify-between hover:bg-white/5 transition-all duration-300 group cursor-pointer"
173+
>
174+
<div className="flex items-center gap-3">
175+
{/* <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-900 to-blue-600 flex items-center justify-center">
176+
<Icon
177+
className="w-4 h-4 text-white"
178+
path="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
179+
/>
180+
</div> */}
181+
<span className="text-lg font-semibold text-gray-200">
182+
View Examples
183+
</span>
184+
</div>
185+
<div
186+
className={`transform transition-transform duration-300 ${isOpen ? "rotate-180" : "rotate-0"}`}
187+
>
188+
<Icon
189+
className="w-5 h-5 text-gray-100 group-hover:text-white transition-colors"
190+
path="M19 9l-7 7-7-7"
191+
/>
192+
</div>
193+
</button>
194+
195+
{/* Content */}
196+
<div
197+
className={`overflow-hidden transition-all duration-500 ease-in-out ${isOpen ? "max-h-[3000px] opacity-100" : "max-h-0 opacity-0"}`}
198+
>
199+
<div className="p-4 sm:p-6 space-y-4 sm:space-y-6">
200+
{/* Navigation */}
201+
<div className="flex items-center justify-between">
202+
<NavigationButton direction="prev" onClick={prevExample} />
203+
<div className="text-center flex-1 px-4">
204+
<h3 className="text-lg sm:text-xl font-semibold text-gray-200 mb-2">
205+
{currentExampleData.title}
206+
</h3>
207+
<div className="flex justify-center gap-2">
208+
{examples.map((_, index) => (
209+
<DotIndicator
210+
key={index}
211+
index={index}
212+
current={currentExample}
213+
onClick={() => setCurrentExample(index)}
214+
/>
215+
))}
216+
</div>
217+
</div>
218+
<NavigationButton direction="next" onClick={nextExample} />
219+
</div>
220+
221+
{/* Images */}
222+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
223+
<ImagePlaceholder
224+
color="bg-red-500"
225+
label="Before"
226+
imageUrl={currentExampleData.beforeImage}
227+
/>
228+
<ImagePlaceholder
229+
color="bg-green-500"
230+
label="After"
231+
imageUrl={currentExampleData.afterImage}
232+
/>
233+
</div>
234+
235+
{/* Prompt */}
236+
<div className="space-y-2 sm:space-y-3">
237+
<div className="flex items-center gap-2">
238+
<Icon
239+
className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-blue-400 flex-shrink-0"
240+
path="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
241+
/>
242+
<span className="text-xs sm:text-sm font-medium text-gray-300">
243+
Prompt Used
244+
</span>
245+
</div>
246+
<div className="bg-black/20 backdrop-blur-xl rounded-xl p-3 sm:p-4 border border-white/10">
247+
<p className="text-gray-200 leading-relaxed text-sm sm:text-base">
248+
&ldquo;{currentExampleData.prompt}&rdquo;
249+
</p>
250+
</div>
251+
</div>
252+
</div>
253+
</div>
254+
</div>
255+
);
256+
}

src/ui/image-preview.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,21 @@ interface ImagePreviewProps {
88

99
export default function ImagePreview({ imageUrl }: ImagePreviewProps) {
1010
const [imageError, setImageError] = useState(false);
11-
const [imageLoaded, setImageLoaded] = useState(false);
1211

1312
const handleImageLoad = () => {
14-
setImageLoaded(true);
1513
setImageError(false);
1614
console.log("Image loaded successfully:", imageUrl);
1715
};
1816

1917
const handleImageError = () => {
2018
setImageError(true);
21-
setImageLoaded(false);
2219
console.error("Image failed to load:", imageUrl);
2320
};
2421

2522
return (
2623
<div className="flex justify-center items-center w-full aspect-square border rounded-xl inset-0 bg-gradient-to-t from-gray-300/30 to-gray-400/30 shadow-sm">
2724
{imageUrl && imageUrl.trim() !== "" && !imageError ? (
25+
// eslint-disable-next-line @next/next/no-img-element
2826
<img
2927
src={imageUrl}
3028
alt="Generated image"

0 commit comments

Comments
 (0)