diff --git a/.env.local.example b/.env.local.example index ed6ed73..7d36b0a 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1 +1,5 @@ OPENAI_API_KEY=sk-... +ANTHROPIC_API_KEY=sk-ant-... +GEMINI_API_KEY=AI... +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... diff --git a/.gitignore b/.gitignore index 8536099..2a126bb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ .env out/ .shape-build/ +.vercel diff --git a/.notmagic.yaml b/.notmagic.yaml new file mode 100644 index 0000000..fceceb2 --- /dev/null +++ b/.notmagic.yaml @@ -0,0 +1,9 @@ +notmagic: + mode: advisory + checks: + release_readiness: true + ownership_claims: true + observability: true + rollback_scope_split: true + comment: true + check_run: true diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3f10b93 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,24 @@ + +## Best practices for developing on Vercel + +These defaults are optimized for AI coding agents (and humans) working on apps that deploy to Vercel. + +- Treat Vercel Functions as stateless + ephemeral (no durable RAM/FS, no background daemons), use Blob or marketplace integrations for preserving state +- Edge Functions (standalone) are deprecated; prefer Vercel Functions +- Don't start new projects on Vercel KV/Postgres (both discontinued); use Marketplace Redis/Postgres instead +- Store secrets in Vercel Env Variables; not in git or `NEXT_PUBLIC_*` +- Provision Marketplace native integrations with `vercel integration add` (CI/agent-friendly) +- Sync env + project settings with `vercel env pull` / `vercel pull` when you need local/offline parity +- Use `waitUntil` for post-response work; avoid the deprecated Function `context` parameter +- Set Function regions near your primary data source; avoid cross-region DB/service roundtrips +- Tune Fluid Compute knobs (e.g., `maxDuration`, memory/CPU) for long I/O-heavy calls (LLMs, APIs) +- Use Runtime Cache for fast **regional** caching + tag invalidation (don't treat it as global KV) +- Use Cron Jobs for schedules; cron runs in UTC and triggers your production URL via HTTP GET +- Use Vercel Blob for uploads/media; Use Edge Config for small, globally-read config +- If Enable Deployment Protection is enabled, use a bypass secret to directly access them +- Add OpenTelemetry via `@vercel/otel` on Node; don't expect OTEL support on the Edge runtime +- Enable Web Analytics + Speed Insights early +- Use AI Gateway for model routing, set AI_GATEWAY_API_KEY, using a model string (e.g. 'anthropic/claude-sonnet-4.6'), Gateway is already default in AI SDK + needed. Always curl https://ai-gateway.vercel.sh/v1/models first; never trust model IDs from memory +- For durable agent loops or untrusted code: use Workflow (pause/resume/state) + Sandbox; use Vercel MCP for secure infra access + diff --git a/app/api/shape/route.ts b/app/api/shape/route.ts index 25c392a..6962b69 100644 --- a/app/api/shape/route.ts +++ b/app/api/shape/route.ts @@ -3,7 +3,7 @@ import { shape } from "@/lib/shape"; import type { ShapeEngine, ShapeProfile } from "@/lib/types"; const VALID_PROFILES = ["narrative_segment_v0", "concept_blob_v0"]; -const VALID_ENGINES = ["openai", "local"]; +const VALID_ENGINES = ["openai", "anthropic", "gemini", "local"]; export async function POST(request: NextRequest) { try { @@ -28,7 +28,19 @@ export async function POST(request: NextRequest) { ? (engine as ShapeEngine) : "openai"; - const result = await shape(text, profileOverride, engineOverride); + let result; + try { + result = await shape(text, profileOverride, engineOverride); + } catch (primaryErr) { + // If an LLM engine failed, fall back to local with a note + if (engineOverride !== "local") { + result = await shape(text, profileOverride, "local"); + result.engine = "local" as ShapeEngine; + result.fallback_reason = primaryErr instanceof Error ? primaryErr.message : "LLM unavailable"; + } else { + throw primaryErr; + } + } return NextResponse.json(result); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Unknown error"; diff --git a/app/history/page.tsx b/app/history/page.tsx new file mode 100644 index 0000000..1c46769 --- /dev/null +++ b/app/history/page.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AuthProvider, useAuth } from "@/components/auth-provider"; +import { UserMenu } from "@/components/user-menu"; +import { listShapes } from "@/lib/save-shape"; +import type { ShapeResult } from "@/lib/types"; + +interface ShapeRow { + id: string; + title: string; + profile: string; + engine: string; + signal_level: string; + created_at: string; + result: ShapeResult; +} + +function HistoryList() { + const { user, loading: authLoading } = useAuth(); + const [shapes, setShapes] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (authLoading) return; + if (!user) { + setLoading(false); + return; + } + listShapes() + .then((data) => setShapes(data as ShapeRow[])) + .finally(() => setLoading(false)); + }, [user, authLoading]); + + if (authLoading || loading) { + return

Loading…

; + } + + if (!user) { + return ( +

+ + Sign in + {" "} + to see your saved shapes. +

+ ); + } + + if (shapes.length === 0) { + return ( +

+ No shapes yet.{" "} + + Shape something + . +

+ ); + } + + return ( +
+ {shapes.map((s) => { + const spine = s.result?.spine ?? []; + return ( + +
+

{s.title}

+
+ + {s.profile === "narrative_segment_v0" ? "narrative" : "concept"} + + + {s.signal_level} + + + {s.engine === "local" ? "local" : s.engine === "anthropic" ? "claude" : s.engine === "gemini" ? "gemini" : "gpt-4.1"} + +
+
+ {spine.length > 0 && ( +
+ {spine.slice(0, 3).map((line, i) => ( +

+ {line} +

+ ))} + {spine.length > 3 && ( +

+{spine.length - 3} more

+ )} +
+ )} +

+ {new Date(s.created_at).toLocaleDateString("en-US", { + month: "short", day: "numeric", year: "numeric", + hour: "numeric", minute: "2-digit", + })} +

+
+ ); + })} +
+ ); +} + +export default function HistoryPage() { + return ( + +
+ +
+
+

History

+ + back to shape + +
+ +
+
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index acf79d7..8392412 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,7 +3,8 @@ import "./globals.css"; export const metadata: Metadata = { title: "Shape", - description: "Paste text. Get structure. See what survived.", + description: "Paste text, get structured meaning.", + viewport: "width=device-width, initial-scale=1, maximum-scale=1", }; export default function RootLayout({ diff --git a/app/page.tsx b/app/page.tsx index 585145b..1f0a83f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,48 +3,65 @@ import { useState } from "react"; import { ShapeForm } from "@/components/shape-form"; import { ShapeResult as ShapeResultView } from "@/components/shape-result"; +import { AuthProvider } from "@/components/auth-provider"; +import { UserMenu } from "@/components/user-menu"; import type { ShapeResult } from "@/lib/types"; export default function Home() { const [result, setResult] = useState(null); + const [sourceText, setSourceText] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); return ( -
- {/* Top progress bar */} - {loading && ( -
-
+ +
+ {loading && ( +
+
+
+ )} + + + +
+
+
+

+ Shape +

+

+ Paste text, get structured meaning. +

+
+ + History + +
- )} - -
-

- Shape -

-

- Paste text. Get structure. See what survived. -

-
- - { - setResult(data as ShapeResult); - setError(""); - }} - onError={(msg) => { - setError(msg); - setResult(null); - }} - onLoading={setLoading} - /> - - {error && ( -

{error}

- )} - - {result && } -
+ + { + setResult(data as ShapeResult); + setSourceText(text); + setError(""); + }} + onError={(msg) => { + setError(msg); + setResult(null); + }} + onLoading={setLoading} + /> + + {error && ( +

{error}

+ )} + + {result && } +
+ ); } diff --git a/app/s/[id]/page.tsx b/app/s/[id]/page.tsx new file mode 100644 index 0000000..ea478af --- /dev/null +++ b/app/s/[id]/page.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import { ShapeResult as ShapeResultView } from "@/components/shape-result"; +import { AuthProvider } from "@/components/auth-provider"; +import { UserMenu } from "@/components/user-menu"; +import type { ShapeResult } from "@/lib/types"; +import { getSupabase } from "@/lib/supabase"; + +export default function ShapePermalink() { + const { id } = useParams<{ id: string }>(); + const [result, setResult] = useState(null); + const [sourceText, setSourceText] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!id) return; + const supabase = getSupabase(); + supabase + .from("shapes") + .select("result, source_text") + .eq("id", id) + .single() + .then(({ data, error: err }: { data: { result: ShapeResult; source_text: string } | null; error: unknown }) => { + if (err || !data) { + setError("Shape not found"); + } else { + setResult(data.result as ShapeResult); + setSourceText(data.source_text); + } + setLoading(false); + }); + }, [id]); + + return ( + +
+ + + {loading &&

Loading…

} + {error &&

{error}

} + {result && } +
+
+ ); +} diff --git a/components/auth-provider.tsx b/components/auth-provider.tsx new file mode 100644 index 0000000..4d150fb --- /dev/null +++ b/components/auth-provider.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { createContext, useContext, useEffect, useState } from "react"; +import type { User } from "@supabase/supabase-js"; + +const AuthContext = createContext<{ + user: User | null; + loading: boolean; + signOut: () => Promise; +}>({ user: null, loading: false, signOut: async () => {} }); + +export function useAuth() { + return useContext(AuthContext); +} + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + async function init() { + try { + const { getSupabase } = await import("@/lib/supabase"); + const supabase = getSupabase(); + + const { data } = await supabase.auth.getSession(); + if (!cancelled) { + setUser(data.session?.user ?? null); + setLoading(false); + } + + supabase.auth.onAuthStateChange((_event: string, session: { user: User } | null) => { + if (!cancelled) { + setUser(session?.user ?? null); + setLoading(false); + } + }); + } catch { + if (!cancelled) setLoading(false); + } + } + + init(); + return () => { cancelled = true; }; + }, []); + + async function signOut() { + try { + const { getSupabase } = await import("@/lib/supabase"); + await getSupabase().auth.signOut(); + setUser(null); + } catch { + // ignore + } + } + + return ( + + {children} + + ); +} diff --git a/components/shape-form.tsx b/components/shape-form.tsx index eaef0b4..2a8eb88 100644 --- a/components/shape-form.tsx +++ b/components/shape-form.tsx @@ -1,19 +1,20 @@ "use client"; import { useState, useRef } from "react"; -import type { ShapeProfile } from "@/lib/types"; +import type { ShapeProfile, ShapeEngine } from "@/lib/types"; export function ShapeForm({ onResult, onError, onLoading, }: { - onResult: (data: unknown) => void; + onResult: (data: unknown, sourceText: string) => void; onError: (msg: string) => void; onLoading: (loading: boolean) => void; }) { const [text, setText] = useState(""); const [profile, setProfile] = useState("auto"); + const [engine, setEngine] = useState("openai"); const [loading, setLoading] = useState(false); const [elapsed, setElapsed] = useState(0); const timerRef = useRef | null>(null); @@ -40,6 +41,7 @@ export function ShapeForm({ try { const body: Record = { text }; if (profile !== "auto") body.profile = profile; + body.engine = engine; const res = await fetch("/api/shape", { method: "POST", @@ -52,7 +54,7 @@ export function ShapeForm({ if (!res.ok) { onError(data.error || "Something went wrong"); } else { - onResult(data); + onResult(data, text); } } catch { onError("Failed to reach the API"); @@ -76,12 +78,12 @@ export function ShapeForm({ onChange={(e) => setText(e.target.value)} onKeyDown={handleKeyDown} placeholder="Paste messy text here..." - rows={10} + rows={6} disabled={loading} className="w-full bg-neutral-900 border border-neutral-800 rounded-lg p-4 text-sm font-mono text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-neutral-600 resize-y disabled:opacity-50 transition-opacity" /> -
-
+
+
{text.length.toLocaleString()} chars @@ -95,6 +97,17 @@ export function ShapeForm({ + {loading && ( {elapsed.toFixed(1)}s diff --git a/components/shape-result.tsx b/components/shape-result.tsx index 731fb7d..1a629e9 100644 --- a/components/shape-result.tsx +++ b/components/shape-result.tsx @@ -1,9 +1,12 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import type { ShapeResult, NarrativeSegment, ConceptBlob, SupportMap } from "@/lib/types"; import { CastView } from "./cast-view"; import { CheckView } from "./check-view"; +import { useAuth } from "./auth-provider"; +import { SignInModal } from "./sign-in-modal"; +import { saveShape } from "@/lib/save-shape"; function downloadFile(content: string, filename: string, type: string) { const blob = new Blob([content], { type }); @@ -81,14 +84,76 @@ function ConceptView({ output }: { output: ConceptBlob }) { ); } -export function ShapeResult({ result }: { result: ShapeResult }) { +export function ShapeResult({ result, sourceText }: { result: ShapeResult; sourceText: string }) { const [tab, setTab] = useState<"readable" | "json" | "card">("readable"); const [showFull, setShowFull] = useState(false); - const { profile, spine, output, support, casts, check } = result; + const [visibleSpine, setVisibleSpine] = useState(0); + const [titleVisible, setTitleVisible] = useState(false); + const [badgesVisible, setBadgesVisible] = useState(false); + const [saved, setSaved] = useState(false); + const [saving, setSaving] = useState(false); + const [permalink, setPermalink] = useState(null); + const [showSignIn, setShowSignIn] = useState(false); + const [pendingSave, setPendingSave] = useState(false); + const { user } = useAuth(); + const { profile, engine, spine, output, support, casts, check, fallback_reason } = result; + + useEffect(() => { setSaved(false); setPermalink(null); }, [result]); + + // Auto-save after sign-in if save was pending + useEffect(() => { + if (user && pendingSave && !saved && !saving) { + setPendingSave(false); + doSave(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user, pendingSave]); + + async function doSave() { + setSaving(true); + try { + const id = await saveShape(sourceText, result); + setSaved(true); + setPermalink(`${window.location.origin}/s/${id}`); + } catch { + // silently fail — user can retry + } finally { + setSaving(false); + } + } + + async function handleSave() { + if (!user) { + setPendingSave(true); + setShowSignIn(true); + return; + } + await doSave(); + } + + // Staggered reveal: badges → title → spine lines one by one + useEffect(() => { + setVisibleSpine(0); + setTitleVisible(false); + setBadgesVisible(false); + + const t0 = setTimeout(() => setBadgesVisible(true), 100); + const t1 = setTimeout(() => setTitleVisible(true), 350); + + const spineTimers = spine.map((_, i) => + setTimeout(() => setVisibleSpine(i + 1), 600 + i * 400) + ); + + return () => { + clearTimeout(t0); + clearTimeout(t1); + spineTimers.forEach(clearTimeout); + }; + }, [result, spine]); return (
-
+
{profile === "narrative_segment_v0" ? "narrative" : "concept"} @@ -99,20 +164,35 @@ export function ShapeResult({ result }: { result: ShapeResult }) { }`}> {output.signal_level} + + {engine === "local" ? "local" : engine === "anthropic" ? "claude" : engine === "gemini" ? "gemini" : "gpt-4.1"} + {check.compression_holds && ( compressed )}
+ {fallback_reason && ( +

+ fell back to local engine — {fallback_reason} +

+ )} -

{output.title}

+

+ {output.title} +

- {/* Spine — the primary value surface */} + {/* Spine — staggered reveal */} {spine.length > 0 && (
{spine.map((s, i) => ( -

+

{s}

))} @@ -209,7 +289,7 @@ export function ShapeResult({ result }: { result: ShapeResult }) { onClick={() => copyToClipboard(spine.join("\n"))} className="text-xs font-mono px-3 py-1.5 rounded border border-neutral-800 text-neutral-500 hover:text-neutral-300 hover:border-neutral-600 transition-colors" > - Copy spine + Copy summary + + {permalink && ( + + )}
+ {showSignIn && setShowSignIn(false)} />}
); } diff --git a/components/sign-in-modal.tsx b/components/sign-in-modal.tsx new file mode 100644 index 0000000..b8722cb --- /dev/null +++ b/components/sign-in-modal.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState } from "react"; +import { getSupabase } from "@/lib/supabase"; + +export function SignInModal({ onClose }: { onClose: () => void }) { + const [email, setEmail] = useState(""); + const [sent, setSent] = useState(false); + const [error, setError] = useState(""); + + async function handleGoogle() { + const supabase = getSupabase(); + await supabase.auth.signInWithOAuth({ + provider: "google", + options: { redirectTo: window.location.origin }, + }); + } + + async function handleMagicLink() { + if (!email.trim()) return; + setError(""); + const supabase = getSupabase(); + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { emailRedirectTo: window.location.origin }, + }); + if (error) { + setError(error.message); + } else { + setSent(true); + } + } + + return ( +
+
e.stopPropagation()}> +

Save your shape

+

Sign in to save and revisit your meaning objects.

+ + + +
+
+ or +
+
+ + {sent ? ( +

Check your email for the sign-in link.

+ ) : ( +
+ setEmail(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleMagicLink()} + placeholder="you@example.com" + className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-neutral-600" + /> + +
+ )} + + {error &&

{error}

} + + +
+
+ ); +} diff --git a/components/user-menu.tsx b/components/user-menu.tsx new file mode 100644 index 0000000..627ea2d --- /dev/null +++ b/components/user-menu.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useAuth } from "./auth-provider"; +import { SignInModal } from "./sign-in-modal"; + +export function UserMenu() { + const { user, signOut } = useAuth(); + const [showSignIn, setShowSignIn] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + if (!mounted) return null; + + return ( + <> +
+ {user ? ( +
+ + {user.email} + + +
+ ) : ( + + )} +
+ {showSignIn && setShowSignIn(false)} />} + + ); +} diff --git a/docs/notmagic-smoke-conflict.md b/docs/notmagic-smoke-conflict.md new file mode 100644 index 0000000..24ce6b6 --- /dev/null +++ b/docs/notmagic-smoke-conflict.md @@ -0,0 +1,26 @@ +# Not Magic PR Smoke Conflict + +This document is intentionally contradictory so the Not Magic GitHub integration +has a safe PR artifact to review. + +## Claimed Decision + +Ship this change as release-ready. + +## Supporting Notes + +- The UI build passes locally. +- The README-only config change should not affect runtime behavior. +- The PR is advisory and safe to merge from a build perspective. + +## Unresolved Reliance + +- Rollback ownership is not assigned. +- Observability is incomplete because the relevant alarms are muted. +- Production-scale behavior is unproven because the test used only a staging-sized sample. +- No owner has accepted responsibility for restoring service if the release causes user-visible degradation. + +## Expected Not Magic Reading + +The PR can rely on build safety, but it should not rely on release readiness. +The unresolved scopes should include ownership, observability, rollback, and production-scale evidence. diff --git a/lib/engine.ts b/lib/engine.ts index b0f769e..4cf8c87 100644 --- a/lib/engine.ts +++ b/lib/engine.ts @@ -1,6 +1,6 @@ import { buildSystemPrompt } from "./prompt"; import { runLocalShape } from "./local-engine"; -import { runOpenAIShapePrompt } from "./model"; +import { runOpenAIShapePrompt, runAnthropicShapePrompt, runGeminiShapePrompt } from "./model"; import type { ShapeEngine, ShapeProfile } from "./types"; export async function runShapeEngine({ @@ -17,7 +17,15 @@ export async function runShapeEngine({ } const systemPrompt = buildSystemPrompt(profile); - const raw = await runOpenAIShapePrompt({ systemPrompt, userText }); + let raw: string; + + if (engine === "anthropic") { + raw = await runAnthropicShapePrompt({ systemPrompt, userText }); + } else if (engine === "gemini") { + raw = await runGeminiShapePrompt({ systemPrompt, userText }); + } else { + raw = await runOpenAIShapePrompt({ systemPrompt, userText }); + } try { return JSON.parse(raw); diff --git a/lib/model.ts b/lib/model.ts index b5ed588..67f9db6 100644 --- a/lib/model.ts +++ b/lib/model.ts @@ -1,16 +1,36 @@ import OpenAI from "openai"; +import Anthropic from "@anthropic-ai/sdk"; +import { GoogleGenAI } from "@google/genai"; -const client = new OpenAI(); +// Lazy-init all clients to avoid build-time crashes when keys aren't set -export async function runShapePrompt({ +let _openai: OpenAI | null = null; +function getOpenAI() { + if (!_openai) _openai = new OpenAI(); + return _openai; +} + +let _anthropic: Anthropic | null = null; +function getAnthropic() { + if (!_anthropic) _anthropic = new Anthropic(); + return _anthropic; +} + +let _gemini: GoogleGenAI | null = null; +function getGemini() { + if (!_gemini) _gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? "" }); + return _gemini; +} + +export async function runOpenAIShapePrompt({ systemPrompt, userText, }: { systemPrompt: string; userText: string; }): Promise { - const response = await client.chat.completions.create({ - model: "gpt-4.1-mini", + const response = await getOpenAI().chat.completions.create({ + model: "gpt-4.1", temperature: 0, response_format: { type: "json_object" }, messages: [ @@ -18,8 +38,43 @@ export async function runShapePrompt({ { role: "user", content: userText }, ], }); - return response.choices[0]?.message?.content ?? ""; } -export { runShapePrompt as runOpenAIShapePrompt }; +export async function runAnthropicShapePrompt({ + systemPrompt, + userText, +}: { + systemPrompt: string; + userText: string; +}): Promise { + const response = await getAnthropic().messages.create({ + model: "claude-sonnet-4-6", + max_tokens: 4096, + temperature: 0, + system: systemPrompt, + messages: [{ role: "user", content: userText }], + }); + const block = response.content[0]; + if (block.type !== "text") throw new Error("Unexpected Anthropic response type"); + return block.text; +} + +export async function runGeminiShapePrompt({ + systemPrompt, + userText, +}: { + systemPrompt: string; + userText: string; +}): Promise { + const response = await getGemini().models.generateContent({ + model: "gemini-2.5-flash", + contents: userText, + config: { + systemInstruction: systemPrompt, + temperature: 0, + responseMimeType: "application/json", + }, + }); + return response.text ?? ""; +} diff --git a/lib/output-schema.ts b/lib/output-schema.ts index 757c976..7d850e0 100644 --- a/lib/output-schema.ts +++ b/lib/output-schema.ts @@ -43,7 +43,7 @@ export const conceptBlobSchema = z.object({ // Full model response: spine + result + support export const shapeModelResponseSchema = z.object({ - spine: z.array(z.string()).min(1).max(7), + spine: z.array(z.string()).max(7), result: z.union([narrativeSegmentSchema, conceptBlobSchema]), support: supportMapSchema, }); @@ -53,8 +53,29 @@ export type ConceptBlob = z.infer; export type ShapeProfile = "narrative_segment_v0" | "concept_blob_v0"; export type ShapeOutput = NarrativeSegment | ConceptBlob; +function normalizeSupportMap(data: Record): Record { + if (!data || typeof data !== "object") return data; + const support = (data as Record).support; + if (support && typeof support === "object") { + for (const [field, entries] of Object.entries(support as Record)) { + if (Array.isArray(entries)) { + for (const entry of entries) { + if (entry && typeof entry === "object" && "evidence" in entry) { + const e = entry as Record; + if (typeof e.evidence === "string") { + e.evidence = [e.evidence]; + } + } + } + } + } + } + return data; +} + export function validateModelResponse(profile: ShapeProfile, data: unknown) { - const parsed = shapeModelResponseSchema.parse(data); + const normalized = normalizeSupportMap(data as Record); + const parsed = shapeModelResponseSchema.parse(normalized); if (profile === "narrative_segment_v0") { narrativeSegmentSchema.parse(parsed.result); } else { diff --git a/lib/save-shape.ts b/lib/save-shape.ts new file mode 100644 index 0000000..d39ace1 --- /dev/null +++ b/lib/save-shape.ts @@ -0,0 +1,44 @@ +import { getSupabase } from "./supabase"; +import type { ShapeResult } from "./types"; + +export async function saveShape(sourceText: string, result: ShapeResult): Promise { + const supabase = getSupabase(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error("Not signed in"); + + const { data, error } = await supabase.from("shapes").insert({ + user_id: user.id, + source_text: sourceText, + profile: result.profile, + engine: result.engine, + result: result, + title: result.output.title, + signal_level: result.output.signal_level, + }).select("id").single(); + + if (error) throw new Error(error.message); + return data.id; +} + +export async function listShapes() { + const supabase = getSupabase(); + const { data, error } = await supabase + .from("shapes") + .select("id, title, profile, engine, signal_level, created_at, result") + .order("created_at", { ascending: false }); + + if (error) throw new Error(error.message); + return data; +} + +export async function getShape(id: string) { + const supabase = getSupabase(); + const { data, error } = await supabase + .from("shapes") + .select("*") + .eq("id", id) + .single(); + + if (error) throw new Error(error.message); + return data; +} diff --git a/lib/shape.ts b/lib/shape.ts index 28dc7e9..28722b3 100644 --- a/lib/shape.ts +++ b/lib/shape.ts @@ -24,7 +24,9 @@ export async function shape( const parsed = await runShapeEngine({ engine, profile, userText: rawText }); const validated = validateModelResponse(profile, parsed); - const spine = validated.spine; + const spine = validated.spine.length > 0 + ? validated.spine + : [validated.result.title]; const output = validated.result; const support = validated.support as SupportMap; diff --git a/lib/supabase.ts b/lib/supabase.ts new file mode 100644 index 0000000..68f1196 --- /dev/null +++ b/lib/supabase.ts @@ -0,0 +1,15 @@ +import { createBrowserClient } from "@supabase/ssr"; + +let _supabase: ReturnType | null = null; + +export function getSupabase() { + if (!_supabase) { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + if (!url || !key) { + throw new Error("Supabase env vars not set"); + } + _supabase = createBrowserClient(url, key); + } + return _supabase; +} diff --git a/lib/types.ts b/lib/types.ts index 3606482..b044970 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,7 @@ import type { ShapeProfile, NarrativeSegment, ConceptBlob } from "./output-schema"; export type { ShapeProfile, NarrativeSegment, ConceptBlob }; -export type ShapeEngine = "openai" | "local"; +export type ShapeEngine = "openai" | "anthropic" | "gemini" | "local"; export interface SupportEntry { kind: "explicit" | "inferred"; @@ -34,4 +34,5 @@ export interface ShapeResult { host_json_view: object; }; check: ShapeCheck; + fallback_reason?: string; } diff --git a/package-lock.json b/package-lock.json index 2894661..b01193d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@anthropic-ai/sdk": "^0.80.0", + "@google/genai": "^1.46.0", + "@supabase/ssr": "^0.9.0", + "@supabase/supabase-js": "^2.100.1", "@tailwindcss/postcss": "^4.2.2", "@types/node": "^25.5.0", "@types/react": "^19.2.14", @@ -35,6 +39,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.80.0.tgz", + "integrity": "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", @@ -45,6 +78,29 @@ "tslib": "^2.4.0" } }, + "node_modules/@google/genai": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.46.0.tgz", + "integrity": "sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -690,6 +746,168 @@ "node": ">= 10" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@supabase/auth-js": { + "version": "2.100.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.100.1.tgz", + "integrity": "sha512-c5FB4nrG7cs1mLSzFGuIVl2iR2YO5XkSJ96uF4zubYm8YDn71XOi2emE9sBm/avfGCj61jaRBLOvxEAVnpys0Q==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.100.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.100.1.tgz", + "integrity": "sha512-mo8QheoV4KR+wSubtyEWhZUxWnCM7YZ23TncccMAlbWAHb8YTDqRGRm9IalWCAswniKyud6buZCk9snRqI86KA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.100.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.100.1.tgz", + "integrity": "sha512-OIh4mOSo2LdqF2kox76OAPDtcSs+PwKABJOjc6plUV4/LXhFEsI2uwdEEIs7K7fd141qehWEVl/Y+Ts0fNvYsw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.100.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.100.1.tgz", + "integrity": "sha512-FHuRWPX4qZQ4x+0Q+ZrKaBZnOiVGiwsgiAUJM98pYRib1yeaE/fOM1lZ1ozd+4gA8Udw23OyaD8SxKS5mT5NYw==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.0", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.9.0.tgz", + "integrity": "sha512-UFY6otYV3yqCgV+AyHj80vNkTvbf1Gas2LW4dpbQ4ap6p6v3eB2oaDfcI99jsuJzwVBCFU4BJI+oDYyhNk1z0Q==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.97.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.100.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.100.1.tgz", + "integrity": "sha512-x9xpEIoWM4xKiAlwfWTgHPSN6N4Y0aS4FVU4F6ZPbq7Gayw08SrtC6/YH/gOr8CjXQr0HxXYXDop2xGTSjubYA==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.100.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.100.1.tgz", + "integrity": "sha512-CAeFm5sfX8sbTzxoxRafhohreIzl9a7R6qHTck3MrgTqm5M5g/u0IHfEKYzI9w/17r8NINl8UZrw2i08wrO7Iw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.100.1", + "@supabase/functions-js": "2.100.1", + "@supabase/postgrest-js": "2.100.1", + "@supabase/realtime-js": "2.100.1", + "@supabase/storage-js": "2.100.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -982,6 +1200,50 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.11", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", @@ -994,6 +1256,21 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/caniuse-lite": { "version": "1.0.30001781", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", @@ -1020,12 +1297,51 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1035,6 +1351,15 @@ "node": ">=8" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -1048,12 +1373,129 @@ "node": ">=10.13.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1063,6 +1505,49 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -1312,6 +1797,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1321,6 +1812,12 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1420,6 +1917,44 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/openai": { "version": "6.33.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.33.0.tgz", @@ -1441,6 +1976,19 @@ } } }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1475,6 +2023,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -1496,6 +2068,35 @@ "react": "^19.2.4" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -1611,6 +2212,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1636,6 +2243,36 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index ae7c45d..79cff72 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "author": "", "license": "ISC", "dependencies": { + "@anthropic-ai/sdk": "^0.80.0", + "@google/genai": "^1.46.0", + "@supabase/ssr": "^0.9.0", + "@supabase/supabase-js": "^2.100.1", "@tailwindcss/postcss": "^4.2.2", "@types/node": "^25.5.0", "@types/react": "^19.2.14",