Files changed (2) hide show
  1. app/api/image/route.ts +174 -59
  2. app/page.tsx +38 -0
app/api/image/route.ts CHANGED
@@ -1,8 +1,14 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
3
- import { generateText, CoreMessage, MessagePart } from "ai";
4
 
5
  const MODEL_ID = "gemini-2.0-flash-exp";
 
 
 
 
 
 
6
 
7
  interface ImageHistoryPart {
8
  text?: string;
@@ -10,7 +16,7 @@ interface ImageHistoryPart {
10
  }
11
 
12
  interface HistoryMessage {
13
- role: "user" | "assistant";
14
  parts: ImageHistoryPart[];
15
  }
16
 
@@ -18,12 +24,119 @@ interface RequestBody {
18
  prompt: string;
19
  image?: string;
20
  history?: HistoryMessage[];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
  export async function POST(req: NextRequest) {
24
  try {
25
  const body: RequestBody = await req.json();
26
- const { prompt, image: inputImage, history } = body;
27
 
28
  if (!prompt) {
29
  return NextResponse.json(
@@ -32,7 +145,10 @@ export async function POST(req: NextRequest) {
32
  );
33
  }
34
 
35
- const parts: MessagePart[] = [{ type: "text", text: prompt }];
 
 
 
36
 
37
  if (inputImage) {
38
  if (!inputImage.startsWith("data:")) {
@@ -42,68 +158,67 @@ export async function POST(req: NextRequest) {
42
  );
43
  }
44
 
45
- const [meta, base64] = inputImage.split(",");
46
- const mimeType = meta.includes("png") ? "image/png" : "image/jpeg";
47
-
48
  parts.push({
49
- type: "input_image",
50
- image: {
51
- data: base64,
52
- mimeType,
53
- },
54
  });
55
  }
56
 
57
- const formattedHistory: CoreMessage[] =
58
- history?.map((msg) => ({
59
- role: msg.role,
60
- parts: msg.parts.map<MessagePart>((p) => {
61
- if (p.text) {
62
- return { type: "text", text: p.text };
63
- }
64
- if (p.image) {
65
- const [meta, base64] = p.image.split(",");
66
- const mimeType = meta.includes("png")
67
- ? "image/png"
68
- : "image/jpeg";
69
-
70
- return {
71
- type: "input_image",
72
- image: {
73
- data: base64,
74
- mimeType,
75
- },
76
- };
77
- }
78
- return { type: "text", text: "" };
79
- }),
80
- })) ?? [];
81
-
82
- const result = await generateText({
83
- model: GoogleGenerativeAI(MODEL_ID, {
84
- responseModalities: ["text", "image"],
85
- }),
86
- messages: [...formattedHistory, { role: "user", parts }],
87
- });
88
-
89
- const textResponse = result.text;
90
-
91
- let imageBase64: string | null = null;
92
- let mimeType = "image/png";
93
-
94
- for (const part of result.parts ?? []) {
95
- if (part.type === "output_image" && part.image) {
96
- imageBase64 = part.image.data;
97
- mimeType = part.image.mimeType ?? "image/png";
 
 
98
  }
99
  }
100
 
101
- return NextResponse.json({
102
- description: textResponse ?? null,
103
- image: imageBase64
104
- ? `data:${mimeType};base64,${imageBase64}`
105
- : null,
106
- });
 
 
107
  } catch (err) {
108
  const message =
109
  err instanceof Error ? err.message : "Unknown error";
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
3
+ import { generateText } from "ai";
4
 
5
  const MODEL_ID = "gemini-2.0-flash-exp";
6
+ const MODEL_IDS = [
7
+ MODEL_ID,
8
+ "gemini-2.5-flash-image",
9
+ "gemini-3.1-flash-image-preview",
10
+ "gemini-3-pro-image-preview",
11
+ ];
12
 
13
  interface ImageHistoryPart {
14
  text?: string;
 
16
  }
17
 
18
  interface HistoryMessage {
19
+ role: "user" | "model" | "assistant";
20
  parts: ImageHistoryPart[];
21
  }
22
 
 
24
  prompt: string;
25
  image?: string;
26
  history?: HistoryMessage[];
27
+ model?: string;
28
+ }
29
+
30
+ function getModelCandidates(model?: string) {
31
+ const selectedModel = model && MODEL_IDS.includes(model) ? model : MODEL_ID;
32
+ return [selectedModel, ...MODEL_IDS.filter((id) => id !== selectedModel)];
33
+ }
34
+
35
+ function getMimeType(dataUrl: string) {
36
+ return dataUrl.match(/^data:([^;]+);base64,/)?.[1] ?? "image/png";
37
+ }
38
+
39
+ function getBase64Data(dataUrl: string) {
40
+ return dataUrl.includes(",") ? dataUrl.split(",")[1] : dataUrl;
41
+ }
42
+
43
+ function toDataUrl(data: unknown, mediaType = "image/png") {
44
+ if (typeof data === "string") {
45
+ if (data.startsWith("data:")) {
46
+ return data;
47
+ }
48
+ if (data.startsWith("http://") || data.startsWith("https://")) {
49
+ return data;
50
+ }
51
+ return `data:${mediaType};base64,${data}`;
52
+ }
53
+
54
+ if (data instanceof Uint8Array) {
55
+ return `data:${mediaType};base64,${Buffer.from(data).toString("base64")}`;
56
+ }
57
+
58
+ if (data instanceof ArrayBuffer) {
59
+ return `data:${mediaType};base64,${Buffer.from(data).toString("base64")}`;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function extractImage(result: any) {
66
+ for (const file of result.files ?? []) {
67
+ const mediaType = file.mediaType ?? file.mimeType ?? "image/png";
68
+
69
+ if (!mediaType.startsWith("image/")) {
70
+ continue;
71
+ }
72
+
73
+ const image =
74
+ toDataUrl(file.base64, mediaType) ??
75
+ toDataUrl(file.data, mediaType) ??
76
+ toDataUrl(file.uint8Array, mediaType) ??
77
+ toDataUrl(file.url, mediaType);
78
+
79
+ if (image) {
80
+ return image;
81
+ }
82
+ }
83
+
84
+ for (const part of result.parts ?? []) {
85
+ const mediaType = part.image?.mediaType ?? part.image?.mimeType ?? "image/png";
86
+ const image =
87
+ toDataUrl(part.image?.data, mediaType) ??
88
+ toDataUrl(part.image?.base64, mediaType) ??
89
+ toDataUrl(part.image?.url, mediaType);
90
+
91
+ if (part.type === "output_image" && image) {
92
+ return image;
93
+ }
94
+
95
+ if ((part.type === "image" || part.type === "file") && mediaType.startsWith("image/") && image) {
96
+ return image;
97
+ }
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ function formatHistory(history?: HistoryMessage[]) {
104
+ return (
105
+ history
106
+ ?.map((msg) => {
107
+ const role = msg.role === "user" ? "user" : "assistant";
108
+ const parts = msg.parts
109
+ .map((p) => {
110
+ if (p.text) {
111
+ return { type: "text", text: p.text };
112
+ }
113
+
114
+ if (p.image && role === "user") {
115
+ return {
116
+ type: "image",
117
+ image: getBase64Data(p.image),
118
+ mediaType: getMimeType(p.image),
119
+ };
120
+ }
121
+
122
+ return null;
123
+ })
124
+ .filter(Boolean);
125
+
126
+ if (parts.length === 0) {
127
+ return null;
128
+ }
129
+
130
+ return { role, content: parts };
131
+ })
132
+ .filter(Boolean) ?? []
133
+ );
134
  }
135
 
136
  export async function POST(req: NextRequest) {
137
  try {
138
  const body: RequestBody = await req.json();
139
+ const { prompt, image: inputImage, history, model } = body;
140
 
141
  if (!prompt) {
142
  return NextResponse.json(
 
145
  );
146
  }
147
 
148
+ const google = createGoogleGenerativeAI({
149
+ apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY,
150
+ });
151
+ const parts: any[] = [{ type: "text", text: prompt }];
152
 
153
  if (inputImage) {
154
  if (!inputImage.startsWith("data:")) {
 
158
  );
159
  }
160
 
 
 
 
161
  parts.push({
162
+ type: "image",
163
+ image: getBase64Data(inputImage),
164
+ mediaType: getMimeType(inputImage),
 
 
165
  });
166
  }
167
 
168
+ const formattedHistory = formatHistory(history);
169
+ const modelCandidates = getModelCandidates(model);
170
+ const attemptedModels: { model: string; error?: string }[] = [];
171
+
172
+ for (const modelId of modelCandidates) {
173
+ try {
174
+ const result = await generateText({
175
+ model: google(modelId),
176
+ messages: [...formattedHistory, { role: "user", content: parts }] as any,
177
+ providerOptions: {
178
+ google: {
179
+ responseModalities: ["TEXT", "IMAGE"],
180
+ },
181
+ },
182
+ });
183
+
184
+ const image = extractImage(result);
185
+
186
+ if (!image) {
187
+ attemptedModels.push({ model: modelId, error: "No image returned" });
188
+ continue;
189
+ }
190
+
191
+ const fallback = modelId !== modelCandidates[0];
192
+
193
+ return NextResponse.json({
194
+ description: result.text ?? null,
195
+ image,
196
+ model: modelId,
197
+ fallback,
198
+ attemptedModels,
199
+ notice: fallback
200
+ ? {
201
+ type: "info",
202
+ message: `${modelCandidates[0]} did not return a usable image. The result was generated with ${modelId} instead.`,
203
+ }
204
+ : null,
205
+ });
206
+ } catch (err) {
207
+ attemptedModels.push({
208
+ model: modelId,
209
+ error: err instanceof Error ? err.message : "Unknown error",
210
+ });
211
  }
212
  }
213
 
214
+ return NextResponse.json(
215
+ {
216
+ error: "Failed to generate image",
217
+ details: attemptedModels.map((item) => `${item.model}: ${item.error}`).join("\n"),
218
+ attemptedModels,
219
+ },
220
+ { status: 500 }
221
+ );
222
  } catch (err) {
223
  const message =
224
  err instanceof Error ? err.message : "Unknown error";
app/page.tsx CHANGED
@@ -7,13 +7,22 @@ import { ImageIcon, Wand2, ExternalLink } from "lucide-react";
7
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
  import { HistoryItem } from "@/lib/types";
9
 
 
 
 
 
 
 
 
10
  export default function Home() {
11
  const [image, setImage] = useState<string | null>(null);
12
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
13
  const [description, setDescription] = useState<string | null>(null);
14
  const [loading, setLoading] = useState(false);
15
  const [error, setError] = useState<string | null>(null);
 
16
  const [history, setHistory] = useState<HistoryItem[]>([]);
 
17
 
18
  const handleImageSelect = (imageData: string) => {
19
  setImage(imageData || null);
@@ -23,6 +32,7 @@ export default function Home() {
23
  try {
24
  setLoading(true);
25
  setError(null);
 
26
 
27
  // If we have a generated image, use that for editing, otherwise use the uploaded image
28
  const imageToEdit = generatedImage || image;
@@ -32,6 +42,7 @@ export default function Home() {
32
  prompt,
33
  image: imageToEdit,
34
  history: history.length > 0 ? history : undefined,
 
35
  };
36
 
37
  const response = await fetch("/api/image", {
@@ -48,6 +59,7 @@ export default function Home() {
48
  }
49
 
50
  const data = await response.json();
 
51
 
52
  if (data.image) {
53
  // Update the generated image and description
@@ -91,6 +103,7 @@ export default function Home() {
91
  setDescription(null);
92
  setLoading(false);
93
  setError(null);
 
94
  setHistory([]);
95
  };
96
 
@@ -128,6 +141,31 @@ export default function Home() {
128
  </div>
129
  )}
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  {!displayImage && !loading ? (
132
  <>
133
  <ImageUpload
 
7
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
  import { HistoryItem } from "@/lib/types";
9
 
10
+ const MODEL_OPTIONS = [
11
+ "gemini-2.0-flash-exp",
12
+ "gemini-2.5-flash-image",
13
+ "gemini-3.1-flash-image-preview",
14
+ "gemini-3-pro-image-preview",
15
+ ];
16
+
17
  export default function Home() {
18
  const [image, setImage] = useState<string | null>(null);
19
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
20
  const [description, setDescription] = useState<string | null>(null);
21
  const [loading, setLoading] = useState(false);
22
  const [error, setError] = useState<string | null>(null);
23
+ const [notice, setNotice] = useState<string | null>(null);
24
  const [history, setHistory] = useState<HistoryItem[]>([]);
25
+ const [selectedModel, setSelectedModel] = useState(MODEL_OPTIONS[0]);
26
 
27
  const handleImageSelect = (imageData: string) => {
28
  setImage(imageData || null);
 
32
  try {
33
  setLoading(true);
34
  setError(null);
35
+ setNotice(null);
36
 
37
  // If we have a generated image, use that for editing, otherwise use the uploaded image
38
  const imageToEdit = generatedImage || image;
 
42
  prompt,
43
  image: imageToEdit,
44
  history: history.length > 0 ? history : undefined,
45
+ model: selectedModel,
46
  };
47
 
48
  const response = await fetch("/api/image", {
 
59
  }
60
 
61
  const data = await response.json();
62
+ setNotice(data.notice?.message || null);
63
 
64
  if (data.image) {
65
  // Update the generated image and description
 
103
  setDescription(null);
104
  setLoading(false);
105
  setError(null);
106
+ setNotice(null);
107
  setHistory([]);
108
  };
109
 
 
141
  </div>
142
  )}
143
 
144
+ {notice && (
145
+ <div className="p-4 mb-4 text-sm text-blue-700 bg-blue-100 rounded-lg">
146
+ {notice}
147
+ </div>
148
+ )}
149
+
150
+ <div className="space-y-2">
151
+ <label className="text-sm font-medium text-foreground" htmlFor="model-select">
152
+ Model
153
+ </label>
154
+ <select
155
+ id="model-select"
156
+ value={selectedModel}
157
+ onChange={(event) => setSelectedModel(event.target.value)}
158
+ disabled={loading}
159
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
160
+ >
161
+ {MODEL_OPTIONS.map((model) => (
162
+ <option key={model} value={model}>
163
+ {model}
164
+ </option>
165
+ ))}
166
+ </select>
167
+ </div>
168
+
169
  {!displayImage && !loading ? (
170
  <>
171
  <ImageUpload