Reubencf commited on
Commit
2619792
·
1 Parent(s): 1ccbe14

beggining free trials

Browse files
.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({ image: images[0], images, text: texts.join("\n") });
 
 
 
 
 
 
 
 
 
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({ image: images[0] });
 
 
 
 
 
 
 
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>('huggingface');
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
- Gemini API Key:
2189
  </label>
2190
  <Input
2191
  id="api-token"
2192
  type="password"
2193
- placeholder="Enter your Google Gemini API key"
2194
  value={apiToken}
2195
  onChange={(e) => setApiToken(e.target.value)}
2196
- className="w-56"
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
+ }