Reubencf commited on
Commit
a699fd2
·
1 Parent(s): f347973

made some changes /try-on

Browse files
Files changed (4) hide show
  1. app/api/generate/route.ts +52 -0
  2. app/try-on/page.tsx +414 -0
  3. package-lock.json +301 -2
  4. package.json +8 -7
app/api/generate/route.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { GoogleGenAI } from "@google/genai";
3
+
4
+ export const runtime = "nodejs"; // Ensure Node runtime for SDK
5
+
6
+ export async function POST(req: NextRequest) {
7
+ try {
8
+ const { prompt } = (await req.json()) as { prompt?: string };
9
+ if (!prompt || typeof prompt !== "string") {
10
+ return NextResponse.json(
11
+ { error: "Missing prompt" },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ const apiKey = process.env.GOOGLE_API_KEY;
17
+ if (!apiKey) {
18
+ return NextResponse.json(
19
+ { error: "Server is not configured: GOOGLE_API_KEY is missing" },
20
+ { status: 500 }
21
+ );
22
+ }
23
+
24
+ const ai = new GoogleGenAI({ apiKey });
25
+
26
+ const response = await ai.models.generateContent({
27
+ model: "gemini-2.5-flash-image-preview",
28
+ contents: prompt,
29
+ });
30
+
31
+ const parts = (response as any)?.candidates?.[0]?.content?.parts ?? [];
32
+ const images: string[] = [];
33
+ const texts: string[] = [];
34
+
35
+ for (const part of parts) {
36
+ if (part?.inlineData?.data) {
37
+ images.push(`data:image/png;base64,${part.inlineData.data}`);
38
+ } else if (part?.text) {
39
+ texts.push(part.text as string);
40
+ }
41
+ }
42
+
43
+ return NextResponse.json({ images, text: texts.join("\n") });
44
+ } catch (err) {
45
+ console.error("/api/generate error", err);
46
+ return NextResponse.json(
47
+ { error: "Failed to generate image" },
48
+ { status: 500 }
49
+ );
50
+ }
51
+ }
52
+
app/try-on/page.tsx ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ function readFilesAsDataUrls(files: FileList | File[]): Promise<string[]> {
6
+ const arr = Array.from(files as File[]);
7
+ return Promise.all(
8
+ arr.map(
9
+ (file) =>
10
+ new Promise<string>((resolve, reject) => {
11
+ const reader = new FileReader();
12
+ reader.onload = () => resolve(reader.result as string);
13
+ reader.onerror = (e) => reject(e);
14
+ reader.readAsDataURL(file);
15
+ })
16
+ )
17
+ );
18
+ }
19
+
20
+ type ZoneKind = "user" | "clothes";
21
+
22
+ function UploadZone({
23
+ title,
24
+ description,
25
+ onDropData,
26
+ allowMultiple = false,
27
+ }: {
28
+ title: string;
29
+ description: string;
30
+ onDropData: (dataUrls: string[]) => void;
31
+ allowMultiple?: boolean;
32
+ }) {
33
+ const ref = useRef<HTMLDivElement>(null);
34
+ const handleFiles = useCallback(
35
+ async (files: FileList | File[]) => {
36
+ const urls = await readFilesAsDataUrls(files);
37
+ onDropData(urls);
38
+ },
39
+ [onDropData]
40
+ );
41
+
42
+ const onDrop = useCallback(
43
+ async (e: React.DragEvent<HTMLDivElement>) => {
44
+ e.preventDefault();
45
+ const files = e.dataTransfer.files;
46
+ if (files && files.length) {
47
+ await handleFiles(files);
48
+ }
49
+ },
50
+ [handleFiles]
51
+ );
52
+
53
+ const onPaste = useCallback(
54
+ async (e: React.ClipboardEvent<HTMLDivElement>) => {
55
+ const items = e.clipboardData.items;
56
+ const files: File[] = [];
57
+ for (let i = 0; i < items.length; i++) {
58
+ const it = items[i];
59
+ if (it.type.startsWith("image/")) {
60
+ const f = it.getAsFile();
61
+ if (f) files.push(f);
62
+ }
63
+ }
64
+
65
+ if (files.length) {
66
+ await handleFiles(files);
67
+ return;
68
+ }
69
+
70
+ // Fallback: if user pasted a URL (e.g., copied image address)
71
+ const text = e.clipboardData.getData("text");
72
+ if (text && (text.startsWith("http") || text.startsWith("data:image"))) {
73
+ onDropData([text]);
74
+ }
75
+ },
76
+ [handleFiles, onDropData]
77
+ );
78
+
79
+ return (
80
+ <div
81
+ ref={ref}
82
+ tabIndex={0}
83
+ onDrop={onDrop}
84
+ onDragOver={(e) => e.preventDefault()}
85
+ onPaste={onPaste}
86
+ className="rounded-2xl border border-white/10 bg-black/40 text-white p-6 sm:p-8 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/60"
87
+ >
88
+ <div className="flex items-center justify-between mb-4">
89
+ <h2 className="text-lg font-semibold tracking-wide">{title}</h2>
90
+ <span className="text-xs text-white/60">drop • click • paste</span>
91
+ </div>
92
+ <label className="block" aria-label={`${title} uploader`}>
93
+ <input
94
+ type="file"
95
+ accept="image/*"
96
+ multiple={allowMultiple}
97
+ className="hidden"
98
+ onChange={async (e) => {
99
+ if (e.currentTarget.files?.length) {
100
+ await handleFiles(e.currentTarget.files);
101
+ e.currentTarget.value = "";
102
+ }
103
+ }}
104
+ />
105
+ <div className="grid place-items-center rounded-xl border border-dashed border-white/20 hover:border-white/40 cursor-pointer px-6 py-12 sm:py-16 text-center select-none">
106
+ <p className="text-sm leading-6 text-white/80">
107
+ {description}
108
+ </p>
109
+ <p className="mt-3 text-xs text-white/50">
110
+ Upload or paste an image. Tip: click this box and press Ctrl/Cmd+V
111
+ </p>
112
+ </div>
113
+ </label>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export default function TryOnPage() {
119
+ const [userImage, setUserImage] = useState<string | null>(null);
120
+ const [clothingImages, setClothingImages] = useState<string[]>([]);
121
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
122
+
123
+ const selectedClothing = useMemo(
124
+ () => (selectedIndex != null ? clothingImages[selectedIndex] : null),
125
+ [selectedIndex, clothingImages]
126
+ );
127
+
128
+ // Overlay controls
129
+ const [scale, setScale] = useState(1);
130
+ const [offsetX, setOffsetX] = useState(0);
131
+ const [offsetY, setOffsetY] = useState(0);
132
+ const [rotation, setRotation] = useState(0);
133
+
134
+ const dragRef = useRef<HTMLImageElement>(null);
135
+ const dragging = useRef(false);
136
+ const start = useRef<{ x: number; y: number; ox: number; oy: number } | null>(
137
+ null
138
+ );
139
+
140
+ const onPointerDown = (e: React.PointerEvent) => {
141
+ if (!selectedClothing) return;
142
+ dragging.current = true;
143
+ start.current = {
144
+ x: e.clientX,
145
+ y: e.clientY,
146
+ ox: offsetX,
147
+ oy: offsetY,
148
+ };
149
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
150
+ };
151
+ const onPointerMove = (e: React.PointerEvent) => {
152
+ if (!dragging.current || !start.current) return;
153
+ const dx = e.clientX - start.current.x;
154
+ const dy = e.clientY - start.current.y;
155
+ setOffsetX(start.current.ox + dx);
156
+ setOffsetY(start.current.oy + dy);
157
+ };
158
+ const onPointerUp = (e: React.PointerEvent) => {
159
+ dragging.current = false;
160
+ start.current = null;
161
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
162
+ };
163
+
164
+ const addUserImages = (urls: string[]) => {
165
+ setUserImage(urls[0] ?? null);
166
+ };
167
+ const addClothesImages = (urls: string[]) => {
168
+ setClothingImages((prev) => [...urls, ...prev]);
169
+ if (selectedIndex == null && urls.length) setSelectedIndex(0);
170
+ };
171
+
172
+ // Gemini generation
173
+ const [genPrompt, setGenPrompt] = useState(
174
+ "A front-facing fashion product (e.g., jacket, shirt, dress, sunglasses, or accessory) on a plain or transparent background, high quality, photo-realistic."
175
+ );
176
+ const [isGenerating, setIsGenerating] = useState(false);
177
+ const [error, setError] = useState<string | null>(null);
178
+
179
+ const generateWithGemini = async () => {
180
+ try {
181
+ setIsGenerating(true);
182
+ setError(null);
183
+ const res = await fetch("/api/generate", {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json" },
186
+ body: JSON.stringify({ prompt: genPrompt }),
187
+ });
188
+ if (!res.ok) {
189
+ const js = await res.json().catch(() => ({}));
190
+ throw new Error(js.error || `Request failed (${res.status})`);
191
+ }
192
+ const data = (await res.json()) as { images?: string[]; text?: string };
193
+ const imgs = data.images ?? [];
194
+ if (!imgs.length) {
195
+ // Fallback: if model returned only text, just show an error message
196
+ throw new Error(
197
+ "Model returned no images. Try a different prompt, e.g., 'white t-shirt, product photo, front view'."
198
+ );
199
+ }
200
+ addClothesImages(imgs);
201
+ } catch (e: any) {
202
+ setError(e?.message || "Failed to generate image");
203
+ } finally {
204
+ setIsGenerating(false);
205
+ }
206
+ };
207
+
208
+ return (
209
+ <div className="min-h-[100svh] bg-black text-white">
210
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-10">
211
+ {/* Header */}
212
+ <div className="flex items-center justify-between mb-10">
213
+ <h1 className="text-xl sm:text-2xl font-semibold tracking-wide">
214
+ Virtual Try-On (simple overlay)
215
+ </h1>
216
+ <a
217
+ className="text-xs text-white/60 hover:text-white underline underline-offset-4"
218
+ href="/"
219
+ >
220
+ Home
221
+ </a>
222
+ </div>
223
+
224
+ {/* Two columns */}
225
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
226
+ <UploadZone
227
+ title="Your photo"
228
+ description="Drop, click to upload, or paste your portrait. Prefer front-facing photos for best results."
229
+ onDropData={addUserImages}
230
+ />
231
+ <UploadZone
232
+ title="Clothing / accessories"
233
+ description="Add product shots you want to try. You can upload multiple, paste, or use Gemini to generate some."
234
+ onDropData={addClothesImages}
235
+ allowMultiple
236
+ />
237
+ </div>
238
+
239
+ {/* Preview area */}
240
+ <div className="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
241
+ <div className="rounded-2xl border border-white/10 bg-white/5 p-4 sm:p-6">
242
+ <h3 className="mb-3 text-sm font-medium text-white/80">Your photo</h3>
243
+ <div
244
+ className="relative aspect-[4/5] w-full overflow-hidden rounded-xl bg-black grid place-items-center"
245
+ onPointerDown={onPointerDown}
246
+ onPointerMove={onPointerMove}
247
+ onPointerUp={onPointerUp}
248
+ >
249
+ {userImage ? (
250
+ <img
251
+ src={userImage}
252
+ alt="User"
253
+ className="absolute inset-0 h-full w-full object-contain"
254
+ />
255
+ ) : (
256
+ <p className="text-white/50 text-sm">No photo yet</p>
257
+ )}
258
+
259
+ {/* Overlay clothing */}
260
+ {userImage && selectedClothing && (
261
+ <img
262
+ ref={dragRef}
263
+ src={selectedClothing}
264
+ alt="Clothing overlay"
265
+ className="pointer-events-auto select-none"
266
+ style={{
267
+ position: "absolute",
268
+ left: "50%",
269
+ top: "50%",
270
+ transform: `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px)) rotate(${rotation}deg) scale(${scale})`,
271
+ transformOrigin: "center center",
272
+ maxWidth: "70%",
273
+ }}
274
+ />
275
+ )}
276
+ </div>
277
+
278
+ {/* Controls */}
279
+ <div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
280
+ <div>
281
+ <label className="flex items-center justify-between text-xs text-white/70 mb-1">
282
+ <span>Scale</span>
283
+ <span>{scale.toFixed(2)}x</span>
284
+ </label>
285
+ <input
286
+ type="range"
287
+ min={0.3}
288
+ max={3}
289
+ step={0.01}
290
+ value={scale}
291
+ onChange={(e) => setScale(parseFloat(e.target.value))}
292
+ className="w-full"
293
+ />
294
+ </div>
295
+ <div>
296
+ <label className="flex items-center justify-between text-xs text-white/70 mb-1">
297
+ <span>Rotation</span>
298
+ <span>{rotation.toFixed(0)}°</span>
299
+ </label>
300
+ <input
301
+ type="range"
302
+ min={-180}
303
+ max={180}
304
+ step={1}
305
+ value={rotation}
306
+ onChange={(e) => setRotation(parseFloat(e.target.value))}
307
+ className="w-full"
308
+ />
309
+ </div>
310
+ <div>
311
+ <label className="flex items-center justify-between text-xs text-white/70 mb-1">
312
+ <span>Offset X</span>
313
+ <span>{offsetX.toFixed(0)}px</span>
314
+ </label>
315
+ <input
316
+ type="range"
317
+ min={-300}
318
+ max={300}
319
+ step={1}
320
+ value={offsetX}
321
+ onChange={(e) => setOffsetX(parseFloat(e.target.value))}
322
+ className="w-full"
323
+ />
324
+ </div>
325
+ <div>
326
+ <label className="flex items-center justify-between text-xs text-white/70 mb-1">
327
+ <span>Offset Y</span>
328
+ <span>{offsetY.toFixed(0)}px</span>
329
+ </label>
330
+ <input
331
+ type="range"
332
+ min={-300}
333
+ max={300}
334
+ step={1}
335
+ value={offsetY}
336
+ onChange={(e) => setOffsetY(parseFloat(e.target.value))}
337
+ className="w-full"
338
+ />
339
+ </div>
340
+ </div>
341
+ </div>
342
+
343
+ <div className="rounded-2xl border border-white/10 bg-white/5 p-4 sm:p-6">
344
+ <h3 className="mb-3 text-sm font-medium text-white/80">
345
+ Clothing library
346
+ </h3>
347
+ {clothingImages.length === 0 ? (
348
+ <p className="text-white/50 text-sm">No clothing images yet</p>
349
+ ) : (
350
+ <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3">
351
+ {clothingImages.map((src, idx) => (
352
+ <button
353
+ key={idx}
354
+ onClick={() => setSelectedIndex(idx)}
355
+ className={`relative aspect-square overflow-hidden rounded-lg border ${
356
+ selectedIndex === idx
357
+ ? "border-indigo-400"
358
+ : "border-white/10 hover:border-white/30"
359
+ }`}
360
+ title="Select for overlay"
361
+ >
362
+ <img
363
+ src={src}
364
+ alt={`Clothing ${idx + 1}`}
365
+ className="h-full w-full object-cover"
366
+ />
367
+ </button>
368
+ ))}
369
+ </div>
370
+ )}
371
+
372
+ {/* Gemini generator */}
373
+ <div className="mt-6 rounded-xl bg-black/40 border border-white/10 p-4">
374
+ <p className="text-sm text-white/80 font-medium mb-2">
375
+ Generate with Gemini
376
+ </p>
377
+ <div className="flex flex-col gap-3">
378
+ <textarea
379
+ value={genPrompt}
380
+ onChange={(e) => setGenPrompt(e.target.value)}
381
+ rows={3}
382
+ className="w-full rounded-lg bg-black/60 border border-white/10 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/60"
383
+ />
384
+ <div className="flex items-center gap-3">
385
+ <button
386
+ onClick={generateWithGemini}
387
+ disabled={isGenerating}
388
+ className="inline-flex items-center justify-center rounded-md bg-indigo-500 hover:bg-indigo-400 disabled:opacity-60 text-white text-sm font-medium px-4 py-2"
389
+ >
390
+ {isGenerating ? "Generating…" : "Generate"}
391
+ </button>
392
+ {error && (
393
+ <span className="text-xs text-red-400">{error}</span>
394
+ )}
395
+ </div>
396
+ <p className="text-[11px] text-white/50">
397
+ Tip: Ask for a single front-view item with plain background, e.g.
398
+ "black leather jacket, product photo, front view".
399
+ </p>
400
+ </div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+
405
+ <div className="mt-10 text-xs text-white/50">
406
+ Note: This demo performs a simple 2D overlay for exploration. True
407
+ virtual try-on (warping to body shape, segmentation) requires
408
+ specialized vision models not included here.
409
+ </div>
410
+ </div>
411
+ </div>
412
+ );
413
+ }
414
+
package-lock.json CHANGED
@@ -8,6 +8,7 @@
8
  "name": "banana",
9
  "version": "0.1.0",
10
  "dependencies": {
 
11
  "next": "15.5.2",
12
  "react": "19.1.0",
13
  "react-dom": "19.1.0"
@@ -211,6 +212,27 @@
211
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
212
  }
213
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  "node_modules/@humanfs/core": {
215
  "version": "0.19.1",
216
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1890,6 +1912,15 @@
1890
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1891
  }
1892
  },
 
 
 
 
 
 
 
 
 
1893
  "node_modules/ajv": {
1894
  "version": "6.12.6",
1895
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2160,6 +2191,35 @@
2160
  "dev": true,
2161
  "license": "MIT"
2162
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2163
  "node_modules/brace-expansion": {
2164
  "version": "1.1.12",
2165
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -2184,6 +2244,12 @@
2184
  "node": ">=8"
2185
  }
2186
  },
 
 
 
 
 
 
2187
  "node_modules/call-bind": {
2188
  "version": "1.0.8",
2189
  "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -2436,7 +2502,6 @@
2436
  "version": "4.4.1",
2437
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
2438
  "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
2439
- "dev": true,
2440
  "license": "MIT",
2441
  "dependencies": {
2442
  "ms": "^2.1.3"
@@ -2531,6 +2596,15 @@
2531
  "node": ">= 0.4"
2532
  }
2533
  },
 
 
 
 
 
 
 
 
 
2534
  "node_modules/emoji-regex": {
2535
  "version": "9.2.2",
2536
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -3168,6 +3242,12 @@
3168
  "node": ">=0.10.0"
3169
  }
3170
  },
 
 
 
 
 
 
3171
  "node_modules/fast-deep-equal": {
3172
  "version": "3.1.3",
3173
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3350,6 +3430,36 @@
3350
  "url": "https://github.com/sponsors/ljharb"
3351
  }
3352
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3353
  "node_modules/get-intrinsic": {
3354
  "version": "1.3.0",
3355
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3463,6 +3573,32 @@
3463
  "url": "https://github.com/sponsors/ljharb"
3464
  }
3465
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3466
  "node_modules/gopd": {
3467
  "version": "1.2.0",
3468
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -3490,6 +3626,19 @@
3490
  "dev": true,
3491
  "license": "MIT"
3492
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3493
  "node_modules/has-bigints": {
3494
  "version": "1.1.0",
3495
  "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -3584,6 +3733,19 @@
3584
  "node": ">= 0.4"
3585
  }
3586
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3587
  "node_modules/ignore": {
3588
  "version": "5.3.2",
3589
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3947,6 +4109,18 @@
3947
  "url": "https://github.com/sponsors/ljharb"
3948
  }
3949
  },
 
 
 
 
 
 
 
 
 
 
 
 
3950
  "node_modules/is-string": {
3951
  "version": "1.1.1",
3952
  "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
@@ -4106,6 +4280,15 @@
4106
  "js-yaml": "bin/js-yaml.js"
4107
  }
4108
  },
 
 
 
 
 
 
 
 
 
4109
  "node_modules/json-buffer": {
4110
  "version": "3.0.1",
4111
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -4156,6 +4339,27 @@
4156
  "node": ">=4.0"
4157
  }
4158
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4159
  "node_modules/keyv": {
4160
  "version": "4.5.4",
4161
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4585,7 +4789,6 @@
4585
  "version": "2.1.3",
4586
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
4587
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
4588
- "dev": true,
4589
  "license": "MIT"
4590
  },
4591
  "node_modules/nanoid": {
@@ -4709,6 +4912,26 @@
4709
  "node": "^10 || ^12 || >=14"
4710
  }
4711
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4712
  "node_modules/object-assign": {
4713
  "version": "4.1.1",
4714
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5219,6 +5442,26 @@
5219
  "url": "https://github.com/sponsors/ljharb"
5220
  }
5221
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5222
  "node_modules/safe-push-apply": {
5223
  "version": "1.0.0",
5224
  "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -5789,6 +6032,12 @@
5789
  "node": ">=8.0"
5790
  }
5791
  },
 
 
 
 
 
 
5792
  "node_modules/ts-api-utils": {
5793
  "version": "2.1.0",
5794
  "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -5997,6 +6246,35 @@
5997
  "punycode": "^2.1.0"
5998
  }
5999
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6000
  "node_modules/which": {
6001
  "version": "2.0.2",
6002
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6112,6 +6390,27 @@
6112
  "node": ">=0.10.0"
6113
  }
6114
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6115
  "node_modules/yallist": {
6116
  "version": "5.0.0",
6117
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
 
8
  "name": "banana",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@google/genai": "^1.17.0",
12
  "next": "15.5.2",
13
  "react": "19.1.0",
14
  "react-dom": "19.1.0"
 
212
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
213
  }
214
  },
215
+ "node_modules/@google/genai": {
216
+ "version": "1.17.0",
217
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.17.0.tgz",
218
+ "integrity": "sha512-r/OZWN9D8WvYrte3bcKPoLODrZ+2TjfxHm5OOyVHUbdFYIp1C4yJaXX4+sCS8I/+CbN9PxLjU5zm1cgmS7qz+A==",
219
+ "license": "Apache-2.0",
220
+ "dependencies": {
221
+ "google-auth-library": "^9.14.2",
222
+ "ws": "^8.18.0"
223
+ },
224
+ "engines": {
225
+ "node": ">=20.0.0"
226
+ },
227
+ "peerDependencies": {
228
+ "@modelcontextprotocol/sdk": "^1.11.4"
229
+ },
230
+ "peerDependenciesMeta": {
231
+ "@modelcontextprotocol/sdk": {
232
+ "optional": true
233
+ }
234
+ }
235
+ },
236
  "node_modules/@humanfs/core": {
237
  "version": "0.19.1",
238
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
 
1912
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1913
  }
1914
  },
1915
+ "node_modules/agent-base": {
1916
+ "version": "7.1.4",
1917
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1918
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1919
+ "license": "MIT",
1920
+ "engines": {
1921
+ "node": ">= 14"
1922
+ }
1923
+ },
1924
  "node_modules/ajv": {
1925
  "version": "6.12.6",
1926
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
 
2191
  "dev": true,
2192
  "license": "MIT"
2193
  },
2194
+ "node_modules/base64-js": {
2195
+ "version": "1.5.1",
2196
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
2197
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
2198
+ "funding": [
2199
+ {
2200
+ "type": "github",
2201
+ "url": "https://github.com/sponsors/feross"
2202
+ },
2203
+ {
2204
+ "type": "patreon",
2205
+ "url": "https://www.patreon.com/feross"
2206
+ },
2207
+ {
2208
+ "type": "consulting",
2209
+ "url": "https://feross.org/support"
2210
+ }
2211
+ ],
2212
+ "license": "MIT"
2213
+ },
2214
+ "node_modules/bignumber.js": {
2215
+ "version": "9.3.1",
2216
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
2217
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
2218
+ "license": "MIT",
2219
+ "engines": {
2220
+ "node": "*"
2221
+ }
2222
+ },
2223
  "node_modules/brace-expansion": {
2224
  "version": "1.1.12",
2225
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
 
2244
  "node": ">=8"
2245
  }
2246
  },
2247
+ "node_modules/buffer-equal-constant-time": {
2248
+ "version": "1.0.1",
2249
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
2250
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
2251
+ "license": "BSD-3-Clause"
2252
+ },
2253
  "node_modules/call-bind": {
2254
  "version": "1.0.8",
2255
  "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
 
2502
  "version": "4.4.1",
2503
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
2504
  "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
 
2505
  "license": "MIT",
2506
  "dependencies": {
2507
  "ms": "^2.1.3"
 
2596
  "node": ">= 0.4"
2597
  }
2598
  },
2599
+ "node_modules/ecdsa-sig-formatter": {
2600
+ "version": "1.0.11",
2601
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
2602
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
2603
+ "license": "Apache-2.0",
2604
+ "dependencies": {
2605
+ "safe-buffer": "^5.0.1"
2606
+ }
2607
+ },
2608
  "node_modules/emoji-regex": {
2609
  "version": "9.2.2",
2610
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
 
3242
  "node": ">=0.10.0"
3243
  }
3244
  },
3245
+ "node_modules/extend": {
3246
+ "version": "3.0.2",
3247
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
3248
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
3249
+ "license": "MIT"
3250
+ },
3251
  "node_modules/fast-deep-equal": {
3252
  "version": "3.1.3",
3253
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 
3430
  "url": "https://github.com/sponsors/ljharb"
3431
  }
3432
  },
3433
+ "node_modules/gaxios": {
3434
+ "version": "6.7.1",
3435
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
3436
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
3437
+ "license": "Apache-2.0",
3438
+ "dependencies": {
3439
+ "extend": "^3.0.2",
3440
+ "https-proxy-agent": "^7.0.1",
3441
+ "is-stream": "^2.0.0",
3442
+ "node-fetch": "^2.6.9",
3443
+ "uuid": "^9.0.1"
3444
+ },
3445
+ "engines": {
3446
+ "node": ">=14"
3447
+ }
3448
+ },
3449
+ "node_modules/gcp-metadata": {
3450
+ "version": "6.1.1",
3451
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
3452
+ "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
3453
+ "license": "Apache-2.0",
3454
+ "dependencies": {
3455
+ "gaxios": "^6.1.1",
3456
+ "google-logging-utils": "^0.0.2",
3457
+ "json-bigint": "^1.0.0"
3458
+ },
3459
+ "engines": {
3460
+ "node": ">=14"
3461
+ }
3462
+ },
3463
  "node_modules/get-intrinsic": {
3464
  "version": "1.3.0",
3465
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
 
3573
  "url": "https://github.com/sponsors/ljharb"
3574
  }
3575
  },
3576
+ "node_modules/google-auth-library": {
3577
+ "version": "9.15.1",
3578
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
3579
+ "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
3580
+ "license": "Apache-2.0",
3581
+ "dependencies": {
3582
+ "base64-js": "^1.3.0",
3583
+ "ecdsa-sig-formatter": "^1.0.11",
3584
+ "gaxios": "^6.1.1",
3585
+ "gcp-metadata": "^6.1.0",
3586
+ "gtoken": "^7.0.0",
3587
+ "jws": "^4.0.0"
3588
+ },
3589
+ "engines": {
3590
+ "node": ">=14"
3591
+ }
3592
+ },
3593
+ "node_modules/google-logging-utils": {
3594
+ "version": "0.0.2",
3595
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
3596
+ "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
3597
+ "license": "Apache-2.0",
3598
+ "engines": {
3599
+ "node": ">=14"
3600
+ }
3601
+ },
3602
  "node_modules/gopd": {
3603
  "version": "1.2.0",
3604
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
 
3626
  "dev": true,
3627
  "license": "MIT"
3628
  },
3629
+ "node_modules/gtoken": {
3630
+ "version": "7.1.0",
3631
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
3632
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
3633
+ "license": "MIT",
3634
+ "dependencies": {
3635
+ "gaxios": "^6.0.0",
3636
+ "jws": "^4.0.0"
3637
+ },
3638
+ "engines": {
3639
+ "node": ">=14.0.0"
3640
+ }
3641
+ },
3642
  "node_modules/has-bigints": {
3643
  "version": "1.1.0",
3644
  "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
 
3733
  "node": ">= 0.4"
3734
  }
3735
  },
3736
+ "node_modules/https-proxy-agent": {
3737
+ "version": "7.0.6",
3738
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
3739
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
3740
+ "license": "MIT",
3741
+ "dependencies": {
3742
+ "agent-base": "^7.1.2",
3743
+ "debug": "4"
3744
+ },
3745
+ "engines": {
3746
+ "node": ">= 14"
3747
+ }
3748
+ },
3749
  "node_modules/ignore": {
3750
  "version": "5.3.2",
3751
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
4109
  "url": "https://github.com/sponsors/ljharb"
4110
  }
4111
  },
4112
+ "node_modules/is-stream": {
4113
+ "version": "2.0.1",
4114
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
4115
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
4116
+ "license": "MIT",
4117
+ "engines": {
4118
+ "node": ">=8"
4119
+ },
4120
+ "funding": {
4121
+ "url": "https://github.com/sponsors/sindresorhus"
4122
+ }
4123
+ },
4124
  "node_modules/is-string": {
4125
  "version": "1.1.1",
4126
  "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
 
4280
  "js-yaml": "bin/js-yaml.js"
4281
  }
4282
  },
4283
+ "node_modules/json-bigint": {
4284
+ "version": "1.0.0",
4285
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
4286
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
4287
+ "license": "MIT",
4288
+ "dependencies": {
4289
+ "bignumber.js": "^9.0.0"
4290
+ }
4291
+ },
4292
  "node_modules/json-buffer": {
4293
  "version": "3.0.1",
4294
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
 
4339
  "node": ">=4.0"
4340
  }
4341
  },
4342
+ "node_modules/jwa": {
4343
+ "version": "2.0.1",
4344
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
4345
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
4346
+ "license": "MIT",
4347
+ "dependencies": {
4348
+ "buffer-equal-constant-time": "^1.0.1",
4349
+ "ecdsa-sig-formatter": "1.0.11",
4350
+ "safe-buffer": "^5.0.1"
4351
+ }
4352
+ },
4353
+ "node_modules/jws": {
4354
+ "version": "4.0.0",
4355
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
4356
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
4357
+ "license": "MIT",
4358
+ "dependencies": {
4359
+ "jwa": "^2.0.0",
4360
+ "safe-buffer": "^5.0.1"
4361
+ }
4362
+ },
4363
  "node_modules/keyv": {
4364
  "version": "4.5.4",
4365
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
 
4789
  "version": "2.1.3",
4790
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
4791
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
 
4792
  "license": "MIT"
4793
  },
4794
  "node_modules/nanoid": {
 
4912
  "node": "^10 || ^12 || >=14"
4913
  }
4914
  },
4915
+ "node_modules/node-fetch": {
4916
+ "version": "2.7.0",
4917
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
4918
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
4919
+ "license": "MIT",
4920
+ "dependencies": {
4921
+ "whatwg-url": "^5.0.0"
4922
+ },
4923
+ "engines": {
4924
+ "node": "4.x || >=6.0.0"
4925
+ },
4926
+ "peerDependencies": {
4927
+ "encoding": "^0.1.0"
4928
+ },
4929
+ "peerDependenciesMeta": {
4930
+ "encoding": {
4931
+ "optional": true
4932
+ }
4933
+ }
4934
+ },
4935
  "node_modules/object-assign": {
4936
  "version": "4.1.1",
4937
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 
5442
  "url": "https://github.com/sponsors/ljharb"
5443
  }
5444
  },
5445
+ "node_modules/safe-buffer": {
5446
+ "version": "5.2.1",
5447
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
5448
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
5449
+ "funding": [
5450
+ {
5451
+ "type": "github",
5452
+ "url": "https://github.com/sponsors/feross"
5453
+ },
5454
+ {
5455
+ "type": "patreon",
5456
+ "url": "https://www.patreon.com/feross"
5457
+ },
5458
+ {
5459
+ "type": "consulting",
5460
+ "url": "https://feross.org/support"
5461
+ }
5462
+ ],
5463
+ "license": "MIT"
5464
+ },
5465
  "node_modules/safe-push-apply": {
5466
  "version": "1.0.0",
5467
  "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
 
6032
  "node": ">=8.0"
6033
  }
6034
  },
6035
+ "node_modules/tr46": {
6036
+ "version": "0.0.3",
6037
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
6038
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
6039
+ "license": "MIT"
6040
+ },
6041
  "node_modules/ts-api-utils": {
6042
  "version": "2.1.0",
6043
  "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
 
6246
  "punycode": "^2.1.0"
6247
  }
6248
  },
6249
+ "node_modules/uuid": {
6250
+ "version": "9.0.1",
6251
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
6252
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
6253
+ "funding": [
6254
+ "https://github.com/sponsors/broofa",
6255
+ "https://github.com/sponsors/ctavan"
6256
+ ],
6257
+ "license": "MIT",
6258
+ "bin": {
6259
+ "uuid": "dist/bin/uuid"
6260
+ }
6261
+ },
6262
+ "node_modules/webidl-conversions": {
6263
+ "version": "3.0.1",
6264
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
6265
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
6266
+ "license": "BSD-2-Clause"
6267
+ },
6268
+ "node_modules/whatwg-url": {
6269
+ "version": "5.0.0",
6270
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
6271
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
6272
+ "license": "MIT",
6273
+ "dependencies": {
6274
+ "tr46": "~0.0.3",
6275
+ "webidl-conversions": "^3.0.0"
6276
+ }
6277
+ },
6278
  "node_modules/which": {
6279
  "version": "2.0.2",
6280
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
 
6390
  "node": ">=0.10.0"
6391
  }
6392
  },
6393
+ "node_modules/ws": {
6394
+ "version": "8.18.3",
6395
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
6396
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
6397
+ "license": "MIT",
6398
+ "engines": {
6399
+ "node": ">=10.0.0"
6400
+ },
6401
+ "peerDependencies": {
6402
+ "bufferutil": "^4.0.1",
6403
+ "utf-8-validate": ">=5.0.2"
6404
+ },
6405
+ "peerDependenciesMeta": {
6406
+ "bufferutil": {
6407
+ "optional": true
6408
+ },
6409
+ "utf-8-validate": {
6410
+ "optional": true
6411
+ }
6412
+ }
6413
+ },
6414
  "node_modules/yallist": {
6415
  "version": "5.0.0",
6416
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
package.json CHANGED
@@ -9,19 +9,20 @@
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
 
 
12
  "react": "19.1.0",
13
- "react-dom": "19.1.0",
14
- "next": "15.5.2"
15
  },
16
  "devDependencies": {
17
- "typescript": "^5",
 
18
  "@types/node": "^20",
19
  "@types/react": "^19",
20
  "@types/react-dom": "^19",
21
- "@tailwindcss/postcss": "^4",
22
- "tailwindcss": "^4",
23
  "eslint": "^9",
24
  "eslint-config-next": "15.5.2",
25
- "@eslint/eslintrc": "^3"
 
26
  }
27
- }
 
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
12
+ "@google/genai": "^1.17.0",
13
+ "next": "15.5.2",
14
  "react": "19.1.0",
15
+ "react-dom": "19.1.0"
 
16
  },
17
  "devDependencies": {
18
+ "@eslint/eslintrc": "^3",
19
+ "@tailwindcss/postcss": "^4",
20
  "@types/node": "^20",
21
  "@types/react": "^19",
22
  "@types/react-dom": "^19",
 
 
23
  "eslint": "^9",
24
  "eslint-config-next": "15.5.2",
25
+ "tailwindcss": "^4",
26
+ "typescript": "^5"
27
  }
28
+ }