Spaces:
Running
Running
beggining free trials
Browse files- .gitignore +5 -2
- app/api/process/route.ts +64 -2
- app/api/usage/route.ts +72 -0
- app/page.tsx +55 -4
- lib/usage-store.ts +173 -0
- package-lock.json +105 -2
- package.json +2 -1
.gitignore
CHANGED
|
@@ -36,8 +36,11 @@ yarn-error.log*
|
|
| 36 |
# vercel
|
| 37 |
.vercel
|
| 38 |
|
|
|
|
|
|
|
|
|
|
| 39 |
# typescript
|
| 40 |
*.tsbuildinfo
|
| 41 |
next-env.d.ts
|
| 42 |
-
|
| 43 |
-
.vercel
|
|
|
|
| 36 |
# vercel
|
| 37 |
.vercel
|
| 38 |
|
| 39 |
+
# Usage tracking data
|
| 40 |
+
.usage-data.json
|
| 41 |
+
|
| 42 |
# typescript
|
| 43 |
*.tsbuildinfo
|
| 44 |
next-env.d.ts
|
| 45 |
+
|
| 46 |
+
.vercel
|
app/api/process/route.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
| 22 |
import { NextRequest, NextResponse } from "next/server";
|
| 23 |
import { GoogleGenAI } from "@google/genai";
|
| 24 |
import { cookies } from "next/headers";
|
|
|
|
| 25 |
|
| 26 |
// Configure Next.js runtime for Node.js (required for Google AI SDK)
|
| 27 |
export const runtime = "nodejs";
|
|
@@ -90,6 +91,7 @@ export async function POST(req: NextRequest) {
|
|
| 90 |
}
|
| 91 |
|
| 92 |
// Validate and retrieve Google API key from user input or environment
|
|
|
|
| 93 |
const apiKey = body.apiToken || process.env.GOOGLE_API_KEY;
|
| 94 |
if (!apiKey || apiKey === 'your_actual_api_key_here') {
|
| 95 |
return NextResponse.json(
|
|
@@ -98,6 +100,40 @@ export async function POST(req: NextRequest) {
|
|
| 98 |
);
|
| 99 |
}
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
// Initialize Google AI client with the validated API key
|
| 102 |
const ai = new GoogleGenAI({ apiKey });
|
| 103 |
|
|
@@ -244,8 +280,22 @@ The result should look like all subjects were photographed together in the same
|
|
| 244 |
{ status: 500 }
|
| 245 |
);
|
| 246 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
return NextResponse.json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
}
|
| 250 |
|
| 251 |
// Parse input image for non-merge nodes
|
|
@@ -776,8 +826,20 @@ The result should look like all subjects were photographed together in the same
|
|
| 776 |
{ status: 500 }
|
| 777 |
);
|
| 778 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
|
| 780 |
-
return NextResponse.json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
} catch (err: any) {
|
| 782 |
console.error("/api/process error:", err);
|
| 783 |
console.error("Error stack:", err?.stack);
|
|
|
|
| 22 |
import { NextRequest, NextResponse } from "next/server";
|
| 23 |
import { GoogleGenAI } from "@google/genai";
|
| 24 |
import { cookies } from "next/headers";
|
| 25 |
+
import { getUsage, canMakeRequest, recordRequest } from "@/lib/usage-store";
|
| 26 |
|
| 27 |
// Configure Next.js runtime for Node.js (required for Google AI SDK)
|
| 28 |
export const runtime = "nodejs";
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
// Validate and retrieve Google API key from user input or environment
|
| 94 |
+
const userProvidedKey = !!body.apiToken; // Track if user provided their own key
|
| 95 |
const apiKey = body.apiToken || process.env.GOOGLE_API_KEY;
|
| 96 |
if (!apiKey || apiKey === 'your_actual_api_key_here') {
|
| 97 |
return NextResponse.json(
|
|
|
|
| 100 |
);
|
| 101 |
}
|
| 102 |
|
| 103 |
+
// Get client IP for usage tracking
|
| 104 |
+
const getClientIP = (req: NextRequest): string => {
|
| 105 |
+
const forwardedFor = req.headers.get('x-forwarded-for');
|
| 106 |
+
if (forwardedFor) return forwardedFor.split(',')[0].trim();
|
| 107 |
+
const realIP = req.headers.get('x-real-ip');
|
| 108 |
+
if (realIP) return realIP;
|
| 109 |
+
const vercelForwardedFor = req.headers.get('x-vercel-forwarded-for');
|
| 110 |
+
if (vercelForwardedFor) return vercelForwardedFor.split(',')[0].trim();
|
| 111 |
+
const cfConnectingIP = req.headers.get('cf-connecting-ip');
|
| 112 |
+
if (cfConnectingIP) return cfConnectingIP;
|
| 113 |
+
return 'unknown';
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const clientIP = getClientIP(req);
|
| 117 |
+
|
| 118 |
+
// If using default API key, check usage limits
|
| 119 |
+
if (!userProvidedKey) {
|
| 120 |
+
const usage = await getUsage(clientIP);
|
| 121 |
+
|
| 122 |
+
if (!(await canMakeRequest(clientIP))) {
|
| 123 |
+
return NextResponse.json(
|
| 124 |
+
{
|
| 125 |
+
error: `Daily limit reached (${usage.limit} requests/day). Please add your own Gemini API key to continue, or wait until tomorrow.`,
|
| 126 |
+
usage: {
|
| 127 |
+
used: usage.used,
|
| 128 |
+
remaining: 0,
|
| 129 |
+
limit: usage.limit
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
{ status: 429 }
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
// Initialize Google AI client with the validated API key
|
| 138 |
const ai = new GoogleGenAI({ apiKey });
|
| 139 |
|
|
|
|
| 280 |
{ status: 500 }
|
| 281 |
);
|
| 282 |
}
|
| 283 |
+
// Record usage if using default API key (only on success)
|
| 284 |
+
let mergeUsageInfo = null;
|
| 285 |
+
if (!userProvidedKey) {
|
| 286 |
+
mergeUsageInfo = await recordRequest(clientIP);
|
| 287 |
+
}
|
| 288 |
|
| 289 |
+
return NextResponse.json({
|
| 290 |
+
image: images[0],
|
| 291 |
+
images,
|
| 292 |
+
text: texts.join("\n"),
|
| 293 |
+
usage: mergeUsageInfo ? {
|
| 294 |
+
used: mergeUsageInfo.used,
|
| 295 |
+
remaining: mergeUsageInfo.remaining,
|
| 296 |
+
limit: mergeUsageInfo.limit
|
| 297 |
+
} : null
|
| 298 |
+
});
|
| 299 |
}
|
| 300 |
|
| 301 |
// Parse input image for non-merge nodes
|
|
|
|
| 826 |
{ status: 500 }
|
| 827 |
);
|
| 828 |
}
|
| 829 |
+
// Record usage if using default API key (only on success)
|
| 830 |
+
let usageInfo = null;
|
| 831 |
+
if (!userProvidedKey) {
|
| 832 |
+
usageInfo = await recordRequest(clientIP);
|
| 833 |
+
}
|
| 834 |
|
| 835 |
+
return NextResponse.json({
|
| 836 |
+
image: images[0],
|
| 837 |
+
usage: usageInfo ? {
|
| 838 |
+
used: usageInfo.used,
|
| 839 |
+
remaining: usageInfo.remaining,
|
| 840 |
+
limit: usageInfo.limit
|
| 841 |
+
} : null
|
| 842 |
+
});
|
| 843 |
} catch (err: any) {
|
| 844 |
console.error("/api/process error:", err);
|
| 845 |
console.error("Error stack:", err?.stack);
|
app/api/usage/route.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API ROUTE: /api/usage
|
| 3 |
+
*
|
| 4 |
+
* Endpoints for checking and managing API usage quotas.
|
| 5 |
+
*
|
| 6 |
+
* GET: Returns current usage for the requesting IP
|
| 7 |
+
* POST: Records a request (internal use by process route)
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 11 |
+
import { getUsage, canMakeRequest, getDailyLimit } from "@/lib/usage-store";
|
| 12 |
+
|
| 13 |
+
export const runtime = "nodejs";
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Get client IP from request headers
|
| 17 |
+
* Handles various proxy scenarios (Vercel, Cloudflare, etc.)
|
| 18 |
+
*/
|
| 19 |
+
function getClientIP(req: NextRequest): string {
|
| 20 |
+
// Try various headers that might contain the real IP
|
| 21 |
+
const forwardedFor = req.headers.get('x-forwarded-for');
|
| 22 |
+
if (forwardedFor) {
|
| 23 |
+
// x-forwarded-for can contain multiple IPs, the first one is the client
|
| 24 |
+
return forwardedFor.split(',')[0].trim();
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const realIP = req.headers.get('x-real-ip');
|
| 28 |
+
if (realIP) {
|
| 29 |
+
return realIP;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Vercel-specific header
|
| 33 |
+
const vercelForwardedFor = req.headers.get('x-vercel-forwarded-for');
|
| 34 |
+
if (vercelForwardedFor) {
|
| 35 |
+
return vercelForwardedFor.split(',')[0].trim();
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Cloudflare header
|
| 39 |
+
const cfConnectingIP = req.headers.get('cf-connecting-ip');
|
| 40 |
+
if (cfConnectingIP) {
|
| 41 |
+
return cfConnectingIP;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Fallback - this might not be accurate behind proxies
|
| 45 |
+
return 'unknown';
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* GET /api/usage
|
| 50 |
+
*
|
| 51 |
+
* Returns the current usage statistics for the requesting IP
|
| 52 |
+
*/
|
| 53 |
+
export async function GET(req: NextRequest) {
|
| 54 |
+
const ip = getClientIP(req);
|
| 55 |
+
const usage = await getUsage(ip);
|
| 56 |
+
|
| 57 |
+
return NextResponse.json({
|
| 58 |
+
ip: ip.substring(0, 8) + '***', // Partial IP for privacy
|
| 59 |
+
used: usage.used,
|
| 60 |
+
remaining: usage.remaining,
|
| 61 |
+
limit: usage.limit,
|
| 62 |
+
resetDate: usage.resetDate,
|
| 63 |
+
message: usage.remaining > 0
|
| 64 |
+
? `You have ${usage.remaining} free requests remaining today.`
|
| 65 |
+
: `Daily limit reached. Add your own API key to continue or wait until tomorrow.`
|
| 66 |
+
});
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Utility function to export for use in other routes
|
| 71 |
+
*/
|
| 72 |
+
export { getClientIP };
|
app/page.tsx
CHANGED
|
@@ -1066,9 +1066,12 @@ export default function EditorPage() {
|
|
| 1066 |
const [apiToken, setApiToken] = useState("");
|
| 1067 |
const [showHelpSidebar, setShowHelpSidebar] = useState(false);
|
| 1068 |
|
|
|
|
|
|
|
|
|
|
| 1069 |
// Processing Mode: 'nanobananapro' uses Gemini API, 'huggingface' uses HF models
|
| 1070 |
type ProcessingMode = 'nanobananapro' | 'huggingface';
|
| 1071 |
-
const [processingMode, setProcessingMode] = useState<ProcessingMode>('
|
| 1072 |
|
| 1073 |
// Available HF models
|
| 1074 |
const HF_MODELS = {
|
|
@@ -1094,6 +1097,28 @@ export default function EditorPage() {
|
|
| 1094 |
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
| 1095 |
const [hfUser, setHfUser] = useState<{ name?: string; username?: string; avatarUrl?: string } | null>(null);
|
| 1096 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1097 |
|
| 1098 |
const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
|
| 1099 |
const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
|
|
@@ -1640,6 +1665,11 @@ export default function EditorPage() {
|
|
| 1640 |
return n;
|
| 1641 |
}));
|
| 1642 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1643 |
// Add to node's history
|
| 1644 |
const description = unprocessedNodeCount > 1
|
| 1645 |
? `Combined ${unprocessedNodeCount} transformations`
|
|
@@ -1912,6 +1942,11 @@ export default function EditorPage() {
|
|
| 1912 |
const out = js.image || (js.images?.[0] as string) || null;
|
| 1913 |
setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, output: out, isRunning: false } : n)));
|
| 1914 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1915 |
// Add merge result to node's history
|
| 1916 |
if (out) {
|
| 1917 |
const inputLabels = merge.inputs.map((id, index) => {
|
|
@@ -2184,16 +2219,32 @@ export default function EditorPage() {
|
|
| 2184 |
{processingMode === 'nanobananapro' ? (
|
| 2185 |
<>
|
| 2186 |
<div className="h-6 w-px bg-border" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2187 |
<label htmlFor="api-token" className="text-sm font-medium text-muted-foreground">
|
| 2188 |
-
|
| 2189 |
</label>
|
| 2190 |
<Input
|
| 2191 |
id="api-token"
|
| 2192 |
type="password"
|
| 2193 |
-
placeholder="
|
| 2194 |
value={apiToken}
|
| 2195 |
onChange={(e) => setApiToken(e.target.value)}
|
| 2196 |
-
className="w-
|
| 2197 |
/>
|
| 2198 |
</>
|
| 2199 |
) : (
|
|
|
|
| 1066 |
const [apiToken, setApiToken] = useState("");
|
| 1067 |
const [showHelpSidebar, setShowHelpSidebar] = useState(false);
|
| 1068 |
|
| 1069 |
+
// Usage tracking state
|
| 1070 |
+
const [usage, setUsage] = useState<{ used: number; remaining: number; limit: number } | null>(null);
|
| 1071 |
+
|
| 1072 |
// Processing Mode: 'nanobananapro' uses Gemini API, 'huggingface' uses HF models
|
| 1073 |
type ProcessingMode = 'nanobananapro' | 'huggingface';
|
| 1074 |
+
const [processingMode, setProcessingMode] = useState<ProcessingMode>('nanobananapro');
|
| 1075 |
|
| 1076 |
// Available HF models
|
| 1077 |
const HF_MODELS = {
|
|
|
|
| 1097 |
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
| 1098 |
const [hfUser, setHfUser] = useState<{ name?: string; username?: string; avatarUrl?: string } | null>(null);
|
| 1099 |
|
| 1100 |
+
// Fetch usage on mount and when apiToken changes
|
| 1101 |
+
useEffect(() => {
|
| 1102 |
+
const fetchUsage = async () => {
|
| 1103 |
+
try {
|
| 1104 |
+
const res = await fetch('/api/usage');
|
| 1105 |
+
if (res.ok) {
|
| 1106 |
+
const data = await res.json();
|
| 1107 |
+
setUsage({ used: data.used, remaining: data.remaining, limit: data.limit });
|
| 1108 |
+
}
|
| 1109 |
+
} catch (error) {
|
| 1110 |
+
console.error('Failed to fetch usage:', error);
|
| 1111 |
+
}
|
| 1112 |
+
};
|
| 1113 |
+
|
| 1114 |
+
// Only fetch if not using own API key
|
| 1115 |
+
if (!apiToken) {
|
| 1116 |
+
fetchUsage();
|
| 1117 |
+
} else {
|
| 1118 |
+
setUsage(null); // Clear usage when using own key
|
| 1119 |
+
}
|
| 1120 |
+
}, [apiToken]);
|
| 1121 |
+
|
| 1122 |
|
| 1123 |
const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
|
| 1124 |
const merges = nodes.filter((n) => n.type === "MERGE") as MergeNode[];
|
|
|
|
| 1665 |
return n;
|
| 1666 |
}));
|
| 1667 |
|
| 1668 |
+
// Update usage from API response
|
| 1669 |
+
if (data.usage) {
|
| 1670 |
+
setUsage(data.usage);
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
// Add to node's history
|
| 1674 |
const description = unprocessedNodeCount > 1
|
| 1675 |
? `Combined ${unprocessedNodeCount} transformations`
|
|
|
|
| 1942 |
const out = js.image || (js.images?.[0] as string) || null;
|
| 1943 |
setNodes((prev) => prev.map((n) => (n.id === mergeId && n.type === "MERGE" ? { ...n, output: out, isRunning: false } : n)));
|
| 1944 |
|
| 1945 |
+
// Update usage from API response
|
| 1946 |
+
if (js.usage) {
|
| 1947 |
+
setUsage(js.usage);
|
| 1948 |
+
}
|
| 1949 |
+
|
| 1950 |
// Add merge result to node's history
|
| 1951 |
if (out) {
|
| 1952 |
const inputLabels = merge.inputs.map((id, index) => {
|
|
|
|
| 2219 |
{processingMode === 'nanobananapro' ? (
|
| 2220 |
<>
|
| 2221 |
<div className="h-6 w-px bg-border" />
|
| 2222 |
+
{/* Usage info when not using own API key */}
|
| 2223 |
+
{!apiToken && usage && (
|
| 2224 |
+
<div className={`text-xs px-2 py-1 rounded-md ${usage.remaining > 5
|
| 2225 |
+
? 'bg-green-500/20 text-green-400'
|
| 2226 |
+
: usage.remaining > 0
|
| 2227 |
+
? 'bg-yellow-500/20 text-yellow-400'
|
| 2228 |
+
: 'bg-red-500/20 text-red-400'
|
| 2229 |
+
}`}>
|
| 2230 |
+
{usage.remaining}/{usage.limit} free requests
|
| 2231 |
+
</div>
|
| 2232 |
+
)}
|
| 2233 |
+
{apiToken && (
|
| 2234 |
+
<div className="text-xs px-2 py-1 rounded-md bg-blue-500/20 text-blue-400">
|
| 2235 |
+
Using your API key ✓
|
| 2236 |
+
</div>
|
| 2237 |
+
)}
|
| 2238 |
<label htmlFor="api-token" className="text-sm font-medium text-muted-foreground">
|
| 2239 |
+
API Key:
|
| 2240 |
</label>
|
| 2241 |
<Input
|
| 2242 |
id="api-token"
|
| 2243 |
type="password"
|
| 2244 |
+
placeholder={apiToken ? "Your key is set" : "Optional - using free tier"}
|
| 2245 |
value={apiToken}
|
| 2246 |
onChange={(e) => setApiToken(e.target.value)}
|
| 2247 |
+
className="w-48"
|
| 2248 |
/>
|
| 2249 |
</>
|
| 2250 |
) : (
|
lib/usage-store.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* USAGE STORE - IP-Based Rate Limiting with Supabase
|
| 3 |
+
*
|
| 4 |
+
* Tracks API usage per IP address with a daily limit using Supabase.
|
| 5 |
+
* This prevents abuse by storing data in a persistent database.
|
| 6 |
+
*
|
| 7 |
+
* Default limit: 10 requests per day per IP when using the default API key.
|
| 8 |
+
* Users with their own API key bypass this limit.
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
import { createClient } from '@supabase/supabase-js';
|
| 12 |
+
|
| 13 |
+
// Configuration
|
| 14 |
+
const DAILY_LIMIT = 10;
|
| 15 |
+
|
| 16 |
+
// Initialize Supabase client
|
| 17 |
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
| 18 |
+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;
|
| 19 |
+
|
| 20 |
+
// Create Supabase client (only if credentials are available)
|
| 21 |
+
const supabase = supabaseUrl && supabaseKey
|
| 22 |
+
? createClient(supabaseUrl, supabaseKey)
|
| 23 |
+
: null;
|
| 24 |
+
|
| 25 |
+
// Get today's date in YYYY-MM-DD format
|
| 26 |
+
function getToday(): string {
|
| 27 |
+
return new Date().toISOString().split('T')[0];
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Get usage info for an IP address
|
| 32 |
+
*/
|
| 33 |
+
export async function getUsage(ip: string): Promise<{ used: number; remaining: number; limit: number; resetDate: string }> {
|
| 34 |
+
const today = getToday();
|
| 35 |
+
|
| 36 |
+
if (!supabase) {
|
| 37 |
+
console.warn('[Usage Store] Supabase not configured, allowing unlimited access');
|
| 38 |
+
return { used: 0, remaining: DAILY_LIMIT, limit: DAILY_LIMIT, resetDate: today };
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
const { data, error } = await supabase
|
| 43 |
+
.from('usage_tracking')
|
| 44 |
+
.select('request_count')
|
| 45 |
+
.eq('ip_address', ip)
|
| 46 |
+
.eq('date', today)
|
| 47 |
+
.single();
|
| 48 |
+
|
| 49 |
+
if (error && error.code !== 'PGRST116') { // PGRST116 = no rows found
|
| 50 |
+
console.error('[Usage Store] Error fetching usage:', error);
|
| 51 |
+
// On error, allow access but log it
|
| 52 |
+
return { used: 0, remaining: DAILY_LIMIT, limit: DAILY_LIMIT, resetDate: today };
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const used = data?.request_count || 0;
|
| 56 |
+
console.log(`[Usage Store] IP ${ip.substring(0, 8)}*** has used ${used}/${DAILY_LIMIT} requests today`);
|
| 57 |
+
return {
|
| 58 |
+
used,
|
| 59 |
+
remaining: Math.max(0, DAILY_LIMIT - used),
|
| 60 |
+
limit: DAILY_LIMIT,
|
| 61 |
+
resetDate: today
|
| 62 |
+
};
|
| 63 |
+
} catch (error) {
|
| 64 |
+
console.error('[Usage Store] Exception fetching usage:', error);
|
| 65 |
+
return { used: 0, remaining: DAILY_LIMIT, limit: DAILY_LIMIT, resetDate: today };
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Check if an IP can make a request (has remaining quota)
|
| 71 |
+
*/
|
| 72 |
+
export async function canMakeRequest(ip: string): Promise<boolean> {
|
| 73 |
+
const usage = await getUsage(ip);
|
| 74 |
+
return usage.remaining > 0;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Record a request for an IP address
|
| 79 |
+
* Returns the updated usage info
|
| 80 |
+
*/
|
| 81 |
+
export async function recordRequest(ip: string): Promise<{ used: number; remaining: number; limit: number }> {
|
| 82 |
+
const today = getToday();
|
| 83 |
+
|
| 84 |
+
if (!supabase) {
|
| 85 |
+
console.warn('[Usage Store] Supabase not configured, skipping usage recording');
|
| 86 |
+
return { used: 1, remaining: DAILY_LIMIT - 1, limit: DAILY_LIMIT };
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
try {
|
| 90 |
+
// First, try to call the increment_usage RPC function
|
| 91 |
+
const { data: rpcData, error: rpcError } = await supabase
|
| 92 |
+
.rpc('increment_usage', { p_ip: ip, p_date: today });
|
| 93 |
+
|
| 94 |
+
if (!rpcError && rpcData !== null) {
|
| 95 |
+
const used = rpcData as number;
|
| 96 |
+
console.log(`[Usage Store] Recorded request for IP ${ip.substring(0, 8)}***: ${used}/${DAILY_LIMIT}`);
|
| 97 |
+
return {
|
| 98 |
+
used,
|
| 99 |
+
remaining: Math.max(0, DAILY_LIMIT - used),
|
| 100 |
+
limit: DAILY_LIMIT
|
| 101 |
+
};
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// If RPC failed, fall back to manual upsert
|
| 105 |
+
console.log('[Usage Store] RPC failed, trying manual approach:', rpcError);
|
| 106 |
+
|
| 107 |
+
// Check if record exists
|
| 108 |
+
const { data: existingData } = await supabase
|
| 109 |
+
.from('usage_tracking')
|
| 110 |
+
.select('request_count')
|
| 111 |
+
.eq('ip_address', ip)
|
| 112 |
+
.eq('date', today)
|
| 113 |
+
.single();
|
| 114 |
+
|
| 115 |
+
if (existingData) {
|
| 116 |
+
// Update existing record
|
| 117 |
+
const newCount = existingData.request_count + 1;
|
| 118 |
+
const { error: updateError } = await supabase
|
| 119 |
+
.from('usage_tracking')
|
| 120 |
+
.update({
|
| 121 |
+
request_count: newCount,
|
| 122 |
+
last_request_at: new Date().toISOString(),
|
| 123 |
+
updated_at: new Date().toISOString()
|
| 124 |
+
})
|
| 125 |
+
.eq('ip_address', ip)
|
| 126 |
+
.eq('date', today);
|
| 127 |
+
|
| 128 |
+
if (updateError) {
|
| 129 |
+
console.error('[Usage Store] Error updating usage:', updateError);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
console.log(`[Usage Store] Updated request count for IP ${ip.substring(0, 8)}***: ${newCount}/${DAILY_LIMIT}`);
|
| 133 |
+
return {
|
| 134 |
+
used: newCount,
|
| 135 |
+
remaining: Math.max(0, DAILY_LIMIT - newCount),
|
| 136 |
+
limit: DAILY_LIMIT
|
| 137 |
+
};
|
| 138 |
+
} else {
|
| 139 |
+
// Insert new record
|
| 140 |
+
const { error: insertError } = await supabase
|
| 141 |
+
.from('usage_tracking')
|
| 142 |
+
.insert({
|
| 143 |
+
ip_address: ip,
|
| 144 |
+
date: today,
|
| 145 |
+
request_count: 1,
|
| 146 |
+
last_request_at: new Date().toISOString(),
|
| 147 |
+
created_at: new Date().toISOString(),
|
| 148 |
+
updated_at: new Date().toISOString()
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
if (insertError) {
|
| 152 |
+
console.error('[Usage Store] Error inserting usage:', insertError);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
console.log(`[Usage Store] Created new usage record for IP ${ip.substring(0, 8)}***: 1/${DAILY_LIMIT}`);
|
| 156 |
+
return {
|
| 157 |
+
used: 1,
|
| 158 |
+
remaining: DAILY_LIMIT - 1,
|
| 159 |
+
limit: DAILY_LIMIT
|
| 160 |
+
};
|
| 161 |
+
}
|
| 162 |
+
} catch (error) {
|
| 163 |
+
console.error('[Usage Store] Exception recording usage:', error);
|
| 164 |
+
return { used: 1, remaining: DAILY_LIMIT - 1, limit: DAILY_LIMIT };
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Get the daily limit constant
|
| 170 |
+
*/
|
| 171 |
+
export function getDailyLimit(): number {
|
| 172 |
+
return DAILY_LIMIT;
|
| 173 |
+
}
|
package-lock.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
| 12 |
"@google/genai": "^1.17.0",
|
| 13 |
"@huggingface/hub": "^2.6.3",
|
| 14 |
"@huggingface/inference": "^4.8.0",
|
|
|
|
| 15 |
"class-variance-authority": "^0.7.0",
|
| 16 |
"clsx": "^2.1.1",
|
| 17 |
"lucide-react": "^0.542.0",
|
|
@@ -1062,6 +1063,86 @@
|
|
| 1062 |
"dev": true,
|
| 1063 |
"license": "MIT"
|
| 1064 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
"node_modules/@swc/helpers": {
|
| 1066 |
"version": "0.5.15",
|
| 1067 |
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
|
@@ -1383,12 +1464,17 @@
|
|
| 1383 |
"version": "20.19.13",
|
| 1384 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz",
|
| 1385 |
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
|
| 1386 |
-
"dev": true,
|
| 1387 |
"license": "MIT",
|
| 1388 |
"dependencies": {
|
| 1389 |
"undici-types": "~6.21.0"
|
| 1390 |
}
|
| 1391 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1392 |
"node_modules/@types/react": {
|
| 1393 |
"version": "19.1.12",
|
| 1394 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
|
@@ -1409,6 +1495,15 @@
|
|
| 1409 |
"@types/react": "^19.0.0"
|
| 1410 |
}
|
| 1411 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 1413 |
"version": "8.42.0",
|
| 1414 |
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
|
|
@@ -3876,6 +3971,15 @@
|
|
| 3876 |
"node": ">= 14"
|
| 3877 |
}
|
| 3878 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3879 |
"node_modules/ignore": {
|
| 3880 |
"version": "5.3.2",
|
| 3881 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
@@ -6408,7 +6512,6 @@
|
|
| 6408 |
"version": "6.21.0",
|
| 6409 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 6410 |
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 6411 |
-
"dev": true,
|
| 6412 |
"license": "MIT"
|
| 6413 |
},
|
| 6414 |
"node_modules/unrs-resolver": {
|
|
|
|
| 12 |
"@google/genai": "^1.17.0",
|
| 13 |
"@huggingface/hub": "^2.6.3",
|
| 14 |
"@huggingface/inference": "^4.8.0",
|
| 15 |
+
"@supabase/supabase-js": "^2.91.0",
|
| 16 |
"class-variance-authority": "^0.7.0",
|
| 17 |
"clsx": "^2.1.1",
|
| 18 |
"lucide-react": "^0.542.0",
|
|
|
|
| 1063 |
"dev": true,
|
| 1064 |
"license": "MIT"
|
| 1065 |
},
|
| 1066 |
+
"node_modules/@supabase/auth-js": {
|
| 1067 |
+
"version": "2.91.0",
|
| 1068 |
+
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.91.0.tgz",
|
| 1069 |
+
"integrity": "sha512-9ywvsKLsxTwv7fvN5fXzP3UfRreqrX2waylTBDu0lkmeHXa8WtSQS9e0WV9FBduiazYqQbgfBQXBNPRPsRgWOQ==",
|
| 1070 |
+
"license": "MIT",
|
| 1071 |
+
"dependencies": {
|
| 1072 |
+
"tslib": "2.8.1"
|
| 1073 |
+
},
|
| 1074 |
+
"engines": {
|
| 1075 |
+
"node": ">=20.0.0"
|
| 1076 |
+
}
|
| 1077 |
+
},
|
| 1078 |
+
"node_modules/@supabase/functions-js": {
|
| 1079 |
+
"version": "2.91.0",
|
| 1080 |
+
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.91.0.tgz",
|
| 1081 |
+
"integrity": "sha512-WaakXOqLK1mLtBNFXp5o5T+LlI6KZuADSeXz+9ofPRG5OpVSvW148LVJB1DRZ16Phck1a0YqIUswOUgxCz6vMw==",
|
| 1082 |
+
"license": "MIT",
|
| 1083 |
+
"dependencies": {
|
| 1084 |
+
"tslib": "2.8.1"
|
| 1085 |
+
},
|
| 1086 |
+
"engines": {
|
| 1087 |
+
"node": ">=20.0.0"
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
+
"node_modules/@supabase/postgrest-js": {
|
| 1091 |
+
"version": "2.91.0",
|
| 1092 |
+
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.91.0.tgz",
|
| 1093 |
+
"integrity": "sha512-5S41zv2euNpGucvtM4Wy+xOmLznqt/XO+Lh823LOFEQ00ov7QJfvqb6VzIxufvzhooZpmGR0BxvMcJtWxCIFdQ==",
|
| 1094 |
+
"license": "MIT",
|
| 1095 |
+
"dependencies": {
|
| 1096 |
+
"tslib": "2.8.1"
|
| 1097 |
+
},
|
| 1098 |
+
"engines": {
|
| 1099 |
+
"node": ">=20.0.0"
|
| 1100 |
+
}
|
| 1101 |
+
},
|
| 1102 |
+
"node_modules/@supabase/realtime-js": {
|
| 1103 |
+
"version": "2.91.0",
|
| 1104 |
+
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.91.0.tgz",
|
| 1105 |
+
"integrity": "sha512-u2YuJFG35umw8DO9beC27L/jYXm3KhF+73WQwbynMpV0tXsFIA0DOGRM0NgRyy03hJIdO6mxTTwe8efW3yx3Tg==",
|
| 1106 |
+
"license": "MIT",
|
| 1107 |
+
"dependencies": {
|
| 1108 |
+
"@types/phoenix": "^1.6.6",
|
| 1109 |
+
"@types/ws": "^8.18.1",
|
| 1110 |
+
"tslib": "2.8.1",
|
| 1111 |
+
"ws": "^8.18.2"
|
| 1112 |
+
},
|
| 1113 |
+
"engines": {
|
| 1114 |
+
"node": ">=20.0.0"
|
| 1115 |
+
}
|
| 1116 |
+
},
|
| 1117 |
+
"node_modules/@supabase/storage-js": {
|
| 1118 |
+
"version": "2.91.0",
|
| 1119 |
+
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.91.0.tgz",
|
| 1120 |
+
"integrity": "sha512-CI7fsVIBQHfNObqU9kmyQ1GWr+Ug44y4rSpvxT4LdQB9tlhg1NTBov6z7Dlmt8d6lGi/8a9lf/epCDxyWI792g==",
|
| 1121 |
+
"license": "MIT",
|
| 1122 |
+
"dependencies": {
|
| 1123 |
+
"iceberg-js": "^0.8.1",
|
| 1124 |
+
"tslib": "2.8.1"
|
| 1125 |
+
},
|
| 1126 |
+
"engines": {
|
| 1127 |
+
"node": ">=20.0.0"
|
| 1128 |
+
}
|
| 1129 |
+
},
|
| 1130 |
+
"node_modules/@supabase/supabase-js": {
|
| 1131 |
+
"version": "2.91.0",
|
| 1132 |
+
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.91.0.tgz",
|
| 1133 |
+
"integrity": "sha512-Rjb0QqkKrmXMVwUOdEqysPBZ0ZDZakeptTkUa6k2d8r3strBdbWVDqjOdkCjAmvvZMtXecBeyTyMEXD1Zzjfvg==",
|
| 1134 |
+
"license": "MIT",
|
| 1135 |
+
"dependencies": {
|
| 1136 |
+
"@supabase/auth-js": "2.91.0",
|
| 1137 |
+
"@supabase/functions-js": "2.91.0",
|
| 1138 |
+
"@supabase/postgrest-js": "2.91.0",
|
| 1139 |
+
"@supabase/realtime-js": "2.91.0",
|
| 1140 |
+
"@supabase/storage-js": "2.91.0"
|
| 1141 |
+
},
|
| 1142 |
+
"engines": {
|
| 1143 |
+
"node": ">=20.0.0"
|
| 1144 |
+
}
|
| 1145 |
+
},
|
| 1146 |
"node_modules/@swc/helpers": {
|
| 1147 |
"version": "0.5.15",
|
| 1148 |
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
|
|
|
| 1464 |
"version": "20.19.13",
|
| 1465 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz",
|
| 1466 |
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
|
|
|
|
| 1467 |
"license": "MIT",
|
| 1468 |
"dependencies": {
|
| 1469 |
"undici-types": "~6.21.0"
|
| 1470 |
}
|
| 1471 |
},
|
| 1472 |
+
"node_modules/@types/phoenix": {
|
| 1473 |
+
"version": "1.6.7",
|
| 1474 |
+
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
|
| 1475 |
+
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
| 1476 |
+
"license": "MIT"
|
| 1477 |
+
},
|
| 1478 |
"node_modules/@types/react": {
|
| 1479 |
"version": "19.1.12",
|
| 1480 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
|
|
|
| 1495 |
"@types/react": "^19.0.0"
|
| 1496 |
}
|
| 1497 |
},
|
| 1498 |
+
"node_modules/@types/ws": {
|
| 1499 |
+
"version": "8.18.1",
|
| 1500 |
+
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
| 1501 |
+
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
| 1502 |
+
"license": "MIT",
|
| 1503 |
+
"dependencies": {
|
| 1504 |
+
"@types/node": "*"
|
| 1505 |
+
}
|
| 1506 |
+
},
|
| 1507 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 1508 |
"version": "8.42.0",
|
| 1509 |
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
|
|
|
|
| 3971 |
"node": ">= 14"
|
| 3972 |
}
|
| 3973 |
},
|
| 3974 |
+
"node_modules/iceberg-js": {
|
| 3975 |
+
"version": "0.8.1",
|
| 3976 |
+
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
| 3977 |
+
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
| 3978 |
+
"license": "MIT",
|
| 3979 |
+
"engines": {
|
| 3980 |
+
"node": ">=20.0.0"
|
| 3981 |
+
}
|
| 3982 |
+
},
|
| 3983 |
"node_modules/ignore": {
|
| 3984 |
"version": "5.3.2",
|
| 3985 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
|
|
| 6512 |
"version": "6.21.0",
|
| 6513 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 6514 |
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
|
|
| 6515 |
"license": "MIT"
|
| 6516 |
},
|
| 6517 |
"node_modules/unrs-resolver": {
|
package.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
"@google/genai": "^1.17.0",
|
| 14 |
"@huggingface/hub": "^2.6.3",
|
| 15 |
"@huggingface/inference": "^4.8.0",
|
|
|
|
| 16 |
"class-variance-authority": "^0.7.0",
|
| 17 |
"clsx": "^2.1.1",
|
| 18 |
"lucide-react": "^0.542.0",
|
|
@@ -33,4 +34,4 @@
|
|
| 33 |
"tailwindcss": "^4",
|
| 34 |
"typescript": "^5"
|
| 35 |
}
|
| 36 |
-
}
|
|
|
|
| 13 |
"@google/genai": "^1.17.0",
|
| 14 |
"@huggingface/hub": "^2.6.3",
|
| 15 |
"@huggingface/inference": "^4.8.0",
|
| 16 |
+
"@supabase/supabase-js": "^2.91.0",
|
| 17 |
"class-variance-authority": "^0.7.0",
|
| 18 |
"clsx": "^2.1.1",
|
| 19 |
"lucide-react": "^0.542.0",
|
|
|
|
| 34 |
"tailwindcss": "^4",
|
| 35 |
"typescript": "^5"
|
| 36 |
}
|
| 37 |
+
}
|