diff --git a/ai-pdf-chatbot/.env.example b/ai-pdf-chatbot/.env.example
index 77bb26c..e4fd9b1 100644
--- a/ai-pdf-chatbot/.env.example
+++ b/ai-pdf-chatbot/.env.example
@@ -15,3 +15,11 @@ NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
# Same secret InsForge uses internally; the bridge route signs HS256
# with this. Get it via `npx @insforge/cli secrets get JWT_SECRET`.
INSFORGE_JWT_SECRET=replace-with-output-of-cli-secrets-get-JWT_SECRET
+
+# ─── Audio Overview (optional) ────────────────────────────────────────
+# OpenAI key for TTS-driven workspace audio overviews (NotebookLM-style
+# two-host podcast). Leave blank to disable — the workspace audio tab
+# will surface a friendly "configure to enable" prompt instead of
+# crashing. Embeddings + chat still route through InsForge AI without
+# this key.
+OPENAI_API_KEY=
diff --git a/ai-pdf-chatbot/README.md b/ai-pdf-chatbot/README.md
index 3d6416d..df6e98a 100644
--- a/ai-pdf-chatbot/README.md
+++ b/ai-pdf-chatbot/README.md
@@ -1,9 +1,9 @@
- InsForge AI PDF Chatbot
+ InsForge AI Notebook
- Upload PDFs and chat with them. RAG with citations powered by InsForge pgvector + InsForge AI.
+ Open-source NotebookLM for students. Workspaces of PDFs come with auto-generated mindmaps, spaced-repetition flashcards, two-host podcast summaries, and RAG chat that highlights cited passages inside the source PDF.
@@ -17,18 +17,44 @@
## Features
+NotebookLM-style learning features:
+
+- **Workspaces.** Group related PDFs, chats, mindmaps, and flashcards into one notebook-style container. Cross-document RAG inside a workspace.
+- **Inline PDF viewer.** Click a `[n]` citation, the source PDF slides in from the right at the cited page with the passage highlighted.
+- **Mindmap.** Workspace-wide concept tree, generated from PDF summaries and rendered as an interactive Markmap.
+- **Spaced-repetition flashcards.** Generated from the PDF content, dropped into a per-workspace SRS queue (SM-2 lite, three review grades).
+- **Audio Overview.** Two-host podcast summary, opt-in via `OPENAI_API_KEY`. Prompt adapted from [open-notebooklm](https://github.com/gabrielchua/open-notebooklm).
+
+Production-grade plumbing under the hood:
+
- Next.js 16 App Router
- PDF upload (≤ 10 MB) with server-side extraction via `pdfjs-dist`
- Vector search on InsForge pgvector (`vector(1536)` + ivfflat cosine)
-- Streaming chat with bracketed `[n]` source citations
-- **Better Auth** for email + password sign-in — user/session tables live in your InsForge Postgres
-- HS256 bridge JWT from BA → InsForge: RLS reads `requesting_user_id()` so every user only sees their own documents and chats
+- Streaming RAG chat with bracketed `[n]` source citations (NDJSON)
+- **Better Auth** for email + password sign-in. User/session tables live in your InsForge Postgres.
+- HS256 bridge JWT from BA → InsForge: RLS reads `requesting_user_id()` so every user only sees their own data.
- shadcn/ui + Tailwind 4 design tokens
## Demo
[aipdfchat.insforge.site](https://aipdfchat.insforge.site) — sign up with any email, upload a PDF, ask away.
+## Why this template
+
+| | NotebookLM | ChatPDF | LangChain RAG demo | **This template** |
+|---|---|---|---|---|
+| Workspaces + cross-PDF RAG | yes | no | no | yes |
+| Inline PDF passage highlight | partial | no | no | yes |
+| Mindmap from PDFs | yes | no | no | yes |
+| Spaced-repetition flashcards | no | partial | no | yes |
+| Two-host audio podcast | yes | no | no | yes (opt-in) |
+| Self-hosted, your data | no | no | yes | yes |
+| Open source, swap any model | no | no | yes | yes |
+| Multi-tenant out of the box | no | yes | no | yes (Better Auth + RLS) |
+| Ready to ship a real product | no | yes | no | yes |
+
+If you want to fork a NotebookLM-style study tool and run it on infrastructure you control, this is the closest open-source starting point.
+
## Quick Launch
Fastest path:
@@ -93,6 +119,7 @@ npm install
- **pgvector extension** — the migration runs `create extension if not exists vector;` against the `public` schema. Supported on any standard InsForge project.
- **Node 18+** for `node:test` and the ESM `pdfjs-dist` build.
- **InsForge SMTP** configured if you want password resets to send mail. Cloud projects: configured automatically. Self-hosted: `PUT /api/auth/smtp-config`.
+- **OpenAI API key (optional)** — required only for the Audio Overview tab. Set `OPENAI_API_KEY` in `.env.local`; without it the workspace audio tab shows a friendly "configure to enable" prompt and everything else still works.
## Architecture
@@ -128,6 +155,25 @@ Chat ──► /api/chat (NDJSON stream) ──► embed(question) ──► mat
persist assistant message with citations payload
```
+## Audio Overview
+
+The Audio tab generates a NotebookLM-style two-host podcast summary of every PDF in a workspace.
+
+**How it works:**
+
+1. `lib/ai/audio-script.ts` calls InsForge AI (chat completion) with a producer-style prompt adapted from [open-notebooklm](https://github.com/gabrielchua/open-notebooklm). Sarah hosts and interviews Mike, a subject-matter expert. The asymmetric framing prevents the "two co-hosts agreeing with each other" failure mode.
+2. `lib/audio/tts.ts` synthesizes each turn through OpenAI TTS (`gpt-4o-mini-tts`, `nova` voice for Sarah, `onyx` for Mike), 4 turns in parallel.
+3. mp3 frames are concatenated naively (good enough for a study tool; swap in `ffmpeg.wasm` if you need gapless playback).
+4. The final mp3 lands in the `audio-overviews` storage bucket (public read), and `workspaces.audio_url` + `audio_script` are cached so the tab renders instantly on revisit.
+
+**Cost:** about $0.006 per regeneration (700 chars TTS at `tts-1` pricing). The chat completion that drafts the script runs on InsForge AI and counts against your existing AI quota.
+
+**Without `OPENAI_API_KEY`:** the Audio tab shows a friendly "configure to enable" prompt. Every other feature works.
+
+**Want a different TTS?** Edit `lib/audio/tts.ts`. ElevenLabs and Cartesia have OpenAI-compatible HTTP endpoints. The voice-per-speaker mapping (`VOICE_BY_SPEAKER`) is the only thing to swap.
+
+**Want a better script?** Set `UTILITY_MODEL` in `lib/ai/constants.ts` from `gpt-4o-mini` to `gpt-4o`. Cost goes from $0.006 to ~$0.04 per generation, but you start getting analogies, anecdotes, and "aha moments" the mini model can't produce.
+
## Customizing
- **Switch embedding model:** edit `lib/ai/constants.ts` (`EMBEDDING_MODEL`, `EMBEDDING_DIMENSIONS`). If the dimension changes, also `ALTER TABLE document_chunks ALTER COLUMN embedding TYPE vector(N)` and re-ingest existing documents.
diff --git a/ai-pdf-chatbot/app/api/chat/route.ts b/ai-pdf-chatbot/app/api/chat/route.ts
index d20aea4..6809b09 100644
--- a/ai-pdf-chatbot/app/api/chat/route.ts
+++ b/ai-pdf-chatbot/app/api/chat/route.ts
@@ -13,6 +13,9 @@ type Body = {
chatId?: string;
input: string;
documentIds?: string[];
+ // If set when chatId is absent, the new chat is created under that
+ // workspace and RAG retrieval is scoped to that workspace's documents.
+ workspaceId?: string | null;
};
function extractDeltaText(content: unknown): string {
@@ -44,20 +47,32 @@ export async function POST(req: Request) {
const client = createInsforgeServerClient({ accessToken: auth.accessToken });
let chatId = body.chatId;
+ let workspaceId: string | null = body.workspaceId ?? null;
if (!chatId) {
const ins = await client.database
.from('chat_sessions')
.insert({
user_id: ownerId,
+ workspace_id: workspaceId,
title: body.input.trim().slice(0, 60) || 'New chat',
document_ids: body.documentIds ?? [],
})
- .select('id')
+ .select('id, workspace_id')
.single();
if (ins.error || !ins.data) {
return NextResponse.json({ error: ins.error?.message ?? 'Failed to create chat' }, { status: 500 });
}
- chatId = (ins.data as { id: string }).id;
+ chatId = (ins.data as { id: string; workspace_id: string | null }).id;
+ workspaceId = (ins.data as { id: string; workspace_id: string | null }).workspace_id;
+ } else {
+ // For existing chats, read the workspace_id off the row so a stale
+ // client-side cache can't widen retrieval scope.
+ const lookup = await client.database
+ .from('chat_sessions')
+ .select('workspace_id')
+ .eq('id', chatId)
+ .single();
+ workspaceId = (lookup.data as { workspace_id: string | null } | null)?.workspace_id ?? null;
}
const lastMsg = await client.database
@@ -89,7 +104,7 @@ export async function POST(req: Request) {
async start(controller) {
const send = (obj: unknown) => controller.enqueue(encodeNdjson(obj));
try {
- const chunks = await retrieveForQuestion(client, ownerId, inputText, docIds);
+ const chunks = await retrieveForQuestion(client, ownerId, inputText, docIds, workspaceId);
const citations = toCitations(chunks);
send({ type: 'chat', chatId: resolvedChatId });
send({ type: 'citations', data: citations });
diff --git a/ai-pdf-chatbot/app/api/chats/route.ts b/ai-pdf-chatbot/app/api/chats/route.ts
index ca2e05b..3ae4d70 100644
--- a/ai-pdf-chatbot/app/api/chats/route.ts
+++ b/ai-pdf-chatbot/app/api/chats/route.ts
@@ -5,17 +5,30 @@ import { getCurrentAuthState } from '@/lib/auth-state';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
-export async function GET() {
+export async function GET(req: Request) {
const auth = await getCurrentAuthState();
if (!auth.viewer.isAuthenticated || !auth.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
+
+ // Same workspace filter shape as the documents list: ?workspace= or
+ // ?workspace=unsorted. No param returns every chat the user owns.
+ const url = new URL(req.url);
+ const workspace = url.searchParams.get('workspace');
+
const client = createInsforgeServerClient({ accessToken: auth.accessToken });
- const { data, error } = await client.database
+ let query = client.database
.from('chat_sessions')
- .select('id, title, document_ids, created_at, last_message_at')
+ .select('id, workspace_id, title, document_ids, created_at, last_message_at')
.order('last_message_at', { ascending: false });
+ if (workspace === 'unsorted') {
+ query = query.is('workspace_id', null);
+ } else if (workspace) {
+ query = query.eq('workspace_id', workspace);
+ }
+
+ const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ chats: data ?? [] });
}
@@ -26,13 +39,18 @@ export async function POST(req: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
- const body = (await req.json().catch(() => ({}))) as { title?: string; documentIds?: string[] };
+ const body = (await req.json().catch(() => ({}))) as {
+ title?: string;
+ documentIds?: string[];
+ workspaceId?: string | null;
+ };
const client = createInsforgeServerClient({ accessToken: auth.accessToken });
const { data, error } = await client.database
.from('chat_sessions')
.insert({
user_id: auth.viewer.id,
+ workspace_id: body.workspaceId ?? null,
title: body.title?.trim() || 'New chat',
document_ids: body.documentIds ?? [],
})
diff --git a/ai-pdf-chatbot/app/api/documents/[id]/flashcards/route.ts b/ai-pdf-chatbot/app/api/documents/[id]/flashcards/route.ts
index 7533f6e..209e47a 100644
--- a/ai-pdf-chatbot/app/api/documents/[id]/flashcards/route.ts
+++ b/ai-pdf-chatbot/app/api/documents/[id]/flashcards/route.ts
@@ -41,7 +41,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
const docRes = await client.database
.from('documents')
- .select('id, storage_bucket, storage_key, file_name, status')
+ .select('id, workspace_id, storage_bucket, storage_key, file_name, status')
.eq('id', id)
.single();
@@ -50,6 +50,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
}
const doc = docRes.data as {
id: string;
+ workspace_id: string | null;
storage_bucket: string;
storage_key: string;
file_name: string;
@@ -80,10 +81,14 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
const rows = cards.map((c, i) => ({
document_id: id,
+ workspace_id: doc.workspace_id,
user_id: auth.viewer.id,
question: c.question,
answer: c.answer,
sort_order: i,
+ // Fresh cards land in the SRS queue immediately; defaults on the
+ // ease/interval/reps columns take care of the rest.
+ due_at: new Date().toISOString(),
}));
const ins = await client.database
diff --git a/ai-pdf-chatbot/app/api/documents/[id]/route.ts b/ai-pdf-chatbot/app/api/documents/[id]/route.ts
index 2a045d5..4eede6f 100644
--- a/ai-pdf-chatbot/app/api/documents/[id]/route.ts
+++ b/ai-pdf-chatbot/app/api/documents/[id]/route.ts
@@ -5,6 +5,43 @@ import { getCurrentAuthState } from '@/lib/auth-state';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
+export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = (await req.json().catch(() => ({}))) as { workspace_id?: string | null };
+ // Only workspace_id is mutable via this route today. Adding other fields
+ // here keeps the surface flat instead of one route per column.
+ if (!('workspace_id' in body)) {
+ return NextResponse.json({ error: 'No mutable fields provided' }, { status: 400 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+ const { error } = await client.database
+ .from('documents')
+ .update({ workspace_id: body.workspace_id ?? null, updated_at: new Date().toISOString() })
+ .eq('id', id);
+
+ if (error) return NextResponse.json({ error: error.message }, { status: 500 });
+
+ // Cascade the workspace_id to all flashcards generated from this doc so
+ // workspace-wide review queues see them. Documents that don't have
+ // flashcards yet just no-op. Failures here would silently leave the
+ // doc and its flashcards out of sync, so surface them.
+ const cascade = await client.database
+ .from('document_flashcards')
+ .update({ workspace_id: body.workspace_id ?? null })
+ .eq('document_id', id);
+ if (cascade.error) {
+ return NextResponse.json({ error: cascade.error.message }, { status: 500 });
+ }
+
+ return NextResponse.json({ ok: true });
+}
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const auth = await getCurrentAuthState();
diff --git a/ai-pdf-chatbot/app/api/documents/route.ts b/ai-pdf-chatbot/app/api/documents/route.ts
index 4a901d4..b1d1fb8 100644
--- a/ai-pdf-chatbot/app/api/documents/route.ts
+++ b/ai-pdf-chatbot/app/api/documents/route.ts
@@ -5,18 +5,31 @@ import { getCurrentAuthState } from '@/lib/auth-state';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
-export async function GET() {
+export async function GET(req: Request) {
const auth = await getCurrentAuthState();
if (!auth.viewer.isAuthenticated || !auth.accessToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
+ // ?workspace= scopes the list to one workspace.
+ // ?workspace=unsorted returns documents with workspace_id IS NULL.
+ // No param: returns all of the user's documents.
+ const url = new URL(req.url);
+ const workspace = url.searchParams.get('workspace');
+
const client = createInsforgeServerClient({ accessToken: auth.accessToken });
- const { data, error } = await client.database
+ let query = client.database
.from('documents')
- .select('id, file_name, file_size, mime_type, status, error, page_count, summary, suggested_questions, created_at')
+ .select('id, workspace_id, file_name, file_size, mime_type, status, error, page_count, summary, suggested_questions, created_at')
.order('created_at', { ascending: false });
+ if (workspace === 'unsorted') {
+ query = query.is('workspace_id', null);
+ } else if (workspace) {
+ query = query.eq('workspace_id', workspace);
+ }
+
+ const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ documents: data ?? [] });
}
diff --git a/ai-pdf-chatbot/app/api/documents/upload/route.ts b/ai-pdf-chatbot/app/api/documents/upload/route.ts
index 8c15914..9fc2fa3 100644
--- a/ai-pdf-chatbot/app/api/documents/upload/route.ts
+++ b/ai-pdf-chatbot/app/api/documents/upload/route.ts
@@ -17,6 +17,18 @@ export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get('file');
+ const rawWorkspaceId = formData.get('workspaceId');
+ // Validate the client-supplied workspace id format up-front so a bad
+ // value returns a 400 instead of leaking a 500 from a downstream
+ // `invalid input syntax for type uuid` Postgres error.
+ const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
+ let workspaceId: string | null = null;
+ if (typeof rawWorkspaceId === 'string' && rawWorkspaceId) {
+ if (!UUID_RE.test(rawWorkspaceId)) {
+ return NextResponse.json({ error: 'workspaceId must be a UUID' }, { status: 400 });
+ }
+ workspaceId = rawWorkspaceId;
+ }
if (!(file instanceof File)) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
@@ -33,6 +45,7 @@ export async function POST(req: Request) {
.from('documents')
.insert({
user_id: auth.viewer.id,
+ workspace_id: workspaceId,
file_name: file.name,
file_size: file.size,
mime_type: file.type,
diff --git a/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts b/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts
new file mode 100644
index 0000000..2b7397f
--- /dev/null
+++ b/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts
@@ -0,0 +1,64 @@
+import { NextResponse } from 'next/server';
+import { createInsforgeServerClient } from '@/lib/insforge';
+import { getCurrentAuthState } from '@/lib/auth-state';
+import { schedule, type Grade } from '@/lib/srs/schedule';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+const VALID: Grade[] = ['again', 'hard', 'good', 'easy'];
+
+// Applies an SRS grade to a flashcard and stores the new state. The
+// algorithm is a pure function in lib/srs/schedule.ts so the same logic
+// is testable in node:test and unchanged by HTTP plumbing.
+export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = (await req.json().catch(() => ({}))) as { grade?: string };
+ const grade = body.grade as Grade | undefined;
+ if (!grade || !VALID.includes(grade)) {
+ return NextResponse.json({ error: 'Invalid grade' }, { status: 400 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+
+ const cardRes = await client.database
+ .from('document_flashcards')
+ .select('id, ease, interval_days, reps')
+ .eq('id', id)
+ .single();
+ if (cardRes.error || !cardRes.data) {
+ return NextResponse.json({ error: 'Card not found' }, { status: 404 });
+ }
+ const card = cardRes.data as { id: string; ease: number; interval_days: number; reps: number };
+
+ const next = schedule(
+ { ease: card.ease, interval_days: card.interval_days, reps: card.reps },
+ grade,
+ );
+
+ const upd = await client.database
+ .from('document_flashcards')
+ .update({
+ ease: next.ease,
+ interval_days: next.interval_days,
+ reps: next.reps,
+ last_grade: next.last_grade,
+ due_at: next.due_at.toISOString(),
+ })
+ .eq('id', id);
+
+ if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 });
+ return NextResponse.json({
+ ok: true,
+ next: {
+ ease: next.ease,
+ interval_days: next.interval_days,
+ due_at: next.due_at.toISOString(),
+ },
+ });
+}
diff --git a/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts b/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts
new file mode 100644
index 0000000..6e2e432
--- /dev/null
+++ b/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts
@@ -0,0 +1,102 @@
+import { NextResponse } from 'next/server';
+import { createInsforgeServerClient } from '@/lib/insforge';
+import { getCurrentAuthState } from '@/lib/auth-state';
+import { generateAudioScript } from '@/lib/ai/audio-script';
+import { synthesizeScript } from '@/lib/audio/tts';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+// TTS for ~10 turns easily runs 30-60s in series. Lift the timeout off
+// the default 10s edge cap so the route can finish before Vercel kills
+// it on production.
+export const maxDuration = 300;
+
+const BUCKET = 'audio-overviews';
+
+export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.viewer.id || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Audio Overview is opt-in: users wire their own OpenAI key. We
+ // surface a clean 412 instead of throwing so the UI can show a
+ // friendly "Configure OPENAI_API_KEY" prompt.
+ const apiKey = process.env.OPENAI_API_KEY;
+ if (!apiKey) {
+ return NextResponse.json(
+ {
+ error:
+ 'Audio Overview requires an OPENAI_API_KEY environment variable. See the README for setup.',
+ },
+ { status: 412 },
+ );
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+
+ const wsRes = await client.database
+ .from('workspaces')
+ .select('id, name')
+ .eq('id', id)
+ .single();
+ if (wsRes.error || !wsRes.data) {
+ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 });
+ }
+ const ws = wsRes.data as { id: string; name: string };
+
+ const docsRes = await client.database
+ .from('documents')
+ .select('file_name, summary')
+ .eq('workspace_id', id)
+ .eq('status', 'ready');
+ if (docsRes.error) {
+ return NextResponse.json({ error: docsRes.error.message }, { status: 500 });
+ }
+ const docs = (docsRes.data ?? []) as Array<{ file_name: string; summary: string | null }>;
+ if (docs.length === 0) {
+ return NextResponse.json(
+ { error: 'Add at least one ready document before generating an audio overview.' },
+ { status: 409 },
+ );
+ }
+
+ const script = await generateAudioScript(client, ws.name, docs);
+ if (script.length < 4) {
+ return NextResponse.json({ error: 'Could not produce a usable script' }, { status: 500 });
+ }
+
+ const audioBuffer = await synthesizeScript(script, apiKey);
+
+ // Versioned key so the public URL doesn't get stale-cached on
+ // regenerate. The previous audio file lingers but the DB only points
+ // at the latest one.
+ const key = `${auth.viewer.id}/${id}/${Date.now()}.mp3`;
+ const upload = await client.storage
+ .from(BUCKET)
+ .upload(key, new Blob([new Uint8Array(audioBuffer)], { type: 'audio/mpeg' }));
+ if (upload.error) {
+ return NextResponse.json({ error: upload.error.message ?? 'Upload failed' }, { status: 500 });
+ }
+
+ const url = client.storage.from(BUCKET).getPublicUrl(key);
+ const audioUrl = typeof url === 'string' ? url : (url as { data?: { publicUrl?: string } })?.data?.publicUrl;
+ if (!audioUrl) {
+ return NextResponse.json({ error: 'Could not resolve audio URL' }, { status: 500 });
+ }
+
+ const now = new Date().toISOString();
+ const upd = await client.database
+ .from('workspaces')
+ .update({
+ audio_url: audioUrl,
+ audio_script: script,
+ audio_generated_at: now,
+ updated_at: now,
+ })
+ .eq('id', id);
+ if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 });
+
+ return NextResponse.json({ audio_url: audioUrl, script, generated_at: now });
+}
diff --git a/ai-pdf-chatbot/app/api/workspaces/[id]/flashcards/due/route.ts b/ai-pdf-chatbot/app/api/workspaces/[id]/flashcards/due/route.ts
new file mode 100644
index 0000000..798f13f
--- /dev/null
+++ b/ai-pdf-chatbot/app/api/workspaces/[id]/flashcards/due/route.ts
@@ -0,0 +1,63 @@
+import { NextResponse } from 'next/server';
+import { createInsforgeServerClient } from '@/lib/insforge';
+import { getCurrentAuthState } from '@/lib/auth-state';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+// Returns up to 50 due cards in this workspace, joined with their source
+// document file name so the review UI can show provenance under each
+// question. Ordered by due_at so the most overdue card surfaces first.
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+
+ const cardsRes = await client.database
+ .from('document_flashcards')
+ .select('id, document_id, question, answer, ease, interval_days, reps, due_at')
+ .eq('workspace_id', id)
+ .lte('due_at', new Date().toISOString())
+ .order('due_at', { ascending: true })
+ .limit(50);
+
+ if (cardsRes.error) {
+ return NextResponse.json({ error: cardsRes.error.message }, { status: 500 });
+ }
+
+ const cards = (cardsRes.data ?? []) as Array<{
+ id: string;
+ document_id: string;
+ question: string;
+ answer: string;
+ ease: number;
+ interval_days: number;
+ reps: number;
+ due_at: string;
+ }>;
+
+ // Resolve file names in one query rather than per-card. RLS keeps
+ // anything outside this user's documents out by default.
+ const docIds = Array.from(new Set(cards.map((c) => c.document_id)));
+ let nameById = new Map();
+ if (docIds.length > 0) {
+ const docsRes = await client.database
+ .from('documents')
+ .select('id, file_name')
+ .in('id', docIds);
+ if (docsRes.error) {
+ return NextResponse.json({ error: docsRes.error.message }, { status: 500 });
+ }
+ nameById = new Map(
+ ((docsRes.data ?? []) as Array<{ id: string; file_name: string }>).map((d) => [d.id, d.file_name]),
+ );
+ }
+
+ return NextResponse.json({
+ cards: cards.map((c) => ({ ...c, file_name: nameById.get(c.document_id) ?? 'document.pdf' })),
+ });
+}
diff --git a/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts b/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts
new file mode 100644
index 0000000..d56f81b
--- /dev/null
+++ b/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts
@@ -0,0 +1,94 @@
+import { NextResponse } from 'next/server';
+import { createInsforgeServerClient } from '@/lib/insforge';
+import { getCurrentAuthState } from '@/lib/auth-state';
+import { UTILITY_MODEL } from '@/lib/ai/constants';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+// Hard cap on how much of each document summary we feed the LLM. Mindmap
+// quality saturates quickly past a few sentences per doc; raising this
+// just burns tokens.
+const SUMMARY_CHAR_BUDGET = 600;
+
+function buildPrompt(workspaceName: string, docs: Array<{ file_name: string; summary: string | null }>): string {
+ const docBlock = docs
+ .map((d, i) => `${i + 1}. ${d.file_name}\n ${(d.summary ?? '').slice(0, SUMMARY_CHAR_BUDGET)}`)
+ .join('\n\n');
+
+ return `You build mindmaps from study materials. Below are PDF summaries grouped under a workspace named "${workspaceName}". Produce a Markmap-compatible mindmap as Markdown:
+
+- Use "#" for the top-level node (the workspace topic).
+- Use "##", "###", "####" for nested topics.
+- 3 to 5 levels deep.
+- 10 to 30 leaf nodes total.
+- Group related ideas across documents under shared parent nodes; don't just list documents.
+- Concise leaves: short noun phrases, not full sentences.
+- Output ONLY the Markdown. No commentary, no code fences.
+
+PDF summaries:
+${docBlock}`;
+}
+
+// POST regenerates the mindmap and caches the result on workspaces.
+// GET returns the cached markdown (or null) so the client can decide
+// whether to call POST. Keeping write separate from read makes the
+// "regenerate" affordance obvious.
+export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+
+ const wsRes = await client.database
+ .from('workspaces')
+ .select('id, name')
+ .eq('id', id)
+ .single();
+ if (wsRes.error || !wsRes.data) {
+ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 });
+ }
+ const ws = wsRes.data as { id: string; name: string };
+
+ const docsRes = await client.database
+ .from('documents')
+ .select('file_name, summary')
+ .eq('workspace_id', id)
+ .eq('status', 'ready');
+ if (docsRes.error) {
+ return NextResponse.json({ error: docsRes.error.message }, { status: 500 });
+ }
+ const docs = (docsRes.data ?? []) as Array<{ file_name: string; summary: string | null }>;
+ if (docs.length === 0) {
+ return NextResponse.json(
+ { error: 'Add at least one ready document to this workspace before generating a mindmap.' },
+ { status: 409 },
+ );
+ }
+
+ const completion = (await client.ai.chat.completions.create({
+ model: UTILITY_MODEL,
+ messages: [{ role: 'user', content: buildPrompt(ws.name, docs) }],
+ })) as { choices: Array<{ message: { content: string } }> };
+
+ const raw = completion.choices?.[0]?.message?.content ?? '';
+ const markdown = raw
+ .replace(/```[a-zA-Z]*\n?/g, '')
+ .replace(/```/g, '')
+ .trim();
+ if (!markdown) {
+ return NextResponse.json({ error: 'Empty mindmap response' }, { status: 500 });
+ }
+
+ const now = new Date().toISOString();
+ const upd = await client.database
+ .from('workspaces')
+ .update({ mindmap_markdown: markdown, mindmap_generated_at: now, updated_at: now })
+ .eq('id', id);
+ if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 });
+
+ return NextResponse.json({ markdown, generated_at: now });
+}
diff --git a/ai-pdf-chatbot/app/api/workspaces/[id]/route.ts b/ai-pdf-chatbot/app/api/workspaces/[id]/route.ts
new file mode 100644
index 0000000..7b23656
--- /dev/null
+++ b/ai-pdf-chatbot/app/api/workspaces/[id]/route.ts
@@ -0,0 +1,117 @@
+import { NextResponse } from 'next/server';
+import { createInsforgeServerClient } from '@/lib/insforge';
+import { getCurrentAuthState } from '@/lib/auth-state';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+
+ const wsRes = await client.database
+ .from('workspaces')
+ .select('id, name, description, mindmap_markdown, mindmap_generated_at, audio_url, audio_script, audio_generated_at, created_at, updated_at')
+ .eq('id', id)
+ .single();
+
+ if (wsRes.error || !wsRes.data) {
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
+ }
+
+ // Fetch related counts in parallel. RLS keeps everything scoped to the
+ // current user automatically — the workspace SELECT above already proved
+ // ownership.
+ const [docsRes, chatsRes, dueRes] = await Promise.all([
+ client.database.from('documents').select('id', { count: 'exact', head: true }).eq('workspace_id', id),
+ client.database.from('chat_sessions').select('id', { count: 'exact', head: true }).eq('workspace_id', id),
+ client.database
+ .from('document_flashcards')
+ .select('id', { count: 'exact', head: true })
+ .eq('workspace_id', id)
+ .lte('due_at', new Date().toISOString()),
+ ]);
+
+ const firstError = docsRes.error ?? chatsRes.error ?? dueRes.error;
+ if (firstError) {
+ return NextResponse.json({ error: firstError.message }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ workspace: wsRes.data,
+ counts: {
+ documents: docsRes.count ?? 0,
+ chats: chatsRes.count ?? 0,
+ due_flashcards: dueRes.count ?? 0,
+ },
+ });
+}
+
+export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = (await req.json().catch(() => ({}))) as { name?: string; description?: string };
+ const patch: Record = { updated_at: new Date().toISOString() };
+ if (typeof body.name === 'string') {
+ const trimmed = body.name.trim();
+ if (!trimmed) return NextResponse.json({ error: 'Name cannot be empty' }, { status: 400 });
+ patch.name = trimmed;
+ }
+ if (typeof body.description === 'string') {
+ patch.description = body.description.trim() || null;
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+ // Use `select` with single-row coerce so we can tell apart "0 rows
+ // matched" (404, e.g. someone else's workspace under RLS) from a
+ // successful update. Without this PATCH was returning ok for any id.
+ const { data, error } = await client.database
+ .from('workspaces')
+ .update(patch)
+ .eq('id', id)
+ .select('id');
+ if (error) return NextResponse.json({ error: error.message }, { status: 500 });
+ if (!data || (Array.isArray(data) && data.length === 0)) {
+ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 });
+ }
+ return NextResponse.json({ ok: true });
+}
+
+export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+ // Documents/chats keep existing rows alive by setting workspace_id to
+ // null via the FK ON DELETE SET NULL. The workspace's own cached audio
+ // file in storage still needs an explicit cleanup pass.
+ const wsRes = await client.database
+ .from('workspaces')
+ .select('audio_url')
+ .eq('id', id)
+ .single();
+ const audioUrl = (wsRes.data as { audio_url: string | null } | null)?.audio_url ?? null;
+ if (audioUrl) {
+ const match = audioUrl.match(/audio-overviews\/(.+?)(?:\?|$)/);
+ const key = match?.[1];
+ if (key) {
+ await client.storage.from('audio-overviews').remove(decodeURIComponent(key)).catch(() => undefined);
+ }
+ }
+
+ const { error } = await client.database.from('workspaces').delete().eq('id', id);
+ if (error) return NextResponse.json({ error: error.message }, { status: 500 });
+ return NextResponse.json({ ok: true });
+}
diff --git a/ai-pdf-chatbot/app/api/workspaces/route.ts b/ai-pdf-chatbot/app/api/workspaces/route.ts
new file mode 100644
index 0000000..4c987c5
--- /dev/null
+++ b/ai-pdf-chatbot/app/api/workspaces/route.ts
@@ -0,0 +1,52 @@
+import { NextResponse } from 'next/server';
+import { createInsforgeServerClient } from '@/lib/insforge';
+import { getCurrentAuthState } from '@/lib/auth-state';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+ const { data, error } = await client.database
+ .from('workspaces')
+ .select('id, name, description, mindmap_generated_at, audio_url, audio_generated_at, created_at, updated_at')
+ .order('updated_at', { ascending: false });
+
+ if (error) return NextResponse.json({ error: error.message }, { status: 500 });
+ return NextResponse.json({ workspaces: data ?? [] });
+}
+
+export async function POST(req: Request) {
+ const auth = await getCurrentAuthState();
+ if (!auth.viewer.isAuthenticated || !auth.viewer.id || !auth.accessToken) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = (await req.json().catch(() => ({}))) as { name?: unknown; description?: unknown };
+ // .trim() throws on non-strings, so check types before reaching for it.
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
+ if (!name) {
+ return NextResponse.json({ error: 'Name is required' }, { status: 400 });
+ }
+ const description =
+ typeof body.description === 'string' ? body.description.trim() || null : null;
+
+ const client = createInsforgeServerClient({ accessToken: auth.accessToken });
+ const { data, error } = await client.database
+ .from('workspaces')
+ .insert({
+ user_id: auth.viewer.id,
+ name,
+ description,
+ })
+ .select()
+ .single();
+
+ if (error || !data) return NextResponse.json({ error: error?.message ?? 'Insert failed' }, { status: 500 });
+ return NextResponse.json({ workspace: data });
+}
diff --git a/ai-pdf-chatbot/app/chat/[chatId]/page.tsx b/ai-pdf-chatbot/app/chat/[chatId]/page.tsx
index ebe2f9e..c845527 100644
--- a/ai-pdf-chatbot/app/chat/[chatId]/page.tsx
+++ b/ai-pdf-chatbot/app/chat/[chatId]/page.tsx
@@ -2,12 +2,11 @@
import { use, useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
-import { ChatShell } from '@/components/chat-shell';
import { ChatInput } from '@/components/chat-input';
import { ChatMessage } from '@/components/chat-message';
-import { CitationRail } from '@/components/citation-rail';
import { ShareChatButton } from '@/components/share-chat-button';
import { useChatStream } from '@/lib/stream/use-chat-stream';
+import { useSetRailCitations } from '@/lib/chat/rail-context';
import type { ChatMessageRow } from '@/lib/types';
type Citation = ChatMessageRow['citations'][number];
@@ -20,6 +19,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ chatId: s
const [chatTitle, setChatTitle] = useState('');
const [shareToken, setShareToken] = useState(null);
const { state, send } = useChatStream();
+ const setRailCitations = useSetRailCitations();
const load = useCallback(async () => {
const res = await fetch(`/api/chats/${chatId}`);
@@ -59,9 +59,16 @@ export default function ChatDetailPage({ params }: { params: Promise<{ chatId: s
const streamingText = state.phase === 'streaming' ? state.text : '';
const streamingCitations = state.phase === 'streaming' ? state.citations : activeCitations;
+ // Sync the right-rail with whatever this page considers the live
+ // citation set. The layout-hosted ChatShell renders the rail from
+ // this context, so we don't return one here.
+ useEffect(() => {
+ setRailCitations(streamingCitations);
+ }, [setRailCitations, streamingCitations]);
+
return (
- }>
-
+ <>
+
{chatTitle || 'Chat'}
-
+ >
);
}
diff --git a/ai-pdf-chatbot/app/chat/layout.tsx b/ai-pdf-chatbot/app/chat/layout.tsx
new file mode 100644
index 0000000..b65e33a
--- /dev/null
+++ b/ai-pdf-chatbot/app/chat/layout.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import { ChatShell } from '@/components/chat-shell';
+import { RailProvider } from '@/lib/chat/rail-context';
+
+// Hoists ChatShell out of the per-page render so that navigating between
+// /chat and /chat/[id] does NOT unmount + remount the sidebar (which
+// was the cause of the recent-chats list visibly flashing on each
+// click). The page itself only renders the main content; the citation
+// rail is pushed up through RailContext.
+export default function ChatLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ai-pdf-chatbot/app/chat/page.tsx b/ai-pdf-chatbot/app/chat/page.tsx
index 7d24f38..1c3ad9b 100644
--- a/ai-pdf-chatbot/app/chat/page.tsx
+++ b/ai-pdf-chatbot/app/chat/page.tsx
@@ -1,14 +1,13 @@
'use client';
-import { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
+import { Suspense, useEffect, useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
import { Sparkles } from 'lucide-react';
import { toast } from 'sonner';
-import { ChatShell } from '@/components/chat-shell';
import { ChatInput } from '@/components/chat-input';
import { ChatMessage } from '@/components/chat-message';
-import { CitationRail } from '@/components/citation-rail';
import { useChatStream } from '@/lib/stream/use-chat-stream';
+import { useSetRailCitations } from '@/lib/chat/rail-context';
import type { ChatMessageRow } from '@/lib/types';
type Citation = ChatMessageRow['citations'][number];
@@ -21,20 +20,37 @@ type DocSummaryRow = {
suggested_questions: string[];
};
+// useSearchParams() requires a Suspense boundary in Next 16 so the page
+// can pre-render. We wrap the inner component instead of marking the
+// whole page dynamic.
export default function ChatHomePage() {
+ return (
+
+
+
+ );
+}
+
+function ChatHomePageInner() {
const router = useRouter();
+ const searchParams = useSearchParams();
+ // `/chat?workspace=
` scopes new chats to that workspace. Empty when
+ // the user lands on /chat directly (Unsorted chats).
+ const workspaceId = searchParams?.get('workspace') ?? null;
const { state, send } = useChatStream();
const [pendingInput, setPendingInput] = useState(null);
const [citations] = useState([]);
const [suggestions, setSuggestions] = useState([]);
+ const setRailCitations = useSetRailCitations();
// Pull a few suggested questions from the most recently uploaded, ready
- // document so a brand-new chat has a one-tap starting point. Falls back
- // to the empty state if no doc is ready yet.
+ // document. When a workspace is set, only consider docs in that
+ // workspace so suggestions stay relevant.
useEffect(() => {
let cancelled = false;
void (async () => {
- const res = await fetch('/api/documents');
+ const url = workspaceId ? `/api/documents?workspace=${workspaceId}` : '/api/documents';
+ const res = await fetch(url);
if (!res.ok) return;
const data = (await res.json()) as { documents: DocSummaryRow[] };
if (cancelled) return;
@@ -46,11 +62,11 @@ export default function ChatHomePage() {
return () => {
cancelled = true;
};
- }, []);
+ }, [workspaceId]);
async function handleSubmit(input: string) {
setPendingInput(input);
- const result = await send({ input });
+ const result = await send({ input, workspaceId });
if (!result) {
if (state.phase === 'error') toast.error(state.message);
return;
@@ -61,8 +77,12 @@ export default function ChatHomePage() {
const streamingText = state.phase === 'streaming' ? state.text : '';
const streamingCitations = state.phase === 'streaming' ? state.citations : citations;
+ useEffect(() => {
+ setRailCitations(streamingCitations);
+ }, [setRailCitations, streamingCitations]);
+
return (
- }>
+ <>
{pendingInput ? (
@@ -104,6 +124,6 @@ export default function ChatHomePage() {
)}
-
+ >
);
}
diff --git a/ai-pdf-chatbot/app/documents/page.tsx b/ai-pdf-chatbot/app/documents/page.tsx
index 4f34cd5..ed76d7c 100644
--- a/ai-pdf-chatbot/app/documents/page.tsx
+++ b/ai-pdf-chatbot/app/documents/page.tsx
@@ -9,6 +9,7 @@ import { DocumentUploader } from '@/components/document-uploader';
type DocRow = {
id: string;
+ workspace_id: string | null;
file_name: string;
file_size: number;
status: 'processing' | 'ready' | 'failed';
diff --git a/ai-pdf-chatbot/app/globals.css b/ai-pdf-chatbot/app/globals.css
index 6033898..36ebc63 100644
--- a/ai-pdf-chatbot/app/globals.css
+++ b/ai-pdf-chatbot/app/globals.css
@@ -120,6 +120,18 @@
}
}
+/*
+ * pdfjs text layer highlight for cited chunks. The text layer spans sit
+ * on top of the rendered PDF and default to transparent backgrounds, so
+ * a translucent yellow fill is enough to make the cited passage pop
+ * without obscuring the underlying glyphs.
+ */
+.rag-citation-highlight {
+ background-color: rgba(255, 213, 79, 0.55);
+ border-radius: 2px;
+ box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.35);
+}
+
@layer utilities {
.page-shell {
@apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8;
diff --git a/ai-pdf-chatbot/app/workspaces/[id]/page.tsx b/ai-pdf-chatbot/app/workspaces/[id]/page.tsx
new file mode 100644
index 0000000..ad2d13f
--- /dev/null
+++ b/ai-pdf-chatbot/app/workspaces/[id]/page.tsx
@@ -0,0 +1,454 @@
+'use client';
+
+import { use, useCallback, useEffect, useMemo, useState } from 'react';
+import dynamic from 'next/dynamic';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import {
+ ArrowLeft,
+ AudioLines,
+ Brain,
+ FileText,
+ GraduationCap,
+ Loader2,
+ MessageSquare,
+ Pencil,
+ RefreshCw,
+ Trash2,
+} from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { AudioOverview } from '@/components/audio-overview';
+import { DocumentList } from '@/components/document-list';
+import { DocumentUploader } from '@/components/document-uploader';
+import type { AudioScriptTurn } from '@/lib/types';
+import { cn } from '@/lib/utils';
+
+// markmap-view depends on d3 + a browser-only SVG mount, so it can't run
+// during the server-side render pass for this client page.
+const MindmapView = dynamic(
+ () => import('@/components/mindmap-view').then((m) => m.MindmapView),
+ { ssr: false, loading: () =>
Loading mindmap…
},
+);
+
+type WorkspaceDetail = {
+ workspace: {
+ id: string;
+ name: string;
+ description: string | null;
+ mindmap_markdown: string | null;
+ mindmap_generated_at: string | null;
+ audio_url: string | null;
+ audio_script: AudioScriptTurn[] | null;
+ audio_generated_at: string | null;
+ };
+ counts: {
+ documents: number;
+ chats: number;
+ due_flashcards: number;
+ };
+};
+
+type DocRow = {
+ id: string;
+ workspace_id: string | null;
+ file_name: string;
+ file_size: number;
+ status: 'processing' | 'ready' | 'failed';
+ error: string | null;
+ page_count: number | null;
+ summary: string | null;
+ created_at: string;
+};
+
+type ChatRow = {
+ id: string;
+ title: string;
+ last_message_at: string;
+};
+
+type Tab = 'documents' | 'chats' | 'mindmap' | 'review' | 'audio';
+
+const TABS: { id: Tab; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
+ { id: 'documents', label: 'Documents', icon: FileText },
+ { id: 'chats', label: 'Chats', icon: MessageSquare },
+ { id: 'mindmap', label: 'Mindmap', icon: Brain },
+ { id: 'review', label: 'Review', icon: GraduationCap },
+ { id: 'audio', label: 'Audio', icon: AudioLines },
+];
+
+export default function WorkspaceDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = use(params);
+ const router = useRouter();
+ const [detail, setDetail] = useState
(null);
+ const [tab, setTab] = useState('documents');
+ const [editing, setEditing] = useState(false);
+ const [editName, setEditName] = useState('');
+ const [editDescription, setEditDescription] = useState('');
+
+ const refresh = useCallback(async () => {
+ try {
+ const res = await fetch(`/api/workspaces/${id}`);
+ if (res.ok) {
+ const data = (await res.json()) as WorkspaceDetail;
+ setDetail(data);
+ setEditName(data.workspace.name);
+ setEditDescription(data.workspace.description ?? '');
+ return;
+ }
+ if (res.status === 404) {
+ toast.error('Workspace not found');
+ router.push('/workspaces');
+ return;
+ }
+ toast.error('Could not load workspace');
+ } catch {
+ toast.error('Could not load workspace');
+ }
+ }, [id, router]);
+
+ useEffect(() => {
+ void refresh();
+ }, [refresh]);
+
+ async function handleSaveMeta() {
+ const trimmed = editName.trim();
+ if (!trimmed) return;
+ const res = await fetch(`/api/workspaces/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: trimmed, description: editDescription.trim() }),
+ });
+ if (!res.ok) {
+ toast.error('Could not save changes');
+ return;
+ }
+ setEditing(false);
+ await refresh();
+ }
+
+ async function handleDelete() {
+ if (!confirm('Delete this workspace? Its PDFs and chats will move to Unsorted.')) return;
+ const res = await fetch(`/api/workspaces/${id}`, { method: 'DELETE' });
+ if (!res.ok) {
+ toast.error('Could not delete workspace');
+ return;
+ }
+ router.push('/workspaces');
+ }
+
+ if (!detail) {
+ return (
+
+ );
+ }
+
+ const ws = detail.workspace;
+
+ return (
+
+
+
+
+
+ All workspaces
+
+
+
+
setEditing((v) => !v)}>
+
+
+
+
+
+
+
+
+ {editing ? (
+
+ ) : (
+
+
{ws.name}
+ {ws.description ? (
+
{ws.description}
+ ) : null}
+
+ )}
+
+
+ {TABS.map(({ id: tabId, label, icon: Icon }) => (
+ setTab(tabId)}
+ className={cn(
+ 'inline-flex items-center gap-1.5 border-b-2 px-3 py-2 text-sm transition',
+ tab === tabId
+ ? 'border-foreground text-foreground'
+ : 'border-transparent text-muted-foreground hover:text-foreground',
+ )}
+ >
+
+ {label}
+ {tabId === 'documents' ? {detail.counts.documents} : null}
+ {tabId === 'chats' ? {detail.counts.chats} : null}
+ {tabId === 'review' && detail.counts.due_flashcards > 0 ? (
+ {detail.counts.due_flashcards}
+ ) : null}
+
+ ))}
+
+
+ {tab === 'documents' ? (
+
+ ) : tab === 'chats' ? (
+
+ ) : tab === 'mindmap' ? (
+
+ ) : tab === 'review' ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function Badge({ children, tone = 'default' }: { children: React.ReactNode; tone?: 'default' | 'due' }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function DocumentsTab({ workspaceId, onChanged }: { workspaceId: string; onChanged: () => void }) {
+ const [docs, setDocs] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const refresh = useCallback(async () => {
+ const res = await fetch(`/api/documents?workspace=${workspaceId}`);
+ if (res.ok) {
+ const data = (await res.json()) as { documents: DocRow[] };
+ setDocs(data.documents ?? []);
+ }
+ setLoading(false);
+ }, [workspaceId]);
+
+ useEffect(() => {
+ void refresh();
+ }, [refresh]);
+
+ // Mirror the polling from the global /documents page so newly-uploaded
+ // PDFs flip from processing → ready without a manual reload.
+ useEffect(() => {
+ if (!docs.some((d) => d.status === 'processing')) return;
+ const handle = setInterval(() => void refresh(), 3000);
+ return () => clearInterval(handle);
+ }, [docs, refresh]);
+
+ return (
+
+
{
+ void refresh();
+ onChanged();
+ }}
+ />
+ {loading ? (
+ Loading…
+ ) : (
+ {
+ void refresh();
+ onChanged();
+ }}
+ />
+ )}
+
+ );
+}
+
+function MindmapTab({
+ workspaceId,
+ initialMarkdown,
+ initialGeneratedAt,
+ onChanged,
+}: {
+ workspaceId: string;
+ initialMarkdown: string | null;
+ initialGeneratedAt: string | null;
+ onChanged: () => void;
+}) {
+ const [markdown, setMarkdown] = useState(initialMarkdown);
+ const [generatedAt, setGeneratedAt] = useState(initialGeneratedAt);
+ const [generating, setGenerating] = useState(false);
+
+ async function generate() {
+ setGenerating(true);
+ try {
+ const res = await fetch(`/api/workspaces/${workspaceId}/mindmap`, { method: 'POST' });
+ if (!res.ok) {
+ const data = (await res.json().catch(() => ({}))) as { error?: string };
+ toast.error(data.error ?? 'Mindmap generation failed');
+ return;
+ }
+ const data = (await res.json()) as { markdown: string; generated_at: string };
+ setMarkdown(data.markdown);
+ setGeneratedAt(data.generated_at);
+ onChanged();
+ } finally {
+ setGenerating(false);
+ }
+ }
+
+ return (
+
+
+
+ {generatedAt
+ ? `Last generated ${new Date(generatedAt).toLocaleString()}`
+ : 'Mindmap not generated yet. Add a few PDFs and click Generate to build one.'}
+
+
+ {generating ? (
+
+ ) : (
+
+ )}
+ {markdown ? 'Regenerate' : 'Generate mindmap'}
+
+
+ {markdown ? (
+
+ ) : (
+
+ No mindmap yet.
+
+ )}
+
+ );
+}
+
+function ReviewTab({ workspaceId, dueCount }: { workspaceId: string; dueCount: number }) {
+ return (
+
+
+
+ {dueCount === 0 ? 'No cards due right now' : `${dueCount} card${dueCount === 1 ? '' : 's'} due`}
+
+
+ Flashcards generated from any PDF in this workspace land here on a
+ spaced-repetition schedule. Three grades reschedule the card, "Again" brings it back in 5 minutes.
+
+ {dueCount === 0 ? (
+
+ Start reviewing
+
+ ) : (
+
+
+ Start reviewing
+
+
+ )}
+
+ );
+}
+
+function ChatsTab({ workspaceId }: { workspaceId: string }) {
+ const [chats, setChats] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ void (async () => {
+ const res = await fetch(`/api/chats?workspace=${workspaceId}`);
+ if (res.ok && !cancelled) {
+ const data = (await res.json()) as { chats: ChatRow[] };
+ setChats(data.chats ?? []);
+ }
+ if (!cancelled) setLoading(false);
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [workspaceId]);
+
+ const newChatHref = useMemo(() => `/chat?workspace=${workspaceId}`, [workspaceId]);
+
+ return (
+
+
+
+
+
+ New chat in this workspace
+
+
+
+ {loading ? (
+
Loading…
+ ) : chats.length === 0 ? (
+
+ No chats yet. Start one and it will be scoped to this workspace's PDFs.
+
+ ) : (
+
+ {chats.map((c) => (
+
+
+ {c.title}
+
+ {new Date(c.last_message_at).toLocaleDateString()}
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/ai-pdf-chatbot/app/workspaces/[id]/review/page.tsx b/ai-pdf-chatbot/app/workspaces/[id]/review/page.tsx
new file mode 100644
index 0000000..c87ebd8
--- /dev/null
+++ b/ai-pdf-chatbot/app/workspaces/[id]/review/page.tsx
@@ -0,0 +1,176 @@
+'use client';
+
+import { use, useCallback, useEffect, useState } from 'react';
+import Link from 'next/link';
+import { ArrowLeft, Check, Loader2, Sparkles } from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import type { Grade } from '@/lib/srs/schedule';
+import { cn } from '@/lib/utils';
+
+type DueCard = {
+ id: string;
+ document_id: string;
+ file_name: string;
+ question: string;
+ answer: string;
+ ease: number;
+ interval_days: number;
+ reps: number;
+ due_at: string;
+};
+
+const GRADES: { id: Grade; label: string; tone: string }[] = [
+ { id: 'again', label: 'Again', tone: 'bg-red-100 text-red-900 hover:bg-red-200 dark:bg-red-950/40 dark:text-red-100' },
+ { id: 'hard', label: 'Hard', tone: 'bg-orange-100 text-orange-900 hover:bg-orange-200 dark:bg-orange-950/40 dark:text-orange-100' },
+ { id: 'good', label: 'Good', tone: 'bg-emerald-100 text-emerald-900 hover:bg-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-100' },
+ { id: 'easy', label: 'Easy', tone: 'bg-sky-100 text-sky-900 hover:bg-sky-200 dark:bg-sky-950/40 dark:text-sky-100' },
+];
+
+export default function ReviewPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = use(params);
+ const [cards, setCards] = useState(null);
+ const [index, setIndex] = useState(0);
+ const [revealed, setRevealed] = useState(false);
+ const [grading, setGrading] = useState(false);
+
+ const load = useCallback(async () => {
+ try {
+ const res = await fetch(`/api/workspaces/${id}/flashcards/due`);
+ if (!res.ok) {
+ toast.error('Could not load review queue');
+ setCards([]);
+ return;
+ }
+ const data = (await res.json()) as { cards: DueCard[] };
+ setCards(data.cards ?? []);
+ setIndex(0);
+ setRevealed(false);
+ } catch {
+ toast.error('Could not load review queue');
+ setCards([]);
+ }
+ }, [id]);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ async function handleGrade(grade: Grade) {
+ if (!cards || index >= cards.length) return;
+ const current = cards[index];
+ setGrading(true);
+ try {
+ const res = await fetch(`/api/flashcards/${current.id}/grade`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ grade }),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ setIndex((i) => i + 1);
+ setRevealed(false);
+ } catch {
+ toast.error('Could not save grade');
+ } finally {
+ setGrading(false);
+ }
+ }
+
+ if (cards === null) {
+ return (
+
+ );
+ }
+
+ const total = cards.length;
+ const done = index >= total;
+ const card = !done ? cards[index] : null;
+
+ return (
+
+
+
+
+
+ Back to workspace
+
+
+
+ {Math.min(index, total)} / {total}
+
+
+
+ {total === 0 ? (
+
}
+ title="No cards due"
+ body="Generate flashcards on a document in this workspace, then come back here to review."
+ />
+ ) : done ? (
+
}
+ title="Today's review done"
+ body="Come back tomorrow for the next batch. The schedule moves cards out automatically."
+ />
+ ) : (
+
+
+
From {card?.file_name}
+
{card?.question}
+ {revealed ? (
+
+ {card?.answer}
+
+ ) : (
+
setRevealed(true)}>
+ Show answer
+
+ )}
+
+
+ {revealed ? (
+
+ {GRADES.map((g) => (
+ handleGrade(g.id)}
+ disabled={grading}
+ className={cn(
+ 'rounded-xl px-3 py-3 text-sm font-medium transition disabled:opacity-50',
+ g.tone,
+ )}
+ >
+ {g.label}
+
+ ))}
+
+ ) : null}
+
+ )}
+
+ );
+}
+
+function EmptyState({
+ icon,
+ title,
+ body,
+}: {
+ icon: React.ReactNode;
+ title: string;
+ body: string;
+}) {
+ return (
+
+ {icon}
+
{title}
+
{body}
+
+ );
+}
diff --git a/ai-pdf-chatbot/app/workspaces/page.tsx b/ai-pdf-chatbot/app/workspaces/page.tsx
new file mode 100644
index 0000000..83034c0
--- /dev/null
+++ b/ai-pdf-chatbot/app/workspaces/page.tsx
@@ -0,0 +1,156 @@
+'use client';
+
+import { useCallback, useEffect, useState } from 'react';
+import Link from 'next/link';
+import { ArrowLeft, FolderOpen, Plus } from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+
+type WorkspaceRow = {
+ id: string;
+ name: string;
+ description: string | null;
+ mindmap_generated_at: string | null;
+ audio_url: string | null;
+ audio_generated_at: string | null;
+ updated_at: string;
+};
+
+export default function WorkspacesPage() {
+ const [workspaces, setWorkspaces] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [creating, setCreating] = useState(false);
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+
+ const refresh = useCallback(async () => {
+ try {
+ const res = await fetch('/api/workspaces');
+ if (res.ok) {
+ const data = (await res.json()) as { workspaces: WorkspaceRow[] };
+ setWorkspaces(data.workspaces ?? []);
+ } else {
+ toast.error('Could not load workspaces');
+ }
+ } catch {
+ toast.error('Could not load workspaces');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void refresh();
+ }, [refresh]);
+
+ async function handleCreate(e: React.FormEvent) {
+ e.preventDefault();
+ const trimmed = name.trim();
+ if (!trimmed) return;
+ setSubmitting(true);
+ try {
+ const res = await fetch('/api/workspaces', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: trimmed, description: description.trim() || undefined }),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ setName('');
+ setDescription('');
+ setCreating(false);
+ await refresh();
+ } catch {
+ toast.error('Could not create workspace');
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+ Back to chat
+
+
+
setCreating((v) => !v)}>
+
+ New workspace
+
+
+
Workspaces
+
+ Group related PDFs, chats, and study tools together. Like a NotebookLM notebook.
+
+
+ {creating ? (
+
+ ) : null}
+
+ {loading ? (
+
Loading…
+ ) : workspaces.length === 0 ? (
+
+
+
+ No workspaces yet. Create one to start organizing your PDFs.
+
+ {!creating ? (
+
setCreating(true)}>
+
+ Create your first workspace
+
+ ) : null}
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/ai-pdf-chatbot/components/audio-overview.tsx b/ai-pdf-chatbot/components/audio-overview.tsx
new file mode 100644
index 0000000..6ac19ee
--- /dev/null
+++ b/ai-pdf-chatbot/components/audio-overview.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import { useState } from 'react';
+import { AudioLines, Loader2, RefreshCw } from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import type { AudioScriptTurn } from '@/lib/types';
+import { cn } from '@/lib/utils';
+
+type Props = {
+ workspaceId: string;
+ initialAudioUrl: string | null;
+ initialScript: AudioScriptTurn[] | null;
+ initialGeneratedAt: string | null;
+ onChanged: () => void;
+};
+
+// Plays the workspace audio overview and shows the underlying script as
+// a transcript. We intentionally don't sync transcript highlight to
+// playback time — OpenAI TTS doesn't return per-turn timing, and
+// estimating from character counts is unreliable enough that it tends
+// to mislead more than it helps. Skip-by-turn could be added later.
+export function AudioOverview({
+ workspaceId,
+ initialAudioUrl,
+ initialScript,
+ initialGeneratedAt,
+ onChanged,
+}: Props) {
+ const [audioUrl, setAudioUrl] = useState(initialAudioUrl);
+ const [script, setScript] = useState(initialScript);
+ const [generatedAt, setGeneratedAt] = useState(initialGeneratedAt);
+ const [generating, setGenerating] = useState(false);
+
+ async function generate() {
+ if (generating) return;
+ setGenerating(true);
+ try {
+ const res = await fetch(`/api/workspaces/${workspaceId}/audio`, { method: 'POST' });
+ const data = (await res.json().catch(() => ({}))) as {
+ audio_url?: string;
+ script?: AudioScriptTurn[];
+ generated_at?: string;
+ error?: string;
+ };
+ if (!res.ok) {
+ if (res.status === 412) {
+ toast.error('Configure OPENAI_API_KEY to enable Audio Overview');
+ } else {
+ toast.error(data.error ?? 'Audio generation failed');
+ }
+ return;
+ }
+ setAudioUrl(data.audio_url ?? null);
+ setScript(data.script ?? null);
+ setGeneratedAt(data.generated_at ?? null);
+ onChanged();
+ } catch {
+ // Network errors and request aborts land here; the inner block only
+ // covers non-ok HTTP responses, not transport failures.
+ toast.error('Audio generation failed');
+ } finally {
+ setGenerating(false);
+ }
+ }
+
+ return (
+
+
+
+ {generatedAt
+ ? `Last generated ${new Date(generatedAt).toLocaleString()}`
+ : 'Two hosts will chat through the key ideas from your PDFs.'}
+
+
+ {generating ? (
+
+ ) : (
+
+ )}
+ {audioUrl ? 'Regenerate' : 'Generate audio'}
+
+
+
+ {!audioUrl ? (
+
+
+
No audio yet
+
+ Generation takes 30 to 60 seconds. Needs `OPENAI_API_KEY` in your env.
+
+
+ ) : (
+
+
+
+ Your browser does not support audio playback.
+
+
+
+ {script && script.length > 0 ? (
+
+
Transcript
+
+ {script.map((turn, i) => (
+
+ {turn.speaker}
+ {turn.text}
+
+ ))}
+
+
+ ) : null}
+
+ )}
+
+ );
+}
diff --git a/ai-pdf-chatbot/components/chat-message.tsx b/ai-pdf-chatbot/components/chat-message.tsx
index e02d61b..50f2729 100644
--- a/ai-pdf-chatbot/components/chat-message.tsx
+++ b/ai-pdf-chatbot/components/chat-message.tsx
@@ -6,6 +6,8 @@ import { cn } from '@/lib/utils';
type Citation = ChatMessageRow['citations'][number];
+type OpenFn = (cite: Citation) => void;
+
function CitationButton({
marker,
cite,
@@ -14,7 +16,7 @@ function CitationButton({
}: {
marker: number;
cite: Citation | undefined;
- open: (documentId: string | null | undefined, pageNumber: number | null | undefined) => void;
+ open: OpenFn;
loading: boolean;
}) {
const title = cite
@@ -26,7 +28,7 @@ function CitationButton({
type="button"
onClick={(e) => {
e.preventDefault();
- if (cite?.document_id) open(cite.document_id, cite.page_number);
+ if (cite?.document_id) open(cite);
}}
disabled={loading || !cite?.document_id}
title={title}
@@ -43,7 +45,7 @@ function CitationButton({
function renderWithCitations(
text: string,
citations: Citation[],
- open: (documentId: string | null | undefined, pageNumber: number | null | undefined) => void,
+ open: OpenFn,
loadingId: string | null,
) {
if (citations.length === 0) return text;
diff --git a/ai-pdf-chatbot/components/chat-shell.tsx b/ai-pdf-chatbot/components/chat-shell.tsx
index 8fecb12..b8c11eb 100644
--- a/ai-pdf-chatbot/components/chat-shell.tsx
+++ b/ai-pdf-chatbot/components/chat-shell.tsx
@@ -1,27 +1,30 @@
'use client';
import Link from 'next/link';
-import { useRouter } from 'next/navigation';
+import { usePathname, useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { FileText, Menu, MessageSquare, Pencil, Plus, Trash2, X } from 'lucide-react';
+import { FileText, FolderOpen, Menu, MessageSquare, Pencil, Plus, Trash2, X } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { SidebarAccountBar } from '@/components/sidebar-account-bar';
+import { CitationRail } from '@/components/citation-rail';
+import { PdfDrawer } from '@/components/pdf-drawer';
+import { PdfViewerProvider } from '@/lib/pdf/viewer-context';
+import { useRailCitations } from '@/lib/chat/rail-context';
import { authClient } from '@/lib/auth-client';
import type { AuthViewer } from '@/lib/types';
import { cn } from '@/lib/utils';
type ChatRow = { id: string; title: string; last_message_at: string };
-export function ChatShell({
- activeChatId,
- children,
- rail,
-}: {
- activeChatId?: string;
- children: React.ReactNode;
- rail: React.ReactNode;
-}) {
+export function ChatShell({ children }: { children: React.ReactNode }) {
+ // Rendered by app/chat/layout.tsx so its sidebar state survives navigation
+ // between /chat and /chat/[id]. activeChatId is derived from the URL, not
+ // a prop, and the citation rail is pulled from RailContext (pages push
+ // their citations up).
+ const pathname = usePathname();
+ const activeChatId = pathname?.startsWith('/chat/') ? pathname.split('/')[2] : undefined;
+ const railCitations = useRailCitations();
const router = useRouter();
const [chats, setChats] = useState([]);
const [drawerOpen, setDrawerOpen] = useState(false);
@@ -48,9 +51,18 @@ export function ChatShell({
setChats(data.chats ?? []);
}, []);
+ // Initial fetch on mount only. Re-fetching on every activeChatId change
+ // caused the entire sidebar list to flash on every chat navigation.
+ // For new chats, the stream hook dispatches a `chats:changed` event
+ // when it finishes so the sidebar updates without per-navigation churn.
useEffect(() => {
void loadChats();
- }, [loadChats, activeChatId]);
+ const onChanged = () => {
+ void loadChats();
+ };
+ window.addEventListener('chats:changed', onChanged);
+ return () => window.removeEventListener('chats:changed', onChanged);
+ }, [loadChats]);
// Close the mobile drawer whenever the user navigates to a new chat.
useEffect(() => {
@@ -105,6 +117,11 @@ export function ChatShell({
const sidebarBody = (
<>
+ {/* Header strip that aligns with the chat detail page's title bar on
+ the right, so both top borders sit on the same horizontal line. */}
+
@@ -112,6 +129,12 @@ export function ChatShell({
New chat
+
+
+
+ Workspaces
+
+
@@ -187,13 +210,16 @@ export function ChatShell({
))}
-
-
-
+ {/* No border-t here: the chat input on the right has its own border
+ at a different y-coordinate (because input height ≠ account bar
+ height), and putting one on the sidebar bottom too made the
+ mismatch obvious. Account bar floats as a self-contained card. */}
+
>
);
return (
+
{/* Desktop sidebar */}
@@ -241,9 +267,13 @@ export function ChatShell({
- {rail}
+
+
+ {/* Inline PDF viewer overlays the chat when a citation is opened. */}
+
+
);
}
diff --git a/ai-pdf-chatbot/components/citation-rail.tsx b/ai-pdf-chatbot/components/citation-rail.tsx
index a82e639..ae0b06a 100644
--- a/ai-pdf-chatbot/components/citation-rail.tsx
+++ b/ai-pdf-chatbot/components/citation-rail.tsx
@@ -12,13 +12,13 @@ function OpenSourceButton({
loading,
}: {
cite: Citation;
- open: (documentId: string | null | undefined, pageNumber: number | null | undefined) => void;
+ open: (cite: Citation) => void;
loading: boolean;
}) {
return (
open(cite.document_id, cite.page_number)}
+ onClick={() => open(cite)}
disabled={loading || !cite.document_id}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
diff --git a/ai-pdf-chatbot/components/document-list.tsx b/ai-pdf-chatbot/components/document-list.tsx
index 4f646e3..79cc440 100644
--- a/ai-pdf-chatbot/components/document-list.tsx
+++ b/ai-pdf-chatbot/components/document-list.tsx
@@ -6,9 +6,11 @@ import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { DocumentStatusBadge } from './document-status-badge';
import { FlashcardsModal } from './flashcards-modal';
+import { WorkspacePicker } from './workspace-picker';
type DocRow = {
id: string;
+ workspace_id: string | null;
file_name: string;
file_size: number;
status: 'processing' | 'ready' | 'failed';
@@ -18,7 +20,18 @@ type DocRow = {
created_at: string;
};
-export function DocumentList({ documents, onChanged }: { documents: DocRow[]; onChanged: () => void }) {
+// showWorkspacePicker toggles the per-row "move to workspace" affordance.
+// The global /documents page shows it; the workspace detail page hides it
+// since every row is already known to belong to that workspace.
+export function DocumentList({
+ documents,
+ onChanged,
+ showWorkspacePicker = true,
+}: {
+ documents: DocRow[];
+ onChanged: () => void;
+ showWorkspacePicker?: boolean;
+}) {
const [flashcardsFor, setFlashcardsFor] = useState(null);
async function handleDelete(id: string, name: string) {
@@ -39,7 +52,11 @@ export function DocumentList({ documents, onChanged }: { documents: DocRow[]; on
<>
{documents.map((d) => (
-
+
{d.file_name}
@@ -52,6 +69,9 @@ export function DocumentList({ documents, onChanged }: { documents: DocRow[]; on
+ {showWorkspacePicker ? (
+
+ ) : null}
{d.status === 'ready' ? (
void }) {
+// workspaceId, when provided, attaches the uploaded document to that
+// workspace immediately. Used by the workspace detail page; the global
+// /documents page passes nothing so uploads land in "Unsorted".
+export function DocumentUploader({
+ onUploaded,
+ workspaceId,
+}: {
+ onUploaded: () => void;
+ workspaceId?: string | null;
+}) {
const inputRef = useRef(null);
const [isUploading, setIsUploading] = useState(false);
@@ -24,6 +33,7 @@ export function DocumentUploader({ onUploaded }: { onUploaded: () => void }) {
try {
const fd = new FormData();
fd.append('file', file);
+ if (workspaceId) fd.append('workspaceId', workspaceId);
const res = await fetch('/api/documents/upload', { method: 'POST', body: fd });
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? 'Upload failed');
diff --git a/ai-pdf-chatbot/components/mindmap-view.tsx b/ai-pdf-chatbot/components/mindmap-view.tsx
new file mode 100644
index 0000000..9ae2f66
--- /dev/null
+++ b/ai-pdf-chatbot/components/mindmap-view.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import { useEffect, useRef } from 'react';
+import { Transformer } from 'markmap-lib';
+import { Markmap } from 'markmap-view';
+
+const transformer = new Transformer();
+
+// SVG-based mindmap renderer. markmap-view mounts directly onto an SVG
+// element and runs its own zoom/pan via d3, so the parent just provides
+// a sized container.
+export function MindmapView({ markdown }: { markdown: string }) {
+ const svgRef = useRef(null);
+ const mmRef = useRef(null);
+
+ useEffect(() => {
+ if (!svgRef.current) return;
+ const { root } = transformer.transform(markdown);
+ if (!mmRef.current) {
+ mmRef.current = Markmap.create(svgRef.current, undefined, root);
+ } else {
+ mmRef.current.setData(root);
+ mmRef.current.fit();
+ }
+ }, [markdown]);
+
+ // Re-fit on container resize so the mindmap stays centered when the
+ // tab panel resizes (window resize, sidebar collapse, etc.).
+ useEffect(() => {
+ const svg = svgRef.current;
+ if (!svg) return;
+ const observer = new ResizeObserver(() => {
+ mmRef.current?.fit();
+ });
+ observer.observe(svg);
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/ai-pdf-chatbot/components/pdf-drawer.tsx b/ai-pdf-chatbot/components/pdf-drawer.tsx
new file mode 100644
index 0000000..5c87331
--- /dev/null
+++ b/ai-pdf-chatbot/components/pdf-drawer.tsx
@@ -0,0 +1,130 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import dynamic from 'next/dynamic';
+import { Loader2, X } from 'lucide-react';
+import { toast } from 'sonner';
+import { usePdfViewer } from '@/lib/pdf/viewer-context';
+import { cn } from '@/lib/utils';
+
+// react-pdf pulls in the pdfjs worker on import and crashes during the
+// Next 16 server-side bundle phase. Loading it through next/dynamic with
+// ssr:false makes sure the whole subtree only ever renders on the client.
+const PdfViewer = dynamic(() => import('./pdf-viewer').then((m) => m.PdfViewer), {
+ ssr: false,
+ loading: () => (
+
+
+ Loading viewer…
+
+ ),
+});
+
+export function PdfDrawer() {
+ const ctx = usePdfViewer();
+ const [url, setUrl] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const target = ctx?.current ?? null;
+ const documentId = target?.documentId ?? null;
+
+ const close = ctx?.close;
+
+ // Fetch a fresh presigned URL whenever the target document changes.
+ // Re-using a URL across documents would 403 on the second open. We
+ // depend on `documentId` + `close` (a stable useCallback inside the
+ // context provider) rather than on the whole `ctx` object so the
+ // effect doesn't re-run on every viewer-state change that produces a
+ // new context value (e.g. opening the same document a second time).
+ useEffect(() => {
+ if (!documentId) {
+ setUrl(null);
+ return;
+ }
+ let cancelled = false;
+ setLoading(true);
+ void (async () => {
+ try {
+ const res = await fetch(`/api/documents/${documentId}/url`);
+ if (!res.ok) throw new Error(`status ${res.status}`);
+ const data = (await res.json()) as { url: string };
+ if (!cancelled) setUrl(data.url);
+ } catch {
+ if (!cancelled) {
+ toast.error('Could not open source PDF');
+ close?.();
+ }
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [documentId, close]);
+
+ // Esc closes the drawer. Scoped to mount so we don't intercept Esc
+ // when the drawer is hidden.
+ useEffect(() => {
+ if (!target) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') ctx?.close();
+ };
+ document.addEventListener('keydown', onKey);
+ return () => document.removeEventListener('keydown', onKey);
+ }, [target, ctx]);
+
+ if (!ctx) return null;
+
+ return (
+ <>
+ {/* Backdrop (mobile only — desktop drawer leaves chat readable). */}
+ {target ? (
+ ctx.close()}
+ className="fixed inset-0 z-30 bg-black/30 md:hidden"
+ />
+ ) : null}
+
+ >
+ );
+}
diff --git a/ai-pdf-chatbot/components/pdf-viewer.tsx b/ai-pdf-chatbot/components/pdf-viewer.tsx
new file mode 100644
index 0000000..a879bcc
--- /dev/null
+++ b/ai-pdf-chatbot/components/pdf-viewer.tsx
@@ -0,0 +1,157 @@
+'use client';
+
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Document, Page } from 'react-pdf';
+import 'react-pdf/dist/Page/AnnotationLayer.css';
+import 'react-pdf/dist/Page/TextLayer.css';
+import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
+import { ensurePdfWorker } from '@/lib/pdf/pdf-worker';
+
+// Highlight class is applied by toggling this on text-layer spans whose
+// content starts with the citation prefix. Defined as a string so it can
+// be added/removed without touching React state.
+const HIGHLIGHT_CLASS = 'rag-citation-highlight';
+
+type Props = {
+ fileUrl: string;
+ initialPage?: number;
+ highlightPrefix?: string | null;
+};
+
+// Self-contained react-pdf wrapper. Renders a single page at a time with
+// page navigation; on each page render, scans the text layer for the
+// chunk prefix and highlights the matching spans. Keeps things minimal
+// so the drawer can stay narrow and still be useful on mobile.
+export function PdfViewer({ fileUrl, initialPage = 1, highlightPrefix = null }: Props) {
+ const [numPages, setNumPages] = useState(null);
+ const [page, setPage] = useState(initialPage);
+ const pageWrapperRef = useRef(null);
+
+ useEffect(() => {
+ ensurePdfWorker();
+ }, []);
+
+ // Reset when a new citation target is opened
+ useEffect(() => {
+ setPage(initialPage);
+ }, [initialPage, fileUrl]);
+
+ const onLoadSuccess = useCallback(({ numPages: n }: { numPages: number }) => {
+ setNumPages(n);
+ }, []);
+
+ const onPageRender = useCallback(() => {
+ const wrapper = pageWrapperRef.current;
+ if (!wrapper) return;
+ const layer = wrapper.querySelector('.react-pdf__Page__textContent');
+ if (!layer) return;
+
+ // Always clear stale highlights first so opening a citation without a
+ // snippet doesn't keep the previous citation's yellow text on the page.
+ layer.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((el) => {
+ el.classList.remove(HIGHLIGHT_CLASS);
+ });
+ if (!highlightPrefix) return;
+
+ // Span granularity in pdfjs text layer can vary, so we look for the
+ // first span whose text contains the prefix and walk forward until we
+ // cover ~prefix length. Imperfect on hyphenated/multi-span words but
+ // good enough for "scroll the cited sentence into view" UX.
+ const spans = Array.from(layer.querySelectorAll('span'));
+ const normalized = normalize(highlightPrefix).slice(0, 60);
+ if (!normalized) return;
+
+ let firstHit: HTMLElement | null = null;
+ let consumed = 0;
+ for (const span of spans) {
+ const text = normalize(span.textContent ?? '');
+ if (!firstHit) {
+ if (text && normalized.startsWith(text.slice(0, Math.min(text.length, 12)))) {
+ firstHit = span;
+ span.classList.add(HIGHLIGHT_CLASS);
+ consumed = text.length;
+ }
+ } else if (consumed < normalized.length) {
+ span.classList.add(HIGHLIGHT_CLASS);
+ consumed += text.length;
+ } else {
+ break;
+ }
+ }
+
+ firstHit?.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ }, [highlightPrefix]);
+
+ return (
+
+
+ setPage((p) => Math.max(1, p - 1))}
+ disabled={page <= 1}
+ className="inline-flex items-center gap-1 rounded p-1 text-muted-foreground hover:bg-muted disabled:opacity-40"
+ aria-label="Previous page"
+ >
+
+
+
+ Page {page}
+ {numPages ? ` / ${numPages}` : null}
+
+ {
+ // Don't advance past the document end, and don't advance at all
+ // until react-pdf reports the page count.
+ if (!numPages) return;
+ setPage((p) => Math.min(numPages, p + 1));
+ }}
+ disabled={!numPages || page >= numPages}
+ className="inline-flex items-center gap-1 rounded p-1 text-muted-foreground hover:bg-muted disabled:opacity-40"
+ aria-label="Next page"
+ >
+
+
+
+
+
}
+ error={
}
+ className="flex flex-col items-center"
+ >
+
+
+
+
+ );
+}
+
+function PdfLoading({ label }: { label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+function PdfError() {
+ return (
+
+ Could not load this PDF. The presigned URL may have expired — try clicking the citation again.
+
+ );
+}
+
+function normalize(text: string): string {
+ return text.replace(/\s+/g, ' ').trim().toLowerCase();
+}
diff --git a/ai-pdf-chatbot/components/share-chat-button.tsx b/ai-pdf-chatbot/components/share-chat-button.tsx
index e2e8b9d..f4ed22d 100644
--- a/ai-pdf-chatbot/components/share-chat-button.tsx
+++ b/ai-pdf-chatbot/components/share-chat-button.tsx
@@ -78,13 +78,20 @@ export function ShareChatButton({
return (
setOpen((v) => !v)}
aria-expanded={open}
>
{shareToken ? 'Shared' : 'Share'}
+ {shareToken ? (
+
+ ) : null}
{open ? (
diff --git a/ai-pdf-chatbot/components/workspace-picker.tsx b/ai-pdf-chatbot/components/workspace-picker.tsx
new file mode 100644
index 0000000..3c83d99
--- /dev/null
+++ b/ai-pdf-chatbot/components/workspace-picker.tsx
@@ -0,0 +1,159 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { Check, ChevronDown, FolderOpen, Loader2 } from 'lucide-react';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+
+type WorkspaceLite = { id: string; name: string };
+
+// Compact dropdown that lets a user reassign a document to a workspace
+// (or to "Unsorted"). Renders inline so it fits in the documents list
+// row. Closes on outside click and escape.
+export function WorkspacePicker({
+ currentId,
+ onChanged,
+}: {
+ currentId: string | null;
+ onChanged: () => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const [workspaces, setWorkspaces] = useState
(null);
+ const [loading, setLoading] = useState(false);
+ const rootRef = useRef(null);
+
+ useEffect(() => {
+ if (!open) return;
+ let cancelled = false;
+ void (async () => {
+ try {
+ const res = await fetch('/api/workspaces');
+ if (!res.ok) {
+ // Surface as "no workspaces" rather than infinite loader
+ if (!cancelled) setWorkspaces([]);
+ return;
+ }
+ const data = (await res.json()) as { workspaces: WorkspaceLite[] };
+ if (!cancelled) setWorkspaces(data.workspaces ?? []);
+ } catch {
+ if (!cancelled) setWorkspaces([]);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ const handleClick = (e: MouseEvent) => {
+ if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
+ };
+ const handleKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') setOpen(false);
+ };
+ document.addEventListener('mousedown', handleClick);
+ document.addEventListener('keydown', handleKey);
+ return () => {
+ document.removeEventListener('mousedown', handleClick);
+ document.removeEventListener('keydown', handleKey);
+ };
+ }, [open]);
+
+ return (
+
+
setOpen((v) => !v)}
+ className={cn(
+ 'inline-flex items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-xs text-muted-foreground hover:bg-muted',
+ loading && 'opacity-60',
+ )}
+ disabled={loading}
+ >
+
+
+ {currentId ? 'Workspace' : 'Unsorted'}
+
+ {loading ? : }
+
+ {open ? (
+
+
pick(null)}
+ />
+ {workspaces === null ? (
+ Loading…
+ ) : workspaces.length === 0 ? (
+ No workspaces yet. Create one on the Workspaces page.
+ ) : (
+ workspaces.map((ws) => (
+ pick(ws.id)}
+ />
+ ))
+ )}
+
+ ) : null}
+
+ );
+
+ async function pick(nextId: string | null) {
+ setOpen(false);
+ setLoading(true);
+ try {
+ const res = await fetch(`/api/documents/${documentIdFromButton(rootRef.current)}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ workspace_id: nextId }),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ onChanged();
+ } catch {
+ toast.error('Could not move document');
+ } finally {
+ setLoading(false);
+ }
+ }
+}
+
+// The picker reads its parent 's data-doc-id so we don't have to
+// thread the document id through every render path. The list row already
+// sets data-doc-id on the wrapper.
+function documentIdFromButton(el: HTMLElement | null): string | undefined {
+ const wrapper = el?.closest('[data-doc-id]') as HTMLElement | null;
+ return wrapper?.dataset.docId;
+}
+
+function PickerRow({
+ label,
+ active,
+ onPick,
+}: {
+ label: string;
+ active: boolean;
+ onPick: () => void;
+}) {
+ return (
+
+ {label}
+ {active ? : null}
+
+ );
+}
diff --git a/ai-pdf-chatbot/lib/ai/audio-script.ts b/ai-pdf-chatbot/lib/ai/audio-script.ts
new file mode 100644
index 0000000..daf86ed
--- /dev/null
+++ b/ai-pdf-chatbot/lib/ai/audio-script.ts
@@ -0,0 +1,137 @@
+import 'server-only';
+import type { createClient } from '@insforge/sdk';
+import { UTILITY_MODEL } from './constants';
+
+type InsforgeClient = ReturnType;
+
+export type AudioScriptTurn = {
+ speaker: 'Sarah' | 'Mike';
+ text: string;
+};
+
+// Trim each document summary so the joint prompt stays under a few k
+// tokens. Quality of the conversational script saturates well before
+// model context limits.
+const SUMMARY_CHAR_BUDGET = 600;
+
+// Adapted from open-notebooklm (github.com/gabrielchua/open-notebooklm,
+// 2.6k stars). The asymmetric host/guest framing + scratchpad-style
+// brainstorm step are what stop two co-hosts from just agreeing with
+// each other ("Absolutely!" / "Totally!"). Sarah interviews Mike, who
+// plays the subject-matter expert, so questions and answers flow
+// naturally instead of mirrored validations.
+function buildPrompt(
+ workspaceName: string,
+ docs: Array<{ file_name: string; summary: string | null }>,
+): string {
+ const block = docs
+ .map((d, i) => `${i + 1}. ${d.file_name}\n ${(d.summary ?? '').slice(0, SUMMARY_CHAR_BUDGET)}`)
+ .join('\n\n');
+
+ return `You are a world-class podcast producer tasked with transforming the provided PDF summaries from a study workspace named "${workspaceName}" into an engaging and informative podcast script. The input may be unstructured or messy, sourced from PDFs. Your goal is to extract the most interesting and insightful content for a compelling podcast discussion.
+
+# Steps to Follow:
+
+1. **Analyze the Input:**
+ Carefully examine the text, identifying key topics, points, and interesting facts or anecdotes that could drive an engaging podcast conversation. Disregard irrelevant information or formatting issues.
+
+2. **Brainstorm Ideas (internal):**
+ Before writing, mentally brainstorm ways to present the key points engagingly. Consider:
+ - Analogies, storytelling techniques, or hypothetical scenarios to make content relatable
+ - Ways to make complex topics accessible to a general audience
+ - Thought-provoking questions to explore during the podcast
+ - Creative approaches to fill any gaps in the information
+ Do NOT include this brainstorm in your final output.
+
+3. **Craft the Dialogue:**
+ Develop a natural, conversational flow between the host (Sarah) and the guest speaker (Mike, a subject-matter expert on the topics in the materials). Incorporate:
+ - The best ideas from your brainstorming
+ - Clear explanations of complex topics
+ - An engaging and lively tone to captivate listeners
+ - A balance of information and entertainment
+
+ Rules for the dialogue:
+ - The host (Sarah) always initiates the conversation and interviews the guest (Mike)
+ - Include thoughtful questions from the host to guide the discussion
+ - Incorporate natural speech patterns, including occasional verbal fillers (e.g., "um", "well", "you know")
+ - Allow for natural interruptions and back-and-forth between host and guest
+ - Ensure the guest's responses are substantiated by the input text, avoiding unsupported claims
+ - Reference the source PDFs by topic, not by file name
+ - Maintain a PG-rated conversation appropriate for all audiences
+ - The host (Sarah) concludes the conversation
+
+4. **Summarize Key Insights:**
+ Naturally weave a summary of key points into the closing part of the dialogue. This should feel like a casual conversation rather than a formal recap, reinforcing the main takeaways before signing off.
+
+5. **Maintain Authenticity:**
+ Throughout the script, strive for authenticity in the conversation. Include:
+ - Moments of genuine curiosity or surprise from the host
+ - Instances where the guest might briefly struggle to articulate a complex idea
+ - Light-hearted moments or humor when appropriate
+ - Brief personal anecdotes or examples that relate to the topic (within the bounds of the input text)
+
+6. **Consider Pacing and Structure:**
+ Ensure the dialogue has a natural ebb and flow:
+ - Start with a strong hook to grab the listener's attention
+ - Gradually build complexity as the conversation progresses
+ - Include brief "breather" moments for listeners to absorb complex information
+ - End on a high note, perhaps with a thought-provoking question or a call-to-action for listeners
+
+IMPORTANT RULES:
+- Each line of dialogue should be no more than 100 characters (about 5-8 seconds spoken).
+- 10 to 18 total turns alternating between Sarah and Mike, starting with Sarah.
+- Total spoken length should fit roughly 5 to 8 minutes of audio.
+
+Return ONLY a valid JSON object with this exact schema, no scratchpad, no markdown fences, no commentary:
+{"turns":[{"speaker":"Sarah","text":"..."},{"speaker":"Mike","text":"..."}]}
+The "speaker" value must be exactly "Sarah" or "Mike". Begin your response directly with the opening brace.
+
+PDF summaries:
+${block}`;
+}
+
+export async function generateAudioScript(
+ client: InsforgeClient,
+ workspaceName: string,
+ docs: Array<{ file_name: string; summary: string | null }>,
+): Promise {
+ const completion = (await client.ai.chat.completions.create({
+ model: UTILITY_MODEL,
+ messages: [{ role: 'user', content: buildPrompt(workspaceName, docs) }],
+ })) as { choices: Array<{ message: { content: string } }> };
+
+ const raw = completion.choices?.[0]?.message?.content ?? '{}';
+ return parseScript(raw);
+}
+
+function parseScript(raw: string): AudioScriptTurn[] {
+ // The richer producer-style prompt sometimes leaks brainstorm text or
+ // wraps JSON in code fences. Strip fences, then carve out the JSON by
+ // its outermost brace pair so leading commentary doesn't break us.
+ let cleaned = raw.replace(/```[a-zA-Z]*\n?/g, '').replace(/```/g, '').trim();
+ const first = cleaned.indexOf('{');
+ const last = cleaned.lastIndexOf('}');
+ if (first >= 0 && last > first) cleaned = cleaned.slice(first, last + 1);
+
+ try {
+ const parsed = JSON.parse(cleaned) as { turns?: Array<{ speaker?: string; text?: string }> };
+ const turns = Array.isArray(parsed.turns) ? parsed.turns : [];
+ return turns
+ .map((t) => ({
+ speaker: t.speaker === 'Mike' ? 'Mike' : 'Sarah',
+ text: typeof t.text === 'string' ? t.text.trim() : '',
+ }))
+ .filter((t): t is AudioScriptTurn => t.text.length > 0);
+ } catch (err) {
+ // Avoid logging raw model output: it contains PDF-derived text that
+ // can leak user content to server logs. Just record the failure mode
+ // and shapes so the route can surface a clean error to the caller.
+ const head = cleaned.slice(0, 80);
+ console.error('[audio-script] JSON parse failed', {
+ error: err instanceof Error ? err.message : String(err),
+ cleanedLength: cleaned.length,
+ startsWith: head.startsWith('{') ? 'brace' : 'other',
+ });
+ return [];
+ }
+}
diff --git a/ai-pdf-chatbot/lib/audio/tts.ts b/ai-pdf-chatbot/lib/audio/tts.ts
new file mode 100644
index 0000000..2d6d493
--- /dev/null
+++ b/ai-pdf-chatbot/lib/audio/tts.ts
@@ -0,0 +1,57 @@
+import 'server-only';
+import type { AudioScriptTurn } from '@/lib/ai/audio-script';
+
+const OPENAI_URL = 'https://api.openai.com/v1/audio/speech';
+const MODEL = 'gpt-4o-mini-tts';
+// nova / onyx are OpenAI's lightest "natural conversational" voices —
+// nova reads warm/feminine, onyx reads neutral/masculine.
+const VOICE_BY_SPEAKER: Record = {
+ Sarah: 'nova',
+ Mike: 'onyx',
+};
+
+async function synthesizeOne(text: string, voice: string, apiKey: string): Promise {
+ const res = await fetch(OPENAI_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: JSON.stringify({ model: MODEL, voice, input: text, response_format: 'mp3' }),
+ });
+ if (!res.ok) {
+ const err = await res.text().catch(() => res.statusText);
+ throw new Error(`TTS failed (${res.status}): ${err.slice(0, 200)}`);
+ }
+ return Buffer.from(await res.arrayBuffer());
+}
+
+// Synthesizes each turn in parallel (capped to avoid OpenAI rate limit
+// 429s), then naive-concats the mp3 frames into a single stream.
+//
+// Why naive concat works: mp3 is a frame-oriented format with no
+// mandatory file header, so adjacent files' frames are still decodable
+// by browsers, ffmpeg, VLC, etc. It's not strictly spec-compliant
+// (there'll be a tiny gap on some decoders) but it's enough for a
+// study-tool overview where exact gapless playback isn't the bar.
+// For production-grade audio, swap this for ffmpeg.wasm.
+export async function synthesizeScript(
+ turns: AudioScriptTurn[],
+ apiKey: string,
+): Promise {
+ if (turns.length === 0) throw new Error('Empty script');
+
+ const CONCURRENCY = 4;
+ const buffers: Buffer[] = new Array(turns.length);
+
+ for (let i = 0; i < turns.length; i += CONCURRENCY) {
+ const batch = turns.slice(i, i + CONCURRENCY).map((t, j) =>
+ synthesizeOne(t.text, VOICE_BY_SPEAKER[t.speaker], apiKey).then((buf) => {
+ buffers[i + j] = buf;
+ }),
+ );
+ await Promise.all(batch);
+ }
+
+ return Buffer.concat(buffers);
+}
diff --git a/ai-pdf-chatbot/lib/chat/rail-context.tsx b/ai-pdf-chatbot/lib/chat/rail-context.tsx
new file mode 100644
index 0000000..730a379
--- /dev/null
+++ b/ai-pdf-chatbot/lib/chat/rail-context.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import { createContext, useContext, useMemo, useState, type ReactNode } from 'react';
+import type { ChatMessageRow } from '@/lib/types';
+
+type Citation = ChatMessageRow['citations'][number];
+
+type Ctx = {
+ citations: Citation[];
+ setCitations: (citations: Citation[]) => void;
+};
+
+const RailContext = createContext(null);
+
+// The chat sidebar lives in the chat layout so it survives navigation
+// between /chat and /chat/[id]. The citation rail still belongs to each
+// page (its data is tied to that page's streaming state), so the page
+// pushes its citations up through this context and the layout renders
+// the rail with them.
+export function RailProvider({ children }: { children: ReactNode }) {
+ const [citations, setCitations] = useState([]);
+ const value = useMemo(() => ({ citations, setCitations }), [citations]);
+ return {children} ;
+}
+
+export function useRailCitations(): Citation[] {
+ return useContext(RailContext)?.citations ?? [];
+}
+
+export function useSetRailCitations(): (c: Citation[]) => void {
+ const ctx = useContext(RailContext);
+ return ctx?.setCitations ?? (() => {});
+}
diff --git a/ai-pdf-chatbot/lib/hooks/use-open-citation.ts b/ai-pdf-chatbot/lib/hooks/use-open-citation.ts
index 5779e89..e975ad2 100644
--- a/ai-pdf-chatbot/lib/hooks/use-open-citation.ts
+++ b/ai-pdf-chatbot/lib/hooks/use-open-citation.ts
@@ -2,24 +2,44 @@
import { useState } from 'react';
import { toast } from 'sonner';
+import { usePdfViewer } from '@/lib/pdf/viewer-context';
-// Shared open-citation handler. Both the in-message `[n]` button
-// (components/chat-message.tsx) and the source rail's "Open" link
-// (components/citation-rail.tsx) need the same flow: fetch a
-// short-lived presigned URL for the source PDF, then open it in a new
-// tab at the cited page. Centralizing here avoids drift between the
-// two surfaces.
+type CitationLike = {
+ document_id?: string | null;
+ file_name?: string | null;
+ page_number?: number | null;
+ snippet?: string | null;
+};
+
+// Shared open-citation handler. When the surface is wrapped in
+// PdfViewerProvider (the chat shell), opens an inline PDF drawer at the
+// cited page and highlights the matching text. Falls back to a
+// presigned URL in a new tab on surfaces without a provider — the
+// public share page in particular.
export function useOpenCitation() {
const [loadingId, setLoadingId] = useState(null);
+ const viewer = usePdfViewer();
- async function open(documentId: string | null | undefined, pageNumber: number | null | undefined) {
+ async function open(citation: CitationLike) {
+ const documentId = citation.document_id;
if (!documentId) return;
+
+ if (viewer) {
+ viewer.open({
+ documentId,
+ fileName: citation.file_name ?? 'document.pdf',
+ pageNumber: citation.page_number ?? null,
+ highlightPrefix: citation.snippet ?? null,
+ });
+ return;
+ }
+
setLoadingId(documentId);
try {
const res = await fetch(`/api/documents/${documentId}/url`);
if (!res.ok) throw new Error(`status ${res.status}`);
const { url } = (await res.json()) as { url: string };
- const fragment = pageNumber ? `#page=${pageNumber}` : '';
+ const fragment = citation.page_number ? `#page=${citation.page_number}` : '';
window.open(url + fragment, '_blank', 'noopener,noreferrer');
} catch {
toast.error('Could not open source PDF');
diff --git a/ai-pdf-chatbot/lib/pdf/pdf-worker.ts b/ai-pdf-chatbot/lib/pdf/pdf-worker.ts
new file mode 100644
index 0000000..98f47de
--- /dev/null
+++ b/ai-pdf-chatbot/lib/pdf/pdf-worker.ts
@@ -0,0 +1,17 @@
+'use client';
+
+import { pdfjs } from 'react-pdf';
+
+// pdfjs ships a separate worker bundle that must be loaded outside the
+// main JS bundle. react-pdf's documented pattern is to set workerSrc at
+// module load time (an import side effect), not inside a component
+// effect callback, so the worker is ready before the first
+// render. The unpkg CDN is the lowest-friction default for a template;
+// self-host this if you want offline support or strict CSP: copy
+// `node_modules/pdfjs-dist/build/pdf.worker.min.mjs` into `public/` and
+// point workerSrc at `/pdf.worker.min.mjs` instead.
+pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
+
+// Kept as a backwards-compatible no-op so callers can still express
+// "I depend on the pdf worker being configured" via an explicit call.
+export function ensurePdfWorker(): void {}
diff --git a/ai-pdf-chatbot/lib/pdf/viewer-context.tsx b/ai-pdf-chatbot/lib/pdf/viewer-context.tsx
new file mode 100644
index 0000000..e68daf1
--- /dev/null
+++ b/ai-pdf-chatbot/lib/pdf/viewer-context.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
+
+export type PdfViewerTarget = {
+ documentId: string;
+ fileName: string;
+ pageNumber: number | null;
+ // First ~60 chars of the cited chunk. Used to locate the actual text
+ // span inside the rendered page's text layer for highlight + scroll.
+ highlightPrefix: string | null;
+};
+
+type Ctx = {
+ current: PdfViewerTarget | null;
+ open: (target: PdfViewerTarget) => void;
+ close: () => void;
+};
+
+const PdfViewerContext = createContext(null);
+
+export function PdfViewerProvider({ children }: { children: ReactNode }) {
+ const [current, setCurrent] = useState(null);
+
+ const open = useCallback((target: PdfViewerTarget) => setCurrent(target), []);
+ const close = useCallback(() => setCurrent(null), []);
+
+ const value = useMemo(() => ({ current, open, close }), [current, open, close]);
+ return {children} ;
+}
+
+// Returns null when used outside a provider so opt-in callers (the
+// share-page citation rail, for example) can fall back to the old
+// new-tab behaviour without crashing.
+export function usePdfViewer(): Ctx | null {
+ return useContext(PdfViewerContext);
+}
diff --git a/ai-pdf-chatbot/lib/rag/retrieve.ts b/ai-pdf-chatbot/lib/rag/retrieve.ts
index 40b99d9..e809e8a 100644
--- a/ai-pdf-chatbot/lib/rag/retrieve.ts
+++ b/ai-pdf-chatbot/lib/rag/retrieve.ts
@@ -14,6 +14,7 @@ export async function retrieveForQuestion(
ownerId: string,
question: string,
documentIds?: string[],
+ workspaceId?: string | null,
): Promise {
const [embedding] = await embedTexts(client, [question]);
@@ -31,6 +32,7 @@ export async function retrieveForQuestion(
match_count: MATCH_CHUNK_COUNT,
owner: ownerId,
doc_filter: documentIds && documentIds.length > 0 ? documentIds : null,
+ workspace_filter: workspaceId ?? null,
});
if (rpcRes.error) throw new Error(rpcRes.error.message ?? 'Vector search failed');
diff --git a/ai-pdf-chatbot/lib/srs/schedule.ts b/ai-pdf-chatbot/lib/srs/schedule.ts
new file mode 100644
index 0000000..3653669
--- /dev/null
+++ b/ai-pdf-chatbot/lib/srs/schedule.ts
@@ -0,0 +1,101 @@
+// SM-2 lite spaced-repetition scheduler. Three review grades plus an
+// "again" lapse, like Mochi / Quizlet's Smart Mode. Standard SM-2 uses
+// five grades (0-5) which is more accurate but cognitively heavier and
+// not the right default for a templates target audience (students who
+// have never used Anki).
+//
+// Pure function — no IO, no clock dependency outside the explicit `now`
+// argument — so the unit test in scripts/srs.test.mjs can pin time.
+
+export type Grade = 'again' | 'hard' | 'good' | 'easy';
+
+export type CardState = {
+ ease: number;
+ interval_days: number;
+ reps: number;
+};
+
+export type Scheduled = CardState & {
+ due_at: Date;
+ last_grade: number; // 0 again, 1 hard, 2 good, 3 easy
+};
+
+const GRADE_CODE: Record = {
+ again: 0,
+ hard: 1,
+ good: 2,
+ easy: 3,
+};
+
+const FIRST_GOOD_DAYS = 1;
+const FIRST_EASY_DAYS = 3;
+const AGAIN_LAPSE_MINUTES = 5;
+const MIN_EASE = 1.3;
+
+export function schedule(state: CardState, grade: Grade, now: Date = new Date()): Scheduled {
+ const code = GRADE_CODE[grade];
+
+ if (grade === 'again') {
+ return {
+ ease: Math.max(MIN_EASE, state.ease - 0.2),
+ interval_days: 0,
+ reps: state.reps + 1,
+ last_grade: code,
+ due_at: addMinutes(now, AGAIN_LAPSE_MINUTES),
+ };
+ }
+
+ if (state.reps === 0 || state.interval_days <= 0) {
+ // Brand-new card or one we just lapsed: skip the geometric step and
+ // use a flat first-interval. Avoids the "good = 6h" surprise that
+ // happens if you mechanically apply ease * 0 = 0 → next-day floor.
+ const days = grade === 'easy' ? FIRST_EASY_DAYS : FIRST_GOOD_DAYS;
+ const nextEase =
+ grade === 'easy'
+ ? state.ease + 0.15
+ : grade === 'hard'
+ ? Math.max(MIN_EASE, state.ease - 0.05)
+ : state.ease;
+ return {
+ ease: nextEase,
+ interval_days: days,
+ reps: state.reps + 1,
+ last_grade: code,
+ due_at: addDays(now, days),
+ };
+ }
+
+ // Repeat review on a mature card: standard SM-2 style geometric step.
+ let nextEase = state.ease;
+ let nextInterval = state.interval_days;
+ if (grade === 'hard') {
+ nextEase = Math.max(MIN_EASE, state.ease - 0.05);
+ nextInterval = state.interval_days * 1.2;
+ } else if (grade === 'good') {
+ nextInterval = state.interval_days * state.ease;
+ } else {
+ // easy
+ nextEase = state.ease + 0.15;
+ nextInterval = state.interval_days * state.ease * 1.3;
+ }
+
+ // Clamp the smallest meaningful step to one day so "good" twice in a
+ // row never asks the user again the same day.
+ if (nextInterval < 1) nextInterval = 1;
+
+ return {
+ ease: nextEase,
+ interval_days: nextInterval,
+ reps: state.reps + 1,
+ last_grade: code,
+ due_at: addDays(now, nextInterval),
+ };
+}
+
+function addDays(d: Date, days: number): Date {
+ return new Date(d.getTime() + days * 24 * 60 * 60 * 1000);
+}
+
+function addMinutes(d: Date, mins: number): Date {
+ return new Date(d.getTime() + mins * 60 * 1000);
+}
diff --git a/ai-pdf-chatbot/lib/stream/use-chat-stream.ts b/ai-pdf-chatbot/lib/stream/use-chat-stream.ts
index c53a5e9..5c162e6 100644
--- a/ai-pdf-chatbot/lib/stream/use-chat-stream.ts
+++ b/ai-pdf-chatbot/lib/stream/use-chat-stream.ts
@@ -23,7 +23,12 @@ export function useChatStream() {
const abortRef = useRef(null);
const send = useCallback(
- async (params: { input: string; chatId?: string; documentIds?: string[] }) => {
+ async (params: {
+ input: string;
+ chatId?: string;
+ documentIds?: string[];
+ workspaceId?: string | null;
+ }) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
@@ -62,10 +67,24 @@ export function useChatStream() {
return null;
} else if (event.type === 'done') {
setState({ phase: 'idle' });
+ // Tell the sidebar to refresh ONLY when a brand new chat was
+ // created. Continuing a conversation already on the list
+ // intentionally does NOT trigger a refetch even though the
+ // chat would re-sort to the top by last_message_at. Re-sorting
+ // on every assistant turn is the visual jitter the
+ // layout-hoisted sidebar (chat/layout.tsx) was introduced to
+ // fix. The trade-off is mild list staleness for a still UI,
+ // recoverable on next mount.
+ if (!params.chatId && resolvedChatId && typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('chats:changed'));
+ }
return { chatId: resolvedChatId, content: text, citations };
}
}
setState({ phase: 'idle' });
+ if (!params.chatId && resolvedChatId && typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('chats:changed'));
+ }
return { chatId: resolvedChatId, content: text, citations };
} catch (err) {
if (controller.signal.aborted) {
diff --git a/ai-pdf-chatbot/lib/types.ts b/ai-pdf-chatbot/lib/types.ts
index 7debe5d..12b0cba 100644
--- a/ai-pdf-chatbot/lib/types.ts
+++ b/ai-pdf-chatbot/lib/types.ts
@@ -5,9 +5,39 @@ export type AuthViewer = {
name: string | null;
};
+export type WorkspaceRow = {
+ id: string;
+ user_id: string;
+ name: string;
+ description: string | null;
+ mindmap_markdown: string | null;
+ mindmap_generated_at: string | null;
+ audio_url: string | null;
+ audio_script: AudioScriptTurn[] | null;
+ audio_generated_at: string | null;
+ created_at: string;
+ updated_at: string;
+};
+
+export type WorkspaceSummary = {
+ id: string;
+ name: string;
+ description: string | null;
+ updated_at: string;
+ document_count: number;
+ chat_count: number;
+ due_flashcard_count: number;
+};
+
+export type AudioScriptTurn = {
+ speaker: 'Sarah' | 'Mike';
+ text: string;
+};
+
export type DocumentRow = {
id: string;
user_id: string;
+ workspace_id: string | null;
file_name: string;
file_size: number;
mime_type: string;
@@ -23,6 +53,7 @@ export type DocumentRow = {
export type ChatSessionRow = {
id: string;
user_id: string;
+ workspace_id: string | null;
title: string;
document_ids: string[];
created_at: string;
diff --git a/ai-pdf-chatbot/migrations/db_init.sql b/ai-pdf-chatbot/migrations/db_init.sql
index 19a9ddf..7754ef4 100644
--- a/ai-pdf-chatbot/migrations/db_init.sql
+++ b/ai-pdf-chatbot/migrations/db_init.sql
@@ -6,6 +6,11 @@
-- After this lands, `npm run auth:migrate` populates better_auth.{user,
-- session,account,verification} via Better Auth's own Kysely migrations.
+-- ivfflat index creation on document_chunks.embedding needs more than the
+-- 16MB default maintenance_work_mem on InsForge cloud. Bump it for this
+-- session so `CREATE INDEX ... ivfflat` doesn't fail with "memory required".
+set maintenance_work_mem = '128MB';
+
create extension if not exists pgcrypto;
create extension if not exists vector;
@@ -27,10 +32,34 @@ $$;
-- user_id columns are `text` because Better Auth issues string IDs (the
-- same `sub` claim the bridge JWT carries).
+-- NotebookLM-style container that groups PDFs + chats + mindmap + flashcards
+-- + audio overview together. All downstream tables (documents, chat_sessions,
+-- document_flashcards) carry a nullable workspace_id so legacy rows still
+-- live under "Unsorted" until the user organizes them.
+create table if not exists public.workspaces (
+ id uuid primary key default gen_random_uuid(),
+ user_id text not null,
+ name text not null,
+ description text,
+ -- Cached LLM artifacts so the mindmap/audio surfaces don't re-bill on
+ -- every visit. Cleared by the "Regenerate" actions on each tab.
+ mindmap_markdown text,
+ mindmap_generated_at timestamptz,
+ audio_url text,
+ audio_script jsonb,
+ audio_generated_at timestamptz,
+ created_at timestamptz not null default now(),
+ updated_at timestamptz not null default now()
+);
+
+create index if not exists workspaces_user_idx
+ on public.workspaces (user_id, updated_at desc);
+
-- A single uploaded PDF
create table if not exists public.documents (
id uuid primary key default gen_random_uuid(),
user_id text not null,
+ workspace_id uuid references public.workspaces(id) on delete set null,
file_name text not null,
file_size integer not null,
mime_type text not null,
@@ -47,13 +76,17 @@ create table if not exists public.documents (
);
-- Idempotent column adds for projects whose `documents` table predates
--- the summary / suggested_questions fields.
+-- the summary / suggested_questions / workspace_id fields.
alter table public.documents add column if not exists summary text;
alter table public.documents add column if not exists suggested_questions jsonb not null default '[]'::jsonb;
+alter table public.documents add column if not exists workspace_id uuid references public.workspaces(id) on delete set null;
create index if not exists documents_user_idx
on public.documents (user_id, created_at desc);
+create index if not exists documents_workspace_idx
+ on public.documents (workspace_id) where workspace_id is not null;
+
-- A chunk of a PDF + its embedding
create table if not exists public.document_chunks (
id uuid primary key default gen_random_uuid(),
@@ -80,6 +113,7 @@ create index if not exists document_chunks_user_idx
create table if not exists public.chat_sessions (
id uuid primary key default gen_random_uuid(),
user_id text not null,
+ workspace_id uuid references public.workspaces(id) on delete set null,
title text not null default 'New chat',
document_ids uuid[] not null default '{}',
share_token text unique,
@@ -89,12 +123,16 @@ create table if not exists public.chat_sessions (
);
alter table public.chat_sessions add column if not exists share_token text;
+alter table public.chat_sessions add column if not exists workspace_id uuid references public.workspaces(id) on delete set null;
create unique index if not exists chat_sessions_share_token_idx
on public.chat_sessions (share_token) where share_token is not null;
create index if not exists chat_sessions_user_idx
on public.chat_sessions (user_id, last_message_at desc);
+create index if not exists chat_sessions_workspace_idx
+ on public.chat_sessions (workspace_id) where workspace_id is not null;
+
-- Chat message
create table if not exists public.chat_messages (
id uuid primary key default gen_random_uuid(),
@@ -112,21 +150,39 @@ create index if not exists chat_messages_chat_sort_idx
-- Flashcards generated from a document. Lives in its own table so the
-- documents row stays cheap to fetch in lists and the cards can be
--- regenerated/extended independently.
+-- regenerated/extended independently. Carries SM-2 lite SRS state so a
+-- workspace-wide review queue can pull due cards across all documents.
create table if not exists public.document_flashcards (
id uuid primary key default gen_random_uuid(),
document_id uuid not null references public.documents(id) on delete cascade,
+ workspace_id uuid references public.workspaces(id) on delete set null,
user_id text not null,
question text not null,
answer text not null,
sort_order integer not null,
+ due_at timestamptz not null default now(),
+ last_grade smallint,
+ ease real not null default 2.5,
+ interval_days real not null default 0,
+ reps integer not null default 0,
created_at timestamptz not null default now(),
unique (document_id, sort_order)
);
+-- Idempotent adds for projects whose flashcards table predates SRS state.
+alter table public.document_flashcards add column if not exists workspace_id uuid references public.workspaces(id) on delete set null;
+alter table public.document_flashcards add column if not exists due_at timestamptz not null default now();
+alter table public.document_flashcards add column if not exists last_grade smallint;
+alter table public.document_flashcards add column if not exists ease real not null default 2.5;
+alter table public.document_flashcards add column if not exists interval_days real not null default 0;
+alter table public.document_flashcards add column if not exists reps integer not null default 0;
+
create index if not exists document_flashcards_doc_idx
on public.document_flashcards (document_id, sort_order asc);
+create index if not exists document_flashcards_due_idx
+ on public.document_flashcards (user_id, due_at);
+
-- Bump chat_sessions.last_message_at on each new message
create or replace function public.touch_chat_session()
returns trigger language plpgsql as $$
@@ -146,11 +202,14 @@ for each row execute function public.touch_chat_session();
-- RAG retrieval (RLS-aware via explicit owner argument — text now, to
-- match the user_id column type and the BA-bridged JWT sub claim).
+-- workspace_filter scopes retrieval to a single workspace's documents
+-- when set; doc_filter further narrows within that workspace.
create or replace function public.match_document_chunks(
query_embedding vector(1536),
match_count integer,
owner text,
- doc_filter uuid[] default null
+ doc_filter uuid[] default null,
+ workspace_filter uuid default null
)
returns table (
chunk_id uuid,
@@ -167,19 +226,28 @@ language sql stable as $$
dc.page_number,
1 - (dc.embedding <=> query_embedding) as similarity
from public.document_chunks dc
+ join public.documents d on d.id = dc.document_id
where dc.user_id = owner
and (doc_filter is null or dc.document_id = any(doc_filter))
+ and (workspace_filter is null or d.workspace_id = workspace_filter)
order by dc.embedding <=> query_embedding
limit match_count;
$$;
-- RLS — policies read the BA `sub` claim through requesting_user_id().
+alter table public.workspaces enable row level security;
alter table public.documents enable row level security;
alter table public.document_chunks enable row level security;
alter table public.chat_sessions enable row level security;
alter table public.chat_messages enable row level security;
alter table public.document_flashcards enable row level security;
+drop policy if exists workspaces_owner on public.workspaces;
+create policy workspaces_owner on public.workspaces
+ for all
+ using (user_id = public.requesting_user_id())
+ with check (user_id = public.requesting_user_id());
+
drop policy if exists documents_owner on public.documents;
create policy documents_owner on public.documents
for all
@@ -284,3 +352,27 @@ drop policy if exists pdf_documents_owner_delete on storage.objects;
create policy pdf_documents_owner_delete on storage.objects
for delete to authenticated
using (bucket = 'pdf-documents' and uploaded_by = public.requesting_user_id());
+
+-- audio-overviews bucket — generated podcast-style summaries per workspace.
+-- Public read so the tag works without a signed URL; owner-only
+-- writes/deletes so users can't poison each other's audio.
+drop policy if exists audio_overviews_public_read on storage.objects;
+create policy audio_overviews_public_read on storage.objects
+ for select to anon, authenticated
+ using (bucket = 'audio-overviews');
+
+drop policy if exists audio_overviews_owner_insert on storage.objects;
+create policy audio_overviews_owner_insert on storage.objects
+ for insert to authenticated
+ with check (bucket = 'audio-overviews' and uploaded_by = public.requesting_user_id());
+
+drop policy if exists audio_overviews_owner_delete on storage.objects;
+create policy audio_overviews_owner_delete on storage.objects
+ for delete to authenticated
+ using (bucket = 'audio-overviews' and uploaded_by = public.requesting_user_id());
+
+-- PostgREST caches the table schema on startup and does NOT auto-refresh
+-- after a freshly-imported migration. Without this notify, the SDK hits
+-- 404 ("Cannot POST /api/database/") until the gateway is
+-- restarted. Sending NOTIFY pgrst makes the gateway reload immediately.
+notify pgrst, 'reload schema';
diff --git a/ai-pdf-chatbot/package-lock.json b/ai-pdf-chatbot/package-lock.json
index d3becc4..18bb20e 100644
--- a/ai-pdf-chatbot/package-lock.json
+++ b/ai-pdf-chatbot/package-lock.json
@@ -15,17 +15,20 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.446.0",
+ "markmap-lib": "^0.18.12",
+ "markmap-view": "^0.18.12",
"next": "16.2.0",
"next-themes": "^0.4.6",
+ "pdfjs-dist": "^5.7.284",
"pg": "^8.13.0",
"react": "19.2.0",
"react-dom": "19.2.0",
+ "react-pdf": "^10.4.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"unpdf": "^1.6.2"
},
"devDependencies": {
- "@better-auth/cli": "^1.4.21",
"@tailwindcss/postcss": "^4.1.13",
"@types/jsonwebtoken": "^9.0.8",
"@types/node": "24.6.2",
@@ -51,952 +54,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@babel/code-frame": {
+ "node_modules/@babel/runtime": {
"version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.29.7",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
- "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz",
- "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
- "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.29.7",
- "@babel/helper-member-expression-to-functions": "^7.29.7",
- "@babel/helper-optimise-call-expression": "^7.29.7",
- "@babel/helper-replace-supers": "^7.29.7",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-globals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz",
- "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-optimise-call-expression": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
- "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-replace-supers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
- "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-member-expression-to-functions": "^7.29.7",
- "@babel/helper-optimise-call-expression": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
- "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.7"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
- "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
- "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
- "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-display-name": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz",
- "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz",
- "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.29.7",
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-plugin-utils": "^7.29.7",
- "@babel/plugin-syntax-jsx": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-development": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz",
- "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-transform-react-jsx": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-pure-annotations": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz",
- "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.29.7",
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-typescript": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
- "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.29.7",
- "@babel/helper-create-class-features-plugin": "^7.29.7",
- "@babel/helper-plugin-utils": "^7.29.7",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
- "@babel/plugin-syntax-typescript": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-react": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz",
- "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "@babel/plugin-transform-react-display-name": "^7.29.7",
- "@babel/plugin-transform-react-jsx": "^7.29.7",
- "@babel/plugin-transform-react-jsx-development": "^7.29.7",
- "@babel/plugin-transform-react-pure-annotations": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-typescript": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz",
- "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "@babel/plugin-syntax-jsx": "^7.29.7",
- "@babel/plugin-transform-modules-commonjs": "^7.29.7",
- "@babel/plugin-transform-typescript": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
- "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@better-auth/cli": {
- "version": "1.4.21",
- "resolved": "https://registry.npmjs.org/@better-auth/cli/-/cli-1.4.21.tgz",
- "integrity": "sha512-bKEa8BupnZxNjLk9ZDntvgQGm5jogeE2wHdMbYifhet3GTyxgDi6pXoOK8+aqHYQGg1C3OALi9hVVWnrv7JJWQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.28.4",
- "@babel/preset-react": "^7.27.1",
- "@babel/preset-typescript": "^7.27.1",
- "@better-auth/core": "1.4.21",
- "@better-auth/telemetry": "1.4.21",
- "@better-auth/utils": "0.3.0",
- "@clack/prompts": "^0.11.0",
- "@mrleebo/prisma-ast": "^0.13.0",
- "@prisma/client": "^5.22.0",
- "@types/pg": "^8.15.5",
- "better-auth": "1.4.21",
- "better-sqlite3": "^12.2.0",
- "c12": "^3.2.0",
- "chalk": "^5.6.2",
- "commander": "^12.1.0",
- "dotenv": "^17.2.2",
- "drizzle-orm": "^0.41.0",
- "open": "^10.2.0",
- "pg": "^8.16.3",
- "prettier": "^3.6.2",
- "prompts": "^2.4.2",
- "semver": "^7.7.2",
- "yocto-spinner": "^0.2.3",
- "zod": "^4.3.5"
- },
- "bin": {
- "better-auth": "dist/index.mjs"
- }
- },
- "node_modules/@better-auth/cli/node_modules/@better-auth/core": {
- "version": "1.4.21",
- "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.21.tgz",
- "integrity": "sha512-R4s7pwShkqB21fZ599QASbXxqFcoxanLyz7DHSX6SJPNYV748wBLsm3xM9VrjfvWMpS+cQUErOCt9yWT1hMn6w==",
- "dev": true,
- "dependencies": {
- "@standard-schema/spec": "^1.0.0",
- "zod": "^4.3.5"
- },
- "peerDependencies": {
- "@better-auth/utils": "0.3.0",
- "@better-fetch/fetch": "1.1.21",
- "better-call": "1.1.8",
- "jose": "^6.1.0",
- "kysely": "^0.28.5",
- "nanostores": "^1.0.1"
- }
- },
- "node_modules/@better-auth/cli/node_modules/@better-auth/telemetry": {
- "version": "1.4.21",
- "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.4.21.tgz",
- "integrity": "sha512-LX+FGMZnhR2KQZ0idHH1+UwlXvkOl6P8w3Gne4TtjvUCt3QjG9FKIuP9JD3MAmEEkwGt0SoAPHPJEGTjUl3ydg==",
- "dev": true,
- "dependencies": {
- "@better-auth/utils": "0.3.0",
- "@better-fetch/fetch": "1.1.21"
- },
- "peerDependencies": {
- "@better-auth/core": "1.4.21"
- }
- },
- "node_modules/@better-auth/cli/node_modules/better-auth": {
- "version": "1.4.21",
- "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.21.tgz",
- "integrity": "sha512-qdrIZS7xnGF2HPBV5wYNPWTkPojhauOOjz1+MhLvwFy+zXpgLofQmWsI5I9DY+ef845NKt93XcgpyAc4RPPT9A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@better-auth/core": "1.4.21",
- "@better-auth/telemetry": "1.4.21",
- "@better-auth/utils": "0.3.0",
- "@better-fetch/fetch": "1.1.21",
- "@noble/ciphers": "^2.0.0",
- "@noble/hashes": "^2.0.0",
- "better-call": "1.1.8",
- "defu": "^6.1.4",
- "jose": "^6.1.0",
- "kysely": "^0.28.5",
- "nanostores": "^1.0.1",
- "zod": "^4.3.5"
- },
- "peerDependencies": {
- "@lynx-js/react": "*",
- "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
- "@sveltejs/kit": "^2.0.0",
- "@tanstack/react-start": "^1.0.0",
- "@tanstack/solid-start": "^1.0.0",
- "better-sqlite3": "^12.0.0",
- "drizzle-kit": ">=0.31.4",
- "drizzle-orm": ">=0.41.0",
- "mongodb": "^6.0.0 || ^7.0.0",
- "mysql2": "^3.0.0",
- "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
- "pg": "^8.0.0",
- "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0",
- "solid-js": "^1.0.0",
- "svelte": "^4.0.0 || ^5.0.0",
- "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
- "vue": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "@lynx-js/react": {
- "optional": true
- },
- "@prisma/client": {
- "optional": true
- },
- "@sveltejs/kit": {
- "optional": true
- },
- "@tanstack/react-start": {
- "optional": true
- },
- "@tanstack/solid-start": {
- "optional": true
- },
- "better-sqlite3": {
- "optional": true
- },
- "drizzle-kit": {
- "optional": true
- },
- "drizzle-orm": {
- "optional": true
- },
- "mongodb": {
- "optional": true
- },
- "mysql2": {
- "optional": true
- },
- "next": {
- "optional": true
- },
- "pg": {
- "optional": true
- },
- "prisma": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "solid-js": {
- "optional": true
- },
- "svelte": {
- "optional": true
- },
- "vitest": {
- "optional": true
- },
- "vue": {
- "optional": true
- }
- }
- },
- "node_modules/@better-auth/cli/node_modules/better-call": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.8.tgz",
- "integrity": "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@better-auth/utils": "^0.3.0",
- "@better-fetch/fetch": "^1.1.4",
- "rou3": "^0.7.10",
- "set-cookie-parser": "^2.7.1"
- },
- "peerDependencies": {
- "zod": "^4.0.0"
- },
- "peerDependenciesMeta": {
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/@better-auth/cli/node_modules/drizzle-orm": {
- "version": "0.41.0",
- "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.41.0.tgz",
- "integrity": "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==",
- "dev": true,
- "license": "Apache-2.0",
- "peerDependencies": {
- "@aws-sdk/client-rds-data": ">=3",
- "@cloudflare/workers-types": ">=4",
- "@electric-sql/pglite": ">=0.2.0",
- "@libsql/client": ">=0.10.0",
- "@libsql/client-wasm": ">=0.10.0",
- "@neondatabase/serverless": ">=0.10.0",
- "@op-engineering/op-sqlite": ">=2",
- "@opentelemetry/api": "^1.4.1",
- "@planetscale/database": ">=1",
- "@prisma/client": "*",
- "@tidbcloud/serverless": "*",
- "@types/better-sqlite3": "*",
- "@types/pg": "*",
- "@types/sql.js": "*",
- "@vercel/postgres": ">=0.8.0",
- "@xata.io/client": "*",
- "better-sqlite3": ">=7",
- "bun-types": "*",
- "expo-sqlite": ">=14.0.0",
- "gel": ">=2",
- "knex": "*",
- "kysely": "*",
- "mysql2": ">=2",
- "pg": ">=8",
- "postgres": ">=3",
- "sql.js": ">=1",
- "sqlite3": ">=5"
- },
- "peerDependenciesMeta": {
- "@aws-sdk/client-rds-data": {
- "optional": true
- },
- "@cloudflare/workers-types": {
- "optional": true
- },
- "@electric-sql/pglite": {
- "optional": true
- },
- "@libsql/client": {
- "optional": true
- },
- "@libsql/client-wasm": {
- "optional": true
- },
- "@neondatabase/serverless": {
- "optional": true
- },
- "@op-engineering/op-sqlite": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@planetscale/database": {
- "optional": true
- },
- "@prisma/client": {
- "optional": true
- },
- "@tidbcloud/serverless": {
- "optional": true
- },
- "@types/better-sqlite3": {
- "optional": true
- },
- "@types/pg": {
- "optional": true
- },
- "@types/sql.js": {
- "optional": true
- },
- "@vercel/postgres": {
- "optional": true
- },
- "@xata.io/client": {
- "optional": true
- },
- "better-sqlite3": {
- "optional": true
- },
- "bun-types": {
- "optional": true
- },
- "expo-sqlite": {
- "optional": true
- },
- "gel": {
- "optional": true
- },
- "knex": {
- "optional": true
- },
- "kysely": {
- "optional": true
- },
- "mysql2": {
- "optional": true
- },
- "pg": {
- "optional": true
- },
- "postgres": {
- "optional": true
- },
- "prisma": {
- "optional": true
- },
- "sql.js": {
- "optional": true
- },
- "sqlite3": {
- "optional": true
- }
- }
- },
- "node_modules/@better-auth/cli/node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "node_modules/@better-auth/utils": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz",
- "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@better-fetch/fetch": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
},
- "node_modules/@chevrotain/cst-dts-gen": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
- "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@chevrotain/gast": "10.5.0",
- "@chevrotain/types": "10.5.0",
- "lodash": "4.17.21"
- }
- },
- "node_modules/@chevrotain/gast": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz",
- "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@chevrotain/types": "10.5.0",
- "lodash": "4.17.21"
- }
- },
- "node_modules/@chevrotain/types": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz",
- "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@chevrotain/utils": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz",
- "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@clack/core": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
- "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picocolors": "^1.0.0",
- "sisteransi": "^1.0.5"
- }
- },
- "node_modules/@clack/prompts": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
- "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@clack/core": "0.5.0",
- "picocolors": "^1.0.0",
- "sisteransi": "^1.0.5"
- }
- },
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
@@ -1449,6 +520,16 @@
"node": ">=18"
}
},
+ "node_modules/@gera2ld/jsx-dom": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@gera2ld/jsx-dom/-/jsx-dom-2.2.2.tgz",
+ "integrity": "sha512-EOqf31IATRE6zS1W1EoWmXZhGfLAoO9FIlwTtHduSrBdud4npYBxYAkv8dZ5hudDPwJeeSjn40kbCL4wAzr8dA==",
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.21.5"
+ }
+ },
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
@@ -2025,29 +1106,280 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/canvas": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
+ "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
+ "license": "MIT",
+ "optional": true,
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-x64": "0.1.100",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.100",
+ "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
+ "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
+ "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
+ "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
+ "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
+ "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
+ "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
+ "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
+ "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
+ "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
+ "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
+ "cpu": [
+ "arm64"
+ ],
"license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
}
},
- "node_modules/@mrleebo/prisma-ast": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz",
- "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==",
- "dev": true,
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
+ "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
+ "cpu": [
+ "x64"
+ ],
"license": "MIT",
- "dependencies": {
- "chevrotain": "^10.5.0",
- "lilconfig": "^2.1.0"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">=16"
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@next/env": {
@@ -2229,25 +1561,6 @@
"node": ">=14"
}
},
- "node_modules/@prisma/client": {
- "version": "5.22.0",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
- "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
- "devOptional": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=16.13"
- },
- "peerDependencies": {
- "prisma": "*"
- },
- "peerDependenciesMeta": {
- "prisma": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -2666,26 +1979,20 @@
"@types/react": "^19.2.0"
}
},
- "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==",
- "devOptional": true,
- "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/@vscode/markdown-it-katex": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.2.tgz",
+ "integrity": "sha512-+4IIv5PgrmhKvW/3LpkpkGg257OViEhXkOOgCyj5KMsjsOfnRXkni8XAuuF9Ui5p3B8WnUovlDXAQNb8RJ/RaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "katex": "^0.16.4"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.32",
@@ -2966,48 +2273,22 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
- "node_modules/better-sqlite3": {
- "version": "12.10.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
- "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
- "devOptional": true,
- "hasInstallScript": true,
- "license": "MIT",
- "dependencies": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
- },
- "engines": {
- "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
- }
- },
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "file-uri-to-path": "1.0.0"
- }
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
},
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
+ "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/browserslist": {
- "version": "4.28.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
- "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
- "dev": true,
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
"funding": [
{
"type": "opencollective",
@@ -3015,317 +2296,553 @@
},
{
"type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/cheerio": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
+ "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.12",
- "caniuse-lite": "^1.0.30001782",
- "electron-to-chromium": "^1.5.328",
- "node-releases": "^2.0.36",
- "update-browserslist-db": "^1.2.3"
- },
- "bin": {
- "browserslist": "cli.js"
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.1.0",
+ "encoding-sniffer": "^0.2.0",
+ "htmlparser2": "^9.1.0",
+ "parse5": "^7.1.2",
+ "parse5-htmlparser2-tree-adapter": "^7.0.0",
+ "parse5-parser-stream": "^7.1.2",
+ "undici": "^6.19.5",
+ "whatwg-mimetype": "^4.0.0"
},
"engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ "node": ">=18.17"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "devOptional": true,
- "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/cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
"dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
}
},
- "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/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
},
- "node_modules/bundle-name": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
- "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
- "dev": true,
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "license": "BSD-2-Clause",
"dependencies": {
- "run-applescript": "^7.0.0"
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
},
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=18"
+ "node": ">= 6"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/fb55"
}
},
- "node_modules/c12": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
- "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
- "dev": true,
- "license": "MIT",
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
"dependencies": {
- "chokidar": "^5.0.0",
- "confbox": "^0.2.4",
- "defu": "^6.1.6",
- "dotenv": "^17.3.1",
- "exsolve": "^1.0.8",
- "giget": "^3.2.0",
- "jiti": "^2.6.1",
- "ohash": "^2.0.11",
- "pathe": "^2.0.3",
- "perfect-debounce": "^2.1.0",
- "pkg-types": "^2.3.0",
- "rc9": "^3.0.1"
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
},
- "peerDependencies": {
- "magicast": "*"
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
},
- "peerDependenciesMeta": {
- "magicast": {
- "optional": true
- }
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/caniuse-lite": {
- "version": "1.0.30001793",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
- "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "dev": true,
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"license": "MIT",
"engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "node": ">= 10"
}
},
- "node_modules/chevrotain": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz",
- "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@chevrotain/cst-dts-gen": "10.5.0",
- "@chevrotain/gast": "10.5.0",
- "@chevrotain/types": "10.5.0",
- "@chevrotain/utils": "10.5.0",
- "lodash": "4.17.21",
- "regexp-to-ast": "0.5.0"
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/chokidar": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
- "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
"dependencies": {
- "readdirp": "^5.0.0"
+ "d3-dsv": "1 - 3"
},
"engines": {
- "node": ">= 20.19.0"
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
},
- "funding": {
- "url": "https://paulmillr.com/funding/"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "devOptional": true,
- "license": "ISC"
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/class-variance-authority": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
- "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
- "license": "Apache-2.0",
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
"dependencies": {
- "clsx": "^2.1.1"
+ "d3-array": "2.5.0 - 3"
},
- "funding": {
- "url": "https://polar.sh/cva"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/client-only": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
- "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
- "license": "MIT"
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "license": "MIT",
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
"engines": {
- "node": ">=6"
+ "node": ">=12"
}
},
- "node_modules/commander": {
- "version": "12.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
- "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
- "node_modules/confbox": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
- "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
- "dev": true,
- "license": "MIT"
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
},
- "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==",
- "devOptional": true,
- "license": "MIT"
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "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",
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
"dependencies": {
- "ms": "^2.1.3"
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
},
"engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "node": ">=12"
}
},
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "devOptional": true,
- "license": "MIT",
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
"dependencies": {
- "mimic-response": "^3.1.0"
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
},
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=12"
}
},
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "devOptional": true,
- "license": "MIT",
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
"engines": {
- "node": ">=4.0.0"
+ "node": ">=12"
}
},
- "node_modules/default-browser": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
- "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
"dependencies": {
- "bundle-name": "^4.1.0",
- "default-browser-id": "^5.0.0"
+ "d3-path": "^3.1.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/default-browser-id": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
- "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
}
},
- "node_modules/define-lazy-prop": {
+ "node_modules/d3-zoom": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
- "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
- "dev": true,
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
"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"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
"node_modules/defu": {
@@ -3334,12 +2851,23 @@
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
- "node_modules/destr": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
- "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
- "dev": true,
- "license": "MIT"
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
},
"node_modules/detect-libc": {
"version": "2.1.2",
@@ -3351,17 +2879,59 @@
"node": ">=8"
}
},
- "node_modules/dotenv": {
- "version": "17.4.2",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
- "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
- "dev": true,
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
},
"funding": {
- "url": "https://dotenvx.com"
+ "url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/ecdsa-sig-formatter": {
@@ -3373,21 +2943,17 @@
"safe-buffer": "^5.0.1"
}
},
- "node_modules/electron-to-chromium": {
- "version": "1.5.361",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
- "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/end-of-stream": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
- "devOptional": true,
+ "node_modules/encoding-sniffer": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
+ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
"license": "MIT",
"dependencies": {
- "once": "^1.4.0"
+ "iconv-lite": "^0.6.3",
+ "whatwg-encoding": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/engine.io-client": {
@@ -3426,6 +2992,18 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
@@ -3468,47 +3046,6 @@
"@esbuild/win32-x64": "0.28.0"
}
},
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/expand-template": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "devOptional": true,
- "license": "(MIT OR WTFPL)",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/exsolve": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
- "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/fs-constants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3524,33 +3061,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/giget": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
- "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "giget": "dist/cli.mjs"
- }
- },
- "node_modules/github-from-package": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -3558,90 +3068,53 @@
"dev": true,
"license": "ISC"
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "devOptional": true,
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
+ "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
+ "url": "https://github.com/sponsors/fb55"
}
],
- "license": "BSD-3-Clause"
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/is-docker": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
- "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
- "dev": true,
"license": "MIT",
- "bin": {
- "is-docker": "cli.js"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.1.0",
+ "entities": "^4.5.0"
}
},
- "node_modules/is-inside-container": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
- "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
- "dev": true,
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
- "is-docker": "^3.0.0"
- },
- "bin": {
- "is-inside-container": "cli.js"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=0.10.0"
}
},
- "node_modules/is-wsl": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
- "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-inside-container": "^1.0.0"
- },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
"engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=12"
}
},
"node_modules/jiti": {
@@ -3667,35 +3140,8 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
@@ -3739,14 +3185,20 @@
"safe-buffer": "^5.0.1"
}
},
- "node_modules/kleur": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
- "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
- "dev": true,
+ "node_modules/katex": {
+ "version": "0.16.47",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
+ "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
"license": "MIT",
- "engines": {
- "node": ">=6"
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
}
},
"node_modules/kysely": {
@@ -4031,23 +3483,25 @@
"url": "https://opencollective.com/parcel"
}
},
- "node_modules/lilconfig": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
- "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
- "dev": true,
+ "node_modules/linkify-it": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
+ "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">=10"
+ "dependencies": {
+ "uc.micro": "^2.0.0"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -4090,14 +3544,16 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
"dependencies": {
- "yallist": "^3.0.2"
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
}
},
"node_modules/lucide-react": {
@@ -4119,36 +3575,160 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
- "node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "devOptional": true,
+ "node_modules/make-cancellable-promise": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
+ "integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
"license": "MIT",
- "engines": {
- "node": ">=10"
- },
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
}
},
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "devOptional": true,
+ "node_modules/make-event-props": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
+ "integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
"license": "MIT",
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
}
},
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "devOptional": true,
+ "node_modules/markdown-it": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.1",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it-ins": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-4.0.0.tgz",
+ "integrity": "sha512-sWbjK2DprrkINE4oYDhHdCijGT+MIDhEupjSHLXe5UXeVr5qmVxs/nTUVtgi0Oh/qtF+QKV0tNWDhQBEPxiMew==",
+ "license": "MIT"
+ },
+ "node_modules/markdown-it-mark": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-4.0.0.tgz",
+ "integrity": "sha512-YLhzaOsU9THO/cal0lUjfMjrqSMPjjyjChYM7oyj4DnyaXEzA8gnW6cVJeyCrCVeyesrY2PlEdUYJSPFYL4Nkg==",
+ "license": "MIT"
+ },
+ "node_modules/markdown-it-sub": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-2.0.0.tgz",
+ "integrity": "sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA==",
+ "license": "MIT"
+ },
+ "node_modules/markdown-it-sup": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-2.0.0.tgz",
+ "integrity": "sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA==",
+ "license": "MIT"
+ },
+ "node_modules/markmap-common": {
+ "version": "0.18.9",
+ "resolved": "https://registry.npmjs.org/markmap-common/-/markmap-common-0.18.9.tgz",
+ "integrity": "sha512-MV2HQO7IGIm3jWEJXSG8vmdpqf4WIDXcEyAEN52lrWR1qD53Zg5l81JwjXoZ2l0rY5mofKYqUFlmdM2fqTGMVg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@gera2ld/jsx-dom": "^2.2.2",
+ "npm2url": "^0.2.4"
+ }
+ },
+ "node_modules/markmap-html-parser": {
+ "version": "0.18.11",
+ "resolved": "https://registry.npmjs.org/markmap-html-parser/-/markmap-html-parser-0.18.11.tgz",
+ "integrity": "sha512-+kC5C4sCGntGUhGvTa5VIb5rtM75cSy/VCy3tzZoNAcn2qZGdgYvljN0WvjsOzrEzp+V6XKgwzO0u2TdzNAiOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "cheerio": "1.0.0"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/markmap-lib": {
+ "version": "0.18.12",
+ "resolved": "https://registry.npmjs.org/markmap-lib/-/markmap-lib-0.18.12.tgz",
+ "integrity": "sha512-WCA4OT+b71jYg0e4PS/6NRKqihod5OpPsvw1jEGHQwCtqQrY/yXXCeRyuL3axOS5cMy5pV8BSl4CwKfJU1LxJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@vscode/markdown-it-katex": "^1.1.0",
+ "highlight.js": "^11.8.0",
+ "katex": "^0.16.8",
+ "markdown-it": "^14.1.0",
+ "markdown-it-ins": "^4.0.0",
+ "markdown-it-mark": "^4.0.0",
+ "markdown-it-sub": "^2.0.0",
+ "markdown-it-sup": "^2.0.0",
+ "markmap-html-parser": "0.18.11",
+ "markmap-view": "0.18.12",
+ "prismjs": "^1.29.0",
+ "yaml": "^2.5.1"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/markmap-view": {
+ "version": "0.18.12",
+ "resolved": "https://registry.npmjs.org/markmap-view/-/markmap-view-0.18.12.tgz",
+ "integrity": "sha512-D8bzT1YwIC/8rkbwm6WzigVUrpOAGv7ioEGTi1Lj+Oo8gO5sAm6hhli27jvTgUcZ9TwBeIWZ+dSUP+AupYUGlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "d3": "^7.8.5"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
+ "node_modules/merge-refs": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
+ "integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4188,13 +3768,6 @@
"node": "^20.0.0 || >=22.0.0"
}
},
- "node_modules/napi-build-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/next": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz",
@@ -4286,78 +3859,85 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/node-abi": {
- "version": "3.92.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz",
- "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==",
- "devOptional": true,
+ "node_modules/npm2url": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/npm2url/-/npm2url-0.2.4.tgz",
+ "integrity": "sha512-arzGp/hQz0Ey+ZGhF64XVH7Xqwd+1Q/po5uGiBbzph8ebX6T0uvt3N7c1nBHQNsQVykQgHhqoRTX7JFcHecGuw==",
"license": "MIT",
+ "peer": true
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
"dependencies": {
- "semver": "^7.3.5"
+ "boolbase": "^1.0.0"
},
- "engines": {
- "node": ">=10"
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
- "node_modules/node-releases": {
- "version": "2.0.46",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
- "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
- "dev": true,
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/ohash": {
- "version": "2.0.11",
- "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
- "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "devOptional": true,
- "license": "ISC",
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+ "license": "MIT",
"dependencies": {
- "wrappy": "1"
+ "domhandler": "^5.0.3",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/open": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
- "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
- "dev": true,
+ "node_modules/parse5-parser-stream": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
- "default-browser": "^5.2.1",
- "define-lazy-prop": "^3.0.0",
- "is-inside-container": "^1.0.0",
- "wsl-utils": "^0.1.0"
+ "parse5": "^7.0.0"
},
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=18"
+ "node": ">=0.12"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/perfect-debounce": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
- "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
- "dev": true,
- "license": "MIT"
+ "node_modules/pdfjs-dist": {
+ "version": "5.7.284",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz",
+ "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22.13.0 || >=24"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.100"
+ }
},
"node_modules/pg": {
"version": "8.21.0",
@@ -4454,18 +4034,6 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
- "node_modules/pkg-types": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
- "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "confbox": "^0.2.4",
- "exsolve": "^1.0.8",
- "pathe": "^2.0.3"
- }
- },
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
@@ -4534,100 +4102,22 @@
"node": ">=0.10.0"
}
},
- "node_modules/prebuild-install": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^2.0.0",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- },
- "bin": {
- "prebuild-install": "bin.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/prettier": {
- "version": "3.8.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
- "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
- "dev": true,
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
- "bin": {
- "prettier": "bin/prettier.cjs"
- },
"engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
+ "node": ">=6"
}
},
- "node_modules/prompts": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
- "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
- "dev": true,
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
- "dependencies": {
- "kleur": "^3.0.3",
- "sisteransi": "^1.0.5"
- },
"engines": {
- "node": ">= 6"
- }
- },
- "node_modules/pump": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "devOptional": true,
- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/rc9": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
- "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "defu": "^6.1.6",
- "destr": "^2.0.5"
+ "node": ">=6"
}
},
"node_modules/react": {
@@ -4651,41 +4141,52 @@
"react": "^19.2.0"
}
},
- "node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "devOptional": true,
+ "node_modules/react-pdf": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.4.1.tgz",
+ "integrity": "sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA==",
"license": "MIT",
"dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
+ "clsx": "^2.0.0",
+ "dequal": "^2.0.3",
+ "make-cancellable-promise": "^2.0.0",
+ "make-event-props": "^2.0.0",
+ "merge-refs": "^2.0.0",
+ "pdfjs-dist": "5.4.296",
+ "tiny-invariant": "^1.0.0",
+ "warning": "^4.0.0"
},
- "engines": {
- "node": ">= 6"
+ "funding": {
+ "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/readdirp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
- "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
- "dev": true,
- "license": "MIT",
+ "node_modules/react-pdf/node_modules/pdfjs-dist": {
+ "version": "5.4.296",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
+ "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 20.19.0"
+ "node": ">=20.16.0 || >=22.3.0"
},
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.80"
}
},
- "node_modules/regexp-to-ast": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz",
- "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==",
- "dev": true,
- "license": "MIT"
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
},
"node_modules/rou3": {
"version": "0.7.12",
@@ -4693,18 +4194,11 @@
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
"license": "MIT"
},
- "node_modules/run-applescript": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
- "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
@@ -4726,6 +4220,12 @@
],
"license": "MIT"
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -4744,13 +4244,6 @@
"node": ">=10"
}
},
- "node_modules/set-cookie-parser": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
- "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -4796,60 +4289,6 @@
"@img/sharp-win32-x64": "0.34.5"
}
},
- "node_modules/simple-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "devOptional": true,
- "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/simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "devOptional": true,
- "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",
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
- "node_modules/sisteransi": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
- "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
@@ -4906,26 +4345,6 @@
"node": ">= 10.x"
}
},
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -4980,35 +4399,11 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/tar-fs": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
- "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
- }
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
@@ -5041,19 +4436,6 @@
"fsevents": "~2.3.3"
}
},
- "node_modules/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -5068,6 +4450,21 @@
"node": ">=14.17"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
+ "node_modules/undici": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz",
+ "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
"node_modules/undici-types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
@@ -5089,50 +4486,43 @@
}
}
},
- "node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
+ "loose-envify": "^1.0.0"
}
},
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -5143,13 +4533,6 @@
"webidl-conversions": "^3.0.0"
}
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "devOptional": true,
- "license": "ISC"
- },
"node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
@@ -5171,22 +4554,6 @@
}
}
},
- "node_modules/wsl-utils": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
- "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-wsl": "^3.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
@@ -5204,40 +4571,19 @@
"node": ">=0.4"
}
},
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/yocto-spinner": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.3.tgz",
- "integrity": "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yoctocolors": "^2.1.1"
- },
- "engines": {
- "node": ">=18.19"
+ "node_modules/yaml": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/yoctocolors": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
- "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
- "dev": true,
- "license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">= 14.6"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
diff --git a/ai-pdf-chatbot/package.json b/ai-pdf-chatbot/package.json
index be0578a..6e41f21 100644
--- a/ai-pdf-chatbot/package.json
+++ b/ai-pdf-chatbot/package.json
@@ -8,7 +8,7 @@
"start": "next start",
"typecheck": "tsc --noEmit",
"auth:migrate": "npx -y auth@latest migrate --config ./lib/auth.ts -y",
- "setup": "npm run auth:migrate && npx -y @insforge/cli storage create-bucket pdf-documents --private",
+ "setup": "npm run auth:migrate && npx -y @insforge/cli storage create-bucket pdf-documents --private && npx -y @insforge/cli storage create-bucket audio-overviews --public",
"test": "node --import tsx --test scripts/*.test.mjs"
},
"dependencies": {
@@ -19,11 +19,15 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.446.0",
+ "markmap-lib": "^0.18.12",
+ "markmap-view": "^0.18.12",
"next": "16.2.0",
"next-themes": "^0.4.6",
+ "pdfjs-dist": "^5.7.284",
"pg": "^8.13.0",
"react": "19.2.0",
"react-dom": "19.2.0",
+ "react-pdf": "^10.4.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"unpdf": "^1.6.2"
diff --git a/ai-pdf-chatbot/scripts/srs.test.mjs b/ai-pdf-chatbot/scripts/srs.test.mjs
new file mode 100644
index 0000000..b284778
--- /dev/null
+++ b/ai-pdf-chatbot/scripts/srs.test.mjs
@@ -0,0 +1,59 @@
+import { test } from 'node:test';
+import { strict as assert } from 'node:assert';
+import { schedule } from '../lib/srs/schedule.ts';
+
+const NOW = new Date('2026-06-01T00:00:00.000Z');
+const fresh = { ease: 2.5, interval_days: 0, reps: 0 };
+
+test('first review "good" schedules one day out', () => {
+ const next = schedule(fresh, 'good', NOW);
+ assert.equal(next.interval_days, 1);
+ assert.equal(next.reps, 1);
+ assert.equal(next.last_grade, 2);
+ assert.equal(next.due_at.toISOString(), '2026-06-02T00:00:00.000Z');
+});
+
+test('first review "easy" schedules three days out and bumps ease', () => {
+ const next = schedule(fresh, 'easy', NOW);
+ assert.equal(next.interval_days, 3);
+ assert.equal(next.ease, 2.65);
+});
+
+test('"again" on a mature card resets interval and lapses ease', () => {
+ const mature = { ease: 2.5, interval_days: 10, reps: 4 };
+ const next = schedule(mature, 'again', NOW);
+ assert.equal(next.interval_days, 0);
+ assert.equal(next.ease, 2.3);
+ assert.equal(next.last_grade, 0);
+ // due in 5 minutes
+ const delta = next.due_at.getTime() - NOW.getTime();
+ assert.equal(delta, 5 * 60 * 1000);
+});
+
+test('"good" on a mature card geometric-steps by ease', () => {
+ const mature = { ease: 2.5, interval_days: 4, reps: 3 };
+ const next = schedule(mature, 'good', NOW);
+ assert.equal(next.interval_days, 10);
+});
+
+test('ease has a hard floor at 1.3', () => {
+ const wobbly = { ease: 1.4, interval_days: 5, reps: 5 };
+ const lapsed = schedule(wobbly, 'again', NOW);
+ assert.equal(lapsed.ease, 1.3);
+ const harder = schedule({ ease: 1.32, interval_days: 5, reps: 5 }, 'hard', NOW);
+ assert.equal(harder.ease, 1.3);
+});
+
+test('"hard" on a mature card steps by 1.2', () => {
+ const mature = { ease: 2.5, interval_days: 4, reps: 3 };
+ const next = schedule(mature, 'hard', NOW);
+ assert.equal(next.interval_days, 4.8);
+ assert.equal(next.ease, 2.45);
+});
+
+test('next interval clamped to at least one day', () => {
+ // tiny mature interval + low ease — geometric step rounds to < 1
+ const fragile = { ease: 1.3, interval_days: 0.5, reps: 2 };
+ const next = schedule(fragile, 'good', NOW);
+ assert.equal(next.interval_days, 1);
+});
diff --git a/registry.json b/registry.json
index 7d6b249..0bb45bf 100644
--- a/registry.json
+++ b/registry.json
@@ -53,12 +53,12 @@
},
{
"slug": "ai-pdf-chatbot",
- "name": "AI PDF Chatbot",
- "description": "Drop a PDF, get a chatbot that answers questions with citations back to the source page. Production-grade RAG without LangChain — pgvector + InsForge AI doing the work.",
+ "name": "AI Notebook",
+ "description": "Open-source NotebookLM for students. Drop PDFs into workspaces and get auto-generated mindmaps, flashcards, two-host podcast summaries, and RAG chat that highlights cited passages inside the source PDF.",
"category": "ai",
"framework": "nextjs",
- "features": ["RAG", "pgvector", "OpenRouter", "Better Auth"],
- "tags": ["rag", "pdf", "chatbot", "embeddings", "pgvector", "langchain-alternative"],
+ "features": ["RAG", "react-pdf", "OpenAI TTS", "Better Auth"],
+ "tags": ["pdf", "study-tool", "notebooklm", "rag", "flashcards", "pgvector"],
"cover": "assets/covers/ai-pdf-chatbot.png",
"demo_url": "https://aipdfchat.insforge.site",
"author": "InsForge",