Spaces:
Running
Running
made some changes /try-on
Browse files- app/api/generate/route.ts +52 -0
- app/try-on/page.tsx +414 -0
- package-lock.json +301 -2
- 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 |
-
"
|
|
|
|
| 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 |
-
"
|
|
|
|
| 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 |
+
}
|