From b691ab47ba75685a6d1848904a9ab534227e900c Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 22 Mar 2026 05:54:39 +0530 Subject: [PATCH 01/29] feat: introduce new `pageIndex-notebooklm` kit for document indexing and chat functionality. --- .../pageIndex-notebooklm/.env.example | 12 + .../assistant/pageIndex-notebooklm/.gitignore | 49 + kits/assistant/pageIndex-notebooklm/README.md | 159 ++ .../actions/orchestrate.ts | 100 + .../pageIndex-notebooklm/app/globals.css | 55 + .../pageIndex-notebooklm/app/layout.tsx | 34 + .../pageIndex-notebooklm/app/page.tsx | 281 +++ .../pageIndex-notebooklm/components.json | 21 + .../components/ChatWindow.tsx | 171 ++ .../components/DocumentList.tsx | 69 + .../components/DocumentUpload.tsx | 99 + .../components/TreeViewer.tsx | 150 ++ .../pageIndex-notebooklm/config.json | 38 + .../flows/chat-with-pdf/README.md | 65 + .../flows/chat-with-pdf/config.json | 275 +++ .../flows/chat-with-pdf/inputs.json | 62 + .../flows/chat-with-pdf/meta.json | 9 + .../README.md | 63 + .../config.json | 154 ++ .../inputs.json | 1 + .../meta.json | 9 + .../flows/flow-4-get-tree-structure/README.md | 63 + .../flow-4-get-tree-structure/config.json | 145 ++ .../flow-4-get-tree-structure/inputs.json | 14 + .../flows/flow-4-get-tree-structure/meta.json | 9 + .../flows/flow-list-all-documents/README.md | 62 + .../flows/flow-list-all-documents/config.json | 106 + .../flows/flow-list-all-documents/inputs.json | 14 + .../flows/flow-list-all-documents/meta.json | 9 + .../lib/lamatic-client.ts | 28 + .../pageIndex-notebooklm/lib/types.ts | 59 + .../pageIndex-notebooklm/next-env.d.ts | 5 + .../pageIndex-notebooklm/next.config.mjs | 11 + .../pageIndex-notebooklm/package-lock.json | 1706 +++++++++++++++++ .../pageIndex-notebooklm/package.json | 28 + .../pageIndex-notebooklm/postcss.config.mjs | 7 + .../pageIndex-notebooklm/tsconfig.json | 27 + .../updated/ChatWindow.tsx | 182 ++ .../pageIndex-notebooklm/updated/README.md | 159 ++ .../updated/TreeViewer.tsx | 114 ++ .../pageIndex-notebooklm/updated/config.json | 50 + .../pageIndex-notebooklm/updated/main.py | 803 ++++++++ .../pageIndex-notebooklm/updated/types.ts | 59 + 43 files changed, 5536 insertions(+) create mode 100644 kits/assistant/pageIndex-notebooklm/.env.example create mode 100644 kits/assistant/pageIndex-notebooklm/.gitignore create mode 100644 kits/assistant/pageIndex-notebooklm/README.md create mode 100644 kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts create mode 100644 kits/assistant/pageIndex-notebooklm/app/globals.css create mode 100644 kits/assistant/pageIndex-notebooklm/app/layout.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/app/page.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/components.json create mode 100644 kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/config.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md create mode 100644 kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/inputs.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/meta.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/meta.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/config.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/inputs.json create mode 100644 kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/meta.json create mode 100644 kits/assistant/pageIndex-notebooklm/lib/lamatic-client.ts create mode 100644 kits/assistant/pageIndex-notebooklm/lib/types.ts create mode 100644 kits/assistant/pageIndex-notebooklm/next-env.d.ts create mode 100644 kits/assistant/pageIndex-notebooklm/next.config.mjs create mode 100644 kits/assistant/pageIndex-notebooklm/package-lock.json create mode 100644 kits/assistant/pageIndex-notebooklm/package.json create mode 100644 kits/assistant/pageIndex-notebooklm/postcss.config.mjs create mode 100644 kits/assistant/pageIndex-notebooklm/tsconfig.json create mode 100644 kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/updated/README.md create mode 100644 kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx create mode 100644 kits/assistant/pageIndex-notebooklm/updated/config.json create mode 100644 kits/assistant/pageIndex-notebooklm/updated/main.py create mode 100644 kits/assistant/pageIndex-notebooklm/updated/types.ts diff --git a/kits/assistant/pageIndex-notebooklm/.env.example b/kits/assistant/pageIndex-notebooklm/.env.example new file mode 100644 index 00000000..fb79d815 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/.env.example @@ -0,0 +1,12 @@ +# Lamatic Project Settings +# Get these from: studio.lamatic.ai → Settings → API Keys +LAMATIC_API_KEY="YOUR_LAMATIC_API_KEY" +LAMATIC_PROJECT_ID="YOUR_LAMATIC_PROJECT_ID" +LAMATIC_API_URL="YOUR_LAMATIC_API_ENDPOINT" + +# Flow IDs +# Get each Flow ID from: Lamatic Studio → Flow → three-dot menu → Copy ID +FLOW_ID_UPLOAD="YOUR_UPLOAD_FLOW_ID" +FLOW_ID_CHAT="YOUR_CHAT_FLOW_ID" +FLOW_ID_LIST="YOUR_LIST_FLOW_ID" +FLOW_ID_TREE="YOUR_TREE_FLOW_ID" diff --git a/kits/assistant/pageIndex-notebooklm/.gitignore b/kits/assistant/pageIndex-notebooklm/.gitignore new file mode 100644 index 00000000..2551eb02 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/.gitignore @@ -0,0 +1,49 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build outputs +dist/ +build/ +.next/ +out/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo + +# TypeScript +*.tsbuildinfo + +# Cache +.cache/ +.parcel-cache/ +.turbo/ + +# Testing +coverage/ + +# Misc +*.pem diff --git a/kits/assistant/pageIndex-notebooklm/README.md b/kits/assistant/pageIndex-notebooklm/README.md new file mode 100644 index 00000000..a206566e --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/README.md @@ -0,0 +1,159 @@ +# PageIndex NotebookLM — AgentKit + +Upload any PDF and chat with it using **vectorless, tree-structured RAG**. +No vector database. No chunking. Just a hierarchical document index built from the table of contents. + +--- + +## How It Works + +A 7-stage FastAPI pipeline (running on Railway, powered by Groq) processes each PDF: + +1. **TOC Detection** — concurrent LLM scan of first 20 pages +2. **TOC Extraction** — multi-pass extraction with completion verification +3. **TOC → JSON** — structured flat list with hierarchy (`1`, `1.1`, `1.2.3`) +4. **Physical Index Assignment** — verify each section starts on the correct page (±3 scan) +5. **Tree Build** — nested structure with exact `start_index` + `end_index` per section +6. **Summaries** — concurrent 1-2 sentence summary per node (≤200 chars) +7. **Page Verification** — fuzzy match each node title against actual page text + +At query time, the LLM navigates the tree like a table of contents to pick the right sections, then fetches verbatim page content using the exact `start_index → end_index` range. + +--- + +## Stack + +| Layer | Technology | +|-------|-----------| +| Orchestration | Lamatic AI (4 flows) | +| LLM | Groq — llama-3.3-70b-versatile (multi-key pool, free tier) | +| Indexing API | FastAPI on Railway | +| Storage | Supabase (PostgreSQL) | +| Frontend | Next.js + Tailwind CSS | + +--- + +## Prerequisites + +- [Lamatic AI](https://lamatic.ai) account (free) +- [Groq](https://console.groq.com) account (free — create multiple for key pool) +- [Railway](https://railway.app) account (for FastAPI server) +- [Supabase](https://supabase.com) account (free tier) +- Node.js 18+ + +--- + +## Setup + +### 1. Deploy the FastAPI Server (Railway) + +```bash +# Clone your fork, then: +cd pageindex-server # contains main.py + requirements.txt +railway init +railway up +``` + +Add these environment variables in Railway dashboard: + +| Variable | Value | +|----------|-------| +| `SERVER_API_KEY` | Any secret string (e.g. `openssl rand -hex 16`) | +| `GROQ_API_KEY_1` | First Groq API key | +| `GROQ_API_KEY_2` | Second Groq API key (optional — more keys = higher throughput) | + +Note your Railway URL: `https://your-app.up.railway.app` + +### 2. Set Up Supabase + +Run this SQL in Supabase SQL Editor: + +```sql +create table documents ( + id uuid default gen_random_uuid() primary key, + doc_id text unique not null, + file_name text, + file_url text, + tree jsonb, + raw_text text, + tree_node_count integer default 0, + status text default 'completed', + created_at timestamptz default now() +); +alter table documents enable row level security; +create policy "service_access" on documents for all using (true); +``` + +### 3. Set Up Lamatic Flows + +Import all 4 flows from the `flows/` folder into Lamatic Studio, then add these secrets in **Lamatic → Settings → Secrets**: + +| Secret | Value | +|--------|-------| +| `SERVER_API_KEY` | Same value as Railway | +| `SUPABASE_URL` | `https://xxx.supabase.co` | +| `SUPABASE_ANON_KEY` | From Supabase Settings → API | + +### 4. Install and Configure the Kit + +```bash +cd kits/assistant/pageindex-notebooklm +npm install +cp .env.example .env.local +``` + +Fill in `.env.local`: + +``` +LAMATIC_API_KEY=... # Lamatic → Settings → API Keys +LAMATIC_PROJECT_ID=... # Lamatic → Settings → Project ID +LAMATIC_API_URL=... # Lamatic → Settings → API Docs → Endpoint + +FLOW_ID_UPLOAD=... # Flow 1 → three-dot menu → Copy ID +FLOW_ID_CHAT=... # Flow 2 → three-dot menu → Copy ID +FLOW_ID_LIST=... # Flow 3 → three-dot menu → Copy ID +FLOW_ID_TREE=... # Flow 4 → three-dot menu → Copy ID +``` + +### 5. Run Locally + +```bash +npm run dev +# → http://localhost:3000 +``` + +--- + +## Flows + +| Flow | File | Purpose | +|------|------|---------| +| Upload | `flows/pageindex-upload/` | Download PDF → 7-stage pipeline → save tree to Supabase | +| Chat | `flows/pageindex-chat/` | Tree search → page fetch → Groq answer | +| List | `flows/pageindex-list/` | List all documents from Supabase | +| Tree | `flows/pageindex-tree/` | Return full tree JSON for a document | + +--- + +## Deploying to Vercel + +```bash +# Push your branch first +git checkout -b feat/pageindex-notebooklm +git add kits/assistant/pageindex-notebooklm/ +git commit -m "feat: Add PageIndex NotebookLM — vectorless tree-structured RAG" +git push origin feat/pageindex-notebooklm +``` + +Then in Vercel: +1. Import your forked repo +2. Set **Root Directory** → `kits/assistant/pageindex-notebooklm` +3. Add all 7 env vars from `.env.local` +4. Deploy + +--- + +## Author + +**Saurabh Tiwari** — [st108113@gmail.com](mailto:st108113@gmail.com) +GitHub: [@Skt329](https://github.com/Skt329) diff --git a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts new file mode 100644 index 00000000..bc39a321 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts @@ -0,0 +1,100 @@ +"use server"; + +import { lamaticClient } from "@/lib/lamatic-client"; + +// ── helpers ────────────────────────────────────────────────── +function safeParseJSON(value: unknown, fallback: T): T { + if (typeof value === "string") { + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } + } + if (value !== null && value !== undefined) return value as T; + return fallback; +} + +// ── Flow 1: Upload PDF → build tree → save to Supabase ─────── +export async function uploadDocument(file_url: string, file_name: string) { + try { + const response = await lamaticClient.executeFlow( + process.env.FLOW_ID_UPLOAD!, + { file_url, file_name } + ); + // Response shape: { status, result: { doc_id, file_name, ... } } + return response.result ?? response; + } catch (error) { + console.error("Upload flow error:", error); + throw new Error("Failed to upload document"); + } +} + +// ── Flow 2: Chat (tree search → page fetch → answer) ───────── +export async function chatWithDocument( + doc_id: string, + query: string, + messages: Array<{ role: string; content: string }> +) { + try { + const response = await lamaticClient.executeFlow( + process.env.FLOW_ID_CHAT!, + { doc_id, query, messages } + ); + + const data = response.result ?? response; + + // retrieved_nodes comes back as a JSON string from the Code Node + return { + ...data, + retrieved_nodes: safeParseJSON(data?.retrieved_nodes, []), + }; + } catch (error) { + console.error("Chat flow error:", error); + throw new Error("Failed to get answer"); + } +} + +// ── Flow 3: List all documents ──────────────────────────────── +export async function listDocuments() { + try { + const response = await lamaticClient.executeFlow( + process.env.FLOW_ID_LIST!, + {} + ); + + const data = response.result ?? response; + + // documents comes back as a JSON string (JSON.stringify applied in Code Node) + return { + ...data, + documents: safeParseJSON(data?.documents, []), + total: Number(data?.total) || 0, + }; + } catch (error) { + console.error("List flow error:", error); + throw new Error("Failed to list documents"); + } +} + +// ── Flow 4: Get full tree structure ─────────────────────────── +export async function getDocumentTree(doc_id: string) { + try { + const response = await lamaticClient.executeFlow( + process.env.FLOW_ID_TREE!, + { doc_id } + ); + + const data = response.result ?? response; + + // tree comes back as a JSON string (JSON.stringify applied in Code Node) + return { + ...data, + tree: safeParseJSON(data?.tree, []), + tree_node_count: Number(data?.tree_node_count) || 0, + }; + } catch (error) { + console.error("Tree flow error:", error); + throw new Error("Failed to get document tree"); + } +} diff --git a/kits/assistant/pageIndex-notebooklm/app/globals.css b/kits/assistant/pageIndex-notebooklm/app/globals.css new file mode 100644 index 00000000..2d18e573 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/app/globals.css @@ -0,0 +1,55 @@ +@import "tailwindcss"; + +@layer base { + :root { + --bg: #0d0f14; + --surface: #13161d; + --surface-2: #1a1e28; + --border: #252a38; + --border-hover: #323847; + --accent: #6366f1; + --accent-hover: #818cf8; + --accent-dim: rgba(99,102,241,0.12); + --text-primary: #e8eaf0; + --text-secondary: #8b91a8; + --text-muted: #4b5168; + --amber: #f59e0b; + --amber-dim: rgba(245,158,11,0.12); + --green: #10b981; + --red: #ef4444; + --radius: 10px; + --shadow: 0 4px 24px rgba(0,0,0,0.4); + } + + * { + box-sizing: border-box; + } + + html, body { + height: 100%; + margin: 0; + padding: 0; + } + + body { + background: var(--bg); + color: var(--text-primary); + font-family: 'DM Sans', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + line-height: 1.5; + } + + ::-webkit-scrollbar { + width: 4px; + } + ::-webkit-scrollbar-track { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; + } + ::-webkit-scrollbar-thumb:hover { + background: var(--border-hover); + } +} diff --git a/kits/assistant/pageIndex-notebooklm/app/layout.tsx b/kits/assistant/pageIndex-notebooklm/app/layout.tsx new file mode 100644 index 00000000..2e620b3b --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { DM_Sans, DM_Mono } from "next/font/google"; +import "./globals.css"; + +const dmSans = DM_Sans({ + subsets: ["latin"], + variable: "--font-sans", + display: "swap", +}); + +const dmMono = DM_Mono({ + subsets: ["latin"], + weight: ["400", "500"], + variable: "--font-mono", + display: "swap", +}); + +export const metadata: Metadata = { + title: "PageIndex NotebookLM — Vectorless Document Intelligence", + description: + "Chat with your documents using PageIndex's agentic tree-structured retrieval. No vectors, no chunking — powered by Lamatic and Groq.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/kits/assistant/pageIndex-notebooklm/app/page.tsx b/kits/assistant/pageIndex-notebooklm/app/page.tsx new file mode 100644 index 00000000..9b428341 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/app/page.tsx @@ -0,0 +1,281 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { listDocuments, getDocumentTree } from "@/actions/orchestrate"; +import { Document, TreeNode, RetrievedNode } from "@/lib/types"; +import DocumentUpload from "@/components/DocumentUpload"; +import DocumentList from "@/components/DocumentList"; +import ChatWindow from "@/components/ChatWindow"; +import TreeViewer from "@/components/TreeViewer"; + +export default function Page() { + const [documents, setDocuments] = useState([]); + const [selectedDoc, setSelectedDoc] = useState(null); + const [tree, setTree] = useState([]); + const [treeLoading, setTreeLoading] = useState(false); + const [listLoading, setListLoading] = useState(false); + const [highlightedIds, setHighlightedIds] = useState([]); + const [activeTab, setActiveTab] = useState<"chat" | "tree">("chat"); + + const fetchDocuments = useCallback(async () => { + setListLoading(true); + try { + const result = await listDocuments(); + if (Array.isArray(result?.documents)) setDocuments(result.documents); + } catch { + // silent + } finally { + setListLoading(false); + } + }, []); + + useEffect(() => { fetchDocuments(); }, [fetchDocuments]); + + async function handleSelectDoc(doc: Document) { + setSelectedDoc(doc); + setHighlightedIds([]); + setActiveTab("chat"); + setTreeLoading(true); + try { + const result = await getDocumentTree(doc.doc_id); + if (Array.isArray(result?.tree)) setTree(result.tree); + } catch { + setTree([]); + } finally { + setTreeLoading(false); + } + } + + function handleRetrievedNodes(nodes: RetrievedNode[]) { + setHighlightedIds(nodes.map((n) => n.node_id)); + } + + return ( +
+ {/* Header */} +
+
+ + + +
+
+

+ PageIndex NotebookLM +

+

+ Vectorless RAG · Tree-structured retrieval +

+
+
+ + Powered by PageIndex + Groq + +
+
+ + {/* Body */} +
+ {/* Sidebar */} + + + {/* Main */} +
+ {selectedDoc ? ( +
+ {/* Tab bar */} +
+ {(["chat", "tree"] as const).map((tab) => ( + + ))} + + {selectedDoc.file_name} + +
+ + {/* Content */} +
+ {activeTab === "chat" ? ( + + ) : ( +
+ {treeLoading ? ( +
+ + + + + Loading tree… +
+ ) : tree.length > 0 ? ( + + ) : ( +
+ No tree structure available. +
+ )} +
+ )} +
+
+ ) : ( +
+
+
+ + + +
+

+ Select a document +

+

+ Upload a PDF or pick one from the sidebar to start chatting. +

+
+ How PageIndex works + Builds a hierarchical tree index — like a table of contents optimised for AI. The LLM navigates the tree to find relevant sections, then fetches verbatim page content. No vectors, no chunking. +
+
+
+ )} +
+
+ + +
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/components.json b/kits/assistant/pageIndex-notebooklm/components.json new file mode 100644 index 00000000..4ee62ee1 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx new file mode 100644 index 00000000..e20e3515 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { chatWithDocument } from "@/actions/orchestrate"; +import { Message, RetrievedNode } from "@/lib/types"; + +interface Props { + docId: string; + docName: string; + onRetrievedNodes?: (nodes: RetrievedNode[]) => void; +} + +export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const [sourcesOpen, setSourcesOpen] = useState(false); + const [lastNodes, setLastNodes] = useState([]); + const [lastThinking, setLastThinking] = useState(""); + const bottomRef = useRef(null); + + useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, loading]); + useEffect(() => { setMessages([]); setLastNodes([]); setLastThinking(""); setSourcesOpen(false); }, [docId]); + + async function handleSend(e: React.FormEvent) { + e.preventDefault(); + if (!input.trim() || loading) return; + const userMsg: Message = { role: "user", content: input.trim() }; + const newMsgs = [...messages, userMsg]; + setMessages(newMsgs); + setInput(""); + setLoading(true); + try { + const result = await chatWithDocument(docId, userMsg.content, newMsgs); + setMessages(prev => [...prev, { role: "assistant", content: result.answer || "Sorry, no answer found." }]); + if (Array.isArray(result.retrieved_nodes) && result.retrieved_nodes.length) { + setLastNodes(result.retrieved_nodes); + setLastThinking(result.thinking || ""); + onRetrievedNodes?.(result.retrieved_nodes); + } + } catch { + setMessages(prev => [...prev, { role: "assistant", content: "Something went wrong. Please try again." }]); + } finally { + setLoading(false); + } + } + + return ( +
+ {/* Messages */} +
+ {messages.length === 0 && ( +
+
+ + + +
+
+

Ask anything

+

+ The tree index navigates to the right page range in {docName} +

+
+
+ )} + + {messages.map((msg, i) => ( +
+
+ {msg.content} +
+
+ ))} + + {loading && ( +
+
+ + {[0,1,2].map(i => ( + + ))} + + Searching tree… +
+
+ )} +
+
+ + {/* Sources panel — now showing start_index→end_index page ranges */} + {lastNodes.length > 0 && ( +
+ + + {sourcesOpen && ( +
+ {lastThinking && ( +
+ Tree reasoning: {lastThinking} +
+ )} + {lastNodes.map(node => ( +
+
+ {node.title} + + pp.{node.start_index}–{node.end_index} + +
+ {node.summary && ( +

+ {node.summary} +

+ )} +

+ {node.page_content} +

+
+ ))} +
+ )} +
+ )} + + {/* Input */} +
+ setInput(e.target.value)} + placeholder="Ask a question about this document…" + disabled={loading} + style={{ flex: 1, padding: "9px 14px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: "8px", fontSize: "13px", color: "var(--text-primary)", outline: "none", transition: "border-color 0.15s" }} + onFocus={e => (e.currentTarget.style.borderColor = "var(--accent)")} + onBlur={e => (e.currentTarget.style.borderColor = "var(--border)")} + /> + +
+ + +
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx new file mode 100644 index 00000000..5d643949 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Document } from "@/lib/types"; + +interface Props { + documents: Document[]; + selectedId: string | null; + onSelect: (doc: Document) => void; +} + +export default function DocumentList({ documents, selectedId, onSelect }: Props) { + if (documents.length === 0) { + return ( +
+ + + +

No documents yet.

+

Upload a PDF to get started.

+
+ ); + } + + return ( +
    + {documents.map((doc) => { + const active = selectedId === doc.doc_id; + return ( +
  • + +
  • + ); + })} +
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx b/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx new file mode 100644 index 00000000..f8854bdb --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useState, useRef } from "react"; +import { uploadDocument } from "@/actions/orchestrate"; +import { UploadResponse } from "@/lib/types"; + +interface Props { onUploaded: () => void; } +type Status = "idle" | "uploading" | "success" | "error"; + +export default function DocumentUpload({ onUploaded }: Props) { + const [status, setStatus] = useState("idle"); + const [message, setMessage] = useState(""); + const [dragging, setDragging] = useState(false); + const inputRef = useRef(null); + + async function processFile(file: File) { + if (!file.type.includes("pdf") && !file.name.endsWith(".md")) { + setStatus("error"); + setMessage("Only PDF and Markdown files are supported."); + return; + } + setStatus("uploading"); + try { + const dataUrl = await fileToDataUrl(file); + const result = (await uploadDocument(dataUrl, file.name)) as UploadResponse; + if (result?.error) { setStatus("error"); setMessage(result.error); } + else { setStatus("success"); setMessage(`${result.tree_node_count} nodes indexed`); onUploaded(); } + } catch { + setStatus("error"); setMessage("Upload failed. Check your flow."); + } + setTimeout(() => { setStatus("idle"); setMessage(""); }, 3500); + } + + function fileToDataUrl(file: File): Promise { + return new Promise((res, rej) => { + const r = new FileReader(); + r.onload = () => res(r.result as string); + r.onerror = rej; + r.readAsDataURL(file); + }); + } + + const iconColor = dragging ? "var(--accent)" : status === "success" ? "var(--green)" : status === "error" ? "var(--red)" : "var(--text-muted)"; + const borderColor = dragging ? "var(--accent)" : status === "success" ? "var(--green)" : status === "error" ? "var(--red)" : "var(--border)"; + + return ( +
status === "idle" && inputRef.current?.click()} + onDragOver={(e) => { e.preventDefault(); setDragging(true); }} + onDragLeave={() => setDragging(false)} + onDrop={(e) => { e.preventDefault(); setDragging(false); const f = e.dataTransfer.files[0]; if (f) processFile(f); }} + style={{ + border: `1px dashed ${borderColor}`, + borderRadius: "10px", + padding: "16px", + cursor: status === "idle" ? "pointer" : "default", + background: dragging ? "var(--accent-dim)" : "var(--surface-2)", + display: "flex", flexDirection: "column", alignItems: "center", gap: "6px", + textAlign: "center", + transition: "all 0.2s", + }} + > + { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ""; }} /> + + {status === "uploading" ? ( + <> +
+ + + + Indexing document… +
+

Building tree structure

+ + ) : status === "success" ? ( + <> +
+ + Ready +
+

{message}

+ + ) : status === "error" ? ( + <> +
Upload failed
+

{message}

+ + ) : ( + <> + + + +

Upload a document

+

PDF or Markdown · drag & drop

+ + )} +
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx new file mode 100644 index 00000000..f4e1f62d --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useState } from "react"; +import { TreeNode } from "@/lib/types"; + +interface Props { + tree: TreeNode[]; + fileName: string; + highlightedIds: string[]; +} + +function TreeNodeRow({ node, depth, highlightedIds }: { node: TreeNode; depth: number; highlightedIds: string[] }) { + const [open, setOpen] = useState(depth < 2); + const isHighlighted = highlightedIds.includes(node.node_id); + const hasChildren = node.nodes && node.nodes.length > 0; + const pageSpan = node.start_index === node.end_index + ? `p.${node.start_index}` + : `pp.${node.start_index}–${node.end_index}`; + + return ( +
+
hasChildren && setOpen(o => !o)} + style={{ + display: "flex", alignItems: "flex-start", gap: "8px", + padding: `7px 10px 7px ${10 + depth * 18}px`, + borderRadius: "8px", + cursor: hasChildren ? "pointer" : "default", + background: isHighlighted ? "var(--amber-dim)" : "transparent", + border: isHighlighted ? "1px solid rgba(245,158,11,0.3)" : "1px solid transparent", + transition: "background 0.15s", + marginBottom: "2px", + }} + onMouseEnter={e => { if (!isHighlighted) e.currentTarget.style.background = "var(--surface-2)"; }} + onMouseLeave={e => { if (!isHighlighted) e.currentTarget.style.background = "transparent"; }} + > + {/* Expand/collapse icon */} + + {hasChildren ? ( + + + + ) : ( + + + + )} + + + {/* Content */} +
+
+ + {node.title} + + + {pageSpan} + + {isHighlighted && ( + + retrieved + + )} +
+ {/* Summary — the key new field from updated workflow */} + {node.summary && ( +

+ {node.summary} +

+ )} +
+
+ + {open && hasChildren && node.nodes!.map(child => ( + + ))} +
+ ); +} + +export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { + const totalNodes = (nodes: TreeNode[]): number => + nodes.reduce((acc, n) => acc + 1 + totalNodes(n.nodes || []), 0); + + return ( +
+ {/* Header */} +
+
+

Document Tree

+

+ {fileName} +

+
+
+ {highlightedIds.length > 0 && ( + + {highlightedIds.length} retrieved + + )} + + {totalNodes(tree)} nodes + +
+
+ + {/* Tree */} +
+ {tree.map(node => ( + + ))} +
+ + {/* Retrieved footer */} + {highlightedIds.length > 0 && ( +
+ + + + {highlightedIds.length} node{highlightedIds.length !== 1 ? "s" : ""} used in last answer — highlighted above +
+ )} +
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/config.json b/kits/assistant/pageIndex-notebooklm/config.json new file mode 100644 index 00000000..ae8f0b0d --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/config.json @@ -0,0 +1,38 @@ +{ + "name": "Flow Collection - 4 Flows", + "description": "A collection of 4 flows exported from Lamatic", + "tags": [], + "author": { + "name": "Lamatic AI", + "email": "info@lamatic.ai" + }, + "steps": [ + { + "id": "chat-with-pdf", + "type": "mandatory", + "envKey": "FLOW_CHAT_WITH_PDF" + }, + { + "id": "flow-4-get-tree-structure", + "type": "mandatory", + "envKey": "FLOW_FLOW_4_GET_TREE_STRUCTURE" + }, + { + "id": "flow-list-all-documents", + "type": "mandatory", + "envKey": "FLOW_FLOW_LIST_ALL_DOCUMENTS" + }, + { + "id": "flow-1-upload-pdf-build-tree-save", + "type": "mandatory", + "envKey": "FLOW_FLOW_1_UPLOAD_PDF_BUILD_TREE_SAVE" + } + ], + "integrations": [], + "features": [], + "demoUrl": "", + "githubUrl": "", + "deployUrl": "", + "documentationUrl": "", + "imageUrl": "" +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md new file mode 100644 index 00000000..b1266394 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md @@ -0,0 +1,65 @@ +# Chat with Pdf + +## About This Flow + +This flow automates a workflow with **7 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. + +## Flow Components + +This workflow includes the following node types: +- API Request +- Postgres +- Code +- Generate JSON +- Generate Text +- API Response + +## Configuration Requirements + +This flow requires configuration for **3 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. + +## Files Included + +- **config.json** - Complete flow structure with nodes and connections +- **inputs.json** - Private inputs requiring configuration +- **meta.json** - Flow metadata and information + +## Next Steps + +### Share with the Community + +Help grow the Lamatic ecosystem by contributing this flow to AgentKit! + +1. **Fork the Repository** + - Visit [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Fork the repository to your GitHub account + +2. **Prepare Your Submission** + - Create a new folder with a descriptive name for your flow + - Add all files from this package (`config.json`, `inputs.json`, `meta.json`) + - Write a comprehensive README.md that includes: + - Clear description of what the flow does + - Use cases and benefits + - Step-by-step setup instructions + - Required credentials and how to obtain them + - Example inputs and expected outputs + - Screenshots or diagrams (optional but recommended) + +3. **Open a Pull Request** + - Commit your changes with a descriptive message + - Push to your forked repository + - Open a PR to [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Add a clear description of your flow in the PR + +Your contribution will help others build amazing automations! 🚀 + +## Support + +For questions or issues with this flow: +- Review the node documentation for specific integrations +- Check the Lamatic documentation at docs.lamatic.ai +- Contact support for assistance + +--- +*Exported from Lamatic Flow Editor* +*Generated on 3/22/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json new file mode 100644 index 00000000..4bdb2247 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json @@ -0,0 +1,275 @@ +{ + "nodes": [ + { + "id": "triggerNode_1", + "data": { + "modes": {}, + "nodeId": "graphqlNode", + "values": { + "id": "triggerNode_1", + "nodeName": "API Request", + "responeType": "realtime", + "advance_schema": "{\n \"doc_id\": \"string\",\n \"query\": \"string\",\n \"messages\": \"string\"\n}" + }, + "trigger": true + }, + "type": "triggerNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 0 + }, + "selected": false + }, + { + "id": "postgresNode_817", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "postgresNode", + "values": { + "id": "postgresNode_817", + "query": "SELECT tree, raw_text, file_name FROM documents WHERE doc_id = '{{triggerNode_1.output.doc_id}}' LIMIT 1;", + "action": "runQuery", + "nodeName": "Postgres", + "credentials": "" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 130 + }, + "selected": false + }, + { + "id": "codeNode_429", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "schema": { + "toc_json": "string", + "node_count": "number" + }, + "values": { + "id": "codeNode_429", + "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\n\nfunction stripToTOC(nodes) {\n return nodes.map(node => ({\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index, // was page_index\n end_index: node.end_index, // new — exact section boundary\n description: node.summary, // was node.text\n children: (node.nodes || []).map(c => ({\n node_id: c.node_id,\n title: c.title,\n start_index: c.start_index,\n end_index: c.end_index\n }))\n }));\n}\n\noutput = {\n toc_json: JSON.stringify(stripToTOC(tree)),\n node_count: tree.length\n};", + "nodeName": "Code" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 260 + }, + "selected": false + }, + { + "id": "InstructorLLMNode_432", + "data": { + "label": "dynamicNode node", + "modes": {}, + "nodeId": "InstructorLLMNode", + "values": { + "id": "InstructorLLMNode_432", + "tools": [], + "schema": "{\n \"type\": \"object\",\n \"properties\": {\n \"thinking\": {\n \"type\": \"string\"\n },\n \"node_list\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n}", + "prompts": [ + { + "id": "187c2f4b-c23d-4545-abef-73dc897d6b7b", + "role": "system", + "content": "You are a document retrieval expert using tree-based reasoning. Your job is to identify which sections of a document tree are most relevant to answer the user's query." + }, + { + "id": "187c2f4b-c23d-4545-abef-73dc897d6b7d", + "role": "user", + "content": "You are given a query and a document table of contents (TOC). Navigate the TOC structure and find which sections likely contain the answer. Query: {{triggerNode_1.output.query}} Document TOC (titles and structure only): {{codeNode_429.output.toc_json}} \nEach node has start_index and end_index showing which pages it covers.Reply with: - thinking: your reasoning about which sections contain the answer (scan like a human reading a TOC) - node_list: array of 2-3 node_ids most likely to contain the answer" + } + ], + "memories": "[]", + "messages": "[]", + "nodeName": "Generate JSON", + "attachments": "", + "generativeModelName": "" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 390 + }, + "selected": false + }, + { + "id": "codeNode_358", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "schema": { + "context": "string", + "retrieved_nodes": "array" + }, + "values": { + "id": "codeNode_358", + "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}}\nconst rawText = {{postgresNode_817.output.queryResult[0].raw_text}};\nconst nodeList = {{InstructorLLMNode_432.output.node_list}};\n\n// Flatten tree to node_id map\nfunction flattenTree(nodes, map = {}) {\n for (const n of nodes) {\n map[n.node_id] = n;\n if (n.nodes && n.nodes.length > 0) flattenTree(n.nodes, map);\n }\n return map;\n}\n\nconst nodeMap = flattenTree(tree);\nconst selectedNodes = nodeList.map(id => nodeMap[id]).filter(Boolean);\n\n// PageIndex: use exact start_index → end_index range (no guessing)\nconst retrieved = selectedNodes.map(node => {\n const startPage = node.start_index || 1;\n const endPage = node.end_index || startPage + 2;\n let pageContent = \"\";\n\n for (let p = startPage; p <= Math.min(endPage, startPage + 4); p++) {\n const marker = `[PAGE ${p}]`;\n const nextMarker = `[PAGE ${p + 1}]`;\n if (rawText.includes(marker)) {\n const start = rawText.indexOf(marker) + marker.length;\n const end = rawText.includes(nextMarker)\n ? rawText.indexOf(nextMarker)\n : Math.min(start + 3000, rawText.length);\n pageContent += rawText.slice(start, end).trim() + \"\\n\\n\";\n }\n }\n\n return {\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n summary: node.summary, // was node.text\n page_content: pageContent.trim() || node.summary\n };\n});\n\nconst context = retrieved\n .map(n => `[Section: \"${n.title}\" | Pages: ${n.start_index}–${n.end_index}]\\n${n.page_content}`)\n .join(\"\\n\\n---\\n\\n\");\n\noutput = { context, retrieved_nodes: retrieved };", + "nodeName": "Code" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 520 + }, + "selected": false + }, + { + "id": "LLMNode_392", + "data": { + "label": "dynamicNode node", + "modes": {}, + "nodeId": "LLMNode", + "values": { + "id": "LLMNode_392", + "tools": [], + "prompts": [ + { + "id": "187c2f4b-c23d-4545-abef-73dc897d6b7b", + "role": "system", + "content": "You are a research assistant. Answer questions based ONLY on the retrieved document sections. Always cite the section title and page number in your answer. If the answer is not found in the provided sections, say so clearly — do not guess." + }, + { + "id": "187c2f4b-c23d-4545-abef-73dc897d6b7d", + "role": "user", + "content": "Retrieved document sections: {{codeNode_358.output.context}} User question: {{triggerNode_1.output.query}}" + } + ], + "memories": "[]", + "messages": "{{triggerNode_1.output.messages}}[]", + "nodeName": "Generate Text", + "attachments": "", + "credentials": "", + "generativeModelName": "" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 650 + }, + "selected": true + }, + { + "id": "responseNode_triggerNode_1", + "data": { + "label": "Response", + "nodeId": "graphqlResponseNode", + "values": { + "id": "responseNode_triggerNode_1", + "headers": "{\"content-type\":\"application/json\"}", + "retries": "0", + "nodeName": "API Response", + "webhookUrl": "", + "retry_delay": "0", + "outputMapping": "{\n \"answer\": \"{{LLMNode_392.output.generatedResponse}}\",\n \"retrieved_nodes\": \"{{codeNode_358.output.retrieved_nodes}}\",\n \"thinking\": \"{{InstructorLLMNode_432.output.thinking}}\",\n \"doc_id\": \"{{triggerNode_1.output.doc_id}}\"\n}" + }, + "isResponseNode": true + }, + "type": "responseNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 780 + }, + "selected": false + } + ], + "edges": [ + { + "id": "triggerNode_1-postgresNode_817", + "type": "defaultEdge", + "source": "triggerNode_1", + "target": "postgresNode_817", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "InstructorLLMNode_432-codeNode_358", + "type": "defaultEdge", + "source": "InstructorLLMNode_432", + "target": "codeNode_358", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "postgresNode_817-codeNode_429", + "type": "defaultEdge", + "source": "postgresNode_817", + "target": "codeNode_429", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "codeNode_429-InstructorLLMNode_432", + "type": "defaultEdge", + "source": "codeNode_429", + "target": "InstructorLLMNode_432", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "codeNode_358-LLMNode_392", + "type": "defaultEdge", + "source": "codeNode_358", + "target": "LLMNode_392", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "LLMNode_392-responseNode_triggerNode_1", + "type": "defaultEdge", + "source": "LLMNode_392", + "target": "responseNode_triggerNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "response-trigger_triggerNode_1", + "type": "responseEdge", + "source": "triggerNode_1", + "target": "responseNode_triggerNode_1", + "sourceHandle": "to-response", + "targetHandle": "from-trigger" + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/inputs.json b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/inputs.json new file mode 100644 index 00000000..1a1bb9eb --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/inputs.json @@ -0,0 +1,62 @@ +{ + "postgresNode_817": [ + { + "name": "credentials", + "label": "Credentials", + "description": "Select the credentials for postgres authentication.", + "type": "select", + "isCredential": true, + "required": true, + "defaultValue": "", + "isPrivate": true + } + ], + "InstructorLLMNode_432": [ + { + "name": "generativeModelName", + "label": "Generative Model Name", + "type": "model", + "mode": "instructor", + "description": "Select the model to generate text based on the prompt.", + "modelType": "generator/text", + "required": true, + "isPrivate": true, + "defaultValue": [ + { + "configName": "configA", + "type": "generator/text", + "provider_name": "", + "credential_name": "", + "params": {} + } + ], + "typeOptions": { + "loadOptionsMethod": "listModels" + } + } + ], + "LLMNode_392": [ + { + "name": "generativeModelName", + "label": "Generative Model Name", + "type": "model", + "modelType": "generator/text", + "mode": "chat", + "description": "Select the model to generate text based on the prompt.", + "required": true, + "defaultValue": [ + { + "configName": "configA", + "type": "generator/text", + "provider_name": "", + "credential_name": "", + "params": {} + } + ], + "typeOptions": { + "loadOptionsMethod": "listModels" + }, + "isPrivate": true + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/meta.json b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/meta.json new file mode 100644 index 00000000..dbeff2fa --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/meta.json @@ -0,0 +1,9 @@ +{ + "name": "Chat with Pdf", + "description": "", + "tags": [], + "testInput": "", + "githubUrl": "", + "documentationUrl": "", + "deployUrl": "" +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md new file mode 100644 index 00000000..d6d72055 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md @@ -0,0 +1,63 @@ +# Flow 1 Upload PDF Build Tree Save + +## About This Flow + +This flow automates a workflow with **4 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. + +## Flow Components + +This workflow includes the following node types: +- API Request +- API +- Code +- API Response + +## Configuration Requirements + +This flow requires configuration for **0 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. + +## Files Included + +- **config.json** - Complete flow structure with nodes and connections +- **inputs.json** - Private inputs requiring configuration +- **meta.json** - Flow metadata and information + +## Next Steps + +### Share with the Community + +Help grow the Lamatic ecosystem by contributing this flow to AgentKit! + +1. **Fork the Repository** + - Visit [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Fork the repository to your GitHub account + +2. **Prepare Your Submission** + - Create a new folder with a descriptive name for your flow + - Add all files from this package (`config.json`, `inputs.json`, `meta.json`) + - Write a comprehensive README.md that includes: + - Clear description of what the flow does + - Use cases and benefits + - Step-by-step setup instructions + - Required credentials and how to obtain them + - Example inputs and expected outputs + - Screenshots or diagrams (optional but recommended) + +3. **Open a Pull Request** + - Commit your changes with a descriptive message + - Push to your forked repository + - Open a PR to [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Add a clear description of your flow in the PR + +Your contribution will help others build amazing automations! 🚀 + +## Support + +For questions or issues with this flow: +- Review the node documentation for specific integrations +- Check the Lamatic documentation at docs.lamatic.ai +- Contact support for assistance + +--- +*Exported from Lamatic Flow Editor* +*Generated on 3/22/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json new file mode 100644 index 00000000..a8bc76e0 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -0,0 +1,154 @@ +{ + "nodes": [ + { + "id": "triggerNode_1", + "data": { + "modes": {}, + "nodeId": "graphqlNode", + "values": { + "id": "triggerNode_1", + "nodeName": "API Request", + "responeType": "realtime", + "advance_schema": "{\n \"file_url\": \"string\",\n \"file_name\": \"string\"\n}" + }, + "trigger": true + }, + "type": "triggerNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 0 + }, + "selected": false + }, + { + "id": "apiNode_948", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "apiNode", + "values": { + "id": "apiNode_948", + "url": "https://pageindex-fastapi-production.up.railway.app/doc/", + "body": "{\"file_url\": \"{{triggerNode_1.output.file_url}}\", \"file_name\": \"{{triggerNode_1.output.file_name}}\"}", + "method": "POST", + "headers": "{\"x-api-key\":\"{{secrets.project.SERVER_API_KEY}}\",\"Content-Type\":\"application/json\"}", + "retries": "0", + "nodeName": "API", + "retry_deplay": "0", + "convertXmlResponseToJson": false + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 130 + }, + "selected": false + }, + { + "id": "codeNode_570", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "schema": { + "error": "string", + "doc_id": "string", + "status": "string", + "success": "boolean", + "file_name": "string", + "status_code": "number", + "tree_node_count": "number" + }, + "values": { + "id": "codeNode_570", + "code": "\n\nvar apiData = {{apiNode_948.output}};\n\n// Parse if Lamatic injected it as a string\nif (typeof apiData === \"string\") {\n apiData = JSON.parse(apiData);\n}\n\nvar doc_id = apiData.doc_id || \"\";\nvar file_name = apiData.file_name || \"\";\nvar tree = apiData.tree || [];\nvar raw_text = apiData.raw_text || \"\";\nvar tree_node_count = apiData.tree_node_count || 0;\nvar file_url = {{triggerNode_1.output.file_url}} || \"\";\nvar supabase_url = {{secrets.project.SUPABASE_URL}} || \"\";\nvar supabase_key = {{secrets.project.SUPABASE_ANON_KEY}} || \"\";\n\nvar payload = JSON.stringify({\n doc_id: doc_id,\n file_name: file_name,\n file_url: file_url,\n tree: tree,\n raw_text: raw_text,\n tree_node_count: tree_node_count,\n status: \"completed\"\n});\n\nvar response = await fetch(\n supabase_url + \"/rest/v1/documents\",\n {\n method: \"POST\",\n headers: {\n \"apikey\": supabase_key,\n \"Authorization\": \"Bearer \" + supabase_key,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: payload\n }\n);\n\nvar result = await response.json();\n\nif (!response.ok) {\n output = {\n success: false,\n doc_id: \"\",\n file_name: file_name,\n tree_node_count: 0,\n error: result.message || result.detail || JSON.stringify(result),\n status_code: response.status\n };\n} else {\n var inserted = Array.isArray(result) ? result[0] : result;\n output = {\n success: true,\n doc_id: inserted.doc_id || doc_id,\n file_name: inserted.file_name || file_name,\n tree_node_count: inserted.tree_node_count || tree_node_count,\n status: \"completed\",\n error: \"\",\n status_code: response.status\n };\n}", + "nodeName": "Code" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 260 + }, + "selected": false + }, + { + "id": "responseNode_triggerNode_1", + "data": { + "label": "Response", + "nodeId": "graphqlResponseNode", + "values": { + "id": "responseNode_triggerNode_1", + "headers": "{\"content-type\":\"application/json\"}", + "retries": "0", + "nodeName": "API Response", + "webhookUrl": "", + "retry_delay": "0", + "outputMapping": "{\n \"doc_id\": \"{{apiNode_948.output}}.doc_id\",\n \"file_name\": \"{{apiNode_948.output}}.file_name\",\n \"tree_node_count\": \"{{apiNode_948.output}}.tree_node_count\",\n \"status\": \"completed\"\n}" + }, + "disabled": false, + "isResponseNode": true + }, + "type": "responseNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 390 + }, + "selected": true + } + ], + "edges": [ + { + "id": "triggerNode_1-apiNode_948", + "type": "defaultEdge", + "source": "triggerNode_1", + "target": "apiNode_948", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "apiNode_948-codeNode_570-252", + "type": "defaultEdge", + "source": "apiNode_948", + "target": "codeNode_570", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "codeNode_570-responseNode_triggerNode_1-839", + "data": {}, + "type": "defaultEdge", + "source": "codeNode_570", + "target": "responseNode_triggerNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "response-trigger_triggerNode_1", + "type": "responseEdge", + "source": "triggerNode_1", + "target": "responseNode_triggerNode_1", + "sourceHandle": "to-response", + "targetHandle": "from-trigger" + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json new file mode 100644 index 00000000..2b46e5e6 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json @@ -0,0 +1,9 @@ +{ + "name": "Flow 1 Upload PDF Build Tree Save", + "description": "", + "tags": [], + "testInput": "", + "githubUrl": "", + "documentationUrl": "", + "deployUrl": "" +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md new file mode 100644 index 00000000..832a7f78 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md @@ -0,0 +1,63 @@ +# Flow 4 Get tree structure + +## About This Flow + +This flow automates a workflow with **4 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. + +## Flow Components + +This workflow includes the following node types: +- API Request +- Postgres +- Code +- API Response + +## Configuration Requirements + +This flow requires configuration for **1 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. + +## Files Included + +- **config.json** - Complete flow structure with nodes and connections +- **inputs.json** - Private inputs requiring configuration +- **meta.json** - Flow metadata and information + +## Next Steps + +### Share with the Community + +Help grow the Lamatic ecosystem by contributing this flow to AgentKit! + +1. **Fork the Repository** + - Visit [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Fork the repository to your GitHub account + +2. **Prepare Your Submission** + - Create a new folder with a descriptive name for your flow + - Add all files from this package (`config.json`, `inputs.json`, `meta.json`) + - Write a comprehensive README.md that includes: + - Clear description of what the flow does + - Use cases and benefits + - Step-by-step setup instructions + - Required credentials and how to obtain them + - Example inputs and expected outputs + - Screenshots or diagrams (optional but recommended) + +3. **Open a Pull Request** + - Commit your changes with a descriptive message + - Push to your forked repository + - Open a PR to [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Add a clear description of your flow in the PR + +Your contribution will help others build amazing automations! 🚀 + +## Support + +For questions or issues with this flow: +- Review the node documentation for specific integrations +- Check the Lamatic documentation at docs.lamatic.ai +- Contact support for assistance + +--- +*Exported from Lamatic Flow Editor* +*Generated on 3/22/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json new file mode 100644 index 00000000..9f4a1064 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json @@ -0,0 +1,145 @@ +{ + "nodes": [ + { + "id": "triggerNode_1", + "data": { + "modes": {}, + "nodeId": "graphqlNode", + "values": { + "id": "triggerNode_1", + "nodeName": "API Request", + "responeType": "realtime", + "advance_schema": "{\n \"doc_id\": \"string\"\n}" + }, + "trigger": true + }, + "type": "triggerNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 0 + }, + "selected": false + }, + { + "id": "postgresNode_658", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "postgresNode", + "values": { + "id": "postgresNode_658", + "query": "SELECT tree, file_name, tree_node_count, created_at FROM documents WHERE doc_id = '{{triggerNode_1.output.doc_id}}' LIMIT 1;", + "action": "runQuery", + "nodeName": "Postgres", + "credentials": "" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 130 + }, + "selected": false + }, + { + "id": "codeNode_639", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "schema": { + "tree": "string", + "file_name": "string", + "created_at": "string", + "tree_node_count": "number" + }, + "values": { + "id": "codeNode_639", + "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst doc = {{postgresNode_658.output.queryResult[0]}};\n\noutput = {\n tree: JSON.stringify(doc.tree),\n file_name: doc.file_name,\n tree_node_count: doc.tree_node_count,\n created_at: doc.created_at\n};", + "nodeName": "Code" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 260 + }, + "selected": false + }, + { + "id": "responseNode_triggerNode_1", + "data": { + "label": "Response", + "nodeId": "graphqlResponseNode", + "values": { + "id": "responseNode_triggerNode_1", + "headers": "{\"content-type\":\"application/json\"}", + "retries": "0", + "nodeName": "API Response", + "webhookUrl": "", + "retry_delay": "0", + "outputMapping": "{\n \"tree\": \"{{codeNode_639.output.tree}}\",\n \"file_name\": \"{{codeNode_639.output.file_name}}\",\n \"tre_node_count\": \"{{codeNode_639.output.tree_node_count}}\",\n \"created_at\": \"{{codeNode_639.output.created_at}}\"\n}" + }, + "isResponseNode": true + }, + "type": "responseNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 390 + }, + "selected": true + } + ], + "edges": [ + { + "id": "triggerNode_1-postgresNode_658", + "type": "defaultEdge", + "source": "triggerNode_1", + "target": "postgresNode_658", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "postgresNode_658-codeNode_639", + "type": "defaultEdge", + "source": "postgresNode_658", + "target": "codeNode_639", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "codeNode_639-responseNode_triggerNode_1", + "type": "defaultEdge", + "source": "codeNode_639", + "target": "responseNode_triggerNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "response-trigger_triggerNode_1", + "type": "responseEdge", + "source": "triggerNode_1", + "target": "responseNode_triggerNode_1", + "sourceHandle": "to-response", + "targetHandle": "from-trigger" + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json new file mode 100644 index 00000000..d5996d3b --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json @@ -0,0 +1,14 @@ +{ + "postgresNode_658": [ + { + "name": "credentials", + "label": "Credentials", + "description": "Select the credentials for postgres authentication.", + "type": "select", + "isCredential": true, + "required": true, + "defaultValue": "", + "isPrivate": true + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/meta.json b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/meta.json new file mode 100644 index 00000000..f7e7ed51 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/meta.json @@ -0,0 +1,9 @@ +{ + "name": "Flow 4 Get tree structure", + "description": "", + "tags": [], + "testInput": "", + "githubUrl": "", + "documentationUrl": "", + "deployUrl": "" +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md new file mode 100644 index 00000000..c5d53e25 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md @@ -0,0 +1,62 @@ +# flow list all documents + +## About This Flow + +This flow automates a workflow with **3 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. + +## Flow Components + +This workflow includes the following node types: +- API Request +- Postgres +- API Response + +## Configuration Requirements + +This flow requires configuration for **1 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. + +## Files Included + +- **config.json** - Complete flow structure with nodes and connections +- **inputs.json** - Private inputs requiring configuration +- **meta.json** - Flow metadata and information + +## Next Steps + +### Share with the Community + +Help grow the Lamatic ecosystem by contributing this flow to AgentKit! + +1. **Fork the Repository** + - Visit [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Fork the repository to your GitHub account + +2. **Prepare Your Submission** + - Create a new folder with a descriptive name for your flow + - Add all files from this package (`config.json`, `inputs.json`, `meta.json`) + - Write a comprehensive README.md that includes: + - Clear description of what the flow does + - Use cases and benefits + - Step-by-step setup instructions + - Required credentials and how to obtain them + - Example inputs and expected outputs + - Screenshots or diagrams (optional but recommended) + +3. **Open a Pull Request** + - Commit your changes with a descriptive message + - Push to your forked repository + - Open a PR to [github.com/Lamatic/AgentKit](https://github.com/Lamatic/AgentKit) + - Add a clear description of your flow in the PR + +Your contribution will help others build amazing automations! 🚀 + +## Support + +For questions or issues with this flow: +- Review the node documentation for specific integrations +- Check the Lamatic documentation at docs.lamatic.ai +- Contact support for assistance + +--- +*Exported from Lamatic Flow Editor* +*Generated on 3/22/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/config.json new file mode 100644 index 00000000..8a796aa1 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/config.json @@ -0,0 +1,106 @@ +{ + "nodes": [ + { + "id": "triggerNode_1", + "data": { + "modes": {}, + "nodeId": "graphqlNode", + "values": { + "nodeName": "API Request", + "responeType": "realtime", + "advance_schema": "{\"sampleInput\":\"string\"}" + }, + "trigger": true + }, + "type": "triggerNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 0 + }, + "selected": false + }, + { + "id": "postgresNode_831", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "postgresNode", + "values": { + "id": "postgresNode_831", + "query": "SELECT doc_id, file_name, file_url, tree_node_count, status, created_at FROM documents ORDER BY created_at DESC;", + "action": "runQuery", + "nodeName": "Postgres", + "credentials": "" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 130 + }, + "selected": false + }, + { + "id": "responseNode_triggerNode_1", + "data": { + "label": "Response", + "nodeId": "graphqlResponseNode", + "values": { + "id": "responseNode_triggerNode_1", + "headers": "{\"content-type\":\"application/json\"}", + "retries": "0", + "nodeName": "API Response", + "webhookUrl": "", + "retry_delay": "0", + "outputMapping": "{\n \"documents\": \"{{postgresNode_831.output.queryResult}}\",\n \"total\": \"{{postgresNode_831.output.status}}\"\n}" + }, + "isResponseNode": true + }, + "type": "responseNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 260 + }, + "selected": true + } + ], + "edges": [ + { + "id": "triggerNode_1-postgresNode_831", + "type": "defaultEdge", + "source": "triggerNode_1", + "target": "postgresNode_831", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "postgresNode_831-responseNode_triggerNode_1", + "type": "defaultEdge", + "source": "postgresNode_831", + "target": "responseNode_triggerNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "response-trigger_triggerNode_1", + "type": "responseEdge", + "source": "triggerNode_1", + "target": "responseNode_triggerNode_1", + "sourceHandle": "to-response", + "targetHandle": "from-trigger" + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/inputs.json b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/inputs.json new file mode 100644 index 00000000..4d220daf --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/inputs.json @@ -0,0 +1,14 @@ +{ + "postgresNode_831": [ + { + "name": "credentials", + "label": "Credentials", + "description": "Select the credentials for postgres authentication.", + "type": "select", + "isCredential": true, + "required": true, + "defaultValue": "", + "isPrivate": true + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/meta.json b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/meta.json new file mode 100644 index 00000000..e8a3a17c --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/meta.json @@ -0,0 +1,9 @@ +{ + "name": "flow list all documents", + "description": "", + "tags": [], + "testInput": "", + "githubUrl": "", + "documentationUrl": "", + "deployUrl": "" +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/lib/lamatic-client.ts b/kits/assistant/pageIndex-notebooklm/lib/lamatic-client.ts new file mode 100644 index 00000000..50df4435 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/lib/lamatic-client.ts @@ -0,0 +1,28 @@ +import { Lamatic } from "lamatic"; + +if ( + !process.env.FLOW_ID_UPLOAD || + !process.env.FLOW_ID_CHAT || + !process.env.FLOW_ID_LIST || + !process.env.FLOW_ID_TREE +) { + throw new Error( + "One or more Flow IDs are missing. Set FLOW_ID_UPLOAD, FLOW_ID_CHAT, FLOW_ID_LIST, FLOW_ID_TREE in your .env file." + ); +} + +if ( + !process.env.LAMATIC_API_URL || + !process.env.LAMATIC_PROJECT_ID || + !process.env.LAMATIC_API_KEY +) { + throw new Error( + "Lamatic API credentials missing. Set LAMATIC_API_URL, LAMATIC_PROJECT_ID, LAMATIC_API_KEY in your .env file." + ); +} + +export const lamaticClient = new Lamatic({ + endpoint: process.env.LAMATIC_API_URL!, + projectId: process.env.LAMATIC_PROJECT_ID!, + apiKey: process.env.LAMATIC_API_KEY!, +}); diff --git a/kits/assistant/pageIndex-notebooklm/lib/types.ts b/kits/assistant/pageIndex-notebooklm/lib/types.ts new file mode 100644 index 00000000..28e52b23 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/lib/types.ts @@ -0,0 +1,59 @@ +export interface Document { + doc_id: string; + file_name: string; + file_url: string; + tree_node_count: number; + status: string; + created_at: string; +} + +export interface TreeNode { + node_id: string; + title: string; + start_index: number; // physical page where section starts + end_index: number; // physical page where section ends + summary: string; // short 1-2 sentence AI description (≤200 chars) + nodes?: TreeNode[]; +} + +export interface RetrievedNode { + node_id: string; + title: string; + start_index: number; // exact start page + end_index: number; // exact end page + summary: string; // short description from tree node + page_content: string; // verbatim PDF text fetched from start→end range +} + +export interface Message { + role: "user" | "assistant"; + content: string; +} + +export interface ChatResponse { + answer: string; + retrieved_nodes: RetrievedNode[]; + thinking: string; + doc_id: string; +} + +export interface UploadResponse { + doc_id: string; + file_name: string; + tree_node_count: string; + status: string; + saved: string; + error: string; +} + +export interface ListResponse { + documents: Document[]; + total: number; +} + +export interface TreeResponse { + tree: TreeNode[]; + file_name: string; + tree_node_count: number; + created_at: string; +} diff --git a/kits/assistant/pageIndex-notebooklm/next-env.d.ts b/kits/assistant/pageIndex-notebooklm/next-env.d.ts new file mode 100644 index 00000000..1b3be084 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/kits/assistant/pageIndex-notebooklm/next.config.mjs b/kits/assistant/pageIndex-notebooklm/next.config.mjs new file mode 100644 index 00000000..bd341913 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/next.config.mjs @@ -0,0 +1,11 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + typescript: { + ignoreBuildErrors: true, + }, + images: { + unoptimized: true, + }, +}; + +export default nextConfig; diff --git a/kits/assistant/pageIndex-notebooklm/package-lock.json b/kits/assistant/pageIndex-notebooklm/package-lock.json new file mode 100644 index 00000000..ccf67215 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/package-lock.json @@ -0,0 +1,1706 @@ +{ + "name": "pageindex-notebooklm", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pageindex-notebooklm", + "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "lamatic": "^0.3.2", + "lucide-react": "^0.511.0", + "next": "15.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.1.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.2", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "postcss": "^8.5.8", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", + "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", + "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", + "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", + "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", + "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", + "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", + "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", + "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", + "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "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/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/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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lamatic": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/lamatic/-/lamatic-0.3.2.tgz", + "integrity": "sha512-oOIpnJmjOxlMuViFsmI3LsbEMFxB7unZXplqgzKeu9hy87kqxP1/K1gU6NMQU+98iy1A3XbW7aQSfSLxvYq3sA==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "0.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", + "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "15.3.1", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.3.1", + "@next/swc-darwin-x64": "15.3.1", + "@next/swc-linux-arm64-gnu": "15.3.1", + "@next/swc-linux-arm64-musl": "15.3.1", + "@next/swc-linux-x64-gnu": "15.3.1", + "@next/swc-linux-x64-musl": "15.3.1", + "@next/swc-win32-arm64-msvc": "15.3.1", + "@next/swc-win32-x64-msvc": "15.3.1", + "sharp": "^0.34.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/kits/assistant/pageIndex-notebooklm/package.json b/kits/assistant/pageIndex-notebooklm/package.json new file mode 100644 index 00000000..74b84160 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/package.json @@ -0,0 +1,28 @@ +{ + "name": "pageindex-notebooklm", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "clsx": "^2.1.1", + "lamatic": "^0.3.2", + "lucide-react": "^0.511.0", + "next": "15.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.1.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.2", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "postcss": "^8.5.8", + "typescript": "^5" + } +} diff --git a/kits/assistant/pageIndex-notebooklm/postcss.config.mjs b/kits/assistant/pageIndex-notebooklm/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/kits/assistant/pageIndex-notebooklm/tsconfig.json b/kits/assistant/pageIndex-notebooklm/tsconfig.json new file mode 100644 index 00000000..d8b93235 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx new file mode 100644 index 00000000..172cf3ae --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { chatWithDocument } from "@/actions/orchestrate"; +import { Message, RetrievedNode } from "@/lib/types"; +import { Send, Loader2, ChevronDown, ChevronUp, BookOpen } from "lucide-react"; +import clsx from "clsx"; + +interface Props { + docId: string; + docName: string; + onRetrievedNodes?: (nodes: RetrievedNode[]) => void; +} + +export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const [expandedSource, setExpandedSource] = useState(null); + const [lastNodes, setLastNodes] = useState([]); + const [lastThinking, setLastThinking] = useState(""); + const bottomRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, loading]); + + async function handleSend(e: React.FormEvent) { + e.preventDefault(); + if (!input.trim() || loading) return; + + const userMsg: Message = { role: "user", content: input.trim() }; + const newMessages = [...messages, userMsg]; + setMessages(newMessages); + setInput(""); + setLoading(true); + + try { + const result = await chatWithDocument(docId, userMsg.content, newMessages); + + const assistantMsg: Message = { + role: "assistant", + content: result.answer || "Sorry, I could not find an answer.", + }; + setMessages((prev) => [...prev, assistantMsg]); + + if (result.retrieved_nodes && Array.isArray(result.retrieved_nodes)) { + setLastNodes(result.retrieved_nodes); + setLastThinking(result.thinking || ""); + onRetrievedNodes?.(result.retrieved_nodes); + } + } catch { + setMessages((prev) => [ + ...prev, + { role: "assistant", content: "Something went wrong. Please try again." }, + ]); + } finally { + setLoading(false); + } + } + + return ( +
+ {/* Header */} +
+ + + Chatting with: {docName} + +
+ + {/* Messages */} +
+ {messages.length === 0 && ( +
+

💬

+

Ask anything about this document

+

The tree index will navigate to the right sections.

+
+ )} + + {messages.map((msg, i) => ( +
+
+ {msg.content} +
+
+ ))} + + {loading && ( +
+
+ + Searching document tree... +
+
+ )} + +
+
+ + {/* Retrieved sources panel */} + {lastNodes.length > 0 && ( +
+ + + {expandedSource && ( +
+ {lastThinking && ( +
+ Tree reasoning: + {lastThinking} +
+ )} + {lastNodes.map((node) => ( +
+
+ {node.title} + pp.{node.start_index}–{node.end_index} +
+

{node.page_content}

+
+ ))} +
+ )} +
+ )} + + {/* Input */} +
+ setInput(e.target.value)} + placeholder="Ask a question about this document..." + disabled={loading} + className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50" + /> + +
+
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/updated/README.md b/kits/assistant/pageIndex-notebooklm/updated/README.md new file mode 100644 index 00000000..a206566e --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/updated/README.md @@ -0,0 +1,159 @@ +# PageIndex NotebookLM — AgentKit + +Upload any PDF and chat with it using **vectorless, tree-structured RAG**. +No vector database. No chunking. Just a hierarchical document index built from the table of contents. + +--- + +## How It Works + +A 7-stage FastAPI pipeline (running on Railway, powered by Groq) processes each PDF: + +1. **TOC Detection** — concurrent LLM scan of first 20 pages +2. **TOC Extraction** — multi-pass extraction with completion verification +3. **TOC → JSON** — structured flat list with hierarchy (`1`, `1.1`, `1.2.3`) +4. **Physical Index Assignment** — verify each section starts on the correct page (±3 scan) +5. **Tree Build** — nested structure with exact `start_index` + `end_index` per section +6. **Summaries** — concurrent 1-2 sentence summary per node (≤200 chars) +7. **Page Verification** — fuzzy match each node title against actual page text + +At query time, the LLM navigates the tree like a table of contents to pick the right sections, then fetches verbatim page content using the exact `start_index → end_index` range. + +--- + +## Stack + +| Layer | Technology | +|-------|-----------| +| Orchestration | Lamatic AI (4 flows) | +| LLM | Groq — llama-3.3-70b-versatile (multi-key pool, free tier) | +| Indexing API | FastAPI on Railway | +| Storage | Supabase (PostgreSQL) | +| Frontend | Next.js + Tailwind CSS | + +--- + +## Prerequisites + +- [Lamatic AI](https://lamatic.ai) account (free) +- [Groq](https://console.groq.com) account (free — create multiple for key pool) +- [Railway](https://railway.app) account (for FastAPI server) +- [Supabase](https://supabase.com) account (free tier) +- Node.js 18+ + +--- + +## Setup + +### 1. Deploy the FastAPI Server (Railway) + +```bash +# Clone your fork, then: +cd pageindex-server # contains main.py + requirements.txt +railway init +railway up +``` + +Add these environment variables in Railway dashboard: + +| Variable | Value | +|----------|-------| +| `SERVER_API_KEY` | Any secret string (e.g. `openssl rand -hex 16`) | +| `GROQ_API_KEY_1` | First Groq API key | +| `GROQ_API_KEY_2` | Second Groq API key (optional — more keys = higher throughput) | + +Note your Railway URL: `https://your-app.up.railway.app` + +### 2. Set Up Supabase + +Run this SQL in Supabase SQL Editor: + +```sql +create table documents ( + id uuid default gen_random_uuid() primary key, + doc_id text unique not null, + file_name text, + file_url text, + tree jsonb, + raw_text text, + tree_node_count integer default 0, + status text default 'completed', + created_at timestamptz default now() +); +alter table documents enable row level security; +create policy "service_access" on documents for all using (true); +``` + +### 3. Set Up Lamatic Flows + +Import all 4 flows from the `flows/` folder into Lamatic Studio, then add these secrets in **Lamatic → Settings → Secrets**: + +| Secret | Value | +|--------|-------| +| `SERVER_API_KEY` | Same value as Railway | +| `SUPABASE_URL` | `https://xxx.supabase.co` | +| `SUPABASE_ANON_KEY` | From Supabase Settings → API | + +### 4. Install and Configure the Kit + +```bash +cd kits/assistant/pageindex-notebooklm +npm install +cp .env.example .env.local +``` + +Fill in `.env.local`: + +``` +LAMATIC_API_KEY=... # Lamatic → Settings → API Keys +LAMATIC_PROJECT_ID=... # Lamatic → Settings → Project ID +LAMATIC_API_URL=... # Lamatic → Settings → API Docs → Endpoint + +FLOW_ID_UPLOAD=... # Flow 1 → three-dot menu → Copy ID +FLOW_ID_CHAT=... # Flow 2 → three-dot menu → Copy ID +FLOW_ID_LIST=... # Flow 3 → three-dot menu → Copy ID +FLOW_ID_TREE=... # Flow 4 → three-dot menu → Copy ID +``` + +### 5. Run Locally + +```bash +npm run dev +# → http://localhost:3000 +``` + +--- + +## Flows + +| Flow | File | Purpose | +|------|------|---------| +| Upload | `flows/pageindex-upload/` | Download PDF → 7-stage pipeline → save tree to Supabase | +| Chat | `flows/pageindex-chat/` | Tree search → page fetch → Groq answer | +| List | `flows/pageindex-list/` | List all documents from Supabase | +| Tree | `flows/pageindex-tree/` | Return full tree JSON for a document | + +--- + +## Deploying to Vercel + +```bash +# Push your branch first +git checkout -b feat/pageindex-notebooklm +git add kits/assistant/pageindex-notebooklm/ +git commit -m "feat: Add PageIndex NotebookLM — vectorless tree-structured RAG" +git push origin feat/pageindex-notebooklm +``` + +Then in Vercel: +1. Import your forked repo +2. Set **Root Directory** → `kits/assistant/pageindex-notebooklm` +3. Add all 7 env vars from `.env.local` +4. Deploy + +--- + +## Author + +**Saurabh Tiwari** — [st108113@gmail.com](mailto:st108113@gmail.com) +GitHub: [@Skt329](https://github.com/Skt329) diff --git a/kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx b/kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx new file mode 100644 index 00000000..63ffe4b8 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState } from "react"; +import { TreeNode } from "@/lib/types"; +import { ChevronRight, ChevronDown, BookOpen } from "lucide-react"; +import clsx from "clsx"; + +interface TreeNodeProps { + node: TreeNode; + depth: number; + highlightedIds?: string[]; +} + +function TreeNodeItem({ node, depth, highlightedIds = [] }: TreeNodeProps) { + const [expanded, setExpanded] = useState(depth < 1); + const hasChildren = node.nodes && node.nodes.length > 0; + const isHighlighted = highlightedIds.includes(node.node_id); + + return ( +
+
hasChildren && setExpanded(!expanded)} + > +
+ {hasChildren ? ( + expanded ? ( + + ) : ( + + ) + ) : ( +
+
+
+ )} +
+ +
+
+ + {node.title} + + + pp.{node.start_index}–{node.end_index} + + {isHighlighted && ( + + retrieved + + )} +
+ {node.summary && ( +

+ {node.summary} +

+ )} +
+
+ + {hasChildren && expanded && ( +
+ {node.nodes!.map((child) => ( + + ))} +
+ )} +
+ ); +} + +interface Props { + tree: TreeNode[]; + fileName: string; + highlightedIds?: string[]; +} + +export default function TreeViewer({ tree, fileName, highlightedIds = [] }: Props) { + return ( +
+
+ + {fileName} + {tree.length} sections +
+
+ {tree.map((node) => ( + + ))} +
+
+ ); +} diff --git a/kits/assistant/pageIndex-notebooklm/updated/config.json b/kits/assistant/pageIndex-notebooklm/updated/config.json new file mode 100644 index 00000000..7657a7f0 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/updated/config.json @@ -0,0 +1,50 @@ +{ + "name": "PageIndex NotebookLM", + "description": "Upload PDFs and chat with them using vectorless, tree-structured RAG powered by PageIndex. A 7-stage pipeline (TOC detection → extraction → JSON transform → physical index assignment → tree build → summaries → page verification) builds a hierarchical document tree with exact page ranges. No vector database, no chunking.", + "tags": ["🤖 Agentic", "📚 RAG", "🗂️ Assistant", "✨ Generative"], + "author": { + "name": "Saurabh Tiwari", + "email": "st108113@gmail.com" + }, + "steps": [ + { + "id": "pageindex-upload", + "type": "mandatory", + "envKey": "FLOW_ID_UPLOAD", + "description": "Uploads PDF, runs 7-stage PageIndex pipeline via FastAPI, saves tree to Supabase" + }, + { + "id": "pageindex-chat", + "type": "mandatory", + "envKey": "FLOW_ID_CHAT", + "description": "Tree-based section search using exact start/end page ranges + Groq answer generation" + }, + { + "id": "pageindex-list", + "type": "mandatory", + "envKey": "FLOW_ID_LIST", + "description": "Lists all uploaded documents from Supabase" + }, + { + "id": "pageindex-tree", + "type": "mandatory", + "envKey": "FLOW_ID_TREE", + "description": "Returns full nested tree structure JSON for a document" + } + ], + "integrations": ["PageIndex", "Supabase", "Groq"], + "features": [ + "Vectorless RAG — no embeddings or vector database", + "7-stage PageIndex pipeline: TOC detection, extraction, physical index assignment, tree build, summaries, verification", + "Exact page-range retrieval (start_index → end_index per section)", + "Multi-key Groq pool with round-robin rotation for free-tier throughput", + "Fallback extraction for documents without a formal TOC", + "Multi-document management with Supabase persistence", + "Interactive collapsible tree visualizer", + "Chat with full conversation history" + ], + "demoUrl": "", + "githubUrl": "https://github.com/Lamatic/AgentKit/tree/main/kits/assistant/pageindex-notebooklm", + "deployUrl": "", + "documentationUrl": "" +} diff --git a/kits/assistant/pageIndex-notebooklm/updated/main.py b/kits/assistant/pageIndex-notebooklm/updated/main.py new file mode 100644 index 00000000..6410f3a9 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/updated/main.py @@ -0,0 +1,803 @@ +""" +PageIndex FastAPI — Full Implementation +Mirrors the VectifyAI/PageIndex architecture with Groq multi-key pool. +""" + +import os +import asyncio +import base64 +import json +import re +import tempfile +import uuid +import itertools +from typing import Optional + +import httpx +from fastapi import FastAPI, HTTPException, Header +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +# ───────────────────────────────────────────────────────────────────────────── +# App setup +# ───────────────────────────────────────────────────────────────────────────── + +app = FastAPI(title="PageIndex — Groq Multi-Key") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + +def verify_key(x_api_key: str): + expected = os.getenv("SERVER_API_KEY", "") + if expected and x_api_key != expected: + raise HTTPException(status_code=401, detail="Invalid API key") + + +# ───────────────────────────────────────────────────────────────────────────── +# Groq Multi-Key Pool +# ───────────────────────────────────────────────────────────────────────────── + +def _load_groq_keys() -> list[str]: + """Auto-detect all available GROQ_API_KEY_N vars (N=1..20) plus legacy GROQ_API_KEY.""" + keys = [] + for i in range(1, 21): + k = os.getenv(f"GROQ_API_KEY_{i}") + if k and k.strip(): + keys.append(k.strip()) + legacy = os.getenv("GROQ_API_KEY", "") + if legacy.strip() and legacy.strip() not in keys: + keys.append(legacy.strip()) + return keys + + +GROQ_KEYS: list[str] = [] +_key_cycle = None +_key_lock = asyncio.Lock() + + +@app.on_event("startup") +async def startup(): + global GROQ_KEYS, _key_cycle + GROQ_KEYS = _load_groq_keys() + if not GROQ_KEYS: + raise RuntimeError("No Groq API keys found. Set GROQ_API_KEY_1 ... GROQ_API_KEY_N") + _key_cycle = itertools.cycle(range(len(GROQ_KEYS))) + print(f"[PageIndex] Loaded {len(GROQ_KEYS)} Groq API key(s)") + + +async def _next_key() -> tuple[int, str]: + async with _key_lock: + idx = next(_key_cycle) + return idx, GROQ_KEYS[idx] + + +async def llm_call( + prompt: str, + max_tokens: int = 1024, + json_mode: bool = True, + system: str = "You are an expert document analyst. Return valid JSON only, no markdown, no extra text.", +) -> str: + max_tokens = min(max_tokens, 4096) + tried_keys: set[int] = set() + + for attempt in range(len(GROQ_KEYS) + 1): + idx, key = await _next_key() + if idx in tried_keys and len(tried_keys) >= len(GROQ_KEYS): + raise HTTPException(status_code=429, detail="All Groq keys rate-limited") + tried_keys.add(idx) + + payload = { + "model": "llama-3.3-70b-versatile", + "messages": [ + {"role": "system", "content": system}, + {"role": "user", "content": prompt}, + ], + "temperature": 0.1, + "max_tokens": max_tokens, + } + if json_mode: + payload["response_format"] = {"type": "json_object"} + + async with httpx.AsyncClient(timeout=120) as client: + resp = await client.post( + "https://api.groq.com/openai/v1/chat/completions", + headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, + json=payload, + ) + + if resp.status_code == 429: + await asyncio.sleep(1) + continue + + data = resp.json() + if "error" in data: + err = data["error"].get("message", str(data["error"])) + if "rate_limit" in err.lower() or "token" in err.lower(): + await asyncio.sleep(1) + continue + raise HTTPException(status_code=500, detail=f"Groq error: {err}") + + return data["choices"][0]["message"]["content"].strip() + + raise HTTPException(status_code=429, detail="All Groq keys exhausted") + + +async def llm_call_many(prompts: list[str], max_tokens: int = 512, json_mode: bool = True) -> list[str]: + tasks = [llm_call(p, max_tokens=max_tokens, json_mode=json_mode) for p in prompts] + return await asyncio.gather(*tasks) + + +def extract_json(text: str) -> dict | list: + text = text.strip() + if text.startswith("```"): + text = re.sub(r"^```(?:json)?\n?", "", text) + text = re.sub(r"\n?```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + for pattern in [r"\{.*\}", r"\[.*\]"]: + m = re.search(pattern, text, re.DOTALL) + if m: + try: + return json.loads(m.group()) + except Exception: + pass + return {} + + +# ───────────────────────────────────────────────────────────────────────────── +# PDF Extraction +# ───────────────────────────────────────────────────────────────────────────── + +def extract_pdf_pages(pdf_path: str) -> tuple[list[tuple[str, int]], int]: + import pypdf + reader = pypdf.PdfReader(pdf_path) + total = len(reader.pages) + pages = [] + for i, page in enumerate(reader.pages): + t = page.extract_text() or "" + pages.append((t.strip(), i + 1)) + return pages, total + + +def get_raw_text(page_list: list[tuple[str, int]]) -> str: + parts = [] + for text, phys in page_list: + if text: + parts.append(f"[PAGE {phys}]\n{text}") + return "\n\n".join(parts) + + +def get_page_content_by_range(page_list: list, start: int, end: int) -> str: + parts = [] + for text, phys in page_list: + if start <= phys <= end: + parts.append(f"\n{text}\n\n") + return "\n".join(parts) + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 1: TOC Detection +# ───────────────────────────────────────────────────────────────────────────── + +async def detect_toc_pages(page_list: list[tuple[str, int]], check_up_to: int = 20) -> list[int]: + candidates = page_list[:check_up_to] + prompts = [] + for text, phys in candidates: + prompts.append(f"""Does the following page contain a Table of Contents (list of chapters/sections with page numbers)? +Note: abstract, summary, notation lists, figure lists are NOT table of contents. + +Page text: +{text[:2000]} + +Return JSON: {{"thinking": "", "toc_detected": "yes or no"}}""") + + results = await llm_call_many(prompts, max_tokens=150, json_mode=True) + + toc_pages = [] + for raw, (text, phys) in zip(results, candidates): + parsed = extract_json(raw) + if isinstance(parsed, dict) and parsed.get("toc_detected") == "yes": + toc_pages.append(phys) + return toc_pages + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 2: TOC Extraction +# ───────────────────────────────────────────────────────────────────────────── + +EXTRACT_TOC_PROMPT = """Extract the FULL table of contents from the given text. +Replace any ......... or dotted leaders with :. +Return ONLY the table of contents text, nothing else.""" + +VERIFY_TOC_COMPLETE_PROMPT = """You are given a raw table of contents and a cleaned/extracted version. +Check if the cleaned version is complete (contains all main sections from the raw). + +Raw TOC: +{raw} + +Cleaned TOC: +{cleaned} + +Return JSON: {{"thinking": "", "completed": "yes or no"}}""" + +CONTINUE_TOC_PROMPT = """Continue the table of contents extraction. Output ONLY the remaining part (not what was already extracted). + +Original raw TOC: +{raw} + +Already extracted: +{partial} + +Continue from where it left off:""" + + +async def extract_toc_with_retry(toc_raw: str, max_attempts: int = 5) -> str: + prompt = f"{EXTRACT_TOC_PROMPT}\n\nGiven text:\n{toc_raw[:4000]}" + extracted, _ = await _llm_with_finish_reason(prompt, max_tokens=2000) + + for attempt in range(max_attempts): + verify_prompt = VERIFY_TOC_COMPLETE_PROMPT.format(raw=toc_raw[:3000], cleaned=extracted) + verify_raw = await llm_call(verify_prompt, max_tokens=200, json_mode=True) + verify = extract_json(verify_raw) + completed = verify.get("completed", "no") if isinstance(verify, dict) else "no" + if completed == "yes": + break + cont_prompt = CONTINUE_TOC_PROMPT.format(raw=toc_raw[:3000], partial=extracted) + continuation, _ = await _llm_with_finish_reason(cont_prompt, max_tokens=1500, json_mode=False) + extracted = extracted + "\n" + continuation + + return extracted + + +async def _llm_with_finish_reason(prompt: str, max_tokens: int = 1024, json_mode: bool = True) -> tuple[str, str]: + max_tokens = min(max_tokens, 4096) + tried: set[int] = set() + + for _ in range(len(GROQ_KEYS) + 1): + idx, key = await _next_key() + if idx in tried and len(tried) >= len(GROQ_KEYS): + raise HTTPException(status_code=429, detail="All Groq keys rate-limited") + tried.add(idx) + + payload = { + "model": "llama-3.3-70b-versatile", + "messages": [ + {"role": "system", "content": "You are an expert document analyst."}, + {"role": "user", "content": prompt}, + ], + "temperature": 0.1, + "max_tokens": max_tokens, + } + if json_mode: + payload["response_format"] = {"type": "json_object"} + + async with httpx.AsyncClient(timeout=120) as client: + resp = await client.post( + "https://api.groq.com/openai/v1/chat/completions", + headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, + json=payload, + ) + + if resp.status_code == 429: + await asyncio.sleep(1) + continue + + data = resp.json() + if "error" in data: + continue + + choice = data["choices"][0] + content = choice["message"]["content"].strip() + finish = choice.get("finish_reason", "stop") + finish_mapped = "finished" if finish in ("stop", "eos") else finish + return content, finish_mapped + + raise HTTPException(status_code=429, detail="All Groq keys exhausted") + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 3: TOC → Structured JSON +# ───────────────────────────────────────────────────────────────────────────── + +TOC_TRANSFORM_PROMPT = """Transform the following table of contents into a JSON array. + +'structure' is the numeric hierarchy: "1", "1.1", "1.2.3" etc. +If a section has no numeric prefix, use sequential numbers. + +Return JSON: +{{ + "table_of_contents": [ + {{"structure": "1", "title": "Introduction", "page": 1}}, + {{"structure": "1.1", "title": "Background", "page": 2}}, + ... + ] +}} + +Table of contents: +{toc_text}""" + + +async def toc_to_json(toc_text: str) -> list[dict]: + prompt = TOC_TRANSFORM_PROMPT.format(toc_text=toc_text[:3000]) + raw, finish = await _llm_with_finish_reason(prompt, max_tokens=3000, json_mode=True) + + parsed = extract_json(raw) + items = [] + if isinstance(parsed, dict) and "table_of_contents" in parsed: + items = parsed["table_of_contents"] + elif isinstance(parsed, list): + items = parsed + + verify_prompt = f"""Is the following cleaned table of contents complete relative to the raw? +Raw: +{toc_text[:2000]} + +Cleaned (JSON): +{json.dumps(items[:30], indent=2)} + +Return JSON: {{"thinking": "", "completed": "yes or no"}}""" + + verify_raw = await llm_call(verify_prompt, max_tokens=200) + verify = extract_json(verify_raw) + + if isinstance(verify, dict) and verify.get("completed") == "no" and len(items) > 0: + last = items[-1] + cont_prompt = f"""Continue the JSON table of contents array from after this entry: +{json.dumps(last)} + +Raw TOC for reference: +{toc_text[:3000]} + +Output ONLY the additional JSON array items (not the already-extracted ones), as a JSON array.""" + cont_raw, _ = await _llm_with_finish_reason(cont_prompt, max_tokens=2000, json_mode=False) + try: + m = re.search(r"\[.*\]", cont_raw, re.DOTALL) + if m: + extra = json.loads(m.group()) + if isinstance(extra, list): + items.extend(extra) + except Exception: + pass + + for item in items: + try: + item["page"] = int(item.get("page") or 0) or None + except (ValueError, TypeError): + item["page"] = None + + return items + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 4: Assign Physical Indices +# ───────────────────────────────────────────────────────────────────────────── + +ASSIGN_INDEX_PROMPT = """You are given a table of contents entry and several document pages. +Find which physical page this section starts on. + +Section title: {title} +TOC page number: {page} + +Document pages (search these): +{pages} + +Return JSON: +{{"thinking": "", + "physical_index": }}""" + + +async def assign_physical_indices( + toc_items: list[dict], page_list: list[tuple[str, int]], total_pages: int +) -> list[dict]: + async def _assign(item: dict) -> dict: + toc_page = item.get("page") + if toc_page is None: + item["physical_index"] = None + return item + + search_start = max(1, toc_page - 1) + search_end = min(total_pages, toc_page + 3) + pages_text = get_page_content_by_range(page_list, search_start, search_end) + + if not pages_text.strip(): + item["physical_index"] = toc_page + return item + + prompt = ASSIGN_INDEX_PROMPT.format( + title=item.get("title", ""), + page=toc_page, + pages=pages_text[:2500], + ) + raw = await llm_call(prompt, max_tokens=200) + parsed = extract_json(raw) + if isinstance(parsed, dict): + pi = parsed.get("physical_index") + try: + item["physical_index"] = int(pi) if pi is not None else toc_page + except (ValueError, TypeError): + item["physical_index"] = toc_page + else: + item["physical_index"] = toc_page + return item + + results = await asyncio.gather(*[_assign(item) for item in toc_items]) + return list(results) + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 5: Build Nested Tree with start_index / end_index +# ───────────────────────────────────────────────────────────────────────────── + +def build_tree_from_flat(items: list[dict], total_pages: int) -> list[dict]: + def parse_depth(structure: str) -> int: + if not structure: + return 1 + return len(structure.split(".")) + + try: + items = sorted(items, key=lambda x: [int(p) for p in (x.get("structure") or "0").split(".")]) + except Exception: + pass + + for i, item in enumerate(items): + start = item.get("physical_index") or item.get("page") or 1 + if i + 1 < len(items): + next_start = items[i + 1].get("physical_index") or items[i + 1].get("page") or (start + 1) + end = max(start, next_start - 1) + else: + end = total_pages + item["_start"] = start + item["_end"] = end + + root: list[dict] = [] + stack: list[tuple[int, list[dict]]] = [(0, root)] + node_counters: dict[str, int] = {} + + for item in items: + depth = parse_depth(item.get("structure", "")) + title = item.get("title", "Untitled") + start = item["_start"] + end = item["_end"] + + parent_id = stack[-1][1][-1]["node_id"] if stack[-1][1] else "" + counter_key = f"depth_{depth}" + node_counters[counter_key] = node_counters.get(counter_key, 0) + 1 + node_id = f"{node_counters[counter_key]:04d}" if depth == 1 else f"{parent_id}_{node_counters[counter_key]:02d}" + + node = { + "node_id": node_id, + "title": title, + "start_index": start, + "end_index": end, + "summary": "", + "nodes": [], + } + + while len(stack) > 1 and stack[-1][0] >= depth: + stack.pop() + + stack[-1][1].append(node) + stack.append((depth, node["nodes"])) + + return root + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 6: Per-Node Summary Generation +# ───────────────────────────────────────────────────────────────────────────── + +SUMMARY_PROMPT = """Read the following document section and write a 1-2 sentence summary of what it covers. +Be concise (under 200 chars). Do NOT include quotes or raw data from the text. +Example: "Covers thermal decomposition of H2O2 catalysts and reaction kinetics." + +Section title: {title} +Section text (first 1500 chars): +{text} + +Return JSON: {{"summary": ""}}""" + + +async def add_node_summaries(tree: list[dict], page_list: list[tuple[str, int]]) -> list[dict]: + def collect_nodes(nodes: list[dict]) -> list[dict]: + flat = [] + for n in nodes: + flat.append(n) + if n.get("nodes"): + flat.extend(collect_nodes(n["nodes"])) + return flat + + all_nodes = collect_nodes(tree) + + async def _summarise(node: dict) -> None: + text = get_page_content_by_range(page_list, node["start_index"], min(node["start_index"] + 1, node["end_index"])) + if not text.strip(): + node["summary"] = f"Section: {node['title']}" + return + prompt = SUMMARY_PROMPT.format(title=node["title"], text=text[:1500]) + raw = await llm_call(prompt, max_tokens=150) + parsed = extract_json(raw) + node["summary"] = parsed.get("summary", node["title"]) if isinstance(parsed, dict) else node["title"] + + await asyncio.gather(*[_summarise(n) for n in all_nodes]) + return tree + + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 7: Per-Node Page Verification +# ───────────────────────────────────────────────────────────────────────────── + +VERIFY_PAGE_PROMPT = """Check if the section titled "{title}" appears or starts on the given page. +Do fuzzy matching — ignore minor spacing differences. + +Page text: +{page_text} + +Return JSON: {{"thinking": "", "answer": "yes or no"}}""" + + +async def verify_node_pages(tree: list[dict], page_list: list[tuple[str, int]], total_pages: int) -> list[dict]: + def collect_nodes(nodes: list[dict]) -> list[dict]: + flat = [] + for n in nodes: + flat.append(n) + if n.get("nodes"): + flat.extend(collect_nodes(n["nodes"])) + return flat + + page_map = {phys: text for text, phys in page_list} + + async def _verify(node: dict) -> None: + si = node.get("start_index", 1) + page_text = page_map.get(si, "") + if not page_text.strip(): + return + + prompt = VERIFY_PAGE_PROMPT.format(title=node["title"], page_text=page_text[:1500]) + raw = await llm_call(prompt, max_tokens=150) + parsed = extract_json(raw) + answer = parsed.get("answer", "yes") if isinstance(parsed, dict) else "yes" + + if answer != "yes": + for delta in [-1, 1, -2, 2]: + candidate = si + delta + if candidate < 1 or candidate > total_pages: + continue + alt_text = page_map.get(candidate, "") + if not alt_text: + continue + prompt2 = VERIFY_PAGE_PROMPT.format(title=node["title"], page_text=alt_text[:1500]) + raw2 = await llm_call(prompt2, max_tokens=150) + p2 = extract_json(raw2) + if isinstance(p2, dict) and p2.get("answer") == "yes": + node["start_index"] = candidate + break + + all_nodes = collect_nodes(tree) + await asyncio.gather(*[_verify(n) for n in all_nodes]) + return tree + + +# ───────────────────────────────────────────────────────────────────────────── +# Fallback: No-TOC page-by-page structure extraction +# ───────────────────────────────────────────────────────────────────────────── + +NO_TOC_INIT_PROMPT = """You are an expert in extracting hierarchical structure from documents. +Read the following pages and extract the sections/headings you find. + +The tags mark page boundaries. + +Return a JSON array: +[ + {{"structure": "1", "title": "Section Title", "physical_index": }}, + ... +] +Only return sections found in the provided pages. + +Document pages: +{pages}""" + +NO_TOC_CONTINUE_PROMPT = """You are continuing to extract sections from a document. +Here is the structure found so far, and the next set of pages to process. +Continue the structure — add sections found in the new pages. + +Previous structure: +{previous} + +New pages: +{pages} + +Return ONLY the NEW sections as a JSON array (not the already-found ones): +[ + {{"structure": "", "title": "...", "physical_index": }}, + ... +]""" + + +async def extract_structure_no_toc(page_list: list[tuple[str, int]], chunk_size: int = 8) -> list[dict]: + all_items: list[dict] = [] + total = len(page_list) + + for chunk_start in range(0, total, chunk_size): + chunk = page_list[chunk_start: chunk_start + chunk_size] + pages_text = "\n\n".join( + f"\n{text}\n" for text, phys in chunk if text.strip() + ) + + if not pages_text.strip(): + continue + + if not all_items: + prompt = NO_TOC_INIT_PROMPT.format(pages=pages_text[:3500]) + else: + prompt = NO_TOC_CONTINUE_PROMPT.format( + previous=json.dumps(all_items[-10:], indent=2), + pages=pages_text[:3000], + ) + + raw, _ = await _llm_with_finish_reason(prompt, max_tokens=2000, json_mode=False) + + parsed = extract_json(raw) + if isinstance(parsed, list): + all_items.extend(parsed) + elif isinstance(parsed, dict): + for v in parsed.values(): + if isinstance(v, list): + all_items.extend(v) + break + + for item in all_items: + pi = item.get("physical_index") + try: + item["physical_index"] = int(str(pi).split("_")[-1].rstrip(">").strip()) if pi else None + except Exception: + item["physical_index"] = None + item.setdefault("page", item.get("physical_index")) + + return all_items + + +# ───────────────────────────────────────────────────────────────────────────── +# Tree utilities +# ───────────────────────────────────────────────────────────────────────────── + +def count_nodes(nodes: list) -> int: + c = 0 + for n in nodes: + c += 1 + if n.get("nodes"): + c += count_nodes(n["nodes"]) + return c + + +# ───────────────────────────────────────────────────────────────────────────── +# Request / Response models +# ───────────────────────────────────────────────────────────────────────────── + +class UploadRequest(BaseModel): + file_url: str + file_name: Optional[str] = "document.pdf" + + +# ───────────────────────────────────────────────────────────────────────────── +# Endpoints +# ───────────────────────────────────────────────────────────────────────────── + +@app.get("/health") +def health(): + return { + "status": "ok", + "model": "llama-3.3-70b-versatile via Groq", + "groq_keys_loaded": len(GROQ_KEYS), + "approach": "PageIndex — 7-stage pipeline (TOC detect→extract→JSON→assign→tree→summaries→verify)", + } + + +@app.post("/doc/") +async def build_document_tree( + req: UploadRequest, + x_api_key: str = Header(...), +): + verify_key(x_api_key) + + if not GROQ_KEYS: + raise HTTPException(status_code=500, detail="No Groq API keys configured") + + # ── Download PDF ──────────────────────────────────────────────────────── + if req.file_url.startswith("data:"): + try: + _, encoded = req.file_url.split(",", 1) + pdf_bytes = base64.b64decode(encoded) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Invalid data URI: {e}") + else: + async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client: + r = await client.get(req.file_url) + if r.status_code != 200: + raise HTTPException(status_code=400, detail=f"Cannot download: HTTP {r.status_code}") + if "text/html" in r.headers.get("content-type", ""): + raise HTTPException( + status_code=400, + detail="URL returned HTML. For Google Drive use: https://drive.google.com/uc?export=download&id=FILE_ID", + ) + pdf_bytes = r.content + + tmp_path = None + try: + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: + tmp.write(pdf_bytes) + tmp_path = tmp.name + + # ── Extract pages ──────────────────────────────────────────────────── + page_list, total_pages = extract_pdf_pages(tmp_path) + raw_text = get_raw_text(page_list) + + if not raw_text.strip(): + raise HTTPException(status_code=400, detail="No text extracted — may be scanned/image-only PDF") + + # ── Stage 1: Detect TOC ────────────────────────────────────────────── + toc_pages = await detect_toc_pages(page_list, check_up_to=min(20, total_pages)) + has_toc = len(toc_pages) > 0 + print(f"[PageIndex] TOC detected on pages: {toc_pages}") + + if has_toc: + # ── Stage 2: Extract TOC text ──────────────────────────────────── + toc_raw_text = "\n\n".join( + page_list[phys - 1][0] for phys in toc_pages if 0 < phys <= total_pages + ) + toc_cleaned = await extract_toc_with_retry(toc_raw_text) + + # ── Stage 3: TOC → structured JSON ────────────────────────────── + toc_items = await toc_to_json(toc_cleaned) + print(f"[PageIndex] TOC items extracted: {len(toc_items)}") + + # ── Stage 4: Assign physical indices ──────────────────────────── + toc_items = await assign_physical_indices(toc_items, page_list, total_pages) + + else: + print("[PageIndex] No TOC found — falling back to page-by-page extraction") + toc_items = await extract_structure_no_toc(page_list) + print(f"[PageIndex] Fallback items extracted: {len(toc_items)}") + + if not toc_items: + raise HTTPException(status_code=500, detail="Failed to extract any structure from the document") + + # ── Stage 5: Build tree ────────────────────────────────────────────── + tree = build_tree_from_flat(toc_items, total_pages) + + # ── Stage 6: Summaries ─────────────────────────────────────────────── + tree = await add_node_summaries(tree, page_list) + + # ── Stage 7: Verify pages ──────────────────────────────────────────── + tree = await verify_node_pages(tree, page_list, total_pages) + + doc_id = f"pi-{uuid.uuid4().hex[:16]}" + + return { + "doc_id": doc_id, + "file_name": req.file_name, + "tree": tree, + "raw_text": raw_text, + "tree_node_count": count_nodes(tree), + "top_level_nodes": len(tree), + "total_pages": total_pages, + "toc_found": has_toc, + "toc_pages": toc_pages, + "status": "completed", + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Pipeline error: {str(e)}") + finally: + if tmp_path: + try: + os.unlink(tmp_path) + except Exception: + pass diff --git a/kits/assistant/pageIndex-notebooklm/updated/types.ts b/kits/assistant/pageIndex-notebooklm/updated/types.ts new file mode 100644 index 00000000..c828f979 --- /dev/null +++ b/kits/assistant/pageIndex-notebooklm/updated/types.ts @@ -0,0 +1,59 @@ +export interface Document { + doc_id: string; + file_name: string; + file_url: string; + tree_node_count: number; + status: string; + created_at: string; +} + +export interface TreeNode { + node_id: string; + title: string; + start_index: number; // physical page where section starts + end_index: number; // physical page where section ends + summary: string; // short 1-2 sentence description (≤200 chars), navigation only + nodes?: TreeNode[]; +} + +export interface RetrievedNode { + node_id: string; + title: string; + start_index: number; // exact start page + end_index: number; // exact end page + summary: string; // short description from tree node + page_content: string; // verbatim PDF text fetched from raw_text using start→end range +} + +export interface Message { + role: "user" | "assistant"; + content: string; +} + +export interface ChatResponse { + answer: string; + retrieved_nodes: RetrievedNode[]; + thinking: string; + doc_id: string; +} + +export interface UploadResponse { + doc_id: string; + file_name: string; + tree_node_count: string; + status: string; + saved: string; // "true" or "false" — comes as string from Lamatic + error: string; +} + +export interface ListResponse { + documents: Document[]; + total: number; +} + +export interface TreeResponse { + tree: TreeNode[]; + file_name: string; + tree_node_count: number; + created_at: string; +} From 6087c30b0a4bb9d0b4344c3728d433130bc5fbab Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 22 Mar 2026 07:31:15 +0530 Subject: [PATCH 02/29] feat: implement server actions for document orchestration, including upload, chat, list, and tree retrieval functionalities. --- .../pageIndex-notebooklm/actions/orchestrate.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts index bc39a321..673da550 100644 --- a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts @@ -22,8 +22,8 @@ export async function uploadDocument(file_url: string, file_name: string) { process.env.FLOW_ID_UPLOAD!, { file_url, file_name } ); - // Response shape: { status, result: { doc_id, file_name, ... } } - return response.result ?? response; + const data = (response.result ?? response) as Record; + return data; } catch (error) { console.error("Upload flow error:", error); throw new Error("Failed to upload document"); @@ -41,8 +41,7 @@ export async function chatWithDocument( process.env.FLOW_ID_CHAT!, { doc_id, query, messages } ); - - const data = response.result ?? response; + const data = (response.result ?? response) as Record; // retrieved_nodes comes back as a JSON string from the Code Node return { @@ -62,8 +61,7 @@ export async function listDocuments() { process.env.FLOW_ID_LIST!, {} ); - - const data = response.result ?? response; + const data = (response.result ?? response) as Record; // documents comes back as a JSON string (JSON.stringify applied in Code Node) return { @@ -84,14 +82,14 @@ export async function getDocumentTree(doc_id: string) { process.env.FLOW_ID_TREE!, { doc_id } ); - - const data = response.result ?? response; + const data = (response.result ?? response) as Record; // tree comes back as a JSON string (JSON.stringify applied in Code Node) + // Note: the flow outputMapping has a typo "tre_node_count" — handle both spellings return { ...data, tree: safeParseJSON(data?.tree, []), - tree_node_count: Number(data?.tree_node_count) || 0, + tree_node_count: Number(data?.tree_node_count ?? data?.tre_node_count) || 0, }; } catch (error) { console.error("Tree flow error:", error); From 0e48947db1c525e6b03e52bec4367f37ed8a489f Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 22 Mar 2026 14:08:54 +0530 Subject: [PATCH 03/29] feat: Implement PageIndex application for document intelligence, including document upload, chat, and tree index visualization. --- .../pageIndex-notebooklm/app/globals.css | 254 +++++++++++++-- .../pageIndex-notebooklm/app/layout.tsx | 28 +- .../pageIndex-notebooklm/app/page.tsx | 299 ++++++++++-------- .../components/ChatWindow.tsx | 248 +++++++++++---- .../components/DocumentList.tsx | 80 +++-- .../components/DocumentUpload.tsx | 117 +++++-- .../components/TreeViewer.tsx | 113 ++++--- .../config.json | 6 +- 8 files changed, 810 insertions(+), 335 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/app/globals.css b/kits/assistant/pageIndex-notebooklm/app/globals.css index 2d18e573..6e08d236 100644 --- a/kits/assistant/pageIndex-notebooklm/app/globals.css +++ b/kits/assistant/pageIndex-notebooklm/app/globals.css @@ -1,55 +1,243 @@ @import "tailwindcss"; +@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,600;12..96,700&family=Geist+Mono:wght@300;400;500&display=swap'); @layer base { :root { - --bg: #0d0f14; - --surface: #13161d; - --surface-2: #1a1e28; - --border: #252a38; - --border-hover: #323847; - --accent: #6366f1; - --accent-hover: #818cf8; - --accent-dim: rgba(99,102,241,0.12); - --text-primary: #e8eaf0; - --text-secondary: #8b91a8; - --text-muted: #4b5168; - --amber: #f59e0b; - --amber-dim: rgba(245,158,11,0.12); - --green: #10b981; - --red: #ef4444; - --radius: 10px; - --shadow: 0 4px 24px rgba(0,0,0,0.4); - } + /* Core palette — warm charcoal + luminous teal */ + --bg: #0a0b0d; + --bg-alt: #0e1013; + --surface: #111316; + --surface-1: #161a1f; + --surface-2: #1c2128; + --surface-3: #222933; - * { - box-sizing: border-box; + /* Borders */ + --border: rgba(255,255,255,0.07); + --border-md: rgba(255,255,255,0.11); + --border-hi: rgba(255,255,255,0.18); + + /* Accent — luminous teal */ + --accent: #2dd4bf; + --accent-2: #14b8a6; + --accent-dim: rgba(45,212,191,0.1); + --accent-glow: rgba(45,212,191,0.18); + + /* Text */ + --text-1: #f0f2f5; + --text-2: #8d97a8; + --text-3: #4a5568; + + /* Status */ + --amber: #f6c90e; + --amber-dim: rgba(246,201,14,0.1); + --green: #34d399; + --red: #f87171; + + /* Typography */ + --font-display: 'Bricolage Grotesque', system-ui, sans-serif; + --font-mono: 'Geist Mono', 'DM Mono', monospace; + + /* Spatial */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 22px; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0,0,0,0.4); + --shadow-md: 0 4px 16px rgba(0,0,0,0.5); + --shadow-lg: 0 12px 40px rgba(0,0,0,0.6); + --glow: 0 0 24px rgba(45,212,191,0.15); + + /* Transitions */ + --ease: cubic-bezier(0.25,0.46,0.45,0.94); + --ease-spring: cubic-bezier(0.34,1.56,0.64,1); } + * { box-sizing: border-box; } + html, body { height: 100%; margin: 0; padding: 0; + scroll-behavior: smooth; } body { background: var(--bg); - color: var(--text-primary); - font-family: 'DM Sans', system-ui, sans-serif; + color: var(--text-1); + font-family: var(--font-display); -webkit-font-smoothing: antialiased; - line-height: 1.5; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + line-height: 1.55; + font-size: 15px; } - ::-webkit-scrollbar { - width: 4px; - } - ::-webkit-scrollbar-track { - background: transparent; + ::selection { + background: var(--accent-dim); + color: var(--accent); } - ::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 4px; + + /* Scrollbar */ + * { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.1) transparent; } - ::-webkit-scrollbar-thumb:hover { - background: var(--border-hover); + ::-webkit-scrollbar { width: 3px; height: 3px; } + ::-webkit-scrollbar-track { background: transparent; } + ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; } + ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); } + + input, button, textarea { + font-family: inherit; } } + +/* ─── Keyframes ─────────────────────────────────────────── */ +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulse-dot { 0%,100%{transform:scale(1);opacity:.35} 50%{transform:scale(1.5);opacity:1} } +@keyframes float-in { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} } +@keyframes slide-in-r { from{opacity:0;transform:translateX(12px)} to{opacity:1;transform:translateX(0)} } +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +@keyframes msg-in-user { from{opacity:0;transform:translateX(20px) scale(.96)} to{opacity:1;transform:translateX(0) scale(1)} } +@keyframes msg-in-ai { from{opacity:0;transform:translateX(-20px) scale(.96)} to{opacity:1;transform:translateX(0) scale(1)} } +@keyframes glow-pulse { 0%,100%{box-shadow:0 0 0 0 var(--accent-glow)} 50%{box-shadow:0 0 20px 4px var(--accent-glow)} } + +/* ─── Utility classes ───────────────────────────────────── */ +.font-mono { font-family: var(--font-mono); } +.text-accent { color: var(--accent); } +.text-muted { color: var(--text-3); } +.text-sub { color: var(--text-2); } + +.surface { background: var(--surface-1); border: 1px solid var(--border); } + +.glow-border { box-shadow: var(--glow); border-color: rgba(45,212,191,0.3) !important; } + +/* Glass panel */ +.glass { + background: rgba(17,19,22,0.7); + backdrop-filter: blur(16px) saturate(160%); + -webkit-backdrop-filter: blur(16px) saturate(160%); + border: 1px solid var(--border); +} + +/* Subtle noise texture overlay via pseudo */ +.noise::before { + content: ''; + position: absolute; inset: 0; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E"); + pointer-events: none; z-index: 0; border-radius: inherit; +} + +/* Gradient accent line (top edge) */ +.accent-line::after { + content: ''; + position: absolute; top: 0; left: 0; right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--accent), transparent); + opacity: 0.6; +} + +/* Pill badge */ +.badge { + display: inline-flex; align-items: center; gap: 4px; + font-family: var(--font-mono); + font-size: 10px; font-weight: 500; + letter-spacing: 0.04em; + padding: 2px 8px; + border-radius: 20px; + border: 1px solid; +} +.badge-accent { + color: var(--accent); background: var(--accent-dim); border-color: rgba(45,212,191,0.25); +} +.badge-amber { + color: var(--amber); background: var(--amber-dim); border-color: rgba(246,201,14,0.25); +} +.badge-default { + color: var(--text-2); background: var(--surface-2); border-color: var(--border); +} + +/* Button variants */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 7px; + padding: 8px 16px; + border-radius: var(--radius-md); + border: 1px solid transparent; + font-family: var(--font-display); + font-size: 13px; font-weight: 500; + cursor: pointer; + transition: all 0.18s var(--ease); + outline: none; white-space: nowrap; flex-shrink: 0; +} +.btn:disabled { opacity: 0.4; cursor: not-allowed; } +.btn-ghost { + background: transparent; border-color: var(--border); color: var(--text-2); +} +.btn-ghost:hover:not(:disabled) { + background: var(--surface-2); border-color: var(--border-md); color: var(--text-1); +} +.btn-accent { + background: var(--accent); border-color: var(--accent); color: #041a17; + font-weight: 600; +} +.btn-accent:hover:not(:disabled) { + background: var(--accent-2); border-color: var(--accent-2); + box-shadow: var(--glow); +} +.btn-icon { + padding: 0; width: 36px; height: 36px; border-radius: var(--radius-md); +} + +/* Tab pill */ +.tab { + display: inline-flex; align-items: center; gap: 6px; + padding: 5px 13px; border-radius: var(--radius-md); + font-size: 13px; font-weight: 500; border: none; cursor: pointer; + transition: all 0.18s var(--ease); + background: transparent; color: var(--text-2); +} +.tab:hover { color: var(--text-1); } +.tab.active { + background: var(--surface-2); color: var(--text-1); + border: 1px solid var(--border-md); + box-shadow: var(--shadow-sm); +} + +/* Input */ +.input { + flex: 1; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 10px 14px; + font-size: 13.5px; + color: var(--text-1); + outline: none; + transition: border-color 0.18s, box-shadow 0.18s; + font-family: var(--font-display); +} +.input::placeholder { color: var(--text-3); } +.input:focus { + border-color: rgba(45,212,191,0.4); + box-shadow: 0 0 0 3px var(--accent-dim); +} + +/* Section label */ +.section-label { + font-family: var(--font-mono); + font-size: 10px; font-weight: 500; + letter-spacing: 0.1em; text-transform: uppercase; + color: var(--text-3); +} + +/* Divider */ +.divider { + height: 1px; + background: var(--border); + flex-shrink: 0; +} diff --git a/kits/assistant/pageIndex-notebooklm/app/layout.tsx b/kits/assistant/pageIndex-notebooklm/app/layout.tsx index 2e620b3b..dbb568e7 100644 --- a/kits/assistant/pageIndex-notebooklm/app/layout.tsx +++ b/kits/assistant/pageIndex-notebooklm/app/layout.tsx @@ -1,34 +1,24 @@ import type { Metadata } from "next"; -import { DM_Sans, DM_Mono } from "next/font/google"; import "./globals.css"; -const dmSans = DM_Sans({ - subsets: ["latin"], - variable: "--font-sans", - display: "swap", -}); - -const dmMono = DM_Mono({ - subsets: ["latin"], - weight: ["400", "500"], - variable: "--font-mono", - display: "swap", -}); - export const metadata: Metadata = { - title: "PageIndex NotebookLM — Vectorless Document Intelligence", + title: "PageIndex — Vectorless Document Intelligence", description: "Chat with your documents using PageIndex's agentic tree-structured retrieval. No vectors, no chunking — powered by Lamatic and Groq.", + viewport: "width=device-width, initial-scale=1", + themeColor: "#0a0b0d", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - - - {children} - + + + + + + {children} ); } diff --git a/kits/assistant/pageIndex-notebooklm/app/page.tsx b/kits/assistant/pageIndex-notebooklm/app/page.tsx index 9b428341..9d153b91 100644 --- a/kits/assistant/pageIndex-notebooklm/app/page.tsx +++ b/kits/assistant/pageIndex-notebooklm/app/page.tsx @@ -22,11 +22,7 @@ export default function Page() { try { const result = await listDocuments(); if (Array.isArray(result?.documents)) setDocuments(result.documents); - } catch { - // silent - } finally { - setListLoading(false); - } + } catch { /* silent */ } finally { setListLoading(false); } }, []); useEffect(() => { fetchDocuments(); }, [fetchDocuments]); @@ -39,11 +35,7 @@ export default function Page() { try { const result = await getDocumentTree(doc.doc_id); if (Array.isArray(result?.tree)) setTree(result.tree); - } catch { - setTree([]); - } finally { - setTreeLoading(false); - } + } catch { setTree([]); } finally { setTreeLoading(false); } } function handleRetrievedNodes(nodes: RetrievedNode[]) { @@ -51,56 +43,77 @@ export default function Page() { } return ( -
- {/* Header */} +
+ + {/* ── Header ─────────────────────────────────── */}
+ {/* Logo mark */}
- - + + +
+
-

- PageIndex NotebookLM +

+ PageIndex

-

- Vectorless RAG · Tree-structured retrieval +

+ Document Intelligence

+
- - Powered by PageIndex + Groq + {/* Live indicator */} +
+ + + ONLINE + +
+ +
+ + + Groq · PageIndex
+ + {/* Accent line bottom */} +
- {/* Body */} + {/* ── Body ───────────────────────────────────── */}
- {/* Sidebar */} + + {/* ── Sidebar ─── */} - {/* Main */} -
+ {/* ── Main ─── */} +
{selectedDoc ? (
+ {/* Tab bar */}
( ))} - - {selectedDoc.file_name} - + +
+ + {selectedDoc.file_name} + +
{/* Content */} -
+
{activeTab === "chat" ? ( {treeLoading ? ( -
- - - +
+ + - Loading tree… + + Building tree… +
) : tree.length > 0 ? ( ) : ( -
+
No tree structure available.
)} @@ -241,41 +256,67 @@ export default function Page() {
) : ( + /* Empty state */
-
+
+ {/* Large icon */}
- - + + +
-

+ +

Select a document

-

- Upload a PDF or pick one from the sidebar to start chatting. +

+ Upload a PDF or Markdown file, then pick it from the sidebar to start an AI conversation.

+ + {/* Info card */}
- How PageIndex works - Builds a hierarchical tree index — like a table of contents optimised for AI. The LLM navigates the tree to find relevant sections, then fetches verbatim page content. No vectors, no chunking. +
+

+ How it works +

+ {[ + ["🌳", "Tree Index", "Builds a hierarchical map of your document like a smart table of contents."], + ["🔍", "Agentic Search", "The LLM navigates the tree to find exactly the right section."], + ["📄", "Verbatim Retrieval", "Returns exact page content — no chunking, no hallucination."], + ].map(([icon, title, desc]) => ( +
+ {icon} +
+

{title as string}

+

{desc as string}

+
+
+ ))}
)}
- -
); } diff --git a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx index e20e3515..2dffe98a 100644 --- a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx @@ -32,7 +32,7 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) setLoading(true); try { const result = await chatWithDocument(docId, userMsg.content, newMsgs); - setMessages(prev => [...prev, { role: "assistant", content: result.answer || "Sorry, no answer found." }]); + setMessages(prev => [...prev, { role: "assistant", content: result.answer || "No answer found." }]); if (Array.isArray(result.retrieved_nodes) && result.retrieved_nodes.length) { setLastNodes(result.retrieved_nodes); setLastThinking(result.thinking || ""); @@ -40,44 +40,130 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) } } catch { setMessages(prev => [...prev, { role: "assistant", content: "Something went wrong. Please try again." }]); - } finally { - setLoading(false); - } + } finally { setLoading(false); } } return (
- {/* Messages */} -
+ + {/* ── Chat header ── */} +
+
+ + {loading ? "Searching tree…" : "READY"} + + + {docName} + +
+ + {/* ── Messages ── */} +
+ {messages.length === 0 && ( -
-
- +
+
+
-

Ask anything

-

- The tree index navigates to the right page range in {docName} +

+ Ask anything +

+

+ The tree index navigates to the right section in{" "} + {docName}

+ + {/* Suggested prompts */} +
+ {["Summarize this document", "What are the key findings?", "Explain the main argument"].map(q => ( + + ))} +
)} {messages.map((msg, i) => ( -
+
+ {msg.role === "assistant" && ( +
+ + + +
+ )}
{msg.content}
@@ -85,32 +171,59 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) ))} {loading && ( -
-
- - {[0,1,2].map(i => ( - +
+
+ + + +
+
+ + {[0, 1, 2].map(i => ( + ))} - Searching tree… + + navigating tree +
)}
- {/* Sources panel — now showing start_index→end_index page ranges */} + {/* ── Sources panel ── */} {lastNodes.length > 0 && ( -
- @@ -118,24 +231,36 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) {sourcesOpen && (
{lastThinking && ( -
- Tree reasoning: {lastThinking} +
+ + Tree reasoning + +
+ {lastThinking}
)} {lastNodes.map(node => ( -
-
- {node.title} - - pp.{node.start_index}–{node.end_index} - +
+
+ {node.title} + pp.{node.start_index}–{node.end_index}
{node.summary && ( -

+

{node.summary}

)} -

+

{node.page_content}

@@ -145,27 +270,36 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props)
)} - {/* Input */} -
+ {/* ── Input ── */} + setInput(e.target.value)} placeholder="Ask a question about this document…" disabled={loading} - style={{ flex: 1, padding: "9px 14px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: "8px", fontSize: "13px", color: "var(--text-primary)", outline: "none", transition: "border-color 0.15s" }} - onFocus={e => (e.currentTarget.style.borderColor = "var(--accent)")} - onBlur={e => (e.currentTarget.style.borderColor = "var(--border)")} /> -
); } diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx index 5d643949..41622a68 100644 --- a/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx @@ -11,55 +11,89 @@ interface Props { export default function DocumentList({ documents, selectedId, onSelect }: Props) { if (documents.length === 0) { return ( -
- - +
+ + + -

No documents yet.

-

Upload a PDF to get started.

+

+ No documents yet +

+

+ Upload a PDF to get started +

); } return (
    - {documents.map((doc) => { + {documents.map((doc, idx) => { const active = selectedId === doc.doc_id; return ( -
  • +
  • ); diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx b/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx index f8854bdb..2692e201 100644 --- a/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx @@ -40,58 +40,109 @@ export default function DocumentUpload({ onUploaded }: Props) { }); } - const iconColor = dragging ? "var(--accent)" : status === "success" ? "var(--green)" : status === "error" ? "var(--red)" : "var(--text-muted)"; - const borderColor = dragging ? "var(--accent)" : status === "success" ? "var(--green)" : status === "error" ? "var(--red)" : "var(--border)"; + const isIdle = status === "idle"; return (
    status === "idle" && inputRef.current?.click()} + onClick={() => isIdle && inputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={(e) => { e.preventDefault(); setDragging(false); const f = e.dataTransfer.files[0]; if (f) processFile(f); }} style={{ - border: `1px dashed ${borderColor}`, - borderRadius: "10px", - padding: "16px", - cursor: status === "idle" ? "pointer" : "default", - background: dragging ? "var(--accent-dim)" : "var(--surface-2)", - display: "flex", flexDirection: "column", alignItems: "center", gap: "6px", + borderRadius: "var(--radius-md)", + padding: "14px 12px", + cursor: isIdle ? "pointer" : "default", + border: `1.5px dashed ${ + dragging ? "var(--accent)" : + status === "success" ? "var(--green)" : + status === "error" ? "var(--red)" : + status === "uploading" ? "rgba(45,212,191,0.3)" : + "var(--border-md)" + }`, + background: dragging + ? "var(--accent-dim)" + : status === "success" + ? "rgba(52,211,153,0.06)" + : status === "error" + ? "rgba(248,113,113,0.06)" + : "var(--surface-2)", + display: "flex", flexDirection: "column", alignItems: "center", gap: "5px", textAlign: "center", - transition: "all 0.2s", + transition: "all 0.22s var(--ease)", + position: "relative", overflow: "hidden", + boxShadow: dragging ? "var(--glow)" : "none", }} > - { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ""; }} /> + { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ""; }} + /> - {status === "uploading" ? ( + {status === "uploading" && ( <> -
    - - - - Indexing document… -
    -

    Building tree structure

    + {/* Progress shimmer line */} +
    + + + +

    Indexing…

    +

    + Building tree structure +

    - ) : status === "success" ? ( + )} + + {status === "success" && ( <> -
    - - Ready +
    + + +
    -

    {message}

    +

    Indexed

    +

    {message}

    - ) : status === "error" ? ( + )} + + {status === "error" && ( <> -
    Upload failed
    -

    {message}

    + + + +

    Failed

    +

    {message}

    - ) : ( + )} + + {isIdle && ( <> - - - -

    Upload a document

    -

    PDF or Markdown · drag & drop

    +
    + + + + + +
    +

    + Upload document +

    +

    + PDF · Markdown · drop or click +

    )}
    diff --git a/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx index f4e1f62d..0b7da450 100644 --- a/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx @@ -18,66 +18,78 @@ function TreeNodeRow({ node, depth, highlightedIds }: { node: TreeNode; depth: n : `pp.${node.start_index}–${node.end_index}`; return ( -
    +
    hasChildren && setOpen(o => !o)} style={{ display: "flex", alignItems: "flex-start", gap: "8px", - padding: `7px 10px 7px ${10 + depth * 18}px`, - borderRadius: "8px", + padding: `7px 10px 7px ${10 + depth * 16}px`, + borderRadius: "var(--radius-md)", cursor: hasChildren ? "pointer" : "default", background: isHighlighted ? "var(--amber-dim)" : "transparent", - border: isHighlighted ? "1px solid rgba(245,158,11,0.3)" : "1px solid transparent", - transition: "background 0.15s", + border: `1px solid ${isHighlighted ? "rgba(246,201,14,0.25)" : "transparent"}`, + transition: "all 0.18s var(--ease)", marginBottom: "2px", + boxShadow: isHighlighted ? "0 0 12px rgba(246,201,14,0.08)" : "none", }} onMouseEnter={e => { if (!isHighlighted) e.currentTarget.style.background = "var(--surface-2)"; }} onMouseLeave={e => { if (!isHighlighted) e.currentTarget.style.background = "transparent"; }} > - {/* Expand/collapse icon */} - + {/* Chevron / dot */} + {hasChildren ? ( - + ) : ( - - - + )} {/* Content */}
    -
    +
    {node.title} {pageSpan} {isHighlighted && ( - + retrieved )}
    - {/* Summary — the key new field from updated workflow */} {node.summary && (

    - {open && hasChildren && node.nodes!.map(child => ( - - ))} + {/* Children */} + {open && hasChildren && ( +
    + {node.nodes!.map(child => ( + + ))} +
    + )}
    ); } @@ -102,31 +123,44 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { return (
    {/* Header */} -
    +
    -

    Document Tree

    -

    +

    + Document Tree +

    +

    {fileName}

    -
    +
    {highlightedIds.length > 0 && ( - + {highlightedIds.length} retrieved )} - + {totalNodes(tree)} nodes
    - {/* Tree */} -
    + {/* Tree body */} +
    {tree.map(node => ( ))} @@ -135,14 +169,17 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { {/* Retrieved footer */} {highlightedIds.length > 0 && (
    - + - {highlightedIds.length} node{highlightedIds.length !== 1 ? "s" : ""} used in last answer — highlighted above + + {highlightedIds.length} NODE{highlightedIds.length !== 1 ? "S" : ""} USED IN LAST ANSWER +
    )}
    diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json index a8bc76e0..ca9e005e 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -72,7 +72,7 @@ }, "values": { "id": "codeNode_570", - "code": "\n\nvar apiData = {{apiNode_948.output}};\n\n// Parse if Lamatic injected it as a string\nif (typeof apiData === \"string\") {\n apiData = JSON.parse(apiData);\n}\n\nvar doc_id = apiData.doc_id || \"\";\nvar file_name = apiData.file_name || \"\";\nvar tree = apiData.tree || [];\nvar raw_text = apiData.raw_text || \"\";\nvar tree_node_count = apiData.tree_node_count || 0;\nvar file_url = {{triggerNode_1.output.file_url}} || \"\";\nvar supabase_url = {{secrets.project.SUPABASE_URL}} || \"\";\nvar supabase_key = {{secrets.project.SUPABASE_ANON_KEY}} || \"\";\n\nvar payload = JSON.stringify({\n doc_id: doc_id,\n file_name: file_name,\n file_url: file_url,\n tree: tree,\n raw_text: raw_text,\n tree_node_count: tree_node_count,\n status: \"completed\"\n});\n\nvar response = await fetch(\n supabase_url + \"/rest/v1/documents\",\n {\n method: \"POST\",\n headers: {\n \"apikey\": supabase_key,\n \"Authorization\": \"Bearer \" + supabase_key,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: payload\n }\n);\n\nvar result = await response.json();\n\nif (!response.ok) {\n output = {\n success: false,\n doc_id: \"\",\n file_name: file_name,\n tree_node_count: 0,\n error: result.message || result.detail || JSON.stringify(result),\n status_code: response.status\n };\n} else {\n var inserted = Array.isArray(result) ? result[0] : result;\n output = {\n success: true,\n doc_id: inserted.doc_id || doc_id,\n file_name: inserted.file_name || file_name,\n tree_node_count: inserted.tree_node_count || tree_node_count,\n status: \"completed\",\n error: \"\",\n status_code: response.status\n };\n}", + "code": "\n\nvar apiData = {{apiNode_948.output}};\n\nif (typeof apiData === \"string\") {\n apiData = JSON.parse(apiData);\n}\n\n// ✅ Strip null bytes and other problematic Unicode escape sequences\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str\n .replace(/\\u0000/g, \"\") // null bytes (main culprit)\n .replace(/\\\\u0000/g, \"\") // escaped null bytes as literal string\n .replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\"); // other control chars\n}\n\nvar doc_id = apiData.doc_id || \"\";\nvar file_name = apiData.file_name || \"\";\nvar tree = apiData.tree || [];\nvar raw_text = sanitize(apiData.raw_text || \"\"); // ✅ sanitized\nvar tree_node_count = apiData.tree_node_count || 0;\nvar file_url = {{triggerNode_1.output.file_url}} || \"\";\nvar supabase_url = {{secrets.project.SUPABASE_URL}} || \"\";\nvar supabase_key = {{secrets.project.SUPABASE_ANON_KEY}} || \"\";\n\n// ✅ Also sanitize the tree JSON (in case summaries/titles have null bytes)\nvar sanitized_tree = JSON.parse(sanitize(JSON.stringify(tree)));\n\nvar payload = JSON.stringify({\n doc_id: doc_id,\n file_name: file_name,\n file_url: file_url,\n tree: sanitized_tree,\n raw_text: raw_text,\n tree_node_count: tree_node_count,\n status: \"completed\"\n});\n\n// ... rest of your fetch code unchanged\n\nvar response = await fetch(\n supabase_url + \"/rest/v1/documents\",\n {\n method: \"POST\",\n headers: {\n \"apikey\": supabase_key,\n \"Authorization\": \"Bearer \" + supabase_key,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: payload\n }\n);\n\nvar result = await response.json();\n\nif (!response.ok) {\n output = {\n success: false,\n doc_id: \"\",\n file_name: file_name,\n tree_node_count: 0,\n error: result.message || result.detail || JSON.stringify(result),\n status_code: response.status\n };\n} else {\n var inserted = Array.isArray(result) ? result[0] : result;\n output = {\n success: true,\n doc_id: inserted.doc_id || doc_id,\n file_name: inserted.file_name || file_name,\n tree_node_count: inserted.tree_node_count || tree_node_count,\n status: \"completed\",\n error: \"\",\n status_code: response.status\n };\n}", "nodeName": "Code" } }, @@ -85,7 +85,7 @@ "x": 0, "y": 260 }, - "selected": false + "selected": true }, { "id": "responseNode_triggerNode_1", @@ -113,7 +113,7 @@ "x": 0, "y": 390 }, - "selected": true + "selected": false } ], "edges": [ From 1b647ba49d263288dc1645abea663932c7d2d6f0 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 22 Mar 2026 15:20:23 +0530 Subject: [PATCH 04/29] chore: Delete an unspecified file. --- .../updated/ChatWindow.tsx | 182 ---- .../pageIndex-notebooklm/updated/README.md | 159 ---- .../updated/TreeViewer.tsx | 114 --- .../pageIndex-notebooklm/updated/config.json | 50 -- .../pageIndex-notebooklm/updated/main.py | 803 ------------------ .../pageIndex-notebooklm/updated/types.ts | 59 -- 6 files changed, 1367 deletions(-) delete mode 100644 kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx delete mode 100644 kits/assistant/pageIndex-notebooklm/updated/README.md delete mode 100644 kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx delete mode 100644 kits/assistant/pageIndex-notebooklm/updated/config.json delete mode 100644 kits/assistant/pageIndex-notebooklm/updated/main.py delete mode 100644 kits/assistant/pageIndex-notebooklm/updated/types.ts diff --git a/kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx deleted file mode 100644 index 172cf3ae..00000000 --- a/kits/assistant/pageIndex-notebooklm/updated/ChatWindow.tsx +++ /dev/null @@ -1,182 +0,0 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import { chatWithDocument } from "@/actions/orchestrate"; -import { Message, RetrievedNode } from "@/lib/types"; -import { Send, Loader2, ChevronDown, ChevronUp, BookOpen } from "lucide-react"; -import clsx from "clsx"; - -interface Props { - docId: string; - docName: string; - onRetrievedNodes?: (nodes: RetrievedNode[]) => void; -} - -export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const [expandedSource, setExpandedSource] = useState(null); - const [lastNodes, setLastNodes] = useState([]); - const [lastThinking, setLastThinking] = useState(""); - const bottomRef = useRef(null); - - useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages, loading]); - - async function handleSend(e: React.FormEvent) { - e.preventDefault(); - if (!input.trim() || loading) return; - - const userMsg: Message = { role: "user", content: input.trim() }; - const newMessages = [...messages, userMsg]; - setMessages(newMessages); - setInput(""); - setLoading(true); - - try { - const result = await chatWithDocument(docId, userMsg.content, newMessages); - - const assistantMsg: Message = { - role: "assistant", - content: result.answer || "Sorry, I could not find an answer.", - }; - setMessages((prev) => [...prev, assistantMsg]); - - if (result.retrieved_nodes && Array.isArray(result.retrieved_nodes)) { - setLastNodes(result.retrieved_nodes); - setLastThinking(result.thinking || ""); - onRetrievedNodes?.(result.retrieved_nodes); - } - } catch { - setMessages((prev) => [ - ...prev, - { role: "assistant", content: "Something went wrong. Please try again." }, - ]); - } finally { - setLoading(false); - } - } - - return ( -
    - {/* Header */} -
    - - - Chatting with: {docName} - -
    - - {/* Messages */} -
    - {messages.length === 0 && ( -
    -

    💬

    -

    Ask anything about this document

    -

    The tree index will navigate to the right sections.

    -
    - )} - - {messages.map((msg, i) => ( -
    -
    - {msg.content} -
    -
    - ))} - - {loading && ( -
    -
    - - Searching document tree... -
    -
    - )} - -
    -
    - - {/* Retrieved sources panel */} - {lastNodes.length > 0 && ( -
    - - - {expandedSource && ( -
    - {lastThinking && ( -
    - Tree reasoning: - {lastThinking} -
    - )} - {lastNodes.map((node) => ( -
    -
    - {node.title} - pp.{node.start_index}–{node.end_index} -
    -

    {node.page_content}

    -
    - ))} -
    - )} -
    - )} - - {/* Input */} -
    - setInput(e.target.value)} - placeholder="Ask a question about this document..." - disabled={loading} - className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50" - /> - -
    -
    - ); -} diff --git a/kits/assistant/pageIndex-notebooklm/updated/README.md b/kits/assistant/pageIndex-notebooklm/updated/README.md deleted file mode 100644 index a206566e..00000000 --- a/kits/assistant/pageIndex-notebooklm/updated/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# PageIndex NotebookLM — AgentKit - -Upload any PDF and chat with it using **vectorless, tree-structured RAG**. -No vector database. No chunking. Just a hierarchical document index built from the table of contents. - ---- - -## How It Works - -A 7-stage FastAPI pipeline (running on Railway, powered by Groq) processes each PDF: - -1. **TOC Detection** — concurrent LLM scan of first 20 pages -2. **TOC Extraction** — multi-pass extraction with completion verification -3. **TOC → JSON** — structured flat list with hierarchy (`1`, `1.1`, `1.2.3`) -4. **Physical Index Assignment** — verify each section starts on the correct page (±3 scan) -5. **Tree Build** — nested structure with exact `start_index` + `end_index` per section -6. **Summaries** — concurrent 1-2 sentence summary per node (≤200 chars) -7. **Page Verification** — fuzzy match each node title against actual page text - -At query time, the LLM navigates the tree like a table of contents to pick the right sections, then fetches verbatim page content using the exact `start_index → end_index` range. - ---- - -## Stack - -| Layer | Technology | -|-------|-----------| -| Orchestration | Lamatic AI (4 flows) | -| LLM | Groq — llama-3.3-70b-versatile (multi-key pool, free tier) | -| Indexing API | FastAPI on Railway | -| Storage | Supabase (PostgreSQL) | -| Frontend | Next.js + Tailwind CSS | - ---- - -## Prerequisites - -- [Lamatic AI](https://lamatic.ai) account (free) -- [Groq](https://console.groq.com) account (free — create multiple for key pool) -- [Railway](https://railway.app) account (for FastAPI server) -- [Supabase](https://supabase.com) account (free tier) -- Node.js 18+ - ---- - -## Setup - -### 1. Deploy the FastAPI Server (Railway) - -```bash -# Clone your fork, then: -cd pageindex-server # contains main.py + requirements.txt -railway init -railway up -``` - -Add these environment variables in Railway dashboard: - -| Variable | Value | -|----------|-------| -| `SERVER_API_KEY` | Any secret string (e.g. `openssl rand -hex 16`) | -| `GROQ_API_KEY_1` | First Groq API key | -| `GROQ_API_KEY_2` | Second Groq API key (optional — more keys = higher throughput) | - -Note your Railway URL: `https://your-app.up.railway.app` - -### 2. Set Up Supabase - -Run this SQL in Supabase SQL Editor: - -```sql -create table documents ( - id uuid default gen_random_uuid() primary key, - doc_id text unique not null, - file_name text, - file_url text, - tree jsonb, - raw_text text, - tree_node_count integer default 0, - status text default 'completed', - created_at timestamptz default now() -); -alter table documents enable row level security; -create policy "service_access" on documents for all using (true); -``` - -### 3. Set Up Lamatic Flows - -Import all 4 flows from the `flows/` folder into Lamatic Studio, then add these secrets in **Lamatic → Settings → Secrets**: - -| Secret | Value | -|--------|-------| -| `SERVER_API_KEY` | Same value as Railway | -| `SUPABASE_URL` | `https://xxx.supabase.co` | -| `SUPABASE_ANON_KEY` | From Supabase Settings → API | - -### 4. Install and Configure the Kit - -```bash -cd kits/assistant/pageindex-notebooklm -npm install -cp .env.example .env.local -``` - -Fill in `.env.local`: - -``` -LAMATIC_API_KEY=... # Lamatic → Settings → API Keys -LAMATIC_PROJECT_ID=... # Lamatic → Settings → Project ID -LAMATIC_API_URL=... # Lamatic → Settings → API Docs → Endpoint - -FLOW_ID_UPLOAD=... # Flow 1 → three-dot menu → Copy ID -FLOW_ID_CHAT=... # Flow 2 → three-dot menu → Copy ID -FLOW_ID_LIST=... # Flow 3 → three-dot menu → Copy ID -FLOW_ID_TREE=... # Flow 4 → three-dot menu → Copy ID -``` - -### 5. Run Locally - -```bash -npm run dev -# → http://localhost:3000 -``` - ---- - -## Flows - -| Flow | File | Purpose | -|------|------|---------| -| Upload | `flows/pageindex-upload/` | Download PDF → 7-stage pipeline → save tree to Supabase | -| Chat | `flows/pageindex-chat/` | Tree search → page fetch → Groq answer | -| List | `flows/pageindex-list/` | List all documents from Supabase | -| Tree | `flows/pageindex-tree/` | Return full tree JSON for a document | - ---- - -## Deploying to Vercel - -```bash -# Push your branch first -git checkout -b feat/pageindex-notebooklm -git add kits/assistant/pageindex-notebooklm/ -git commit -m "feat: Add PageIndex NotebookLM — vectorless tree-structured RAG" -git push origin feat/pageindex-notebooklm -``` - -Then in Vercel: -1. Import your forked repo -2. Set **Root Directory** → `kits/assistant/pageindex-notebooklm` -3. Add all 7 env vars from `.env.local` -4. Deploy - ---- - -## Author - -**Saurabh Tiwari** — [st108113@gmail.com](mailto:st108113@gmail.com) -GitHub: [@Skt329](https://github.com/Skt329) diff --git a/kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx b/kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx deleted file mode 100644 index 63ffe4b8..00000000 --- a/kits/assistant/pageIndex-notebooklm/updated/TreeViewer.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { TreeNode } from "@/lib/types"; -import { ChevronRight, ChevronDown, BookOpen } from "lucide-react"; -import clsx from "clsx"; - -interface TreeNodeProps { - node: TreeNode; - depth: number; - highlightedIds?: string[]; -} - -function TreeNodeItem({ node, depth, highlightedIds = [] }: TreeNodeProps) { - const [expanded, setExpanded] = useState(depth < 1); - const hasChildren = node.nodes && node.nodes.length > 0; - const isHighlighted = highlightedIds.includes(node.node_id); - - return ( -
    -
    hasChildren && setExpanded(!expanded)} - > -
    - {hasChildren ? ( - expanded ? ( - - ) : ( - - ) - ) : ( -
    -
    -
    - )} -
    - -
    -
    - - {node.title} - - - pp.{node.start_index}–{node.end_index} - - {isHighlighted && ( - - retrieved - - )} -
    - {node.summary && ( -

    - {node.summary} -

    - )} -
    -
    - - {hasChildren && expanded && ( -
    - {node.nodes!.map((child) => ( - - ))} -
    - )} -
    - ); -} - -interface Props { - tree: TreeNode[]; - fileName: string; - highlightedIds?: string[]; -} - -export default function TreeViewer({ tree, fileName, highlightedIds = [] }: Props) { - return ( -
    -
    - - {fileName} - {tree.length} sections -
    -
    - {tree.map((node) => ( - - ))} -
    -
    - ); -} diff --git a/kits/assistant/pageIndex-notebooklm/updated/config.json b/kits/assistant/pageIndex-notebooklm/updated/config.json deleted file mode 100644 index 7657a7f0..00000000 --- a/kits/assistant/pageIndex-notebooklm/updated/config.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "PageIndex NotebookLM", - "description": "Upload PDFs and chat with them using vectorless, tree-structured RAG powered by PageIndex. A 7-stage pipeline (TOC detection → extraction → JSON transform → physical index assignment → tree build → summaries → page verification) builds a hierarchical document tree with exact page ranges. No vector database, no chunking.", - "tags": ["🤖 Agentic", "📚 RAG", "🗂️ Assistant", "✨ Generative"], - "author": { - "name": "Saurabh Tiwari", - "email": "st108113@gmail.com" - }, - "steps": [ - { - "id": "pageindex-upload", - "type": "mandatory", - "envKey": "FLOW_ID_UPLOAD", - "description": "Uploads PDF, runs 7-stage PageIndex pipeline via FastAPI, saves tree to Supabase" - }, - { - "id": "pageindex-chat", - "type": "mandatory", - "envKey": "FLOW_ID_CHAT", - "description": "Tree-based section search using exact start/end page ranges + Groq answer generation" - }, - { - "id": "pageindex-list", - "type": "mandatory", - "envKey": "FLOW_ID_LIST", - "description": "Lists all uploaded documents from Supabase" - }, - { - "id": "pageindex-tree", - "type": "mandatory", - "envKey": "FLOW_ID_TREE", - "description": "Returns full nested tree structure JSON for a document" - } - ], - "integrations": ["PageIndex", "Supabase", "Groq"], - "features": [ - "Vectorless RAG — no embeddings or vector database", - "7-stage PageIndex pipeline: TOC detection, extraction, physical index assignment, tree build, summaries, verification", - "Exact page-range retrieval (start_index → end_index per section)", - "Multi-key Groq pool with round-robin rotation for free-tier throughput", - "Fallback extraction for documents without a formal TOC", - "Multi-document management with Supabase persistence", - "Interactive collapsible tree visualizer", - "Chat with full conversation history" - ], - "demoUrl": "", - "githubUrl": "https://github.com/Lamatic/AgentKit/tree/main/kits/assistant/pageindex-notebooklm", - "deployUrl": "", - "documentationUrl": "" -} diff --git a/kits/assistant/pageIndex-notebooklm/updated/main.py b/kits/assistant/pageIndex-notebooklm/updated/main.py deleted file mode 100644 index 6410f3a9..00000000 --- a/kits/assistant/pageIndex-notebooklm/updated/main.py +++ /dev/null @@ -1,803 +0,0 @@ -""" -PageIndex FastAPI — Full Implementation -Mirrors the VectifyAI/PageIndex architecture with Groq multi-key pool. -""" - -import os -import asyncio -import base64 -import json -import re -import tempfile -import uuid -import itertools -from typing import Optional - -import httpx -from fastapi import FastAPI, HTTPException, Header -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel - -# ───────────────────────────────────────────────────────────────────────────── -# App setup -# ───────────────────────────────────────────────────────────────────────────── - -app = FastAPI(title="PageIndex — Groq Multi-Key") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], -) - - -def verify_key(x_api_key: str): - expected = os.getenv("SERVER_API_KEY", "") - if expected and x_api_key != expected: - raise HTTPException(status_code=401, detail="Invalid API key") - - -# ───────────────────────────────────────────────────────────────────────────── -# Groq Multi-Key Pool -# ───────────────────────────────────────────────────────────────────────────── - -def _load_groq_keys() -> list[str]: - """Auto-detect all available GROQ_API_KEY_N vars (N=1..20) plus legacy GROQ_API_KEY.""" - keys = [] - for i in range(1, 21): - k = os.getenv(f"GROQ_API_KEY_{i}") - if k and k.strip(): - keys.append(k.strip()) - legacy = os.getenv("GROQ_API_KEY", "") - if legacy.strip() and legacy.strip() not in keys: - keys.append(legacy.strip()) - return keys - - -GROQ_KEYS: list[str] = [] -_key_cycle = None -_key_lock = asyncio.Lock() - - -@app.on_event("startup") -async def startup(): - global GROQ_KEYS, _key_cycle - GROQ_KEYS = _load_groq_keys() - if not GROQ_KEYS: - raise RuntimeError("No Groq API keys found. Set GROQ_API_KEY_1 ... GROQ_API_KEY_N") - _key_cycle = itertools.cycle(range(len(GROQ_KEYS))) - print(f"[PageIndex] Loaded {len(GROQ_KEYS)} Groq API key(s)") - - -async def _next_key() -> tuple[int, str]: - async with _key_lock: - idx = next(_key_cycle) - return idx, GROQ_KEYS[idx] - - -async def llm_call( - prompt: str, - max_tokens: int = 1024, - json_mode: bool = True, - system: str = "You are an expert document analyst. Return valid JSON only, no markdown, no extra text.", -) -> str: - max_tokens = min(max_tokens, 4096) - tried_keys: set[int] = set() - - for attempt in range(len(GROQ_KEYS) + 1): - idx, key = await _next_key() - if idx in tried_keys and len(tried_keys) >= len(GROQ_KEYS): - raise HTTPException(status_code=429, detail="All Groq keys rate-limited") - tried_keys.add(idx) - - payload = { - "model": "llama-3.3-70b-versatile", - "messages": [ - {"role": "system", "content": system}, - {"role": "user", "content": prompt}, - ], - "temperature": 0.1, - "max_tokens": max_tokens, - } - if json_mode: - payload["response_format"] = {"type": "json_object"} - - async with httpx.AsyncClient(timeout=120) as client: - resp = await client.post( - "https://api.groq.com/openai/v1/chat/completions", - headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, - json=payload, - ) - - if resp.status_code == 429: - await asyncio.sleep(1) - continue - - data = resp.json() - if "error" in data: - err = data["error"].get("message", str(data["error"])) - if "rate_limit" in err.lower() or "token" in err.lower(): - await asyncio.sleep(1) - continue - raise HTTPException(status_code=500, detail=f"Groq error: {err}") - - return data["choices"][0]["message"]["content"].strip() - - raise HTTPException(status_code=429, detail="All Groq keys exhausted") - - -async def llm_call_many(prompts: list[str], max_tokens: int = 512, json_mode: bool = True) -> list[str]: - tasks = [llm_call(p, max_tokens=max_tokens, json_mode=json_mode) for p in prompts] - return await asyncio.gather(*tasks) - - -def extract_json(text: str) -> dict | list: - text = text.strip() - if text.startswith("```"): - text = re.sub(r"^```(?:json)?\n?", "", text) - text = re.sub(r"\n?```$", "", text) - try: - return json.loads(text) - except json.JSONDecodeError: - for pattern in [r"\{.*\}", r"\[.*\]"]: - m = re.search(pattern, text, re.DOTALL) - if m: - try: - return json.loads(m.group()) - except Exception: - pass - return {} - - -# ───────────────────────────────────────────────────────────────────────────── -# PDF Extraction -# ───────────────────────────────────────────────────────────────────────────── - -def extract_pdf_pages(pdf_path: str) -> tuple[list[tuple[str, int]], int]: - import pypdf - reader = pypdf.PdfReader(pdf_path) - total = len(reader.pages) - pages = [] - for i, page in enumerate(reader.pages): - t = page.extract_text() or "" - pages.append((t.strip(), i + 1)) - return pages, total - - -def get_raw_text(page_list: list[tuple[str, int]]) -> str: - parts = [] - for text, phys in page_list: - if text: - parts.append(f"[PAGE {phys}]\n{text}") - return "\n\n".join(parts) - - -def get_page_content_by_range(page_list: list, start: int, end: int) -> str: - parts = [] - for text, phys in page_list: - if start <= phys <= end: - parts.append(f"\n{text}\n\n") - return "\n".join(parts) - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 1: TOC Detection -# ───────────────────────────────────────────────────────────────────────────── - -async def detect_toc_pages(page_list: list[tuple[str, int]], check_up_to: int = 20) -> list[int]: - candidates = page_list[:check_up_to] - prompts = [] - for text, phys in candidates: - prompts.append(f"""Does the following page contain a Table of Contents (list of chapters/sections with page numbers)? -Note: abstract, summary, notation lists, figure lists are NOT table of contents. - -Page text: -{text[:2000]} - -Return JSON: {{"thinking": "", "toc_detected": "yes or no"}}""") - - results = await llm_call_many(prompts, max_tokens=150, json_mode=True) - - toc_pages = [] - for raw, (text, phys) in zip(results, candidates): - parsed = extract_json(raw) - if isinstance(parsed, dict) and parsed.get("toc_detected") == "yes": - toc_pages.append(phys) - return toc_pages - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 2: TOC Extraction -# ───────────────────────────────────────────────────────────────────────────── - -EXTRACT_TOC_PROMPT = """Extract the FULL table of contents from the given text. -Replace any ......... or dotted leaders with :. -Return ONLY the table of contents text, nothing else.""" - -VERIFY_TOC_COMPLETE_PROMPT = """You are given a raw table of contents and a cleaned/extracted version. -Check if the cleaned version is complete (contains all main sections from the raw). - -Raw TOC: -{raw} - -Cleaned TOC: -{cleaned} - -Return JSON: {{"thinking": "", "completed": "yes or no"}}""" - -CONTINUE_TOC_PROMPT = """Continue the table of contents extraction. Output ONLY the remaining part (not what was already extracted). - -Original raw TOC: -{raw} - -Already extracted: -{partial} - -Continue from where it left off:""" - - -async def extract_toc_with_retry(toc_raw: str, max_attempts: int = 5) -> str: - prompt = f"{EXTRACT_TOC_PROMPT}\n\nGiven text:\n{toc_raw[:4000]}" - extracted, _ = await _llm_with_finish_reason(prompt, max_tokens=2000) - - for attempt in range(max_attempts): - verify_prompt = VERIFY_TOC_COMPLETE_PROMPT.format(raw=toc_raw[:3000], cleaned=extracted) - verify_raw = await llm_call(verify_prompt, max_tokens=200, json_mode=True) - verify = extract_json(verify_raw) - completed = verify.get("completed", "no") if isinstance(verify, dict) else "no" - if completed == "yes": - break - cont_prompt = CONTINUE_TOC_PROMPT.format(raw=toc_raw[:3000], partial=extracted) - continuation, _ = await _llm_with_finish_reason(cont_prompt, max_tokens=1500, json_mode=False) - extracted = extracted + "\n" + continuation - - return extracted - - -async def _llm_with_finish_reason(prompt: str, max_tokens: int = 1024, json_mode: bool = True) -> tuple[str, str]: - max_tokens = min(max_tokens, 4096) - tried: set[int] = set() - - for _ in range(len(GROQ_KEYS) + 1): - idx, key = await _next_key() - if idx in tried and len(tried) >= len(GROQ_KEYS): - raise HTTPException(status_code=429, detail="All Groq keys rate-limited") - tried.add(idx) - - payload = { - "model": "llama-3.3-70b-versatile", - "messages": [ - {"role": "system", "content": "You are an expert document analyst."}, - {"role": "user", "content": prompt}, - ], - "temperature": 0.1, - "max_tokens": max_tokens, - } - if json_mode: - payload["response_format"] = {"type": "json_object"} - - async with httpx.AsyncClient(timeout=120) as client: - resp = await client.post( - "https://api.groq.com/openai/v1/chat/completions", - headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, - json=payload, - ) - - if resp.status_code == 429: - await asyncio.sleep(1) - continue - - data = resp.json() - if "error" in data: - continue - - choice = data["choices"][0] - content = choice["message"]["content"].strip() - finish = choice.get("finish_reason", "stop") - finish_mapped = "finished" if finish in ("stop", "eos") else finish - return content, finish_mapped - - raise HTTPException(status_code=429, detail="All Groq keys exhausted") - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 3: TOC → Structured JSON -# ───────────────────────────────────────────────────────────────────────────── - -TOC_TRANSFORM_PROMPT = """Transform the following table of contents into a JSON array. - -'structure' is the numeric hierarchy: "1", "1.1", "1.2.3" etc. -If a section has no numeric prefix, use sequential numbers. - -Return JSON: -{{ - "table_of_contents": [ - {{"structure": "1", "title": "Introduction", "page": 1}}, - {{"structure": "1.1", "title": "Background", "page": 2}}, - ... - ] -}} - -Table of contents: -{toc_text}""" - - -async def toc_to_json(toc_text: str) -> list[dict]: - prompt = TOC_TRANSFORM_PROMPT.format(toc_text=toc_text[:3000]) - raw, finish = await _llm_with_finish_reason(prompt, max_tokens=3000, json_mode=True) - - parsed = extract_json(raw) - items = [] - if isinstance(parsed, dict) and "table_of_contents" in parsed: - items = parsed["table_of_contents"] - elif isinstance(parsed, list): - items = parsed - - verify_prompt = f"""Is the following cleaned table of contents complete relative to the raw? -Raw: -{toc_text[:2000]} - -Cleaned (JSON): -{json.dumps(items[:30], indent=2)} - -Return JSON: {{"thinking": "", "completed": "yes or no"}}""" - - verify_raw = await llm_call(verify_prompt, max_tokens=200) - verify = extract_json(verify_raw) - - if isinstance(verify, dict) and verify.get("completed") == "no" and len(items) > 0: - last = items[-1] - cont_prompt = f"""Continue the JSON table of contents array from after this entry: -{json.dumps(last)} - -Raw TOC for reference: -{toc_text[:3000]} - -Output ONLY the additional JSON array items (not the already-extracted ones), as a JSON array.""" - cont_raw, _ = await _llm_with_finish_reason(cont_prompt, max_tokens=2000, json_mode=False) - try: - m = re.search(r"\[.*\]", cont_raw, re.DOTALL) - if m: - extra = json.loads(m.group()) - if isinstance(extra, list): - items.extend(extra) - except Exception: - pass - - for item in items: - try: - item["page"] = int(item.get("page") or 0) or None - except (ValueError, TypeError): - item["page"] = None - - return items - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 4: Assign Physical Indices -# ───────────────────────────────────────────────────────────────────────────── - -ASSIGN_INDEX_PROMPT = """You are given a table of contents entry and several document pages. -Find which physical page this section starts on. - -Section title: {title} -TOC page number: {page} - -Document pages (search these): -{pages} - -Return JSON: -{{"thinking": "", - "physical_index": }}""" - - -async def assign_physical_indices( - toc_items: list[dict], page_list: list[tuple[str, int]], total_pages: int -) -> list[dict]: - async def _assign(item: dict) -> dict: - toc_page = item.get("page") - if toc_page is None: - item["physical_index"] = None - return item - - search_start = max(1, toc_page - 1) - search_end = min(total_pages, toc_page + 3) - pages_text = get_page_content_by_range(page_list, search_start, search_end) - - if not pages_text.strip(): - item["physical_index"] = toc_page - return item - - prompt = ASSIGN_INDEX_PROMPT.format( - title=item.get("title", ""), - page=toc_page, - pages=pages_text[:2500], - ) - raw = await llm_call(prompt, max_tokens=200) - parsed = extract_json(raw) - if isinstance(parsed, dict): - pi = parsed.get("physical_index") - try: - item["physical_index"] = int(pi) if pi is not None else toc_page - except (ValueError, TypeError): - item["physical_index"] = toc_page - else: - item["physical_index"] = toc_page - return item - - results = await asyncio.gather(*[_assign(item) for item in toc_items]) - return list(results) - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 5: Build Nested Tree with start_index / end_index -# ───────────────────────────────────────────────────────────────────────────── - -def build_tree_from_flat(items: list[dict], total_pages: int) -> list[dict]: - def parse_depth(structure: str) -> int: - if not structure: - return 1 - return len(structure.split(".")) - - try: - items = sorted(items, key=lambda x: [int(p) for p in (x.get("structure") or "0").split(".")]) - except Exception: - pass - - for i, item in enumerate(items): - start = item.get("physical_index") or item.get("page") or 1 - if i + 1 < len(items): - next_start = items[i + 1].get("physical_index") or items[i + 1].get("page") or (start + 1) - end = max(start, next_start - 1) - else: - end = total_pages - item["_start"] = start - item["_end"] = end - - root: list[dict] = [] - stack: list[tuple[int, list[dict]]] = [(0, root)] - node_counters: dict[str, int] = {} - - for item in items: - depth = parse_depth(item.get("structure", "")) - title = item.get("title", "Untitled") - start = item["_start"] - end = item["_end"] - - parent_id = stack[-1][1][-1]["node_id"] if stack[-1][1] else "" - counter_key = f"depth_{depth}" - node_counters[counter_key] = node_counters.get(counter_key, 0) + 1 - node_id = f"{node_counters[counter_key]:04d}" if depth == 1 else f"{parent_id}_{node_counters[counter_key]:02d}" - - node = { - "node_id": node_id, - "title": title, - "start_index": start, - "end_index": end, - "summary": "", - "nodes": [], - } - - while len(stack) > 1 and stack[-1][0] >= depth: - stack.pop() - - stack[-1][1].append(node) - stack.append((depth, node["nodes"])) - - return root - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 6: Per-Node Summary Generation -# ───────────────────────────────────────────────────────────────────────────── - -SUMMARY_PROMPT = """Read the following document section and write a 1-2 sentence summary of what it covers. -Be concise (under 200 chars). Do NOT include quotes or raw data from the text. -Example: "Covers thermal decomposition of H2O2 catalysts and reaction kinetics." - -Section title: {title} -Section text (first 1500 chars): -{text} - -Return JSON: {{"summary": ""}}""" - - -async def add_node_summaries(tree: list[dict], page_list: list[tuple[str, int]]) -> list[dict]: - def collect_nodes(nodes: list[dict]) -> list[dict]: - flat = [] - for n in nodes: - flat.append(n) - if n.get("nodes"): - flat.extend(collect_nodes(n["nodes"])) - return flat - - all_nodes = collect_nodes(tree) - - async def _summarise(node: dict) -> None: - text = get_page_content_by_range(page_list, node["start_index"], min(node["start_index"] + 1, node["end_index"])) - if not text.strip(): - node["summary"] = f"Section: {node['title']}" - return - prompt = SUMMARY_PROMPT.format(title=node["title"], text=text[:1500]) - raw = await llm_call(prompt, max_tokens=150) - parsed = extract_json(raw) - node["summary"] = parsed.get("summary", node["title"]) if isinstance(parsed, dict) else node["title"] - - await asyncio.gather(*[_summarise(n) for n in all_nodes]) - return tree - - -# ───────────────────────────────────────────────────────────────────────────── -# Stage 7: Per-Node Page Verification -# ───────────────────────────────────────────────────────────────────────────── - -VERIFY_PAGE_PROMPT = """Check if the section titled "{title}" appears or starts on the given page. -Do fuzzy matching — ignore minor spacing differences. - -Page text: -{page_text} - -Return JSON: {{"thinking": "", "answer": "yes or no"}}""" - - -async def verify_node_pages(tree: list[dict], page_list: list[tuple[str, int]], total_pages: int) -> list[dict]: - def collect_nodes(nodes: list[dict]) -> list[dict]: - flat = [] - for n in nodes: - flat.append(n) - if n.get("nodes"): - flat.extend(collect_nodes(n["nodes"])) - return flat - - page_map = {phys: text for text, phys in page_list} - - async def _verify(node: dict) -> None: - si = node.get("start_index", 1) - page_text = page_map.get(si, "") - if not page_text.strip(): - return - - prompt = VERIFY_PAGE_PROMPT.format(title=node["title"], page_text=page_text[:1500]) - raw = await llm_call(prompt, max_tokens=150) - parsed = extract_json(raw) - answer = parsed.get("answer", "yes") if isinstance(parsed, dict) else "yes" - - if answer != "yes": - for delta in [-1, 1, -2, 2]: - candidate = si + delta - if candidate < 1 or candidate > total_pages: - continue - alt_text = page_map.get(candidate, "") - if not alt_text: - continue - prompt2 = VERIFY_PAGE_PROMPT.format(title=node["title"], page_text=alt_text[:1500]) - raw2 = await llm_call(prompt2, max_tokens=150) - p2 = extract_json(raw2) - if isinstance(p2, dict) and p2.get("answer") == "yes": - node["start_index"] = candidate - break - - all_nodes = collect_nodes(tree) - await asyncio.gather(*[_verify(n) for n in all_nodes]) - return tree - - -# ───────────────────────────────────────────────────────────────────────────── -# Fallback: No-TOC page-by-page structure extraction -# ───────────────────────────────────────────────────────────────────────────── - -NO_TOC_INIT_PROMPT = """You are an expert in extracting hierarchical structure from documents. -Read the following pages and extract the sections/headings you find. - -The tags mark page boundaries. - -Return a JSON array: -[ - {{"structure": "1", "title": "Section Title", "physical_index": }}, - ... -] -Only return sections found in the provided pages. - -Document pages: -{pages}""" - -NO_TOC_CONTINUE_PROMPT = """You are continuing to extract sections from a document. -Here is the structure found so far, and the next set of pages to process. -Continue the structure — add sections found in the new pages. - -Previous structure: -{previous} - -New pages: -{pages} - -Return ONLY the NEW sections as a JSON array (not the already-found ones): -[ - {{"structure": "", "title": "...", "physical_index": }}, - ... -]""" - - -async def extract_structure_no_toc(page_list: list[tuple[str, int]], chunk_size: int = 8) -> list[dict]: - all_items: list[dict] = [] - total = len(page_list) - - for chunk_start in range(0, total, chunk_size): - chunk = page_list[chunk_start: chunk_start + chunk_size] - pages_text = "\n\n".join( - f"\n{text}\n" for text, phys in chunk if text.strip() - ) - - if not pages_text.strip(): - continue - - if not all_items: - prompt = NO_TOC_INIT_PROMPT.format(pages=pages_text[:3500]) - else: - prompt = NO_TOC_CONTINUE_PROMPT.format( - previous=json.dumps(all_items[-10:], indent=2), - pages=pages_text[:3000], - ) - - raw, _ = await _llm_with_finish_reason(prompt, max_tokens=2000, json_mode=False) - - parsed = extract_json(raw) - if isinstance(parsed, list): - all_items.extend(parsed) - elif isinstance(parsed, dict): - for v in parsed.values(): - if isinstance(v, list): - all_items.extend(v) - break - - for item in all_items: - pi = item.get("physical_index") - try: - item["physical_index"] = int(str(pi).split("_")[-1].rstrip(">").strip()) if pi else None - except Exception: - item["physical_index"] = None - item.setdefault("page", item.get("physical_index")) - - return all_items - - -# ───────────────────────────────────────────────────────────────────────────── -# Tree utilities -# ───────────────────────────────────────────────────────────────────────────── - -def count_nodes(nodes: list) -> int: - c = 0 - for n in nodes: - c += 1 - if n.get("nodes"): - c += count_nodes(n["nodes"]) - return c - - -# ───────────────────────────────────────────────────────────────────────────── -# Request / Response models -# ───────────────────────────────────────────────────────────────────────────── - -class UploadRequest(BaseModel): - file_url: str - file_name: Optional[str] = "document.pdf" - - -# ───────────────────────────────────────────────────────────────────────────── -# Endpoints -# ───────────────────────────────────────────────────────────────────────────── - -@app.get("/health") -def health(): - return { - "status": "ok", - "model": "llama-3.3-70b-versatile via Groq", - "groq_keys_loaded": len(GROQ_KEYS), - "approach": "PageIndex — 7-stage pipeline (TOC detect→extract→JSON→assign→tree→summaries→verify)", - } - - -@app.post("/doc/") -async def build_document_tree( - req: UploadRequest, - x_api_key: str = Header(...), -): - verify_key(x_api_key) - - if not GROQ_KEYS: - raise HTTPException(status_code=500, detail="No Groq API keys configured") - - # ── Download PDF ──────────────────────────────────────────────────────── - if req.file_url.startswith("data:"): - try: - _, encoded = req.file_url.split(",", 1) - pdf_bytes = base64.b64decode(encoded) - except Exception as e: - raise HTTPException(status_code=400, detail=f"Invalid data URI: {e}") - else: - async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client: - r = await client.get(req.file_url) - if r.status_code != 200: - raise HTTPException(status_code=400, detail=f"Cannot download: HTTP {r.status_code}") - if "text/html" in r.headers.get("content-type", ""): - raise HTTPException( - status_code=400, - detail="URL returned HTML. For Google Drive use: https://drive.google.com/uc?export=download&id=FILE_ID", - ) - pdf_bytes = r.content - - tmp_path = None - try: - with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: - tmp.write(pdf_bytes) - tmp_path = tmp.name - - # ── Extract pages ──────────────────────────────────────────────────── - page_list, total_pages = extract_pdf_pages(tmp_path) - raw_text = get_raw_text(page_list) - - if not raw_text.strip(): - raise HTTPException(status_code=400, detail="No text extracted — may be scanned/image-only PDF") - - # ── Stage 1: Detect TOC ────────────────────────────────────────────── - toc_pages = await detect_toc_pages(page_list, check_up_to=min(20, total_pages)) - has_toc = len(toc_pages) > 0 - print(f"[PageIndex] TOC detected on pages: {toc_pages}") - - if has_toc: - # ── Stage 2: Extract TOC text ──────────────────────────────────── - toc_raw_text = "\n\n".join( - page_list[phys - 1][0] for phys in toc_pages if 0 < phys <= total_pages - ) - toc_cleaned = await extract_toc_with_retry(toc_raw_text) - - # ── Stage 3: TOC → structured JSON ────────────────────────────── - toc_items = await toc_to_json(toc_cleaned) - print(f"[PageIndex] TOC items extracted: {len(toc_items)}") - - # ── Stage 4: Assign physical indices ──────────────────────────── - toc_items = await assign_physical_indices(toc_items, page_list, total_pages) - - else: - print("[PageIndex] No TOC found — falling back to page-by-page extraction") - toc_items = await extract_structure_no_toc(page_list) - print(f"[PageIndex] Fallback items extracted: {len(toc_items)}") - - if not toc_items: - raise HTTPException(status_code=500, detail="Failed to extract any structure from the document") - - # ── Stage 5: Build tree ────────────────────────────────────────────── - tree = build_tree_from_flat(toc_items, total_pages) - - # ── Stage 6: Summaries ─────────────────────────────────────────────── - tree = await add_node_summaries(tree, page_list) - - # ── Stage 7: Verify pages ──────────────────────────────────────────── - tree = await verify_node_pages(tree, page_list, total_pages) - - doc_id = f"pi-{uuid.uuid4().hex[:16]}" - - return { - "doc_id": doc_id, - "file_name": req.file_name, - "tree": tree, - "raw_text": raw_text, - "tree_node_count": count_nodes(tree), - "top_level_nodes": len(tree), - "total_pages": total_pages, - "toc_found": has_toc, - "toc_pages": toc_pages, - "status": "completed", - } - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Pipeline error: {str(e)}") - finally: - if tmp_path: - try: - os.unlink(tmp_path) - except Exception: - pass diff --git a/kits/assistant/pageIndex-notebooklm/updated/types.ts b/kits/assistant/pageIndex-notebooklm/updated/types.ts deleted file mode 100644 index c828f979..00000000 --- a/kits/assistant/pageIndex-notebooklm/updated/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -export interface Document { - doc_id: string; - file_name: string; - file_url: string; - tree_node_count: number; - status: string; - created_at: string; -} - -export interface TreeNode { - node_id: string; - title: string; - start_index: number; // physical page where section starts - end_index: number; // physical page where section ends - summary: string; // short 1-2 sentence description (≤200 chars), navigation only - nodes?: TreeNode[]; -} - -export interface RetrievedNode { - node_id: string; - title: string; - start_index: number; // exact start page - end_index: number; // exact end page - summary: string; // short description from tree node - page_content: string; // verbatim PDF text fetched from raw_text using start→end range -} - -export interface Message { - role: "user" | "assistant"; - content: string; -} - -export interface ChatResponse { - answer: string; - retrieved_nodes: RetrievedNode[]; - thinking: string; - doc_id: string; -} - -export interface UploadResponse { - doc_id: string; - file_name: string; - tree_node_count: string; - status: string; - saved: string; // "true" or "false" — comes as string from Lamatic - error: string; -} - -export interface ListResponse { - documents: Document[]; - total: number; -} - -export interface TreeResponse { - tree: TreeNode[]; - file_name: string; - tree_node_count: number; - created_at: string; -} From aa9121c8d4b5b296b376545bfffaae74f9a82d9a Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Fri, 27 Mar 2026 04:53:04 +0530 Subject: [PATCH 05/29] feat: Implement PDF upload, tree generation, and saving workflow for the PageIndex NotebookLM kit. --- .../components/TreeViewer.tsx | 68 +++++-- .../flows/chat-with-pdf/README.md | 2 +- .../README.md | 10 +- .../config.json | 190 ++++++++++++++---- .../inputs.json | 27 ++- .../flows/flow-4-get-tree-structure/README.md | 2 +- .../flows/flow-list-all-documents/README.md | 2 +- .../pageIndex-notebooklm/lib/types.ts | 19 +- 8 files changed, 259 insertions(+), 61 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx index 0b7da450..ff01bf63 100644 --- a/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { TreeNode } from "@/lib/types"; +import { TreeNode, TreeNodeResolved } from "@/lib/types"; interface Props { tree: TreeNode[]; @@ -9,13 +9,53 @@ interface Props { highlightedIds: string[]; } -function TreeNodeRow({ node, depth, highlightedIds }: { node: TreeNode; depth: number; highlightedIds: string[] }) { +/** Build a lookup map and resolve the flat API list into a nested tree. + * Root nodes are those whose node_id appears in the first item's `nodes` + * list OR nodes that are not referenced as children by any other node. + */ +function buildTree(flat: TreeNode[]): TreeNodeResolved[] { + const map = new Map(); + + // First pass: create resolved nodes with empty children arrays + for (const n of flat) { + map.set(n.node_id, { ...n, nodes: [] }); + } + + // Second pass: populate children + const childIds = new Set(); + for (const n of flat) { + const resolved = map.get(n.node_id)!; + for (const childId of n.nodes) { + const child = map.get(childId); + if (child) { + resolved.nodes.push(child); + childIds.add(childId); + } + } + } + + // Roots = nodes not referenced as a child + return flat + .map(n => map.get(n.node_id)!) + .filter(n => !childIds.has(n.node_id)); +} + +function TreeNodeRow({ + node, + depth, + highlightedIds, +}: { + node: TreeNodeResolved; + depth: number; + highlightedIds: string[]; +}) { const [open, setOpen] = useState(depth < 2); const isHighlighted = highlightedIds.includes(node.node_id); - const hasChildren = node.nodes && node.nodes.length > 0; - const pageSpan = node.start_index === node.end_index - ? `p.${node.start_index}` - : `pp.${node.start_index}–${node.end_index}`; + const hasChildren = node.nodes.length > 0; + const pageSpan = + node.start_index === node.end_index + ? `p.${node.start_index}` + : `pp.${node.start_index}–${node.end_index}`; return (
    @@ -107,8 +147,8 @@ function TreeNodeRow({ node, depth, highlightedIds }: { node: TreeNode; depth: n marginLeft: `${22 + depth * 16}px`, paddingLeft: "4px", }}> - {node.nodes!.map(child => ( - + {node.nodes.map((child, i) => ( + ))}
    )} @@ -117,8 +157,10 @@ function TreeNodeRow({ node, depth, highlightedIds }: { node: TreeNode; depth: n } export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { - const totalNodes = (nodes: TreeNode[]): number => - nodes.reduce((acc, n) => acc + 1 + totalNodes(n.nodes || []), 0); + const resolvedRoots = buildTree(tree); + + const totalNodes = (nodes: TreeNodeResolved[]): number => + nodes.reduce((acc, n) => acc + 1 + totalNodes(n.nodes), 0); return (
    )} - {totalNodes(tree)} nodes + {totalNodes(resolvedRoots)} nodes
    {/* Tree body */}
    - {tree.map(node => ( - + {resolvedRoots.map((node, i) => ( + ))}
    diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md index b1266394..a25e2edc 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md @@ -62,4 +62,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/22/2026* +*Generated on 3/27/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md index d6d72055..e7b642f1 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md @@ -2,19 +2,21 @@ ## About This Flow -This flow automates a workflow with **4 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. +This flow automates a workflow with **7 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. ## Flow Components This workflow includes the following node types: - API Request -- API +- Extract from File - Code +- Generate JSON +- Variables - API Response ## Configuration Requirements -This flow requires configuration for **0 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. +This flow requires configuration for **1 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. ## Files Included @@ -60,4 +62,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/22/2026* +*Generated on 3/27/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json index ca9e005e..4dd9bf3f 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -5,6 +5,9 @@ "data": { "modes": {}, "nodeId": "graphqlNode", + "schema": { + "sampleOutput": "string" + }, "values": { "id": "triggerNode_1", "nodeName": "API Request", @@ -25,22 +28,21 @@ "selected": false }, { - "id": "apiNode_948", + "id": "extractFromFileNode_1", "data": { - "label": "dynamicNode node", "logic": [], "modes": {}, - "nodeId": "apiNode", + "nodeId": "extractFromFileNode", + "schema": { + "files": "object" + }, "values": { - "id": "apiNode_948", - "url": "https://pageindex-fastapi-production.up.railway.app/doc/", - "body": "{\"file_url\": \"{{triggerNode_1.output.file_url}}\", \"file_name\": \"{{triggerNode_1.output.file_name}}\"}", - "method": "POST", - "headers": "{\"x-api-key\":\"{{secrets.project.SERVER_API_KEY}}\",\"Content-Type\":\"application/json\"}", - "retries": "0", - "nodeName": "API", - "retry_deplay": "0", - "convertXmlResponseToJson": false + "id": "extractFromFileNode_1", + "format": "pdf", + "fileUrl": "[\"{{triggerNode_1.output.file_url}}\"]", + "nodeName": "Extract PDF", + "joinPages": false, + "operation": "extractFromPDF" } }, "type": "dynamicNode", @@ -52,29 +54,123 @@ "x": 0, "y": 130 }, - "selected": false + "selected": false, + "draggable": false + }, + { + "id": "codeNode_format", + "data": { + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "schema": { + "pages": "array", + "raw_text": "string", + "toc_items": "array", + "page_count": "number", + "pages_json": "string", + "has_native_toc": "boolean" + }, + "values": { + "id": "codeNode_format", + "code": "const files = {{extractFromFileNode_1.output.files}};\nconst file = files[0];\nconst pages = file.data; // array of page strings\nconst tocItems = {{extractFromFileNode_1.output.toc_items}} || [];\n\n// Build raw_text with [PAGE N] markers (same format your query flow expects)\nconst rawText = pages\n .map((text, i) => `[PAGE ${i + 1}]\\n${text}`)\n .join(\"\\n\\n\");\n\noutput = {\n raw_text: rawText,\n pages: pages,\n page_count: pages.length,\n toc_items: tocItems,\n has_native_toc: tocItems.length > 0,\n pages_json: JSON.stringify(pages)\n};", + "nodeName": "Format Pages" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 260 + }, + "selected": false, + "draggable": false }, { - "id": "codeNode_570", + "id": "InstructorLLMNode_tree", + "data": { + "modes": {}, + "nodeId": "InstructorLLMNode", + "schema": {}, + "values": { + "id": "InstructorLLMNode_tree", + "schema": "{\n \"type\": \"object\",\n \"properties\": {\n \"tree\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"node_id\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"start_index\": {\n \"type\": \"number\"\n },\n \"end_index\": {\n \"type\": \"number\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"nodes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": true\n }\n },\n \"tree_node_count\": {\n \"type\": \"number\",\n \"description\": \"Total number of nodes in the tree array\"\n }\n }\n}", + "prompts": [ + { + "role": "system", + "content": "Total pages: {{codeNode_format.output.page_count}}\n{{#if codeNode_format.output.has_native_toc}} Native TOC found — use this as the structure basis: {{codeNode_format.output.toc_items}} {{else}} No TOC found — infer structure from these first 3 pages: {{codeNode_format.output.pages_json}} {{/if}}\nBuild a complete PageIndex tree covering all {{codeNode_format.output.page_count}} pages." + }, + { + "role": "user", + "content": "Total pages: {{codeNode_format.output.page_count}}\n{{#if codeNode_format.output.has_native_toc}} Native TOC found — use this as the structure basis: {{codeNode_format.output.toc_items}} {{else}} No TOC found — infer structure from these first 3 pages: {{codeNode_format.output.pages_json}} {{/if}}\nBuild a complete PageIndex tree covering all {{codeNode_format.output.page_count}} pages." + } + ], + "nodeName": "Generate Tree", + "generativeModelName": "" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 390 + }, + "selected": true, + "draggable": false + }, + { + "id": "variablesNode_617", "data": { "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "variablesNode", + "schema": {}, + "values": { + "id": "variablesNode_617", + "mapping": "{\n \"file_name\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_name}}\"\n },\n \"file_url\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_url}}\"\n },\n \"tree\": {\n \"type\": \"string\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree}}\"\n },\n \"raw_data\": {\n \"type\": \"string\",\n \"value\": \"{{extractFromFileNode_1.output.files}}\"\n },\n \"tre_node_count\": {\n \"type\": \"number\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree_node_count}}\"\n }\n}", + "nodeName": "Variables" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 520 + }, + "selected": false + }, + { + "id": "codeNode_save", + "data": { "logic": [], "modes": {}, "nodeId": "codeNode", "schema": { - "error": "string", + "error": "null", "doc_id": "string", "status": "string", "success": "boolean", "file_name": "string", "status_code": "number", + "response_text": "string", "tree_node_count": "number" }, "values": { - "id": "codeNode_570", - "code": "\n\nvar apiData = {{apiNode_948.output}};\n\nif (typeof apiData === \"string\") {\n apiData = JSON.parse(apiData);\n}\n\n// ✅ Strip null bytes and other problematic Unicode escape sequences\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str\n .replace(/\\u0000/g, \"\") // null bytes (main culprit)\n .replace(/\\\\u0000/g, \"\") // escaped null bytes as literal string\n .replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\"); // other control chars\n}\n\nvar doc_id = apiData.doc_id || \"\";\nvar file_name = apiData.file_name || \"\";\nvar tree = apiData.tree || [];\nvar raw_text = sanitize(apiData.raw_text || \"\"); // ✅ sanitized\nvar tree_node_count = apiData.tree_node_count || 0;\nvar file_url = {{triggerNode_1.output.file_url}} || \"\";\nvar supabase_url = {{secrets.project.SUPABASE_URL}} || \"\";\nvar supabase_key = {{secrets.project.SUPABASE_ANON_KEY}} || \"\";\n\n// ✅ Also sanitize the tree JSON (in case summaries/titles have null bytes)\nvar sanitized_tree = JSON.parse(sanitize(JSON.stringify(tree)));\n\nvar payload = JSON.stringify({\n doc_id: doc_id,\n file_name: file_name,\n file_url: file_url,\n tree: sanitized_tree,\n raw_text: raw_text,\n tree_node_count: tree_node_count,\n status: \"completed\"\n});\n\n// ... rest of your fetch code unchanged\n\nvar response = await fetch(\n supabase_url + \"/rest/v1/documents\",\n {\n method: \"POST\",\n headers: {\n \"apikey\": supabase_key,\n \"Authorization\": \"Bearer \" + supabase_key,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: payload\n }\n);\n\nvar result = await response.json();\n\nif (!response.ok) {\n output = {\n success: false,\n doc_id: \"\",\n file_name: file_name,\n tree_node_count: 0,\n error: result.message || result.detail || JSON.stringify(result),\n status_code: response.status\n };\n} else {\n var inserted = Array.isArray(result) ? result[0] : result;\n output = {\n success: true,\n doc_id: inserted.doc_id || doc_id,\n file_name: inserted.file_name || file_name,\n tree_node_count: inserted.tree_node_count || tree_node_count,\n status: \"completed\",\n error: \"\",\n status_code: response.status\n };\n}", - "nodeName": "Code" - } + "id": "codeNode_save", + "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{extractFromFileNode_1.output.files[0].data}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\n// Sanitize page text\nrawText.forEach((page, i) => {\n rawText[i] = sanitize(page);\n});\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: rawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\noutput = {\n success: response.ok,\n status_code: response.status,\n response_text: response.statusText,\n error: result[0]?.error || result.error || null,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n", + "nodeName": "Save to Supabase" + }, + "disabled": false }, "type": "dynamicNode", "measured": { @@ -83,23 +179,22 @@ }, "position": { "x": 0, - "y": 260 + "y": 650 }, - "selected": true + "selected": false, + "draggable": false }, { "id": "responseNode_triggerNode_1", "data": { "label": "Response", + "modes": {}, "nodeId": "graphqlResponseNode", + "schema": {}, "values": { "id": "responseNode_triggerNode_1", - "headers": "{\"content-type\":\"application/json\"}", - "retries": "0", "nodeName": "API Response", - "webhookUrl": "", - "retry_delay": "0", - "outputMapping": "{\n \"doc_id\": \"{{apiNode_948.output}}.doc_id\",\n \"file_name\": \"{{apiNode_948.output}}.file_name\",\n \"tree_node_count\": \"{{apiNode_948.output}}.tree_node_count\",\n \"status\": \"completed\"\n}" + "outputMapping": "{\n \"doc_id\": \"{{codeNode_save.output.doc_id}}\",\n \"file_name\": \"{{codeNode_save.output.file_name}}\",\n \"tree_node_count\": \"{{codeNode_save.output.tree_node_count}}\",\n \"status\": \"{{codeNode_save.output.status}}\"\n}" }, "disabled": false, "isResponseNode": true @@ -111,39 +206,62 @@ }, "position": { "x": 0, - "y": 390 + "y": 780 }, "selected": false } ], "edges": [ { - "id": "triggerNode_1-apiNode_948", + "id": "triggerNode_1-extractFromFileNode_1", "type": "defaultEdge", "source": "triggerNode_1", - "target": "apiNode_948", + "target": "extractFromFileNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "extractFromFileNode_1-codeNode_format", + "type": "defaultEdge", + "source": "extractFromFileNode_1", + "target": "codeNode_format", "sourceHandle": "bottom", "targetHandle": "top" }, { - "id": "apiNode_948-codeNode_570-252", + "id": "codeNode_format-InstructorLLMNode_tree", "type": "defaultEdge", - "source": "apiNode_948", - "target": "codeNode_570", + "source": "codeNode_format", + "target": "InstructorLLMNode_tree", "sourceHandle": "bottom", "targetHandle": "top" }, { - "id": "codeNode_570-responseNode_triggerNode_1-839", - "data": {}, + "id": "codeNode_save-responseNode_triggerNode_1", "type": "defaultEdge", - "source": "codeNode_570", + "source": "codeNode_save", "target": "responseNode_triggerNode_1", "sourceHandle": "bottom", "targetHandle": "top" }, { - "id": "response-trigger_triggerNode_1", + "id": "variablesNode_617-codeNode_save", + "type": "defaultEdge", + "source": "variablesNode_617", + "target": "codeNode_save", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "InstructorLLMNode_tree-variablesNode_617", + "type": "defaultEdge", + "source": "InstructorLLMNode_tree", + "target": "variablesNode_617", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "response-responseNode_triggerNode_1", "type": "responseEdge", "source": "triggerNode_1", "target": "responseNode_triggerNode_1", diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json index 9e26dfee..a238fe74 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json @@ -1 +1,26 @@ -{} \ No newline at end of file +{ + "InstructorLLMNode_tree": [ + { + "name": "generativeModelName", + "label": "Generative Model Name", + "type": "model", + "mode": "instructor", + "description": "Select the model to generate text based on the prompt.", + "modelType": "generator/text", + "required": true, + "isPrivate": true, + "defaultValue": [ + { + "configName": "configA", + "type": "generator/text", + "provider_name": "", + "credential_name": "", + "params": {} + } + ], + "typeOptions": { + "loadOptionsMethod": "listModels" + } + } + ] +} \ No newline at end of file diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md index 832a7f78..98bc131f 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md @@ -60,4 +60,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/22/2026* +*Generated on 3/27/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md index c5d53e25..1ae0d30a 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md @@ -59,4 +59,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/22/2026* +*Generated on 3/27/2026* diff --git a/kits/assistant/pageIndex-notebooklm/lib/types.ts b/kits/assistant/pageIndex-notebooklm/lib/types.ts index 28e52b23..caf58bf2 100644 --- a/kits/assistant/pageIndex-notebooklm/lib/types.ts +++ b/kits/assistant/pageIndex-notebooklm/lib/types.ts @@ -7,13 +7,24 @@ export interface Document { created_at: string; } +// Raw API shape: nodes is a flat array of child IDs export interface TreeNode { node_id: string; title: string; - start_index: number; // physical page where section starts - end_index: number; // physical page where section ends - summary: string; // short 1-2 sentence AI description (≤200 chars) - nodes?: TreeNode[]; + start_index: number; + end_index: number; + summary: string; + nodes: string[]; // child node_ids +} + +// Resolved shape used internally after building the tree +export interface TreeNodeResolved { + node_id: string; + title: string; + start_index: number; + end_index: number; + summary: string; + nodes: TreeNodeResolved[]; } export interface RetrievedNode { From 75bbe8f07b1ac4e5a5e76057cb66df6c44587990 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Fri, 27 Mar 2026 17:03:46 +0530 Subject: [PATCH 06/29] feat: Add PageIndex document upload, PDF processing, LLM-based tree generation, and data saving flow. --- .../actions/orchestrate.ts | 8 ++- .../components/DocumentUpload.tsx | 10 ++- .../README.md | 4 +- .../config.json | 70 ++++++++++++++----- .../pageIndex-notebooklm/lib/types.ts | 2 - 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts index 673da550..de87a26a 100644 --- a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts @@ -16,11 +16,15 @@ function safeParseJSON(value: unknown, fallback: T): T { } // ── Flow 1: Upload PDF → build tree → save to Supabase ─────── -export async function uploadDocument(file_url: string, file_name: string) { +// Accepts either a remote file_url OR a local file (base64 + mime_type). +export async function uploadDocument( + file_name: string, + options: { file_url?: string; file_base64?: string; mime_type?: string } +) { try { const response = await lamaticClient.executeFlow( process.env.FLOW_ID_UPLOAD!, - { file_url, file_name } + { file_name, ...options } ); const data = (response.result ?? response) as Record; return data; diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx b/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx index 2692e201..19e45534 100644 --- a/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx @@ -22,9 +22,13 @@ export default function DocumentUpload({ onUploaded }: Props) { setStatus("uploading"); try { const dataUrl = await fileToDataUrl(file); - const result = (await uploadDocument(dataUrl, file.name)) as UploadResponse; - if (result?.error) { setStatus("error"); setMessage(result.error); } - else { setStatus("success"); setMessage(`${result.tree_node_count} nodes indexed`); onUploaded(); } + // Split "data:;base64," → { mime_type, file_base64 } + const [meta, file_base64] = dataUrl.split(","); + const mime_type = meta.replace("data:", "").replace(";base64", ""); + const result = (await uploadDocument(file.name, { file_base64, mime_type })) as unknown as UploadResponse; + setStatus("success"); + setMessage(`${result.tree_node_count} nodes indexed`); + onUploaded(); } catch { setStatus("error"); setMessage("Upload failed. Check your flow."); } diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md index e7b642f1..f16a52c8 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md @@ -2,14 +2,14 @@ ## About This Flow -This flow automates a workflow with **7 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. +This flow automates a workflow with **8 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. ## Flow Components This workflow includes the following node types: - API Request -- Extract from File - Code +- Extract from File - Generate JSON - Variables - API Response diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json index 4dd9bf3f..7f08bbd1 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -12,7 +12,7 @@ "id": "triggerNode_1", "nodeName": "API Request", "responeType": "realtime", - "advance_schema": "{\n \"file_url\": \"string\",\n \"file_name\": \"string\"\n}" + "advance_schema": "{\n \"file_url\": \"string\",\n \"file_name\": \"string\",\n \"file_base64\": \"string\",\n \"mime_type\": \"string\"\n}" }, "trigger": true }, @@ -27,6 +27,30 @@ }, "selected": false }, + { + "id": "codeNode_630", + "data": { + "label": "dynamicNode node", + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "values": { + "id": "codeNode_630", + "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst fileBase64 = {{triggerNode_1.output.file_base64}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst fileName = {{triggerNode_1.output.file_name}} || \"document.pdf\";\nconst mimeType = {{triggerNode_1.output.mime_type}} || \"application/pdf\";\n\n// If a URL was provided directly, use it — no storage upload needed\nif (!fileBase64 && fileUrl) {\n output = { resolved_url: fileUrl, file_name: fileName, uploaded_to_storage: false };\n return;\n}\n\nif (!fileBase64) {\n throw new Error(\"No file_base64 or file_url provided\");\n}\n\n// Upload base64 to Supabase Storage\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}}; // from Lamatic secret\nconst serviceKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}}; // from Lamatic secret\n\n// Convert base64 to binary\n// base64 may arrive as a plain string or as data:mime;base64,... URI\nlet b64 = fileBase64;\nif (b64.includes(\",\")) b64 = b64.split(\",\")[1];\n\nconst binaryStr = atob(b64);\nconst bytes = new Uint8Array(binaryStr.length);\nfor (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n}\n\n// Unique storage path\nconst safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\nconst storagePath = `${Date.now()}_${safeName}`;\n\nconst uploadResp = await fetch(\n `${supabaseUrl}/storage/v1/object/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": mimeType,\n \"x-upsert\": \"false\",\n },\n body: bytes,\n }\n);\n\nif (!uploadResp.ok) {\n const errText = await uploadResp.text();\n throw new Error(`Storage upload failed: ${uploadResp.status} — ${errText}`);\n}\n\nconst publicUrl = `${supabaseUrl}/storage/v1/object/public/pdfs/${storagePath}`;\n\noutput = {\n resolved_url: publicUrl,\n file_name: fileName,\n uploaded_to_storage: true,\n};", + "nodeName": "Code" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 0, + "y": 130 + }, + "selected": false + }, { "id": "extractFromFileNode_1", "data": { @@ -39,7 +63,7 @@ "values": { "id": "extractFromFileNode_1", "format": "pdf", - "fileUrl": "[\"{{triggerNode_1.output.file_url}}\"]", + "fileUrl": "[\"{{codeNode_630.output.resolved_url}}\"]", "nodeName": "Extract PDF", "joinPages": false, "operation": "extractFromPDF" @@ -52,7 +76,7 @@ }, "position": { "x": 0, - "y": 130 + "y": 260 }, "selected": false, "draggable": false @@ -84,7 +108,7 @@ }, "position": { "x": 0, - "y": 260 + "y": 390 }, "selected": false, "draggable": false @@ -119,9 +143,9 @@ }, "position": { "x": 0, - "y": 390 + "y": 520 }, - "selected": true, + "selected": false, "draggable": false }, { @@ -145,7 +169,7 @@ }, "position": { "x": 0, - "y": 520 + "y": 650 }, "selected": false }, @@ -167,7 +191,7 @@ }, "values": { "id": "codeNode_save", - "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{extractFromFileNode_1.output.files[0].data}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\n// Sanitize page text\nrawText.forEach((page, i) => {\n rawText[i] = sanitize(page);\n});\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: rawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\noutput = {\n success: response.ok,\n status_code: response.status,\n response_text: response.statusText,\n error: result[0]?.error || result.error || null,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n", + "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{extractFromFileNode_1.output.files[0].data}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{codeNode_630.output.resolver_url}}\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\n// Sanitize page text\nrawText.forEach((page, i) => {\n rawText[i] = sanitize(page);\n});\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: rawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\noutput = {\n success: response.ok,\n status_code: response.status,\n response_text: response.statusText,\n error: result[0]?.error || result.error || null,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n", "nodeName": "Save to Supabase" }, "disabled": false @@ -179,9 +203,9 @@ }, "position": { "x": 0, - "y": 650 + "y": 780 }, - "selected": false, + "selected": true, "draggable": false }, { @@ -206,20 +230,12 @@ }, "position": { "x": 0, - "y": 780 + "y": 910 }, "selected": false } ], "edges": [ - { - "id": "triggerNode_1-extractFromFileNode_1", - "type": "defaultEdge", - "source": "triggerNode_1", - "target": "extractFromFileNode_1", - "sourceHandle": "bottom", - "targetHandle": "top" - }, { "id": "extractFromFileNode_1-codeNode_format", "type": "defaultEdge", @@ -260,6 +276,22 @@ "sourceHandle": "bottom", "targetHandle": "top" }, + { + "id": "triggerNode_1-codeNode_630", + "type": "defaultEdge", + "source": "triggerNode_1", + "target": "codeNode_630", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "codeNode_630-extractFromFileNode_1", + "type": "defaultEdge", + "source": "codeNode_630", + "target": "extractFromFileNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, { "id": "response-responseNode_triggerNode_1", "type": "responseEdge", diff --git a/kits/assistant/pageIndex-notebooklm/lib/types.ts b/kits/assistant/pageIndex-notebooklm/lib/types.ts index caf58bf2..5f011dcc 100644 --- a/kits/assistant/pageIndex-notebooklm/lib/types.ts +++ b/kits/assistant/pageIndex-notebooklm/lib/types.ts @@ -53,8 +53,6 @@ export interface UploadResponse { file_name: string; tree_node_count: string; status: string; - saved: string; - error: string; } export interface ListResponse { From 55e0359c58b1329f6389cad46a075e087228c3c2 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Fri, 27 Mar 2026 17:04:05 +0530 Subject: [PATCH 07/29] feat: Add new `pageIndex-notebooklm` assistant kit, including a `chat-with-pdf` flow. --- kits/assistant/pageIndex-notebooklm/config.json | 10 +++++----- .../flows/chat-with-pdf/config.json | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/config.json b/kits/assistant/pageIndex-notebooklm/config.json index ae8f0b0d..50fc69e6 100644 --- a/kits/assistant/pageIndex-notebooklm/config.json +++ b/kits/assistant/pageIndex-notebooklm/config.json @@ -12,6 +12,11 @@ "type": "mandatory", "envKey": "FLOW_CHAT_WITH_PDF" }, + { + "id": "flow-1-upload-pdf-build-tree-save", + "type": "mandatory", + "envKey": "FLOW_FLOW_1_UPLOAD_PDF_BUILD_TREE_SAVE" + }, { "id": "flow-4-get-tree-structure", "type": "mandatory", @@ -21,11 +26,6 @@ "id": "flow-list-all-documents", "type": "mandatory", "envKey": "FLOW_FLOW_LIST_ALL_DOCUMENTS" - }, - { - "id": "flow-1-upload-pdf-build-tree-save", - "type": "mandatory", - "envKey": "FLOW_FLOW_1_UPLOAD_PDF_BUILD_TREE_SAVE" } ], "integrations": [], diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json index 4bdb2247..138fb356 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json @@ -63,7 +63,7 @@ }, "values": { "id": "codeNode_429", - "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\n\nfunction stripToTOC(nodes) {\n return nodes.map(node => ({\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index, // was page_index\n end_index: node.end_index, // new — exact section boundary\n description: node.summary, // was node.text\n children: (node.nodes || []).map(c => ({\n node_id: c.node_id,\n title: c.title,\n start_index: c.start_index,\n end_index: c.end_index\n }))\n }));\n}\n\noutput = {\n toc_json: JSON.stringify(stripToTOC(tree)),\n node_count: tree.length\n};", + "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\n\n// tree.nodes are string IDs, not objects — just pass them as-is\nfunction stripToTOC(nodes) {\n return nodes.map(node => ({\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n description: node.summary,\n children: node.nodes || [] // keep as string IDs, LLM can still read them\n }));\n}\n\noutput = {\n toc_json: JSON.stringify(stripToTOC(tree)),\n node_count: tree.length\n};", "nodeName": "Code" } }, @@ -92,12 +92,12 @@ { "id": "187c2f4b-c23d-4545-abef-73dc897d6b7b", "role": "system", - "content": "You are a document retrieval expert using tree-based reasoning. Your job is to identify which sections of a document tree are most relevant to answer the user's query." + "content": "You are a document retrieval expert using tree-based reasoning. Your job is to identify which sections of a document tree are most relevant to answer the user's query.\nIMPORTANT RULES:\n- Always prefer LEAF nodes (nodes where \"children\" is an empty array [])\n- Never select root or parent nodes that span many pages\n- Select 2-3 most specific nodes that directly answer the query\n- A good node covers 1-3 pages maximum" }, { "id": "187c2f4b-c23d-4545-abef-73dc897d6b7d", "role": "user", - "content": "You are given a query and a document table of contents (TOC). Navigate the TOC structure and find which sections likely contain the answer. Query: {{triggerNode_1.output.query}} Document TOC (titles and structure only): {{codeNode_429.output.toc_json}} \nEach node has start_index and end_index showing which pages it covers.Reply with: - thinking: your reasoning about which sections contain the answer (scan like a human reading a TOC) - node_list: array of 2-3 node_ids most likely to contain the answer" + "content": "You are given a query and a document table of contents (TOC).\nNavigate the TOC structure and find which LEAF sections (children: []) likely contain the answer.\nQuery: {{triggerNode_1.output.query}}\nDocument TOC (titles and structure only): {{codeNode_429.output.toc_json}}\nEach node has start_index and end_index showing which pages it covers.\nONLY select nodes where children is [] (leaf nodes, not parent sections).\nReply with:\n- thinking: your reasoning about which specific leaf sections contain the answer\n- node_list: array of 2-3 leaf node_ids most likely to contain the answer" } ], "memories": "[]", @@ -131,7 +131,7 @@ }, "values": { "id": "codeNode_358", - "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}}\nconst rawText = {{postgresNode_817.output.queryResult[0].raw_text}};\nconst nodeList = {{InstructorLLMNode_432.output.node_list}};\n\n// Flatten tree to node_id map\nfunction flattenTree(nodes, map = {}) {\n for (const n of nodes) {\n map[n.node_id] = n;\n if (n.nodes && n.nodes.length > 0) flattenTree(n.nodes, map);\n }\n return map;\n}\n\nconst nodeMap = flattenTree(tree);\nconst selectedNodes = nodeList.map(id => nodeMap[id]).filter(Boolean);\n\n// PageIndex: use exact start_index → end_index range (no guessing)\nconst retrieved = selectedNodes.map(node => {\n const startPage = node.start_index || 1;\n const endPage = node.end_index || startPage + 2;\n let pageContent = \"\";\n\n for (let p = startPage; p <= Math.min(endPage, startPage + 4); p++) {\n const marker = `[PAGE ${p}]`;\n const nextMarker = `[PAGE ${p + 1}]`;\n if (rawText.includes(marker)) {\n const start = rawText.indexOf(marker) + marker.length;\n const end = rawText.includes(nextMarker)\n ? rawText.indexOf(nextMarker)\n : Math.min(start + 3000, rawText.length);\n pageContent += rawText.slice(start, end).trim() + \"\\n\\n\";\n }\n }\n\n return {\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n summary: node.summary, // was node.text\n page_content: pageContent.trim() || node.summary\n };\n});\n\nconst context = retrieved\n .map(n => `[Section: \"${n.title}\" | Pages: ${n.start_index}–${n.end_index}]\\n${n.page_content}`)\n .join(\"\\n\\n---\\n\\n\");\n\noutput = { context, retrieved_nodes: retrieved };", + "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\nconst rawTextRaw = {{postgresNode_817.output.queryResult[0].raw_text}};\nconst nodeList = {{InstructorLLMNode_432.output.node_list}};\n\n// Parse raw_text (stored as TEXT in DB)\nconst pages = typeof rawTextRaw === \"string\" ? JSON.parse(rawTextRaw) : rawTextRaw;\n\n// Build flat node map from top-level tree array\nconst nodeMap = {};\nfor (const n of tree) {\n nodeMap[n.node_id] = n;\n}\n\nconst selectedNodes = nodeList.map(id => nodeMap[id]).filter(Boolean);\n\nconst MAX_PAGES_PER_NODE = 2; // hard cap — never more than 2 pages per node\nconst MAX_CHARS_PER_PAGE = 2000; // truncate very long pages\n\nconst retrieved = selectedNodes.map(node => {\n const startPage = (node.start_index || 1) - 1; // 0-based\n const rawEndPage = (node.end_index || node.start_index || 1) - 1;\n\n // Enforce max pages cap\n const endPage = Math.min(rawEndPage, startPage + MAX_PAGES_PER_NODE - 1, pages.length - 1);\n\n const pageSlices = pages\n .slice(startPage, endPage + 1)\n .map(p => p.length > MAX_CHARS_PER_PAGE ? p.slice(0, MAX_CHARS_PER_PAGE) + \"...\" : p);\n\n const pageContent = pageSlices.join(\"\\n\\n\").trim();\n\n return {\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n summary: node.summary,\n page_content: pageContent || node.summary\n };\n});\n\nconst context = retrieved\n .map(n => `[Section: \"${n.title}\" | Pages: ${n.start_index}–${n.end_index}]\\n${n.page_content}`)\n .join(\"\\n\\n---\\n\\n\");\n\noutput = {\n context,\n retrieved_nodes: retrieved,\n total_chars: context.length\n};", "nodeName": "Code" } }, From d77d649852fdc24ff6020efb89bb1ea160b1e2c0 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Fri, 27 Mar 2026 17:14:34 +0530 Subject: [PATCH 08/29] feat: Implement initial PageIndex application layout and a chat interface for document interaction. --- .../pageIndex-notebooklm/app/layout.tsx | 8 +++- .../components/ChatWindow.tsx | 43 ++++++++++++++++--- .../pageIndex-notebooklm/tsconfig.json | 2 +- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/app/layout.tsx b/kits/assistant/pageIndex-notebooklm/app/layout.tsx index dbb568e7..a5dad631 100644 --- a/kits/assistant/pageIndex-notebooklm/app/layout.tsx +++ b/kits/assistant/pageIndex-notebooklm/app/layout.tsx @@ -1,11 +1,15 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import "./globals.css"; export const metadata: Metadata = { title: "PageIndex — Vectorless Document Intelligence", description: "Chat with your documents using PageIndex's agentic tree-structured retrieval. No vectors, no chunking — powered by Lamatic and Groq.", - viewport: "width=device-width, initial-scale=1", +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, themeColor: "#0a0b0d", }; diff --git a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx index 2dffe98a..92bce930 100644 --- a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx @@ -2,7 +2,37 @@ import { useState, useRef, useEffect } from "react"; import { chatWithDocument } from "@/actions/orchestrate"; -import { Message, RetrievedNode } from "@/lib/types"; +import { ChatResponse, Message, RetrievedNode } from "@/lib/types"; + +// Lightweight markdown → HTML (no external deps) +function renderMarkdown(text: string): string { + return text + // Headings + .replace(/^### (.+)$/gm, "

    $1

    ") + .replace(/^## (.+)$/gm, "

    $1

    ") + .replace(/^# (.+)$/gm, "

    $1

    ") + // Bold + italic + .replace(/\*\*\*(.+?)\*\*\*/g, "$1") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1") + // Inline code + .replace(/`([^`]+)`/g, "$1") + // Bullet list items (* or -) + .replace(/^[*-] (.+)$/gm, "
  • $1
  • ") + // Numbered list items + .replace(/^\d+\.\s+(.+)$/gm, "
  • $1
  • ") + // Wrap consecutive
  • blocks in
      + .replace(/((?:
    • [\s\S]*?<\/li>\s*)+)/g, (match) => `
        ${match}
      `) + // Horizontal rule + .replace(/^---$/gm, "
      ") + // Paragraphs: double newline →

      break + .replace(/\n{2,}/g, "

      ") + // Single newline →
      + .replace(/\n/g, "
      ") + // Wrap in paragraph + .replace(/^/, "

      ") + .replace(/$/, "

      "); +} interface Props { docId: string; @@ -31,7 +61,7 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) setInput(""); setLoading(true); try { - const result = await chatWithDocument(docId, userMsg.content, newMsgs); + const result = await chatWithDocument(docId, userMsg.content, newMsgs) as unknown as ChatResponse; setMessages(prev => [...prev, { role: "assistant", content: result.answer || "No answer found." }]); if (Array.isArray(result.retrieved_nodes) && result.retrieved_nodes.length) { setLastNodes(result.retrieved_nodes); @@ -164,9 +194,12 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) border: msg.role === "assistant" ? "1px solid var(--border)" : "none", boxShadow: msg.role === "user" ? "0 4px 14px rgba(45,212,191,0.2)" : "none", letterSpacing: "-0.005em", - }}> - {msg.content} -
  • + }} + {...(msg.role === "assistant" + ? { dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) } } + : { children: msg.content } + )} + />
    ))} diff --git a/kits/assistant/pageIndex-notebooklm/tsconfig.json b/kits/assistant/pageIndex-notebooklm/tsconfig.json index d8b93235..53b445a0 100644 --- a/kits/assistant/pageIndex-notebooklm/tsconfig.json +++ b/kits/assistant/pageIndex-notebooklm/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2018", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, From 9e9b459737209d199661e0c5df4e4d05379e674b Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 29 Mar 2026 00:09:39 +0530 Subject: [PATCH 09/29] feat: initialize PageIndex notebookLM assistant kit with document management and chat capabilities --- .../actions/orchestrate.ts | 49 ++++-- .../pageIndex-notebooklm/app/page.tsx | 79 ++++++--- .../components/ChatWindow.tsx | 28 ++- .../components/DocumentList.tsx | 135 +++++++++++++-- .../pageIndex-notebooklm/config.json | 8 +- .../flows/chat-with-pdf/config.json | 7 +- .../flows/flow-4-get-tree-structure/README.md | 5 +- .../flow-4-get-tree-structure/config.json | 159 +++++++++++++----- .../flow-4-get-tree-structure/inputs.json | 14 +- .../pageIndex-notebooklm/lib/types.ts | 12 ++ 10 files changed, 388 insertions(+), 108 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts index de87a26a..17273d0a 100644 --- a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts @@ -38,23 +38,34 @@ export async function uploadDocument( export async function chatWithDocument( doc_id: string, query: string, - messages: Array<{ role: string; content: string }> + messages: Array<{ role: string; content: string }> = [] ) { try { + if (!process.env.FLOW_ID_CHAT) throw new Error("FLOW_ID_CHAT not set"); + const response = await lamaticClient.executeFlow( process.env.FLOW_ID_CHAT!, - { doc_id, query, messages } + { + doc_id, + query, + messages: JSON.stringify(messages), // pass history as string + } ); - const data = (response.result ?? response) as Record; - // retrieved_nodes comes back as a JSON string from the Code Node + const raw = response as unknown as Record; + const data = (raw.result ?? raw) as Record; + + console.log("[chatWithDocument] response keys:", Object.keys(raw), "data keys:", Object.keys(data)); + return { - ...data, - retrieved_nodes: safeParseJSON(data?.retrieved_nodes, []), + answer: (data.answer as string) ?? "", + messages: data.messages, + retrieved_nodes: safeParseJSON(data.retrieved_nodes, []), + thinking: (data.thinking as string) ?? "", }; } catch (error) { console.error("Chat flow error:", error); - throw new Error("Failed to get answer"); + throw new Error("Failed to get answer: " + (error as Error).message); } } @@ -79,24 +90,36 @@ export async function listDocuments() { } } -// ── Flow 4: Get full tree structure ─────────────────────────── +// ── Flow 4: Get full tree structure (or delete) ──────────────── export async function getDocumentTree(doc_id: string) { try { const response = await lamaticClient.executeFlow( process.env.FLOW_ID_TREE!, - { doc_id } + { doc_id, action: "get_tree" } ); const data = (response.result ?? response) as Record; - - // tree comes back as a JSON string (JSON.stringify applied in Code Node) - // Note: the flow outputMapping has a typo "tre_node_count" — handle both spellings return { ...data, tree: safeParseJSON(data?.tree, []), - tree_node_count: Number(data?.tree_node_count ?? data?.tre_node_count) || 0, + tree_node_count: Number(data?.tree_node_count) || 0, }; } catch (error) { console.error("Tree flow error:", error); throw new Error("Failed to get document tree"); } } + +// ── Flow 4 (delete action): Remove a document ─────────────────── +export async function deleteDocument(doc_id: string) { + try { + const response = await lamaticClient.executeFlow( + process.env.FLOW_ID_TREE!, + { doc_id, action: "delete" } + ); + const data = (response.result ?? response) as Record; + return data as { success: boolean; action: string; message: string; doc_id: string; file_name: string }; + } catch (error) { + console.error("Delete flow error:", error); + throw new Error("Failed to delete document"); + } +} diff --git a/kits/assistant/pageIndex-notebooklm/app/page.tsx b/kits/assistant/pageIndex-notebooklm/app/page.tsx index 9d153b91..03b8ffe0 100644 --- a/kits/assistant/pageIndex-notebooklm/app/page.tsx +++ b/kits/assistant/pageIndex-notebooklm/app/page.tsx @@ -108,6 +108,22 @@ export default function Page() { }} /> + {/* ── Demo notice ── */} +
    + + + + + Demo project · Document processing is limited to 30–50 pages · Chat history is not stored persistently + +
    + {/* ── Body ───────────────────────────────────── */}
    @@ -159,6 +175,14 @@ export default function Page() { documents={documents} selectedId={selectedDoc?.doc_id || null} onSelect={handleSelectDoc} + onDeleted={() => { + fetchDocuments(); + // If the deleted doc was selected, clear the view + setSelectedDoc(prev => { + const stillExists = documents.some(d => d.doc_id === prev?.doc_id); + return stillExists ? prev : null; + }); + }} />
    @@ -225,34 +249,33 @@ export default function Page() {
    - {/* Content */} -
    - {activeTab === "chat" ? ( - - ) : ( -
    - {treeLoading ? ( -
    - - - - - Building tree… - -
    - ) : tree.length > 0 ? ( - - ) : ( -
    - No tree structure available. -
    - )} -
    - )} + {/* Content — both panels stay mounted to preserve state */} +
    + +
    +
    +
    + {treeLoading ? ( +
    + + + + + Building tree… + +
    + ) : tree.length > 0 ? ( + + ) : ( +
    + No tree structure available. +
    + )} +
    ) : ( diff --git a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx index 92bce930..33bfac94 100644 --- a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx @@ -47,10 +47,12 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) const [sourcesOpen, setSourcesOpen] = useState(false); const [lastNodes, setLastNodes] = useState([]); const [lastThinking, setLastThinking] = useState(""); + // Raw Lamatic API message history — passed back each turn for multi-turn context + const [lamaticHistory, setLamaticHistory] = useState>([]); const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, loading]); - useEffect(() => { setMessages([]); setLastNodes([]); setLastThinking(""); setSourcesOpen(false); }, [docId]); + useEffect(() => { setMessages([]); setLastNodes([]); setLastThinking(""); setSourcesOpen(false); setLamaticHistory([]); }, [docId]); async function handleSend(e: React.FormEvent) { e.preventDefault(); @@ -61,8 +63,28 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) setInput(""); setLoading(true); try { - const result = await chatWithDocument(docId, userMsg.content, newMsgs) as unknown as ChatResponse; - setMessages(prev => [...prev, { role: "assistant", content: result.answer || "No answer found." }]); + const result = await chatWithDocument(docId, userMsg.content, lamaticHistory) as unknown as ChatResponse & { messages: unknown }; + const assistantContent = result.answer || "No answer found."; + setMessages(prev => [...prev, { role: "assistant", content: assistantContent }]); + + // Update Lamatic history for the next turn + // Try to use the flow-returned messages first + let nextHistory: Array<{ role: string; content: string }> | null = null; + if (result.messages) { + const parsed = typeof result.messages === "string" + ? (() => { try { return JSON.parse(result.messages as string); } catch { return null; } })() + : result.messages; + if (Array.isArray(parsed) && parsed.length) nextHistory = parsed; + } + // Fallback: build history from UI messages if flow didn't return it + if (!nextHistory) { + nextHistory = [...newMsgs, { role: "assistant", content: assistantContent }].map(m => ({ + role: m.role, + content: m.content, + })); + } + setLamaticHistory(nextHistory); + if (Array.isArray(result.retrieved_nodes) && result.retrieved_nodes.length) { setLastNodes(result.retrieved_nodes); setLastThinking(result.thinking || ""); diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx index 41622a68..5c17720c 100644 --- a/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx @@ -1,14 +1,33 @@ "use client"; +import { useState } from "react"; import { Document } from "@/lib/types"; +import { deleteDocument } from "@/actions/orchestrate"; interface Props { documents: Document[]; selectedId: string | null; onSelect: (doc: Document) => void; + onDeleted?: () => void; } -export default function DocumentList({ documents, selectedId, onSelect }: Props) { +export default function DocumentList({ documents, selectedId, onSelect, onDeleted }: Props) { + const [confirmId, setConfirmId] = useState(null); + const [deletingId, setDeletingId] = useState(null); + + async function handleDelete(doc_id: string) { + setDeletingId(doc_id); + setConfirmId(null); + try { + await deleteDocument(doc_id); + onDeleted?.(); + } catch { + // silently ignore — list refresh will reflect reality + } finally { + setDeletingId(null); + } + } + if (documents.length === 0) { return (
    @@ -32,32 +51,34 @@ export default function DocumentList({ documents, selectedId, onSelect }: Props)
      {documents.map((doc, idx) => { const active = selectedId === doc.doc_id; + const isConfirming = confirmId === doc.doc_id; + const isDeleting = deletingId === doc.doc_id; + return ( -
    • +
    • + + {/* Delete controls — sit outside the main button */} +
      + {isDeleting ? ( + + + + ) : isConfirming ? ( + <> + {/* Confirm */} + + {/* Cancel */} + + + ) : ( + + )} +
      + + {/* Make delete btn visible on row hover via CSS */} +
    • ); })} diff --git a/kits/assistant/pageIndex-notebooklm/config.json b/kits/assistant/pageIndex-notebooklm/config.json index 50fc69e6..f2486683 100644 --- a/kits/assistant/pageIndex-notebooklm/config.json +++ b/kits/assistant/pageIndex-notebooklm/config.json @@ -13,14 +13,14 @@ "envKey": "FLOW_CHAT_WITH_PDF" }, { - "id": "flow-1-upload-pdf-build-tree-save", + "id": "flow-4-get-tree-structure", "type": "mandatory", - "envKey": "FLOW_FLOW_1_UPLOAD_PDF_BUILD_TREE_SAVE" + "envKey": "FLOW_FLOW_4_GET_TREE_STRUCTURE" }, { - "id": "flow-4-get-tree-structure", + "id": "flow-1-upload-pdf-build-tree-save", "type": "mandatory", - "envKey": "FLOW_FLOW_4_GET_TREE_STRUCTURE" + "envKey": "FLOW_FLOW_1_UPLOAD_PDF_BUILD_TREE_SAVE" }, { "id": "flow-list-all-documents", diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json index 138fb356..abeb0fd1 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json @@ -101,7 +101,7 @@ } ], "memories": "[]", - "messages": "[]", + "messages": "{{triggerNode_1.output.messages}}", "nodeName": "Generate JSON", "attachments": "", "generativeModelName": "" @@ -127,6 +127,7 @@ "nodeId": "codeNode", "schema": { "context": "string", + "total_chars": "number", "retrieved_nodes": "array" }, "values": { @@ -168,7 +169,7 @@ } ], "memories": "[]", - "messages": "{{triggerNode_1.output.messages}}[]", + "messages": "{{triggerNode_1.output.messages}}", "nodeName": "Generate Text", "attachments": "", "credentials": "", @@ -198,7 +199,7 @@ "nodeName": "API Response", "webhookUrl": "", "retry_delay": "0", - "outputMapping": "{\n \"answer\": \"{{LLMNode_392.output.generatedResponse}}\",\n \"retrieved_nodes\": \"{{codeNode_358.output.retrieved_nodes}}\",\n \"thinking\": \"{{InstructorLLMNode_432.output.thinking}}\",\n \"doc_id\": \"{{triggerNode_1.output.doc_id}}\"\n}" + "outputMapping": "{\n \"answer\": \"{{LLMNode_392.output.generatedResponse}}\",\n \"messages\": \"{{LLMNode_392.output.messages}}\",\n \"retrieved_nodes\": \"{{codeNode_358.output.retrieved_nodes}}\",\n \"thinking\": \"{{InstructorLLMNode_432.output.thinking}}\",\n \"doc_id\": \"{{triggerNode_1.output.doc_id}}\"\n}" }, "isResponseNode": true }, diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md index 98bc131f..7ea7f7b9 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md @@ -2,19 +2,20 @@ ## About This Flow -This flow automates a workflow with **4 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. +This flow automates a workflow with **6 nodes** working together to process and transform data. The flow is designed to streamline operations and can be easily integrated into your existing systems. ## Flow Components This workflow includes the following node types: - API Request +- Condition - Postgres - Code - API Response ## Configuration Requirements -This flow requires configuration for **1 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. +This flow requires configuration for **2 node(s)** with private inputs (credentials, API keys, model selections, etc.). All required configurations are documented in the `inputs.json` file. ## Files Included diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json index 9f4a1064..ed6a1441 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json @@ -9,7 +9,7 @@ "id": "triggerNode_1", "nodeName": "API Request", "responeType": "realtime", - "advance_schema": "{\n \"doc_id\": \"string\"\n}" + "advance_schema": "{\n \"doc_id\": \"string\",\n \"action\": \"string\"\n}" }, "trigger": true }, @@ -19,23 +19,58 @@ "height": 93 }, "position": { - "x": 0, + "x": 225, "y": 0 }, + "selected": true + }, + { + "id": "conditionNode_944", + "data": { + "label": "Condition", + "modes": {}, + "nodeId": "conditionNode", + "values": { + "id": "conditionNode_944", + "nodeName": "Condition", + "conditions": [ + { + "label": "Condition 1", + "value": "conditionNode_944-addNode_581", + "condition": "{\n \"operator\": null,\n \"operands\": [\n {\n \"name\": \"{{triggerNode_1.output.action}}\",\n \"operator\": \"==\",\n \"value\": \"get_tree\"\n }\n ]\n}" + }, + { + "label": "Else", + "value": "conditionNode_944-addNode_226", + "condition": {} + } + ], + "allowMultipleConditionExecution": false + } + }, + "type": "conditionNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 225, + "y": 130 + }, "selected": false }, { - "id": "postgresNode_658", + "id": "postgresNode_113", "data": { - "label": "dynamicNode node", + "label": "New", "logic": [], "modes": {}, "nodeId": "postgresNode", "values": { - "id": "postgresNode_658", - "query": "SELECT tree, file_name, tree_node_count, created_at FROM documents WHERE doc_id = '{{triggerNode_1.output.doc_id}}' LIMIT 1;", + "id": "postgresNode_113", + "query": "DELETE FROM documents WHERE doc_id = '{{triggerNode_1.output.doc_id}}' RETURNING doc_id, file_name;", "action": "runQuery", - "nodeName": "Postgres", + "nodeName": "Delete Document", "credentials": "" } }, @@ -45,28 +80,24 @@ "height": 93 }, "position": { - "x": 0, - "y": 130 + "x": 450, + "y": 260 }, "selected": false }, { - "id": "codeNode_639", + "id": "postgresNode_206", "data": { - "label": "dynamicNode node", + "label": "New", "logic": [], "modes": {}, - "nodeId": "codeNode", - "schema": { - "tree": "string", - "file_name": "string", - "created_at": "string", - "tree_node_count": "number" - }, + "nodeId": "postgresNode", "values": { - "id": "codeNode_639", - "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst doc = {{postgresNode_658.output.queryResult[0]}};\n\noutput = {\n tree: JSON.stringify(doc.tree),\n file_name: doc.file_name,\n tree_node_count: doc.tree_node_count,\n created_at: doc.created_at\n};", - "nodeName": "Code" + "id": "postgresNode_206", + "query": "SELECT tree, file_name, tree_node_count, created_at FROM documents WHERE doc_id = '{{triggerNode_1.output.doc_id}}' LIMIT 1;", + "action": "runQuery", + "nodeName": "Get Tree", + "credentials": "" } }, "type": "dynamicNode", @@ -80,19 +111,41 @@ }, "selected": false }, + { + "id": "codeNode_merge", + "data": { + "logic": [], + "modes": {}, + "nodeId": "codeNode", + "values": { + "id": "codeNode_merge", + "code": "const action = {{triggerNode_1.output.action}};\n\nif (action === \"get_tree\") {\n const getResult = {{postgresNode_206.output.queryResult}};\n const doc = getResult && getResult[0];\n\n if (!doc) {\n output = {\n success: false,\n action: \"get_tree\",\n message: \"Document not found\",\n doc_id: {{triggerNode_1.output.doc_id}},\n tree: null,\n file_name: null,\n tree_node_count: null,\n created_at: null\n };\n } else {\n output = {\n success: true,\n action: \"get_tree\",\n message: \"Document fetched successfully\",\n doc_id: {{triggerNode_1.output.doc_id}},\n tree: JSON.stringify(doc.tree),\n file_name: doc.file_name,\n tree_node_count: doc.tree_node_count,\n created_at: doc.created_at\n };\n }\n\n} else {\n const deleteResult = {{postgresNode_113.output.queryResult}};\n\n if (!deleteResult || deleteResult.length === 0) {\n output = {\n success: false,\n action: \"delete\",\n message: \"Document not found or already deleted\",\n doc_id: {{triggerNode_1.output.doc_id}},\n tree: null,\n file_name: null,\n tree_node_count: null,\n created_at: null\n };\n } else {\n output = {\n success: true,\n action: \"delete\",\n message: \"Document deleted successfully\",\n doc_id: deleteResult[0].doc_id,\n tree: null,\n file_name: deleteResult[0].file_name,\n tree_node_count: null,\n created_at: null\n };\n }\n}", + "nodeName": "Merge Response" + } + }, + "type": "dynamicNode", + "measured": { + "width": 216, + "height": 93 + }, + "position": { + "x": 225, + "y": 390 + }, + "selected": false, + "draggable": false + }, { "id": "responseNode_triggerNode_1", "data": { "label": "Response", + "modes": {}, "nodeId": "graphqlResponseNode", "values": { "id": "responseNode_triggerNode_1", "headers": "{\"content-type\":\"application/json\"}", - "retries": "0", "nodeName": "API Response", - "webhookUrl": "", - "retry_delay": "0", - "outputMapping": "{\n \"tree\": \"{{codeNode_639.output.tree}}\",\n \"file_name\": \"{{codeNode_639.output.file_name}}\",\n \"tre_node_count\": \"{{codeNode_639.output.tree_node_count}}\",\n \"created_at\": \"{{codeNode_639.output.created_at}}\"\n}" + "outputMapping": "{\n \"success\": \"{{codeNode_merge.output.success}}\",\n \"action\": \"{{codeNode_merge.output.action}}\",\n \"message\": \"{{codeNode_merge.output.message}}\",\n \"doc_id\": \"{{codeNode_merge.output.doc_id}}\",\n \"tree\": \"{{codeNode_merge.output.tree}}\",\n \"file_name\": \"{{codeNode_merge.output.file_name}}\",\n \"tree_node_count\": \"{{codeNode_merge.output.tree_node_count}}\",\n \"created_at\": \"{{codeNode_merge.output.created_at}}\"\n}" }, "isResponseNode": true }, @@ -102,39 +155,69 @@ "height": 93 }, "position": { - "x": 0, - "y": 390 + "x": 225, + "y": 520 }, - "selected": true + "selected": false } ], "edges": [ { - "id": "triggerNode_1-postgresNode_658", + "id": "codeNode_merge-responseNode_triggerNode_1", "type": "defaultEdge", - "source": "triggerNode_1", - "target": "postgresNode_658", + "source": "codeNode_merge", + "target": "responseNode_triggerNode_1", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "conditionNode_944-postgresNode_206", + "data": { + "condition": "Condition 1" + }, + "type": "conditionEdge", + "source": "conditionNode_944", + "target": "postgresNode_206", "sourceHandle": "bottom", "targetHandle": "top" }, { - "id": "postgresNode_658-codeNode_639", + "id": "conditionNode_944-postgresNode_113", + "data": { + "condition": "Else" + }, + "type": "conditionEdge", + "source": "conditionNode_944", + "target": "postgresNode_113", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "postgresNode_206-codeNode_merge", "type": "defaultEdge", - "source": "postgresNode_658", - "target": "codeNode_639", + "source": "postgresNode_206", + "target": "codeNode_merge", "sourceHandle": "bottom", "targetHandle": "top" }, { - "id": "codeNode_639-responseNode_triggerNode_1", + "id": "postgresNode_113-codeNode_merge", "type": "defaultEdge", - "source": "codeNode_639", - "target": "responseNode_triggerNode_1", + "source": "postgresNode_113", + "target": "codeNode_merge", + "sourceHandle": "bottom", + "targetHandle": "top" + }, + { + "id": "triggerNode_1-conditionNode_944", + "type": "defaultEdge", + "source": "triggerNode_1", + "target": "conditionNode_944", "sourceHandle": "bottom", "targetHandle": "top" }, { - "id": "response-trigger_triggerNode_1", + "id": "response-responseNode_triggerNode_1", "type": "responseEdge", "source": "triggerNode_1", "target": "responseNode_triggerNode_1", diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json index d5996d3b..70de2392 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json @@ -1,5 +1,17 @@ { - "postgresNode_658": [ + "postgresNode_113": [ + { + "name": "credentials", + "label": "Credentials", + "description": "Select the credentials for postgres authentication.", + "type": "select", + "isCredential": true, + "required": true, + "defaultValue": "", + "isPrivate": true + } + ], + "postgresNode_206": [ { "name": "credentials", "label": "Credentials", diff --git a/kits/assistant/pageIndex-notebooklm/lib/types.ts b/kits/assistant/pageIndex-notebooklm/lib/types.ts index 5f011dcc..552e1fed 100644 --- a/kits/assistant/pageIndex-notebooklm/lib/types.ts +++ b/kits/assistant/pageIndex-notebooklm/lib/types.ts @@ -61,8 +61,20 @@ export interface ListResponse { } export interface TreeResponse { + success: boolean; + action: string; + message: string; + doc_id: string; tree: TreeNode[]; file_name: string; tree_node_count: number; created_at: string; } + +export interface DeleteResponse { + success: boolean; + action: string; + message: string; + doc_id: string; + file_name: string; +} From 9611d67423d940bfd49269e1b8f0941b747a3dad Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 29 Mar 2026 00:16:17 +0530 Subject: [PATCH 10/29] feat: implement ChatWindow component and orchestration logic for PDF document interaction --- kits/assistant/pageIndex-notebooklm/README.md | 194 +++++++++++++----- .../actions/orchestrate.ts | 16 +- .../components/ChatWindow.tsx | 38 +++- .../pageIndex-notebooklm/config.json | 61 ++++-- .../flows/chat-with-pdf/README.md | 2 +- .../flows/chat-with-pdf/config.json | 8 +- .../README.md | 2 +- .../flows/flow-4-get-tree-structure/README.md | 2 +- .../flows/flow-list-all-documents/README.md | 2 +- 9 files changed, 236 insertions(+), 89 deletions(-) diff --git a/kits/assistant/pageIndex-notebooklm/README.md b/kits/assistant/pageIndex-notebooklm/README.md index a206566e..3e5e0197 100644 --- a/kits/assistant/pageIndex-notebooklm/README.md +++ b/kits/assistant/pageIndex-notebooklm/README.md @@ -1,70 +1,127 @@ # PageIndex NotebookLM — AgentKit -Upload any PDF and chat with it using **vectorless, tree-structured RAG**. -No vector database. No chunking. Just a hierarchical document index built from the table of contents. +Upload any PDF and chat with it using **vectorless, tree-structured RAG** — powered **end-to-end by Lamatic AI flows**. + +> **No vector database. No external Python server. No custom backend code.** +> Just 4 Lamatic flows + a Next.js frontend that implements the full PageIndex pipeline — from PDF ingestion to tree-navigated question answering — entirely within Lamatic's orchestration layer. + +--- + +## What Makes This Different + +Most RAG implementations require a vector database, an embedding model, a retrieval server, and often a separate Python backend. **This kit eliminates all of that.** + +The entire PageIndex pipeline — TOC detection, tree construction, page indexing, summary generation, tree-navigated search, and LLM answering — is implemented as **4 Lamatic AI flows** with zero external servers or Python code. The Next.js frontend communicates exclusively with Lamatic's flow execution API via the official `lamatic` SDK. + +### Key Highlights + +- **100% Lamatic-powered backend** — all document processing, indexing, retrieval, and answering logic lives inside Lamatic flows +- **No vector DB** — uses a hierarchical tree index (built from the document's table of contents) instead of vector embeddings +- **No external server** — no FastAPI, no Railway, no Python — the Lamatic flows handle everything +- **No chunking** — sections are identified by their structural position in the document, not arbitrary text splits + +--- + +## Architecture + +``` +┌────────────────────────────────────────────────────┐ +│ Next.js Frontend │ +│ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌─────────┐ │ +│ │ Document │ │ Chat │ │ Tree │ │Document │ │ +│ │ Upload │ │ Window │ │ Viewer │ │ List │ │ +│ └────┬─────┘ └────┬─────┘ └───┬────┘ └────┬────┘ │ +│ │ │ │ │ │ +│ ┌────┴─────────────┴───────────┴────────────┴───┐ │ +│ │ Server Actions (orchestrate.ts) │ │ +│ └────────────────────┬──────────────────────────┘ │ +│ │ │ +│ Lamatic SDK (lamatic npm) │ +└───────────────────────┼─────────────────────────────┘ + │ + ┌────────────┴────────────┐ + │ Lamatic AI Platform │ + │ │ + │ Flow 1: Upload + Index │ + │ Flow 2: Chat + Retrieve │ + │ Flow 3: List Documents │ + │ Flow 4: Tree / Delete │ + │ │ + │ ┌──────────────┐ │ + │ │ Supabase │ │ + │ │ (PostgreSQL) │ │ + │ └──────────────┘ │ + └──────────────────────────┘ +``` --- ## How It Works -A 7-stage FastAPI pipeline (running on Railway, powered by Groq) processes each PDF: +### Document Ingestion (Flow 1) + +When a PDF is uploaded, the Lamatic flow runs a multi-stage pipeline: -1. **TOC Detection** — concurrent LLM scan of first 20 pages +1. **TOC Detection** — scans the first pages to locate the table of contents 2. **TOC Extraction** — multi-pass extraction with completion verification -3. **TOC → JSON** — structured flat list with hierarchy (`1`, `1.1`, `1.2.3`) -4. **Physical Index Assignment** — verify each section starts on the correct page (±3 scan) -5. **Tree Build** — nested structure with exact `start_index` + `end_index` per section -6. **Summaries** — concurrent 1-2 sentence summary per node (≤200 chars) -7. **Page Verification** — fuzzy match each node title against actual page text +3. **TOC → JSON** — structured flat list with hierarchy identifiers (`1`, `1.1`, `1.2.3`) +4. **Physical Index Assignment** — verifies each section starts on the correct page +5. **Tree Build** — nested tree structure with exact `start_index` + `end_index` per section +6. **Summary Generation** — 1–2 sentence summary per node +7. **Page Verification** — fuzzy-matches node titles against actual page text +8. **Save** — stores the tree + metadata in Supabase + +### Chat & Retrieval (Flow 2) + +At query time, the LLM navigates the tree like a table of contents: +1. Receives the full tree structure with section titles and summaries +2. Selects the most relevant leaf nodes based on the query +3. Fetches verbatim page content using exact `start_index → end_index` ranges +4. Generates an answer grounded in the retrieved content -At query time, the LLM navigates the tree like a table of contents to pick the right sections, then fetches verbatim page content using the exact `start_index → end_index` range. +The frontend receives the answer, the retrieved nodes with page ranges, and the LLM's tree-navigation reasoning — all displayed in the UI. --- ## Stack | Layer | Technology | -|-------|-----------| -| Orchestration | Lamatic AI (4 flows) | -| LLM | Groq — llama-3.3-70b-versatile (multi-key pool, free tier) | -| Indexing API | FastAPI on Railway | -| Storage | Supabase (PostgreSQL) | -| Frontend | Next.js + Tailwind CSS | +|---|---| +| Orchestration & Backend | **Lamatic AI** (4 flows — no external server) | +| Storage | **Supabase** (PostgreSQL) | +| Frontend | **Next.js 15** (App Router, Server Actions) | +| Styling | **CSS custom properties** (dark-mode design system) | +| SDK | **`lamatic`** npm package | + +--- + +## Features + +- **PDF Upload** — drag-and-drop or paste a URL +- **Tree-Structured RAG** — vectorless retrieval using hierarchical document index +- **Multi-Turn Chat** — conversational history maintained across messages +- **Chat Persistence** — conversations saved to `localStorage`, survive page navigations +- **Interactive Tree Viewer** — explore the full document structure, nodes highlight on retrieval +- **Source Panel** — view retrieved sections with page ranges and LLM reasoning +- **Document Management** — list all documents, view trees, delete documents +- **Markdown Rendering** — AI responses rendered with headings, lists, bold, code +- **Responsive Dark UI** — premium design system with animations and micro-interactions --- ## Prerequisites - [Lamatic AI](https://lamatic.ai) account (free) -- [Groq](https://console.groq.com) account (free — create multiple for key pool) -- [Railway](https://railway.app) account (for FastAPI server) - [Supabase](https://supabase.com) account (free tier) - Node.js 18+ +> **That's it.** No Groq account, no Railway, no Python environment needed. + --- ## Setup -### 1. Deploy the FastAPI Server (Railway) - -```bash -# Clone your fork, then: -cd pageindex-server # contains main.py + requirements.txt -railway init -railway up -``` - -Add these environment variables in Railway dashboard: - -| Variable | Value | -|----------|-------| -| `SERVER_API_KEY` | Any secret string (e.g. `openssl rand -hex 16`) | -| `GROQ_API_KEY_1` | First Groq API key | -| `GROQ_API_KEY_2` | Second Groq API key (optional — more keys = higher throughput) | - -Note your Railway URL: `https://your-app.up.railway.app` - -### 2. Set Up Supabase +### 1. Set Up Supabase Run this SQL in Supabase SQL Editor: @@ -84,17 +141,25 @@ alter table documents enable row level security; create policy "service_access" on documents for all using (true); ``` -### 3. Set Up Lamatic Flows +### 2. Import Lamatic Flows + +Import all 4 flows from the `flows/` folder into Lamatic Studio: -Import all 4 flows from the `flows/` folder into Lamatic Studio, then add these secrets in **Lamatic → Settings → Secrets**: +| Flow | Folder | Purpose | +|---|---|---| +| Upload | `flows/flow-1-upload-pdf-build-tree-save/` | PDF → 7-stage pipeline → tree index → Supabase | +| Chat | `flows/chat-with-pdf/` | Tree search → page fetch → LLM answer | +| List | `flows/flow-list-all-documents/` | List all documents from Supabase | +| Tree | `flows/flow-4-get-tree-structure/` | Return full tree JSON or delete a document | + +Add these secrets in **Lamatic → Settings → Secrets**: | Secret | Value | -|--------|-------| -| `SERVER_API_KEY` | Same value as Railway | +|---|---| | `SUPABASE_URL` | `https://xxx.supabase.co` | | `SUPABASE_ANON_KEY` | From Supabase Settings → API | -### 4. Install and Configure the Kit +### 3. Install and Configure ```bash cd kits/assistant/pageindex-notebooklm @@ -104,7 +169,7 @@ cp .env.example .env.local Fill in `.env.local`: -``` +```env LAMATIC_API_KEY=... # Lamatic → Settings → API Keys LAMATIC_PROJECT_ID=... # Lamatic → Settings → Project ID LAMATIC_API_URL=... # Lamatic → Settings → API Docs → Endpoint @@ -115,7 +180,7 @@ FLOW_ID_LIST=... # Flow 3 → three-dot menu → Copy ID FLOW_ID_TREE=... # Flow 4 → three-dot menu → Copy ID ``` -### 5. Run Locally +### 4. Run Locally ```bash npm run dev @@ -124,29 +189,46 @@ npm run dev --- -## Flows +## Project Structure -| Flow | File | Purpose | -|------|------|---------| -| Upload | `flows/pageindex-upload/` | Download PDF → 7-stage pipeline → save tree to Supabase | -| Chat | `flows/pageindex-chat/` | Tree search → page fetch → Groq answer | -| List | `flows/pageindex-list/` | List all documents from Supabase | -| Tree | `flows/pageindex-tree/` | Return full tree JSON for a document | +``` +pageindex-notebooklm/ +├── actions/ +│ └── orchestrate.ts # Server actions — all 4 flow calls via Lamatic SDK +├── app/ +│ ├── globals.css # Design system (CSS custom properties, animations) +│ ├── layout.tsx # Root layout with metadata +│ └── page.tsx # Main page — document list + chat + tree viewer +├── components/ +│ ├── ChatWindow.tsx # Chat UI with markdown, sources, persistence +│ ├── DocumentList.tsx # Document sidebar with search + delete +│ ├── DocumentUpload.tsx # Drag-and-drop / URL upload +│ └── TreeViewer.tsx # Interactive hierarchical tree viewer +├── flows/ +│ ├── flow-1-upload-pdf-build-tree-save/ +│ ├── chat-with-pdf/ +│ ├── flow-list-all-documents/ +│ └── flow-4-get-tree-structure/ +├── lib/ +│ ├── lamatic-client.ts # Lamatic SDK initialization +│ └── types.ts # TypeScript interfaces +├── config.json # Kit metadata +└── .env.example # Environment variable template +``` --- ## Deploying to Vercel ```bash -# Push your branch first git checkout -b feat/pageindex-notebooklm git add kits/assistant/pageindex-notebooklm/ -git commit -m "feat: Add PageIndex NotebookLM — vectorless tree-structured RAG" +git commit -m "feat: PageIndex NotebookLM — end-to-end Lamatic-powered tree RAG" git push origin feat/pageindex-notebooklm ``` Then in Vercel: -1. Import your forked repo +1. Import your repo 2. Set **Root Directory** → `kits/assistant/pageindex-notebooklm` 3. Add all 7 env vars from `.env.local` 4. Deploy @@ -155,5 +237,5 @@ Then in Vercel: ## Author -**Saurabh Tiwari** — [st108113@gmail.com](mailto:st108113@gmail.com) +**Saurabh Tiwari** — [st108113@gmail.com](mailto:st108113@gmail.com) GitHub: [@Skt329](https://github.com/Skt329) diff --git a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts index 17273d0a..0dc17600 100644 --- a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts @@ -43,19 +43,23 @@ export async function chatWithDocument( try { if (!process.env.FLOW_ID_CHAT) throw new Error("FLOW_ID_CHAT not set"); + const payload = { + doc_id, + query, + messages: JSON.stringify(messages), + }; + + console.log("[chatWithDocument] SENDING →", JSON.stringify(payload, null, 2)); + const response = await lamaticClient.executeFlow( process.env.FLOW_ID_CHAT!, - { - doc_id, - query, - messages: JSON.stringify(messages), // pass history as string - } + payload ); const raw = response as unknown as Record; const data = (raw.result ?? raw) as Record; - console.log("[chatWithDocument] response keys:", Object.keys(raw), "data keys:", Object.keys(data)); + console.log("[chatWithDocument] RECEIVED ←", JSON.stringify(data, null, 2)); return { answer: (data.answer as string) ?? "", diff --git a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx index 33bfac94..f9b91a68 100644 --- a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx +++ b/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx @@ -41,18 +41,50 @@ interface Props { } export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) { - const [messages, setMessages] = useState([]); + const storageKey = `chat_${docId}`; + const historyKey = `chat_history_${docId}`; + + const [messages, setMessages] = useState(() => { + if (typeof window === "undefined") return []; + try { const s = localStorage.getItem(storageKey); return s ? JSON.parse(s) : []; } catch { return []; } + }); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [sourcesOpen, setSourcesOpen] = useState(false); const [lastNodes, setLastNodes] = useState([]); const [lastThinking, setLastThinking] = useState(""); // Raw Lamatic API message history — passed back each turn for multi-turn context - const [lamaticHistory, setLamaticHistory] = useState>([]); + const [lamaticHistory, setLamaticHistory] = useState>(() => { + if (typeof window === "undefined") return []; + try { const s = localStorage.getItem(historyKey); return s ? JSON.parse(s) : []; } catch { return []; } + }); const bottomRef = useRef(null); + // Persist messages to localStorage whenever they change + useEffect(() => { + try { localStorage.setItem(storageKey, JSON.stringify(messages)); } catch { /* quota exceeded */ } + }, [messages, storageKey]); + + // Persist lamatic history + useEffect(() => { + try { localStorage.setItem(historyKey, JSON.stringify(lamaticHistory)); } catch { /* quota exceeded */ } + }, [lamaticHistory, historyKey]); + useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, loading]); - useEffect(() => { setMessages([]); setLastNodes([]); setLastThinking(""); setSourcesOpen(false); setLamaticHistory([]); }, [docId]); + + // When docId changes, restore from localStorage (lazy init already handles initial mount) + useEffect(() => { + try { + const saved = localStorage.getItem(`chat_${docId}`); + setMessages(saved ? JSON.parse(saved) : []); + const savedHistory = localStorage.getItem(`chat_history_${docId}`); + setLamaticHistory(savedHistory ? JSON.parse(savedHistory) : []); + } catch { + setMessages([]); + setLamaticHistory([]); + } + setLastNodes([]); setLastThinking(""); setSourcesOpen(false); + }, [docId]); async function handleSend(e: React.FormEvent) { e.preventDefault(); diff --git a/kits/assistant/pageIndex-notebooklm/config.json b/kits/assistant/pageIndex-notebooklm/config.json index f2486683..a43659e1 100644 --- a/kits/assistant/pageIndex-notebooklm/config.json +++ b/kits/assistant/pageIndex-notebooklm/config.json @@ -1,37 +1,66 @@ { - "name": "Flow Collection - 4 Flows", - "description": "A collection of 4 flows exported from Lamatic", - "tags": [], + "name": "PageIndex NotebookLM — Vectorless Tree-Structured RAG", + "description": "Upload any PDF and chat with it using vectorless, tree-structured RAG powered entirely by Lamatic AI flows. No vector database, no external Python server, no chunking — just a hierarchical document index built from the table of contents.", + "tags": [ + "pageindex", + "rag", + "notebooklm", + "tree-structured-rag", + "document-chat", + "pdf", + "lamatic", + "next.js", + "agentkit" + ], "author": { - "name": "Lamatic AI", - "email": "info@lamatic.ai" + "name": "Saurabh Tiwari", + "email": "st108113@gmail.com", + "github": "https://github.com/Skt329" }, "steps": [ { - "id": "chat-with-pdf", + "id": "flow-1-upload-pdf-build-tree-save", "type": "mandatory", - "envKey": "FLOW_CHAT_WITH_PDF" + "envKey": "FLOW_ID_UPLOAD", + "description": "Upload PDF → build hierarchical tree index → save to Supabase" }, { - "id": "flow-4-get-tree-structure", + "id": "chat-with-pdf", "type": "mandatory", - "envKey": "FLOW_FLOW_4_GET_TREE_STRUCTURE" + "envKey": "FLOW_ID_CHAT", + "description": "Tree-navigated search → page content fetch → LLM answer" }, { - "id": "flow-1-upload-pdf-build-tree-save", + "id": "flow-list-all-documents", "type": "mandatory", - "envKey": "FLOW_FLOW_1_UPLOAD_PDF_BUILD_TREE_SAVE" + "envKey": "FLOW_ID_LIST", + "description": "List all uploaded documents from Supabase" }, { - "id": "flow-list-all-documents", + "id": "flow-4-get-tree-structure", "type": "mandatory", - "envKey": "FLOW_FLOW_LIST_ALL_DOCUMENTS" + "envKey": "FLOW_ID_TREE", + "description": "Get full tree JSON for a document or delete a document" } ], - "integrations": [], - "features": [], + "integrations": [ + "lamatic-ai", + "supabase" + ], + "features": [ + "PDF upload with drag-and-drop or URL", + "Vectorless tree-structured RAG (no vector DB)", + "Hierarchical table-of-contents index", + "Multi-turn conversational chat with document", + "Chat history persistence across sessions", + "Interactive tree viewer with node highlighting", + "Source retrieval with page ranges and LLM reasoning", + "Document management (list, view, delete)", + "Markdown-rendered AI responses", + "Responsive dark-mode UI" + ], "demoUrl": "", - "githubUrl": "", + "githubUrl": "https://github.com/Skt329/AgentKit", "deployUrl": "", "documentationUrl": "", "imageUrl": "" diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md index a25e2edc..9293d95c 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md @@ -62,4 +62,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/27/2026* +*Generated on 3/29/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json index abeb0fd1..78b940bd 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json +++ b/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json @@ -63,7 +63,7 @@ }, "values": { "id": "codeNode_429", - "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\n\n// tree.nodes are string IDs, not objects — just pass them as-is\nfunction stripToTOC(nodes) {\n return nodes.map(node => ({\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n description: node.summary,\n children: node.nodes || [] // keep as string IDs, LLM can still read them\n }));\n}\n\noutput = {\n toc_json: JSON.stringify(stripToTOC(tree)),\n node_count: tree.length\n};", + "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\n\n// tree.nodes are string IDs, not objects — just pass them as-is\nfunction stripToTOC(nodes, depth = 0) {\n return nodes.map(node => ({\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n description: node.summary,\n children: node.nodes && node.nodes.length > 0\n ? stripToTOC(node.nodes, depth + 1)\n : []\n }));\n}\n\noutput = {\n toc_json: JSON.stringify(stripToTOC(tree)),\n node_count: tree.length\n};", "nodeName": "Code" } }, @@ -132,7 +132,7 @@ }, "values": { "id": "codeNode_358", - "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\nconst rawTextRaw = {{postgresNode_817.output.queryResult[0].raw_text}};\nconst nodeList = {{InstructorLLMNode_432.output.node_list}};\n\n// Parse raw_text (stored as TEXT in DB)\nconst pages = typeof rawTextRaw === \"string\" ? JSON.parse(rawTextRaw) : rawTextRaw;\n\n// Build flat node map from top-level tree array\nconst nodeMap = {};\nfor (const n of tree) {\n nodeMap[n.node_id] = n;\n}\n\nconst selectedNodes = nodeList.map(id => nodeMap[id]).filter(Boolean);\n\nconst MAX_PAGES_PER_NODE = 2; // hard cap — never more than 2 pages per node\nconst MAX_CHARS_PER_PAGE = 2000; // truncate very long pages\n\nconst retrieved = selectedNodes.map(node => {\n const startPage = (node.start_index || 1) - 1; // 0-based\n const rawEndPage = (node.end_index || node.start_index || 1) - 1;\n\n // Enforce max pages cap\n const endPage = Math.min(rawEndPage, startPage + MAX_PAGES_PER_NODE - 1, pages.length - 1);\n\n const pageSlices = pages\n .slice(startPage, endPage + 1)\n .map(p => p.length > MAX_CHARS_PER_PAGE ? p.slice(0, MAX_CHARS_PER_PAGE) + \"...\" : p);\n\n const pageContent = pageSlices.join(\"\\n\\n\").trim();\n\n return {\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n summary: node.summary,\n page_content: pageContent || node.summary\n };\n});\n\nconst context = retrieved\n .map(n => `[Section: \"${n.title}\" | Pages: ${n.start_index}–${n.end_index}]\\n${n.page_content}`)\n .join(\"\\n\\n---\\n\\n\");\n\noutput = {\n context,\n retrieved_nodes: retrieved,\n total_chars: context.length\n};", + "code": "const tree = {{postgresNode_817.output.queryResult[0].tree}};\nconst rawTextRaw = {{postgresNode_817.output.queryResult[0].raw_text}};\nconst nodeList = {{InstructorLLMNode_432.output.node_list}};\n\nconst pages = typeof rawTextRaw === \"string\" ? JSON.parse(rawTextRaw) : rawTextRaw;\n\n// ── Detect page offset ────────────────────────────────────────────────────────\n// The tree uses PRINTED page numbers from the TOC.\n// The pages array uses PHYSICAL page indices (0-based).\n// Books with roman-numeral front matter have an offset between the two.\n//\n// Strategy: find the first chapter node in the tree (depth 1, lowest start_index).\n// Search physical pages for that chapter's title text.\n// offset = physical_page_found - tree_start_index\n\nfunction detectOffset(tree, pages) {\n // Get all top-level nodes sorted by start_index\n const topLevel = [...tree].sort((a, b) => (a.start_index || 0) - (b.start_index || 0));\n \n for (const node of topLevel) {\n const printedPage = node.start_index;\n if (!printedPage || !node.title) continue;\n \n const titleLower = node.title.toLowerCase().replace(/chapter-?\\d+[-:\\s]*/i, \"\").trim();\n if (titleLower.length < 3) continue;\n\n // Search physical pages around where we expect to find it\n // Front matter is rarely more than 40 pages\n for (let physIdx = 0; physIdx < Math.min(pages.length, 60); physIdx++) {\n const pageText = (pages[physIdx] || \"\").toLowerCase();\n if (pageText.includes(titleLower)) {\n const offset = physIdx - (printedPage - 1); // physIdx is 0-based, printedPage is 1-based\n if (offset !== 0) {\n console.log(`[offset] \"${node.title}\" TOC page ${printedPage} → physical index ${physIdx} → offset +${offset}`);\n }\n return offset;\n }\n }\n }\n return 0; // no offset detected\n}\n\nconst pageOffset = detectOffset(tree, pages);\n\n// ── Flatten tree recursively ──────────────────────────────────────────────────\nfunction flattenTree(nodes, map = {}) {\n for (const n of nodes) {\n map[n.node_id] = n;\n if (n.nodes && n.nodes.length > 0) flattenTree(n.nodes, map);\n }\n return map;\n}\nconst nodeMap = flattenTree(tree);\nconst selectedNodes = nodeList.map(id => nodeMap[id]).filter(Boolean);\n\nconst MAX_PAGES_PER_NODE = 2;\nconst MAX_CHARS_PER_PAGE = 2000;\n\nconst retrieved = selectedNodes.map(node => {\n // Apply offset: convert printed page number → physical array index (0-based)\n const startPhysIdx = (node.start_index || 1) - 1 + pageOffset;\n const endPhysIdx = (node.end_index || node.start_index || 1) - 1 + pageOffset;\n\n const cappedEnd = Math.min(endPhysIdx, startPhysIdx + MAX_PAGES_PER_NODE - 1, pages.length - 1);\n\n const pageSlices = pages\n .slice(startPhysIdx, cappedEnd + 1)\n .map(p => p && p.length > MAX_CHARS_PER_PAGE ? p.slice(0, MAX_CHARS_PER_PAGE) + \"...\" : (p || \"\"));\n\n const pageContent = pageSlices.join(\"\\n\\n\").trim();\n\n return {\n node_id: node.node_id,\n title: node.title,\n start_index: node.start_index,\n end_index: node.end_index,\n summary: node.summary,\n page_content: pageContent || node.summary,\n };\n});\n\nconst context = retrieved\n .map(n => `[Section: \"${n.title}\" | Pages: ${n.start_index}–${n.end_index}]\\n${n.page_content}`)\n .join(\"\\n\\n---\\n\\n\");\n\noutput = {\n context,\n retrieved_nodes: retrieved,\n total_chars: context.length,\n page_offset_detected: pageOffset,\n};", "nodeName": "Code" } }, @@ -145,7 +145,7 @@ "x": 0, "y": 520 }, - "selected": false + "selected": true }, { "id": "LLMNode_392", @@ -185,7 +185,7 @@ "x": 0, "y": 650 }, - "selected": true + "selected": false }, { "id": "responseNode_triggerNode_1", diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md index f16a52c8..47096414 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md @@ -62,4 +62,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/27/2026* +*Generated on 3/29/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md index 7ea7f7b9..69b6c2f6 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md @@ -61,4 +61,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/27/2026* +*Generated on 3/29/2026* diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md index 1ae0d30a..8e43b3ee 100644 --- a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md +++ b/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md @@ -59,4 +59,4 @@ For questions or issues with this flow: --- *Exported from Lamatic Flow Editor* -*Generated on 3/27/2026* +*Generated on 3/29/2026* From f9de116a5930f7f0d62d84c8d6d1d56f042cd7d2 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 29 Mar 2026 00:46:36 +0530 Subject: [PATCH 11/29] fix: rename kit folder to lowercase pageindex-notebooklm --- .../{pageIndex-notebooklm => pageindex-notebooklm}/.env.example | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/.gitignore | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/README.md | 0 .../actions/orchestrate.ts | 0 .../app/globals.css | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/app/layout.tsx | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/app/page.tsx | 0 .../components.json | 0 .../components/ChatWindow.tsx | 0 .../components/DocumentList.tsx | 0 .../components/DocumentUpload.tsx | 0 .../components/TreeViewer.tsx | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/config.json | 0 .../flows/chat-with-pdf/README.md | 0 .../flows/chat-with-pdf/config.json | 0 .../flows/chat-with-pdf/inputs.json | 0 .../flows/chat-with-pdf/meta.json | 0 .../flows/flow-1-upload-pdf-build-tree-save/README.md | 0 .../flows/flow-1-upload-pdf-build-tree-save/config.json | 0 .../flows/flow-1-upload-pdf-build-tree-save/inputs.json | 0 .../flows/flow-1-upload-pdf-build-tree-save/meta.json | 0 .../flows/flow-4-get-tree-structure/README.md | 0 .../flows/flow-4-get-tree-structure/config.json | 0 .../flows/flow-4-get-tree-structure/inputs.json | 0 .../flows/flow-4-get-tree-structure/meta.json | 0 .../flows/flow-list-all-documents/README.md | 0 .../flows/flow-list-all-documents/config.json | 0 .../flows/flow-list-all-documents/inputs.json | 0 .../flows/flow-list-all-documents/meta.json | 0 .../lib/lamatic-client.ts | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/lib/types.ts | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/next-env.d.ts | 0 .../next.config.mjs | 0 .../package-lock.json | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/package.json | 0 .../postcss.config.mjs | 0 .../{pageIndex-notebooklm => pageindex-notebooklm}/tsconfig.json | 0 37 files changed, 0 insertions(+), 0 deletions(-) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/.env.example (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/.gitignore (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/README.md (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/actions/orchestrate.ts (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/app/globals.css (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/app/layout.tsx (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/app/page.tsx (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/components.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/components/ChatWindow.tsx (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/components/DocumentList.tsx (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/components/DocumentUpload.tsx (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/components/TreeViewer.tsx (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/config.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/chat-with-pdf/README.md (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/chat-with-pdf/config.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/chat-with-pdf/inputs.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/chat-with-pdf/meta.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-1-upload-pdf-build-tree-save/README.md (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-1-upload-pdf-build-tree-save/config.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-1-upload-pdf-build-tree-save/inputs.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-1-upload-pdf-build-tree-save/meta.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-4-get-tree-structure/README.md (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-4-get-tree-structure/config.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-4-get-tree-structure/inputs.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-4-get-tree-structure/meta.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-list-all-documents/README.md (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-list-all-documents/config.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-list-all-documents/inputs.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/flows/flow-list-all-documents/meta.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/lib/lamatic-client.ts (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/lib/types.ts (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/next-env.d.ts (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/next.config.mjs (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/package-lock.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/package.json (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/postcss.config.mjs (100%) rename kits/assistant/{pageIndex-notebooklm => pageindex-notebooklm}/tsconfig.json (100%) diff --git a/kits/assistant/pageIndex-notebooklm/.env.example b/kits/assistant/pageindex-notebooklm/.env.example similarity index 100% rename from kits/assistant/pageIndex-notebooklm/.env.example rename to kits/assistant/pageindex-notebooklm/.env.example diff --git a/kits/assistant/pageIndex-notebooklm/.gitignore b/kits/assistant/pageindex-notebooklm/.gitignore similarity index 100% rename from kits/assistant/pageIndex-notebooklm/.gitignore rename to kits/assistant/pageindex-notebooklm/.gitignore diff --git a/kits/assistant/pageIndex-notebooklm/README.md b/kits/assistant/pageindex-notebooklm/README.md similarity index 100% rename from kits/assistant/pageIndex-notebooklm/README.md rename to kits/assistant/pageindex-notebooklm/README.md diff --git a/kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts similarity index 100% rename from kits/assistant/pageIndex-notebooklm/actions/orchestrate.ts rename to kits/assistant/pageindex-notebooklm/actions/orchestrate.ts diff --git a/kits/assistant/pageIndex-notebooklm/app/globals.css b/kits/assistant/pageindex-notebooklm/app/globals.css similarity index 100% rename from kits/assistant/pageIndex-notebooklm/app/globals.css rename to kits/assistant/pageindex-notebooklm/app/globals.css diff --git a/kits/assistant/pageIndex-notebooklm/app/layout.tsx b/kits/assistant/pageindex-notebooklm/app/layout.tsx similarity index 100% rename from kits/assistant/pageIndex-notebooklm/app/layout.tsx rename to kits/assistant/pageindex-notebooklm/app/layout.tsx diff --git a/kits/assistant/pageIndex-notebooklm/app/page.tsx b/kits/assistant/pageindex-notebooklm/app/page.tsx similarity index 100% rename from kits/assistant/pageIndex-notebooklm/app/page.tsx rename to kits/assistant/pageindex-notebooklm/app/page.tsx diff --git a/kits/assistant/pageIndex-notebooklm/components.json b/kits/assistant/pageindex-notebooklm/components.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/components.json rename to kits/assistant/pageindex-notebooklm/components.json diff --git a/kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx similarity index 100% rename from kits/assistant/pageIndex-notebooklm/components/ChatWindow.tsx rename to kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx similarity index 100% rename from kits/assistant/pageIndex-notebooklm/components/DocumentList.tsx rename to kits/assistant/pageindex-notebooklm/components/DocumentList.tsx diff --git a/kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx similarity index 100% rename from kits/assistant/pageIndex-notebooklm/components/DocumentUpload.tsx rename to kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx diff --git a/kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx similarity index 100% rename from kits/assistant/pageIndex-notebooklm/components/TreeViewer.tsx rename to kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx diff --git a/kits/assistant/pageIndex-notebooklm/config.json b/kits/assistant/pageindex-notebooklm/config.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/config.json rename to kits/assistant/pageindex-notebooklm/config.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md b/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/README.md similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/README.md rename to kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/README.md diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json b/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/config.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/config.json rename to kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/config.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/inputs.json b/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/inputs.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/inputs.json rename to kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/inputs.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/meta.json b/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/meta.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/chat-with-pdf/meta.json rename to kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/meta.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md rename to kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/README.md diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json rename to kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json rename to kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/inputs.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json rename to kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/README.md similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/README.md rename to kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/README.md diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/config.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/config.json rename to kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/config.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/inputs.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/inputs.json rename to kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/inputs.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-4-get-tree-structure/meta.json rename to kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/README.md similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/README.md rename to kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/README.md diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/config.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/config.json rename to kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/config.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/inputs.json b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/inputs.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/inputs.json rename to kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/inputs.json diff --git a/kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/flows/flow-list-all-documents/meta.json rename to kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json diff --git a/kits/assistant/pageIndex-notebooklm/lib/lamatic-client.ts b/kits/assistant/pageindex-notebooklm/lib/lamatic-client.ts similarity index 100% rename from kits/assistant/pageIndex-notebooklm/lib/lamatic-client.ts rename to kits/assistant/pageindex-notebooklm/lib/lamatic-client.ts diff --git a/kits/assistant/pageIndex-notebooklm/lib/types.ts b/kits/assistant/pageindex-notebooklm/lib/types.ts similarity index 100% rename from kits/assistant/pageIndex-notebooklm/lib/types.ts rename to kits/assistant/pageindex-notebooklm/lib/types.ts diff --git a/kits/assistant/pageIndex-notebooklm/next-env.d.ts b/kits/assistant/pageindex-notebooklm/next-env.d.ts similarity index 100% rename from kits/assistant/pageIndex-notebooklm/next-env.d.ts rename to kits/assistant/pageindex-notebooklm/next-env.d.ts diff --git a/kits/assistant/pageIndex-notebooklm/next.config.mjs b/kits/assistant/pageindex-notebooklm/next.config.mjs similarity index 100% rename from kits/assistant/pageIndex-notebooklm/next.config.mjs rename to kits/assistant/pageindex-notebooklm/next.config.mjs diff --git a/kits/assistant/pageIndex-notebooklm/package-lock.json b/kits/assistant/pageindex-notebooklm/package-lock.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/package-lock.json rename to kits/assistant/pageindex-notebooklm/package-lock.json diff --git a/kits/assistant/pageIndex-notebooklm/package.json b/kits/assistant/pageindex-notebooklm/package.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/package.json rename to kits/assistant/pageindex-notebooklm/package.json diff --git a/kits/assistant/pageIndex-notebooklm/postcss.config.mjs b/kits/assistant/pageindex-notebooklm/postcss.config.mjs similarity index 100% rename from kits/assistant/pageIndex-notebooklm/postcss.config.mjs rename to kits/assistant/pageindex-notebooklm/postcss.config.mjs diff --git a/kits/assistant/pageIndex-notebooklm/tsconfig.json b/kits/assistant/pageindex-notebooklm/tsconfig.json similarity index 100% rename from kits/assistant/pageIndex-notebooklm/tsconfig.json rename to kits/assistant/pageindex-notebooklm/tsconfig.json From 5d2c814ae2bdb3407a8204fc2dca0f8cdda02aad Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 29 Mar 2026 01:04:48 +0530 Subject: [PATCH 12/29] updated nextjs version --- .../pageindex-notebooklm/app/globals.css | 2 +- .../pageindex-notebooklm/package-lock.json | 81 +++++++++---------- .../pageindex-notebooklm/package.json | 2 +- 3 files changed, 42 insertions(+), 43 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/app/globals.css b/kits/assistant/pageindex-notebooklm/app/globals.css index 6e08d236..338c0082 100644 --- a/kits/assistant/pageindex-notebooklm/app/globals.css +++ b/kits/assistant/pageindex-notebooklm/app/globals.css @@ -1,5 +1,5 @@ -@import "tailwindcss"; @import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,600;12..96,700&family=Geist+Mono:wght@300;400;500&display=swap'); +@import "tailwindcss"; @layer base { :root { diff --git a/kits/assistant/pageindex-notebooklm/package-lock.json b/kits/assistant/pageindex-notebooklm/package-lock.json index ccf67215..1c771ea7 100644 --- a/kits/assistant/pageindex-notebooklm/package-lock.json +++ b/kits/assistant/pageindex-notebooklm/package-lock.json @@ -11,7 +11,7 @@ "clsx": "^2.1.1", "lamatic": "^0.3.2", "lucide-react": "^0.511.0", - "next": "15.3.1", + "next": "15.3.8", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwindcss": "^4.1.4" @@ -565,15 +565,15 @@ } }, "node_modules/@next/env": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", - "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==", + "version": "15.3.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.8.tgz", + "integrity": "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", - "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz", + "integrity": "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==", "cpu": [ "arm64" ], @@ -587,9 +587,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", - "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz", + "integrity": "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==", "cpu": [ "x64" ], @@ -603,9 +603,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", - "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz", + "integrity": "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==", "cpu": [ "arm64" ], @@ -619,9 +619,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", - "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz", + "integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==", "cpu": [ "arm64" ], @@ -635,9 +635,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", - "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz", + "integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==", "cpu": [ "x64" ], @@ -651,9 +651,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", - "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz", + "integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==", "cpu": [ "x64" ], @@ -667,9 +667,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", - "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz", + "integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==", "cpu": [ "arm64" ], @@ -683,9 +683,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", - "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz", + "integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==", "cpu": [ "x64" ], @@ -1413,13 +1413,12 @@ } }, "node_modules/next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", - "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "version": "15.3.8", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.8.tgz", + "integrity": "sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==", "license": "MIT", "dependencies": { - "@next/env": "15.3.1", + "@next/env": "15.3.8", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -1434,14 +1433,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.1", - "@next/swc-darwin-x64": "15.3.1", - "@next/swc-linux-arm64-gnu": "15.3.1", - "@next/swc-linux-arm64-musl": "15.3.1", - "@next/swc-linux-x64-gnu": "15.3.1", - "@next/swc-linux-x64-musl": "15.3.1", - "@next/swc-win32-arm64-msvc": "15.3.1", - "@next/swc-win32-x64-msvc": "15.3.1", + "@next/swc-darwin-arm64": "15.3.5", + "@next/swc-darwin-x64": "15.3.5", + "@next/swc-linux-arm64-gnu": "15.3.5", + "@next/swc-linux-arm64-musl": "15.3.5", + "@next/swc-linux-x64-gnu": "15.3.5", + "@next/swc-linux-x64-musl": "15.3.5", + "@next/swc-win32-arm64-msvc": "15.3.5", + "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { diff --git a/kits/assistant/pageindex-notebooklm/package.json b/kits/assistant/pageindex-notebooklm/package.json index 74b84160..d440b721 100644 --- a/kits/assistant/pageindex-notebooklm/package.json +++ b/kits/assistant/pageindex-notebooklm/package.json @@ -12,7 +12,7 @@ "clsx": "^2.1.1", "lamatic": "^0.3.2", "lucide-react": "^0.511.0", - "next": "15.3.1", + "next": "15.3.8", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwindcss": "^4.1.4" From 53787a174eaa234c69719b28d7cd832c98830cd2 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 29 Mar 2026 02:18:01 +0530 Subject: [PATCH 13/29] checkout: temporary commit for worktree checkout From fb53b74c6218ea8e64b206c2300cfa5cbd8f38c2 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Sun, 29 Mar 2026 02:22:40 +0530 Subject: [PATCH 14/29] feat: add DocumentList component with deletion support and confirmation UI --- .../components/DocumentList.tsx | 159 ++++++++++-------- 1 file changed, 88 insertions(+), 71 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx index 5c17720c..2bc49d7e 100644 --- a/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx +++ b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx @@ -105,6 +105,54 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete

    + {/* Inline trash icon / spinner */} +
    + {isDeleting ? ( + + + + ) : !isConfirming ? ( +
    { e.stopPropagation(); setConfirmId(doc.doc_id); }} + title="Delete document" + className="doc-delete-btn" + style={{ + width: "22px", height: "22px", borderRadius: "var(--radius-sm)", + border: "1px solid transparent", + background: "transparent", + display: "flex", alignItems: "center", justifyContent: "center", + cursor: "pointer", opacity: 0, + transition: "all 0.15s", + }} + onMouseEnter={e => { + e.currentTarget.style.opacity = "1"; + e.currentTarget.style.background = "rgba(248,113,113,0.12)"; + e.currentTarget.style.borderColor = "rgba(248,113,113,0.3)"; + }} + onMouseLeave={e => { + e.currentTarget.style.opacity = "0"; + e.currentTarget.style.background = "transparent"; + e.currentTarget.style.borderColor = "transparent"; + }} + > + + + + + + +
    + ) : ( +
    + )} +
    + {active && !isConfirming && (
    - {/* Delete controls — sit outside the main button */} -
    - {isDeleting ? ( - - - - ) : isConfirming ? ( - <> - {/* Confirm */} - - {/* Cancel */} - - - ) : ( + {/* Confirm/Cancel strip below the row */} + {isConfirming && ( +
    + + Delete this doc? + + - )} -
    +
    + )} {/* Make delete btn visible on row hover via CSS */} + + {deleteError && ( +
  • + {deleteError} +
  • + )} + {documents.map((doc, idx) => { const active = selectedId === doc.doc_id; const isConfirming = confirmId === doc.doc_id; @@ -210,11 +226,6 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete
    )} - {/* Make delete btn visible on row hover via CSS */} - ); })} diff --git a/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx index 19e45534..67fb7b89 100644 --- a/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx +++ b/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { uploadDocument } from "@/actions/orchestrate"; import { UploadResponse } from "@/lib/types"; @@ -12,9 +12,13 @@ export default function DocumentUpload({ onUploaded }: Props) { const [message, setMessage] = useState(""); const [dragging, setDragging] = useState(false); const inputRef = useRef(null); + const resetTimerRef = useRef(null); async function processFile(file: File) { - if (!file.type.includes("pdf") && !file.name.endsWith(".md")) { + if (resetTimerRef.current) clearTimeout(resetTimerRef.current); + + const allowedTypes = ["application/pdf", "text/markdown", "text/x-markdown"]; + if (!allowedTypes.includes(file.type) && !file.name.endsWith(".md")) { setStatus("error"); setMessage("Only PDF and Markdown files are supported."); return; @@ -32,9 +36,15 @@ export default function DocumentUpload({ onUploaded }: Props) { } catch { setStatus("error"); setMessage("Upload failed. Check your flow."); } - setTimeout(() => { setStatus("idle"); setMessage(""); }, 3500); + resetTimerRef.current = setTimeout(() => { setStatus("idle"); setMessage(""); }, 3500); } + useEffect(() => { + return () => { + if (resetTimerRef.current) clearTimeout(resetTimerRef.current); + }; + }, []); + function fileToDataUrl(file: File): Promise { return new Promise((res, rej) => { const r = new FileReader(); diff --git a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx index ff01bf63..a350f69b 100644 --- a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { TreeNode, TreeNodeResolved } from "@/lib/types"; +import { ChevronRight, Circle } from "lucide-react"; interface Props { tree: TreeNode[]; @@ -43,14 +44,14 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { function TreeNodeRow({ node, depth, - highlightedIds, + highlightedIdSet, }: { node: TreeNodeResolved; depth: number; - highlightedIds: string[]; + highlightedIdSet: Set; }) { const [open, setOpen] = useState(depth < 2); - const isHighlighted = highlightedIds.includes(node.node_id); + const isHighlighted = highlightedIdSet.has(node.node_id); const hasChildren = node.nodes.length > 0; const pageSpan = node.start_index === node.end_index @@ -78,19 +79,16 @@ function TreeNodeRow({ {/* Chevron / dot */} {hasChildren ? ( - - - + ) : ( - + )} @@ -148,7 +146,7 @@ function TreeNodeRow({ paddingLeft: "4px", }}> {node.nodes.map((child, i) => ( - + ))}
    )} @@ -158,6 +156,8 @@ function TreeNodeRow({ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { const resolvedRoots = buildTree(tree); + + const highlightedIdSet = useMemo(() => new Set(highlightedIds), [highlightedIds]); const totalNodes = (nodes: TreeNodeResolved[]): number => nodes.reduce((acc, n) => acc + 1 + totalNodes(n.nodes), 0); @@ -204,7 +204,7 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { {/* Tree body */}
    {resolvedRoots.map((node, i) => ( - + ))}
    diff --git a/kits/assistant/pageindex-notebooklm/config.json b/kits/assistant/pageindex-notebooklm/config.json index a43659e1..1139cc1e 100644 --- a/kits/assistant/pageindex-notebooklm/config.json +++ b/kits/assistant/pageindex-notebooklm/config.json @@ -59,9 +59,9 @@ "Markdown-rendered AI responses", "Responsive dark-mode UI" ], - "demoUrl": "", + "demoUrl": "https://pageindex-notebooklm.vercel.app/", "githubUrl": "https://github.com/Skt329/AgentKit", - "deployUrl": "", - "documentationUrl": "", + "deployUrl": "https://pageindex-notebooklm.vercel.app/", + "documentationUrl": "https://github.com/Skt329/AgentKit", "imageUrl": "" } \ No newline at end of file diff --git a/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/meta.json b/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/meta.json index dbeff2fa..f299b139 100644 --- a/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/chat-with-pdf/meta.json @@ -1,9 +1,14 @@ { "name": "Chat with Pdf", - "description": "", - "tags": [], - "testInput": "", - "githubUrl": "", - "documentationUrl": "", - "deployUrl": "" + "description": "chat-with-pdf using NotebookLM", + "tags": [ + "pdf", + "notebooklm", + "assistant", + "document-search" + ], + "testInput": "{}", + "githubUrl": "https://github.com/Skt329/AgentKit", + "documentationUrl": "https://github.com/Skt329/AgentKit", + "deployUrl": "https://pageindex-notebooklm.vercel.app/" } \ No newline at end of file diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json index 7f08bbd1..fc3695c4 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -158,7 +158,7 @@ "schema": {}, "values": { "id": "variablesNode_617", - "mapping": "{\n \"file_name\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_name}}\"\n },\n \"file_url\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_url}}\"\n },\n \"tree\": {\n \"type\": \"string\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree}}\"\n },\n \"raw_data\": {\n \"type\": \"string\",\n \"value\": \"{{extractFromFileNode_1.output.files}}\"\n },\n \"tre_node_count\": {\n \"type\": \"number\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree_node_count}}\"\n }\n}", + "mapping": "{\n \"file_name\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_name}}\"\n },\n \"file_url\": {\n \"type\": \"string\",\n \"value\": \"{{codeNode_630.output.resolved_url}}\"\n },\n \"tree\": {\n \"type\": \"string\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree}}\"\n },\n \"raw_data\": {\n \"type\": \"string\",\n \"value\": \"{{extractFromFileNode_1.output.files}}\"\n },\n \"tree_node_count\": {\n \"type\": \"number\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree_node_count}}\"\n }\n}", "nodeName": "Variables" } }, @@ -191,7 +191,7 @@ }, "values": { "id": "codeNode_save", - "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{extractFromFileNode_1.output.files[0].data}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{codeNode_630.output.resolver_url}}\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\n// Sanitize page text\nrawText.forEach((page, i) => {\n rawText[i] = sanitize(page);\n});\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: rawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\noutput = {\n success: response.ok,\n status_code: response.status,\n response_text: response.statusText,\n error: result[0]?.error || result.error || null,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n", + "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{extractFromFileNode_1.output.files[0].data}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{codeNode_630.output.resolved_url}}\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\n// Sanitize page text\nrawText.forEach((page, i) => {\n rawText[i] = sanitize(page);\n});\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: rawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\noutput = {\n success: response.ok,\n status_code: response.status,\n response_text: response.statusText,\n error: result[0]?.error || result.error || null,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n", "nodeName": "Save to Supabase" }, "disabled": false diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json index 2b46e5e6..464b84e3 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/meta.json @@ -1,9 +1,14 @@ { "name": "Flow 1 Upload PDF Build Tree Save", - "description": "", - "tags": [], - "testInput": "", - "githubUrl": "", - "documentationUrl": "", - "deployUrl": "" + "description": "Upload a PDF, build a tree index, and save to database.", + "tags": [ + "upload", + "pdf", + "pageindex", + "notebooklm" + ], + "testInput": "{}", + "githubUrl": "https://github.com/Skt329/AgentKit", + "documentationUrl": "https://github.com/Skt329/AgentKit", + "deployUrl": "https://pageindex-notebooklm.vercel.app/" } \ No newline at end of file diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json index f7e7ed51..d66bdf4d 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json @@ -1,5 +1,5 @@ { - "name": "Flow 4 Get tree structure", + "name": "Flow 4 Get Tree Structure", "description": "", "tags": [], "testInput": "", diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/config.json index 8a796aa1..0db47693 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/config.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/config.json @@ -61,7 +61,7 @@ "nodeName": "API Response", "webhookUrl": "", "retry_delay": "0", - "outputMapping": "{\n \"documents\": \"{{postgresNode_831.output.queryResult}}\",\n \"total\": \"{{postgresNode_831.output.status}}\"\n}" + "outputMapping": "{\n \"documents\": \"{{postgresNode_831.output.queryResult}}\",\n \"total\": \"{{postgresNode_831.output.queryResult.length}}\"\n}" }, "isResponseNode": true }, diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json index e8a3a17c..b4558432 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json @@ -1,5 +1,5 @@ { - "name": "flow list all documents", + "name": "Flow List All Documents", "description": "", "tags": [], "testInput": "", diff --git a/kits/assistant/pageindex-notebooklm/lib/types.ts b/kits/assistant/pageindex-notebooklm/lib/types.ts index 552e1fed..816301b3 100644 --- a/kits/assistant/pageindex-notebooklm/lib/types.ts +++ b/kits/assistant/pageindex-notebooklm/lib/types.ts @@ -51,7 +51,7 @@ export interface ChatResponse { export interface UploadResponse { doc_id: string; file_name: string; - tree_node_count: string; + tree_node_count: number; status: string; } diff --git a/kits/assistant/pageindex-notebooklm/package-lock.json b/kits/assistant/pageindex-notebooklm/package-lock.json index 1c771ea7..a38f328a 100644 --- a/kits/assistant/pageindex-notebooklm/package-lock.json +++ b/kits/assistant/pageindex-notebooklm/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "clsx": "^2.1.1", + "dompurify": "^3.3.3", "lamatic": "^0.3.2", "lucide-react": "^0.511.0", "next": "15.3.8", @@ -18,6 +19,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.2.2", + "@types/dompurify": "^3.0.5", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -984,6 +986,16 @@ "tailwindcss": "4.2.2" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/node": { "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", @@ -1014,6 +1026,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1077,6 +1096,15 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", diff --git a/kits/assistant/pageindex-notebooklm/package.json b/kits/assistant/pageindex-notebooklm/package.json index d440b721..e5a75c8d 100644 --- a/kits/assistant/pageindex-notebooklm/package.json +++ b/kits/assistant/pageindex-notebooklm/package.json @@ -9,20 +9,22 @@ "lint": "next lint" }, "dependencies": { - "clsx": "^2.1.1", - "lamatic": "^0.3.2", - "lucide-react": "^0.511.0", + "clsx": "2.1.1", + "dompurify": "3.3.3", + "lamatic": "0.3.2", + "lucide-react": "0.511.0", "next": "15.3.8", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwindcss": "^4.1.4" + "react": "19.0.0", + "react-dom": "19.0.0", + "tailwindcss": "4.1.4" }, "devDependencies": { - "@tailwindcss/postcss": "^4.2.2", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "postcss": "^8.5.8", - "typescript": "^5" + "@tailwindcss/postcss": "4.2.2", + "@types/dompurify": "3.0.5", + "@types/node": "20.12.7", + "@types/react": "19.0.0", + "@types/react-dom": "19.0.0", + "postcss": "8.5.8", + "typescript": "5.4.5" } } From b8f4ab30c972bcc73fe704e7f3fade32ebf51b8d Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 00:48:07 +0530 Subject: [PATCH 16/29] feat: implement ChatWindow component with persistent local storage and markdown rendering for document interaction --- kits/assistant/pageindex-notebooklm/README.md | 22 +- .../actions/orchestrate.ts | 8 +- .../components/ChatWindow.tsx | 52 +- .../components/ui/card.tsx | 92 + .../components/ui/collapsible.tsx | 33 + .../flows/flow-4-get-tree-structure/meta.json | 12 +- .../flows/flow-list-all-documents/meta.json | 12 +- .../pageindex-notebooklm/lib/utils.ts | 6 + .../pageindex-notebooklm/package-lock.json | 2191 ++++++++++++++++- .../pageindex-notebooklm/package.json | 4 + 10 files changed, 2264 insertions(+), 168 deletions(-) create mode 100644 kits/assistant/pageindex-notebooklm/components/ui/card.tsx create mode 100644 kits/assistant/pageindex-notebooklm/components/ui/collapsible.tsx create mode 100644 kits/assistant/pageindex-notebooklm/lib/utils.ts diff --git a/kits/assistant/pageindex-notebooklm/README.md b/kits/assistant/pageindex-notebooklm/README.md index d15a722d..729378ab 100644 --- a/kits/assistant/pageindex-notebooklm/README.md +++ b/kits/assistant/pageindex-notebooklm/README.md @@ -192,26 +192,26 @@ npm run dev ## Project Structure ```text -pageindex-notebooklm/ +pageindex-notebooklm/ (TypeScript · Next.js/React) ├── actions/ -│ └── orchestrate.ts # Server actions — all 4 flow calls via Lamatic SDK +│ └── orchestrate.ts # TypeScript — Server actions — all 4 flow calls via Lamatic SDK ├── app/ -│ ├── globals.css # Design system (CSS custom properties, animations) -│ ├── layout.tsx # Root layout with metadata -│ └── page.tsx # Main page — document list + chat + tree viewer +│ ├── globals.css # CSS — Design system (custom properties, animations) +│ ├── layout.tsx # TSX/React — Root layout with metadata +│ └── page.tsx # TSX/React — Main page — document list + chat + tree viewer ├── components/ -│ ├── ChatWindow.tsx # Chat UI with markdown, sources, persistence -│ ├── DocumentList.tsx # Document sidebar with search + delete -│ ├── DocumentUpload.tsx # Drag-and-drop / URL upload -│ └── TreeViewer.tsx # Interactive hierarchical tree viewer +│ ├── ChatWindow.tsx # TSX/React — Chat UI with markdown, sources, persistence +│ ├── DocumentList.tsx # TSX/React — Document sidebar with search + delete +│ ├── DocumentUpload.tsx # TSX/React — Drag-and-drop / URL upload +│ └── TreeViewer.tsx # TSX/React — Interactive hierarchical tree viewer ├── flows/ │ ├── flow-1-upload-pdf-build-tree-save/ │ ├── chat-with-pdf/ │ ├── flow-list-all-documents/ │ └── flow-4-get-tree-structure/ ├── lib/ -│ ├── lamatic-client.ts # Lamatic SDK initialization -│ └── types.ts # TypeScript interfaces +│ ├── lamatic-client.ts # TypeScript — Lamatic SDK initialization +│ └── types.ts # TypeScript — Shared interfaces and types ├── config.json # Kit metadata └── .env.example # Environment variable template ``` diff --git a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts index cae2abe9..86e0896f 100644 --- a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts @@ -118,9 +118,11 @@ export async function getDocumentTree(doc_id: string) { } // ── Flow 4 (delete action): Remove a document ─────────────────── -export async function deleteDocument(doc_id: string, token?: string) { - // TODO: Validate JWT and user permissions before deleting - // if (!token || !verifyJwt(token)) throw new Error("Unauthorized"); +// Note: This demo kit has no authentication layer — deleteDocument is +// intentionally unauthenticated. Production deployments should add JWT +// validation and ownership checks (e.g., verifyJwt(token) + getDocumentOwner) +// before calling the flow. +export async function deleteDocument(doc_id: string) { try { const response = await lamaticClient.executeFlow( process.env.FLOW_ID_TREE!, diff --git a/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx index d5b5dcc5..eb33530b 100644 --- a/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx +++ b/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx @@ -5,6 +5,8 @@ import { chatWithDocument } from "@/actions/orchestrate"; import { ChatResponse, Message, RetrievedNode } from "@/lib/types"; import { MessageSquare, Bot, Search, ChevronDown, Send } from "lucide-react"; import DOMPurify from "dompurify"; +import { Card } from "@/components/ui/card"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; // Lightweight markdown → HTML (no external deps) function renderMarkdown(text: string): string { @@ -138,14 +140,15 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) } return ( -
    + {/* ── Chat header ── */}
    0 && ( -
    - + + - {sourcesOpen && ( +
    {lastThinking && (
    ))}
    - )} -
    +
    + )} {/* ── Input ── */} @@ -383,6 +391,6 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) -
    + ); } diff --git a/kits/assistant/pageindex-notebooklm/components/ui/card.tsx b/kits/assistant/pageindex-notebooklm/components/ui/card.tsx new file mode 100644 index 00000000..acf57dc5 --- /dev/null +++ b/kits/assistant/pageindex-notebooklm/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/kits/assistant/pageindex-notebooklm/components/ui/collapsible.tsx b/kits/assistant/pageindex-notebooklm/components/ui/collapsible.tsx new file mode 100644 index 00000000..2f7a4e7f --- /dev/null +++ b/kits/assistant/pageindex-notebooklm/components/ui/collapsible.tsx @@ -0,0 +1,33 @@ +"use client" + +import { Collapsible as CollapsiblePrimitive } from "radix-ui" + +function Collapsible({ + ...props +}: React.ComponentProps) { + return +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json index d66bdf4d..a4234f4f 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json @@ -1,9 +1,9 @@ { "name": "Flow 4 Get Tree Structure", - "description": "", - "tags": [], - "testInput": "", - "githubUrl": "", - "documentationUrl": "", - "deployUrl": "" + "description": "Retrieve the full hierarchical tree structure for a document or delete a document and its associated data from Supabase.", + "tags": ["tree", "pageindex", "notebooklm", "document-management", "delete"], + "testInput": "{\"doc_id\": \"example-doc-id\", \"action\": \"get_tree\"}", + "githubUrl": "https://github.com/Skt329/AgentKit", + "documentationUrl": "https://github.com/Skt329/AgentKit", + "deployUrl": "https://pageindex-notebooklm.vercel.app/" } \ No newline at end of file diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json index b4558432..c0f4637c 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-list-all-documents/meta.json @@ -1,9 +1,9 @@ { "name": "Flow List All Documents", - "description": "", - "tags": [], - "testInput": "", - "githubUrl": "", - "documentationUrl": "", - "deployUrl": "" + "description": "List all uploaded documents stored in Supabase, returning doc_id, file_name, file_url, tree_node_count, status, and created_at for each document.", + "tags": ["list", "documents", "pageindex", "notebooklm", "supabase"], + "testInput": "{}", + "githubUrl": "https://github.com/Skt329/AgentKit", + "documentationUrl": "https://github.com/Skt329/AgentKit", + "deployUrl": "https://pageindex-notebooklm.vercel.app/" } \ No newline at end of file diff --git a/kits/assistant/pageindex-notebooklm/lib/utils.ts b/kits/assistant/pageindex-notebooklm/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/kits/assistant/pageindex-notebooklm/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/kits/assistant/pageindex-notebooklm/package-lock.json b/kits/assistant/pageindex-notebooklm/package-lock.json index a38f328a..bb0ef997 100644 --- a/kits/assistant/pageindex-notebooklm/package-lock.json +++ b/kits/assistant/pageindex-notebooklm/package-lock.json @@ -8,23 +8,27 @@ "name": "pageindex-notebooklm", "version": "0.1.0", "dependencies": { + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-slot": "^1.2.4", "clsx": "^2.1.1", - "dompurify": "^3.3.3", - "lamatic": "^0.3.2", - "lucide-react": "^0.511.0", + "dompurify": "3.3.3", + "lamatic": "0.3.2", + "lucide-react": "0.511.0", "next": "15.3.8", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwindcss": "^4.1.4" + "radix-ui": "^1.4.3", + "react": "19.0.0", + "react-dom": "19.0.0", + "tailwind-merge": "^3.5.0", + "tailwindcss": "4.1.4" }, "devDependencies": { - "@tailwindcss/postcss": "^4.2.2", - "@types/dompurify": "^3.0.5", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "postcss": "^8.5.8", - "typescript": "^5" + "@tailwindcss/postcss": "4.2.2", + "@types/dompurify": "3.0.5", + "@types/node": "20.12.7", + "@types/react": "19.0.0", + "@types/react-dom": "19.0.0", + "postcss": "8.5.8", + "typescript": "5.4.5" } }, "node_modules/@alloc/quick-lru": { @@ -50,6 +54,44 @@ "tslib": "^2.4.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -612,94 +654,1736 @@ "arm64" ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz", + "integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz", + "integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz", + "integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz", + "integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz", + "integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "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", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz", - "integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==", - "cpu": [ - "arm64" - ], + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz", - "integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==", - "cpu": [ - "x64" - ], + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz", - "integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==", - "cpu": [ - "x64" - ], + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz", - "integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==", - "cpu": [ - "arm64" - ], + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz", - "integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==", - "cpu": [ - "x64" - ], + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -731,6 +2415,13 @@ "tailwindcss": "4.2.2" } }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@tailwindcss/oxide": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", @@ -986,6 +2677,13 @@ "tailwindcss": "4.2.2" } }, + "node_modules/@tailwindcss/postcss/node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/dompurify": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", @@ -997,33 +2695,33 @@ } }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~5.26.4" } }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", + "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", + "devOptional": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==", + "devOptional": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "dependencies": { + "@types/react": "*" } }, "node_modules/@types/trusted-types": { @@ -1033,6 +2731,18 @@ "devOptional": true, "license": "MIT" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1083,7 +2793,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { @@ -1096,6 +2806,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", @@ -1119,6 +2835,15 @@ "node": ">=10.13.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1557,31 +3282,195 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^19.2.4" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "license": "MIT" }, "node_modules/semver": { @@ -1682,10 +3571,20 @@ } } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", + "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", "license": "MIT" }, "node_modules/tapable": { @@ -1709,9 +3608,9 @@ "license": "0BSD" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1723,11 +3622,63 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true, "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } } } } diff --git a/kits/assistant/pageindex-notebooklm/package.json b/kits/assistant/pageindex-notebooklm/package.json index e5a75c8d..f36a2c9e 100644 --- a/kits/assistant/pageindex-notebooklm/package.json +++ b/kits/assistant/pageindex-notebooklm/package.json @@ -9,13 +9,17 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-slot": "1.2.4", "clsx": "2.1.1", "dompurify": "3.3.3", "lamatic": "0.3.2", "lucide-react": "0.511.0", "next": "15.3.8", + "radix-ui": "1.4.3", "react": "19.0.0", "react-dom": "19.0.0", + "tailwind-merge": "3.5.0", "tailwindcss": "4.1.4" }, "devDependencies": { From 9e7aa1074221a41226c1e6bf31acc51642c9e82b Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 01:05:13 +0530 Subject: [PATCH 17/29] feat: implement PageIndex UI with document library, chat interface, and tree visualization --- .../pageindex-notebooklm/actions/orchestrate.ts | 8 +++++--- kits/assistant/pageindex-notebooklm/app/page.tsx | 15 +++++++++++++++ .../components/ChatWindow.tsx | 15 +++++++++------ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts index 86e0896f..ee06297d 100644 --- a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts @@ -84,12 +84,14 @@ export async function listDocuments() { process.env.FLOW_ID_LIST!, {} ); - const data = (response.result ?? response) as Record; + const raw = response as unknown as Record; + const data = (raw.result ?? raw) as Record; + + const documents = safeParseJSON(data?.documents, []); - // documents comes back as a JSON string (JSON.stringify applied in Code Node) return { ...data, - documents: safeParseJSON(data?.documents, []), + documents, total: Number(data?.total) || 0, }; } catch (error) { diff --git a/kits/assistant/pageindex-notebooklm/app/page.tsx b/kits/assistant/pageindex-notebooklm/app/page.tsx index fe71c151..052422d1 100644 --- a/kits/assistant/pageindex-notebooklm/app/page.tsx +++ b/kits/assistant/pageindex-notebooklm/app/page.tsx @@ -26,6 +26,20 @@ export default function Page() { } catch { /* silent */ } finally { setListLoading(false); } }, []); + // One-time migration: clear localStorage chat data corrupted by the old + // persist-effect bug (which wrote one doc's messages into another doc's slot). + // Bump CHAT_STORAGE_VERSION whenever a breaking change requires a fresh wipe. + const CHAT_STORAGE_VERSION = "2"; + useEffect(() => { + if (typeof window === "undefined") return; + if (localStorage.getItem("chat_storage_version") !== CHAT_STORAGE_VERSION) { + Object.keys(localStorage) + .filter(k => k.startsWith("chat_")) + .forEach(k => localStorage.removeItem(k)); + localStorage.setItem("chat_storage_version", CHAT_STORAGE_VERSION); + } + }, []); + useEffect(() => { fetchDocuments(); }, [fetchDocuments]); async function handleSelectDoc(doc: Document) { @@ -246,6 +260,7 @@ export default function Page() { {/* Content — both panels stay mounted to preserve state */}
    (null); - // Persist messages to localStorage whenever they change + // Persist messages — only re-run when messages change. + // Intentionally omit storageKey from deps: if included, a doc switch would + // fire this effect (storageKey changed) with the OLD messages, overwriting + // the new doc's slot before the docId-load effect could populate it. useEffect(() => { - try { localStorage.setItem(storageKey, JSON.stringify(messages)); } catch { /* quota exceeded */ } - }, [messages, storageKey]); + try { localStorage.setItem(`chat_${docId}`, JSON.stringify(messages)); } catch { /* quota exceeded */ } + }, [messages]); // eslint-disable-line react-hooks/exhaustive-deps - // Persist lamatic history + // Persist lamatic history — same reasoning as above. useEffect(() => { - try { localStorage.setItem(historyKey, JSON.stringify(lamaticHistory)); } catch { /* quota exceeded */ } - }, [lamaticHistory, historyKey]); + try { localStorage.setItem(`chat_history_${docId}`, JSON.stringify(lamaticHistory)); } catch { /* quota exceeded */ } + }, [lamaticHistory]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, loading]); From d2e3a990f376a20a02e960717f06adec8c90a995 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 01:08:20 +0530 Subject: [PATCH 18/29] feat: add configuration file for PageIndex NotebookLM kit with vectorless tree-structured RAG flows --- kits/assistant/pageindex-notebooklm/config.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/config.json b/kits/assistant/pageindex-notebooklm/config.json index 1139cc1e..9b5a5910 100644 --- a/kits/assistant/pageindex-notebooklm/config.json +++ b/kits/assistant/pageindex-notebooklm/config.json @@ -62,6 +62,5 @@ "demoUrl": "https://pageindex-notebooklm.vercel.app/", "githubUrl": "https://github.com/Skt329/AgentKit", "deployUrl": "https://pageindex-notebooklm.vercel.app/", - "documentationUrl": "https://github.com/Skt329/AgentKit", - "imageUrl": "" + "documentationUrl": "https://github.com/Skt329/AgentKit" } \ No newline at end of file From 5531576fcbddbe4ab42b7ad59331de1b5cac8898 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 02:17:41 +0530 Subject: [PATCH 19/29] feat: implement document chat interface with persistent storage and retrieval support --- kits/assistant/pageindex-notebooklm/README.md | 10 +- .../actions/orchestrate.ts | 20 +++ .../pageindex-notebooklm/app/page.tsx | 17 ++- .../components/ChatWindow.tsx | 4 +- .../components/DocumentList.tsx | 116 ++++++++++-------- .../components/DocumentUpload.tsx | 25 +++- .../components/TreeViewer.tsx | 52 +++++++- .../pageindex-notebooklm/lib/types.ts | 3 +- 8 files changed, 183 insertions(+), 64 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/README.md b/kits/assistant/pageindex-notebooklm/README.md index 729378ab..7a83dee8 100644 --- a/kits/assistant/pageindex-notebooklm/README.md +++ b/kits/assistant/pageindex-notebooklm/README.md @@ -138,7 +138,12 @@ create table documents ( created_at timestamptz default now() ); alter table documents enable row level security; -create policy "service_access" on documents for all using (true); +-- Only the Supabase service role (used server-side in Lamatic flows) can +-- read and write documents. No direct client-side access is permitted. +create policy "service_role_only" on documents + for all + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); ``` ### 2. Import Lamatic Flows @@ -158,6 +163,9 @@ Add these secrets in **Lamatic → Settings → Secrets**: |---|---| | `SUPABASE_URL` | `https://xxx.supabase.co` | | `SUPABASE_ANON_KEY` | From Supabase Settings → API | +| `SUPABASE_SERVICE_ROLE_KEY` | From Supabase Settings → API — **server-side only, never expose client-side** | + +> **Important:** `SUPABASE_SERVICE_ROLE_KEY` bypasses RLS. Store it in Lamatic Secrets only — never in `.env.local` shipped to the browser. ### 3. Install and Configure diff --git a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts index ee06297d..2817369a 100644 --- a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts @@ -21,6 +21,26 @@ export async function uploadDocument( file_name: string, options: { file_url?: string; file_base64?: string; mime_type?: string } ) { + // ── Validate upload contract ────────────────────────────────── + const hasUrl = Boolean(options.file_url); + const hasBase64 = Boolean(options.file_base64); + + if (hasUrl && hasBase64) { + throw new Error( + "uploadDocument: provide either file_url or file_base64, not both." + ); + } + if (!hasUrl && !hasBase64) { + throw new Error( + "uploadDocument: either file_url or file_base64 must be provided." + ); + } + if (hasBase64 && !options.mime_type) { + throw new Error( + "uploadDocument: mime_type is required when file_base64 is provided." + ); + } + try { const response = await lamaticClient.executeFlow( process.env.FLOW_ID_UPLOAD!, diff --git a/kits/assistant/pageindex-notebooklm/app/page.tsx b/kits/assistant/pageindex-notebooklm/app/page.tsx index 052422d1..e9ad6747 100644 --- a/kits/assistant/pageindex-notebooklm/app/page.tsx +++ b/kits/assistant/pageindex-notebooklm/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { listDocuments, getDocumentTree } from "@/actions/orchestrate"; import { Document, TreeNode, RetrievedNode } from "@/lib/types"; import { BookOpen } from "lucide-react"; @@ -17,6 +17,8 @@ export default function Page() { const [listLoading, setListLoading] = useState(false); const [highlightedIds, setHighlightedIds] = useState([]); const [activeTab, setActiveTab] = useState<"chat" | "tree">("chat"); + // Monotonic counter used to cancel stale tree-fetch results + const treeRequestIdRef = useRef(0); const fetchDocuments = useCallback(async () => { setListLoading(true); @@ -47,10 +49,19 @@ export default function Page() { setHighlightedIds([]); setActiveTab("chat"); setTreeLoading(true); + // Capture this request's id; if the user selects another doc before this + // resolves, requestId will no longer match the ref and we discard the result. + const requestId = ++treeRequestIdRef.current; try { const result = await getDocumentTree(doc.doc_id); + if (requestId !== treeRequestIdRef.current) return; // stale — discard if (Array.isArray(result?.tree)) setTree(result.tree); - } catch { setTree([]); } finally { setTreeLoading(false); } + } catch { + if (requestId !== treeRequestIdRef.current) return; + setTree([]); + } finally { + if (requestId === treeRequestIdRef.current) setTreeLoading(false); + } } function handleRetrievedNodes(nodes: RetrievedNode[]) { @@ -307,7 +318,7 @@ export default function Page() { Select a document

    - Upload a PDF or Markdown file, then pick it from the sidebar to start an AI conversation. + Upload a PDF and pick it from the sidebar to start an AI conversation.

    {/* Info card */} diff --git a/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx b/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx index 15fe7f80..ebe0c7d9 100644 --- a/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx +++ b/kits/assistant/pageindex-notebooklm/components/ChatWindow.tsx @@ -132,7 +132,9 @@ export default function ChatWindow({ docId, docName, onRetrievedNodes }: Props) } setLamaticHistory(nextHistory); - if (Array.isArray(result.retrieved_nodes) && result.retrieved_nodes.length) { + // Always update retrieval state when the field is present — even if empty, + // so that previous turn's sources/highlights are cleared correctly. + if (Array.isArray(result.retrieved_nodes)) { setLastNodes(result.retrieved_nodes); setLastThinking(result.thinking || ""); onRetrievedNodes?.(result.retrieved_nodes); diff --git a/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx index d371fd67..870c2978 100644 --- a/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx +++ b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx @@ -55,8 +55,10 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete return (
      {deleteError && ( @@ -121,54 +123,7 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete

    - {/* Inline trash icon / spinner */} -
    - {isDeleting ? ( - - - - ) : !isConfirming ? ( -
    { e.stopPropagation(); setConfirmId(doc.doc_id); }} - title="Delete document" - className="doc-delete-btn" - style={{ - width: "22px", height: "22px", borderRadius: "var(--radius-sm)", - border: "1px solid transparent", - background: "transparent", - display: "flex", alignItems: "center", justifyContent: "center", - cursor: "pointer", opacity: 0, - transition: "all 0.15s", - }} - onMouseEnter={e => { - e.currentTarget.style.opacity = "1"; - e.currentTarget.style.background = "rgba(248,113,113,0.12)"; - e.currentTarget.style.borderColor = "rgba(248,113,113,0.3)"; - }} - onMouseLeave={e => { - e.currentTarget.style.opacity = "0"; - e.currentTarget.style.background = "transparent"; - e.currentTarget.style.borderColor = "transparent"; - }} - > - - - - - - -
    - ) : ( -
    - )} -
    - + {/* Active indicator dot (purely decorative) */} {active && !isConfirming && (
    )} + + {/* Delete spinner — decorative only, not interactive */} + {isDeleting && ( +
    + + + +
    + )} + {/* ── Delete button ──────────────────────────────────────────────────── + Placed OUTSIDE the row + )} + {/* Confirm/Cancel strip below the row */} {isConfirming && (
    { setStatus("idle"); setMessage(""); }, 3500); return; } setStatus("uploading"); @@ -57,8 +60,20 @@ export default function DocumentUpload({ onUploaded }: Props) { const isIdle = status === "idle"; return ( + // tabIndex + role + onKeyDown make the upload zone keyboard-accessible. + // Keyboard users can Tab to it and press Enter or Space to open the picker.
    isIdle && inputRef.current?.click()} + onKeyDown={(e) => { + if (!isIdle) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + inputRef.current?.click(); + } + }} onDragOver={(e) => { e.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={(e) => { e.preventDefault(); setDragging(false); const f = e.dataTransfer.files[0]; if (f) processFile(f); }} @@ -88,7 +103,7 @@ export default function DocumentUpload({ onUploaded }: Props) { }} > { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ""; }} /> @@ -155,7 +170,7 @@ export default function DocumentUpload({ onUploaded }: Props) { Upload document

    - PDF · Markdown · drop or click + PDF · drop or click

    )} diff --git a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx index a350f69b..cc051799 100644 --- a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx @@ -35,6 +35,33 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { } } + // ── Cycle detection ───────────────────────────────────────────── + // Run a DFS tracking visiting/visited state. A back-edge to a node + // currently in the visiting set means we found a cycle. + type VisitState = "unvisited" | "visiting" | "visited"; + const state = new Map(); + map.forEach((_, id) => state.set(id, "unvisited")); + + function dfs(nodeId: string): void { + state.set(nodeId, "visiting"); + const node = map.get(nodeId)!; + for (const child of node.nodes) { + const childState = state.get(child.node_id); + if (childState === "visiting") { + throw new Error( + `buildTree: cycle detected — node "${child.node_id}" is its own ancestor.` + ); + } + if (childState === "unvisited") dfs(child.node_id); + } + state.set(nodeId, "visited"); + } + + for (const [id, s] of state.entries()) { + if (s === "unvisited") dfs(id); + } + // ── End cycle detection ───────────────────────────────────────── + // Roots = nodes not referenced as a child return flat .map(n => map.get(n.node_id)!) @@ -60,13 +87,31 @@ function TreeNodeRow({ return (
    -
    hasChildren && setOpen(o => !o)} + aria-expanded={hasChildren ? open : undefined} + onKeyDown={(e) => { + if (!hasChildren) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setOpen(o => !o); + } + }} + style={{ + width: "100%", + textAlign: "left", + background: "none", + border: "none", + padding: 0, + cursor: hasChildren ? "pointer" : "default", + }} + > +
    + - {/* Children */} + {/* Children — rendered outside the toggle button */} {open && hasChildren && (
    Date: Tue, 31 Mar 2026 02:32:21 +0530 Subject: [PATCH 20/29] feat: implement document upload and hierarchical tree visualization components for NotebookLM-style page indexing --- .../pageindex-notebooklm/app/page.tsx | 33 ++++--------------- .../components/DocumentList.tsx | 28 +++------------- .../components/DocumentUpload.tsx | 21 +++--------- .../components/TreeViewer.tsx | 20 +++++++---- 4 files changed, 30 insertions(+), 72 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/app/page.tsx b/kits/assistant/pageindex-notebooklm/app/page.tsx index e9ad6747..1e5d09cd 100644 --- a/kits/assistant/pageindex-notebooklm/app/page.tsx +++ b/kits/assistant/pageindex-notebooklm/app/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { listDocuments, getDocumentTree } from "@/actions/orchestrate"; import { Document, TreeNode, RetrievedNode } from "@/lib/types"; -import { BookOpen } from "lucide-react"; +import { BookOpen, AlertCircle, RefreshCw, MessageSquare, List, Loader2 } from "lucide-react"; import DocumentUpload from "@/components/DocumentUpload"; import DocumentList from "@/components/DocumentList"; import ChatWindow from "@/components/ChatWindow"; @@ -139,9 +139,7 @@ export default function Page() { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0, }}> - - - + Demo project · Document processing is limited to 30–50 pages · Chat history is not stored persistently @@ -180,15 +178,7 @@ export default function Page() { title="Refresh" style={{ width: "26px", height: "26px", borderRadius: "var(--radius-sm)", border: "none" }} > - - - - - +
    @@ -236,18 +226,9 @@ export default function Page() { onClick={() => setActiveTab(tab)} > {tab === "chat" ? ( - - - + ) : ( - - - - - - - - + )} {tab === "chat" ? "Chat" : "Tree Index"} {tab === "tree" && highlightedIds.length > 0 && ( @@ -281,9 +262,7 @@ export default function Page() {
    {treeLoading ? (
    - - - + Building tree… diff --git a/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx index 870c2978..7d48c454 100644 --- a/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx +++ b/kits/assistant/pageindex-notebooklm/components/DocumentList.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { FileText, Loader2, Trash2 } from "lucide-react"; import { Document } from "@/lib/types"; import { deleteDocument } from "@/actions/orchestrate"; @@ -36,12 +37,7 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete if (documents.length === 0) { return (
    - - - - +

    No documents yet

    @@ -98,12 +94,7 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete display: "flex", alignItems: "center", justifyContent: "center", marginTop: "1px", transition: "all 0.18s var(--ease)", }}> - - - - +
    @@ -135,10 +126,7 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete {/* Delete spinner — decorative only, not interactive */} {isDeleting && (
    - - - +
    )} @@ -184,13 +172,7 @@ export default function DocumentList({ documents, selectedId, onSelect, onDelete e.currentTarget.style.borderColor = "transparent"; }} > - - - - - - + )} diff --git a/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx b/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx index 44460e84..ec7cc91e 100644 --- a/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx +++ b/kits/assistant/pageindex-notebooklm/components/DocumentUpload.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useRef, useEffect } from "react"; +import { Loader2, Check, AlertCircle, Upload } from "lucide-react"; import { uploadDocument } from "@/actions/orchestrate"; import { UploadResponse } from "@/lib/types"; @@ -117,9 +118,7 @@ export default function DocumentUpload({ onUploaded }: Props) { backgroundSize: "200% 100%", animation: "shimmer 1.5s linear infinite", }} /> - - - +

    Indexing…

    Building tree structure @@ -130,9 +129,7 @@ export default function DocumentUpload({ onUploaded }: Props) { {status === "success" && ( <>

    - - - +

    Indexed

    {message}

    @@ -141,9 +138,7 @@ export default function DocumentUpload({ onUploaded }: Props) { {status === "error" && ( <> - - - +

    Failed

    {message}

    @@ -158,13 +153,7 @@ export default function DocumentUpload({ onUploaded }: Props) { display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.18s var(--ease)", }}> - - - - - +

    Upload document diff --git a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx index cc051799..9c1d7880 100644 --- a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx @@ -2,7 +2,7 @@ import { useState, useMemo } from "react"; import { TreeNode, TreeNodeResolved } from "@/lib/types"; -import { ChevronRight, Circle } from "lucide-react"; +import { ChevronRight, Circle, Search } from "lucide-react"; interface Props { tree: TreeNode[]; @@ -201,8 +201,18 @@ function TreeNodeRow({ } export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { - const resolvedRoots = buildTree(tree); - + let resolvedRoots: TreeNodeResolved[]; + try { + resolvedRoots = buildTree(tree); + } catch (err) { + console.error("TreeViewer: buildTree failed —", err); + return ( +

    + Invalid tree structure detected. Please re-upload the document. +
    + ); + } + const highlightedIdSet = useMemo(() => new Set(highlightedIds), [highlightedIds]); const totalNodes = (nodes: TreeNodeResolved[]): number => @@ -262,9 +272,7 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { background: "var(--amber-dim)", display: "flex", alignItems: "center", gap: "7px", }}> - - - + {highlightedIds.length} NODE{highlightedIds.length !== 1 ? "S" : ""} USED IN LAST ANSWER From 8f86dab4c7825a269247dbf0dd328ec9b9baa380 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 02:58:48 +0530 Subject: [PATCH 21/29] feat: implement server actions for document management and chat orchestration --- .../assistant/pageindex-notebooklm/actions/orchestrate.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts index 2817369a..eaa2be5e 100644 --- a/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts +++ b/kits/assistant/pageindex-notebooklm/actions/orchestrate.ts @@ -63,10 +63,16 @@ export async function chatWithDocument( try { // removed FLOW_ID_CHAT check as it is checked in lamatic-client.ts + // Cap history: strip "No answer found." turns (they corrupt LLM context) + // then keep only the last 10 messages to avoid token-limit failures. + const trimmedMessages = messages + .filter(m => !(m.role === "assistant" && m.content === "No answer found.")) + .slice(-10); + const payload = { doc_id, query, - messages: JSON.stringify(messages), + messages: JSON.stringify(trimmedMessages), }; if (process.env.NODE_ENV !== "production") { From 711ee8a916db4ad232406e12d89afb22ac80f90b Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 03:10:48 +0530 Subject: [PATCH 22/29] feat: implement TreeViewer component for hierarchical document navigation with cycle detection and highlighting --- .../assistant/pageindex-notebooklm/components/TreeViewer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx index 9c1d7880..817ecc4d 100644 --- a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx @@ -201,6 +201,9 @@ function TreeNodeRow({ } export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { + // Must be declared before any early return to satisfy Rules of Hooks + const highlightedIdSet = useMemo(() => new Set(highlightedIds), [highlightedIds]); + let resolvedRoots: TreeNodeResolved[]; try { resolvedRoots = buildTree(tree); @@ -213,8 +216,6 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) { ); } - const highlightedIdSet = useMemo(() => new Set(highlightedIds), [highlightedIds]); - const totalNodes = (nodes: TreeNodeResolved[]): number => nodes.reduce((acc, n) => acc + 1 + totalNodes(n.nodes), 0); From faeb8e44dcd10aef96d43c24fadceb2984ef1497 Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Tue, 31 Mar 2026 03:22:50 +0530 Subject: [PATCH 23/29] feat: implement TreeViewer component for hierarchical document visualization with cycle detection and highlighting support --- .../pageindex-notebooklm/components/TreeViewer.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx index 817ecc4d..b024b045 100644 --- a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx @@ -89,6 +89,7 @@ function TreeNodeRow({
    {/* Semantic button wrapper — enables keyboard toggle and exposes state */} + ) : ( +
    +
    { if (!isHighlighted) e.currentTarget.style.background = "var(--surface-2)"; }} + onMouseLeave={e => { if (!isHighlighted) e.currentTarget.style.background = "transparent"; }} + > + {/* Chevron / dot */} + + - {isHighlighted && ( - - retrieved - - )} + + {/* Content */} +
    +
    + + {node.title} + + + {pageSpan} + + {isHighlighted && ( + + retrieved + + )} +
    + {node.summary && ( +

    + {node.summary} +

    + )} +
    - {node.summary && ( -

    - {node.summary} -

    - )}
    -
    - + )} {/* Children — rendered outside the toggle button */} {open && hasChildren && ( @@ -252,9 +322,9 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) {

    - {highlightedIds.length > 0 && ( + {highlightedIdSet.size > 0 && ( - {highlightedIds.length} retrieved + {highlightedIdSet.size} retrieved )} @@ -271,7 +341,7 @@ export default function TreeViewer({ tree, fileName, highlightedIds }: Props) {
    {/* Retrieved footer */} - {highlightedIds.length > 0 && ( + {highlightedIdSet.size > 0 && (
    - {highlightedIds.length} NODE{highlightedIds.length !== 1 ? "S" : ""} USED IN LAST ANSWER + {highlightedIdSet.size} NODE{highlightedIdSet.size !== 1 ? "S" : ""} USED IN LAST ANSWER
    )} diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json index fd405585..c5395a3a 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-4-get-tree-structure/meta.json @@ -6,4 +6,5 @@ "githubUrl": "https://github.com/Skt329/AgentKit", "documentationUrl": "https://github.com/Skt329/AgentKit", "deployUrl": "https://pageindex-notebooklm.vercel.app/" -} \ No newline at end of file +} + \ No newline at end of file From 45f6db7e835b381d35e7412d1bd8236404a2c30a Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Wed, 1 Apr 2026 16:22:35 +0530 Subject: [PATCH 28/29] feat: add PDF upload, extraction, and tree generation flow configuration --- .../flows/flow-1-upload-pdf-build-tree-save/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json index 1f546dbe..60956531 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -36,7 +36,7 @@ "nodeId": "codeNode", "values": { "id": "codeNode_630", - "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst fileBase64 = {{triggerNode_1.output.file_base64}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst fileName = {{triggerNode_1.output.file_name}} || \"document.pdf\";\nconst mimeType = {{triggerNode_1.output.mime_type}} || \"application/pdf\";\n\n// If a URL was provided directly, use it — no storage upload needed\nif (!fileBase64 && fileUrl) {\n output = { resolved_url: fileUrl, file_name: fileName, uploaded_to_storage: false };\n return;\n}\n\nif (!fileBase64) {\n throw new Error(\"No file_base64 or file_url provided\");\n}\n\n// Upload base64 to Supabase Storage\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}}; // from Lamatic secret\nconst serviceKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}}; // from Lamatic secret\n\n// Convert base64 to binary\n// base64 may arrive as a plain string or as data:mime;base64,... URI\nlet b64 = fileBase64;\nif (b64.includes(\",\")) b64 = b64.split(\",\")[1];\n\nconst binaryStr = atob(b64);\nconst bytes = new Uint8Array(binaryStr.length);\nfor (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n}\n\n// Unique storage path\nconst safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\nconst storagePath = `${Date.now()}_${safeName}`;\n\nconst uploadResp = await fetch(\n `${supabaseUrl}/storage/v1/object/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": mimeType,\n \"x-upsert\": \"false\",\n },\n body: bytes,\n }\n);\n\nif (!uploadResp.ok) {\n const errText = await uploadResp.text();\n throw new Error(`Storage upload failed: ${uploadResp.status} — ${errText}`);\n}\n\nconst publicUrl = `${supabaseUrl}/storage/v1/object/public/pdfs/${storagePath}`;\n\noutput = {\n resolved_url: publicUrl,\n file_name: fileName,\n uploaded_to_storage: true,\n};", + "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst fileBase64 = {{triggerNode_1.output.file_base64}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst fileName = {{triggerNode_1.output.file_name}} || \"document.pdf\";\nconst mimeType = {{triggerNode_1.output.mime_type}} || \"application/pdf\";\n\n// If a URL was provided directly, use it — no storage upload needed\nif (!fileBase64 && fileUrl) {\n output = { resolved_url: fileUrl, file_name: fileName, uploaded_to_storage: false };\n return;\n}\n\nif (!fileBase64) {\n throw new Error(\"No file_base64 or file_url provided\");\n}\n\n// Upload base64 to Supabase Storage\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}}; // from Lamatic secret\nconst serviceKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}}; // from Lamatic secret\n\n// Convert base64 to binary\n// base64 may arrive as a plain string or as data:mime;base64,... URI\nlet b64 = fileBase64;\nif (b64.includes(\",\")) b64 = b64.split(\",\")[1];\n\nconst binaryStr = atob(b64);\nconst bytes = new Uint8Array(binaryStr.length);\nfor (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n}\n\n// Unique storage path\nconst safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\nconst storagePath = `${Date.now()}_${safeName}`;\n\nconst uploadResp = await fetch(\n `${supabaseUrl}/storage/v1/object/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": mimeType,\n \"x-upsert\": \"false\",\n },\n body: bytes,\n }\n);\n\nif (!uploadResp.ok) {\n const errText = await uploadResp.text();\n throw new Error(`Storage upload failed: ${uploadResp.status} — ${errText}`);\n}\n\nconst signResp = await fetch(\n `${supabaseUrl}/storage/v1/object/sign/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ expiresIn: 3600 })\n }\n);\nif (!signResp.ok) throw new Error(`Signed URL failed: ${signResp.status}`);\nconst signData = await signResp.json();\nconst signedUrl = `${supabaseUrl}/storage/v1${signData.signedURL}`;\n\noutput = {\n resolved_url: signedUrl,\n file_name: fileName,\n uploaded_to_storage: true,\n};", "nodeName": "Code" } }, From 335e75dd97c1a82f968eacf4d4d09527cda3ffdd Mon Sep 17 00:00:00 2001 From: Saurabh Tiwari Date: Wed, 1 Apr 2026 17:00:17 +0530 Subject: [PATCH 29/29] feat: add TreeViewer component and configuration for hierarchical document navigation --- .../components/TreeViewer.tsx | 30 +++++++++++-------- .../config.json | 17 +++++++---- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx index 6e0e2425..fd9464d4 100644 --- a/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx +++ b/kits/assistant/pageindex-notebooklm/components/TreeViewer.tsx @@ -13,6 +13,7 @@ interface Props { /** Build a lookup map and resolve the flat API list into a nested tree. * Root nodes are those whose node_id appears in the first item's `nodes` * list OR nodes that are not referenced as children by any other node. + * Tolerates LLM hallucinations by gracefully ignoring duplicate IDs, missing references, DAGs, and cycles. */ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { const map = new Map(); @@ -20,7 +21,8 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { // First pass: create resolved nodes with empty children arrays for (const n of flat) { if (map.has(n.node_id)) { - throw new Error(`buildTree: Duplicate node_id detected: "${n.node_id}". Cannot create TreeNode.`); + console.warn(`buildTree: Duplicate node_id detected: "${n.node_id}". Skipping.`); + continue; } map.set(n.node_id, { ...n, nodes: [] }); } @@ -28,14 +30,18 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { // Second pass: populate children const parentMap = new Map(); for (const n of flat) { - const resolved = map.get(n.node_id)!; + const resolved = map.get(n.node_id); + if (!resolved) continue; + for (const childId of n.nodes) { const child = map.get(childId); if (!child) { - throw new Error(`buildTree: Missing reference for childId "${childId}" requested by parent "${n.node_id}". Cannot create TreeNodeResolved.`); + console.warn(`buildTree: Missing reference for childId "${childId}" requested by parent "${n.node_id}". Skipping.`); + continue; } if (parentMap.has(childId)) { - throw new Error(`buildTree: Shared child detected. Child "${childId}" is claimed by multiple parents ("${parentMap.get(childId)}" and "${n.node_id}"). DAG structures are not supported for TreeNodeResolved.`); + console.warn(`buildTree: Shared child detected. Child "${childId}" is claimed by multiple parents. Skipping.`); + continue; } parentMap.set(childId, n.node_id); resolved.nodes.push(child); @@ -43,8 +49,6 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { } // ── Cycle detection ───────────────────────────────────────────── - // Run a DFS tracking visiting/visited state. A back-edge to a node - // currently in the visiting set means we found a cycle. type VisitState = "unvisited" | "visiting" | "visited"; const state = new Map(); map.forEach((_, id) => state.set(id, "unvisited")); @@ -52,15 +56,19 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { function dfs(nodeId: string): void { state.set(nodeId, "visiting"); const node = map.get(nodeId)!; + + const validChildren = []; for (const child of node.nodes) { const childState = state.get(child.node_id); if (childState === "visiting") { - throw new Error( - `buildTree: cycle detected — node "${child.node_id}" is its own ancestor.` - ); + console.warn(`buildTree: cycle detected — node "${child.node_id}" is its own ancestor. Skipping back-edge.`); + continue; } + validChildren.push(child); if (childState === "unvisited") dfs(child.node_id); } + node.nodes = validChildren; + state.set(nodeId, "visited"); } @@ -70,9 +78,7 @@ function buildTree(flat: TreeNode[]): TreeNodeResolved[] { // ── End cycle detection ───────────────────────────────────────── // Roots = nodes not referenced as a child - return flat - .map(n => map.get(n.node_id)!) - .filter(n => !parentMap.has(n.node_id)); + return Array.from(map.values()).filter(n => !parentMap.has(n.node_id)); } function TreeNodeRow({ diff --git a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json index 60956531..77015213 100644 --- a/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json +++ b/kits/assistant/pageindex-notebooklm/flows/flow-1-upload-pdf-build-tree-save/config.json @@ -34,9 +34,14 @@ "logic": [], "modes": {}, "nodeId": "codeNode", + "schema": { + "file_name": "string", + "resolved_url": "string", + "uploaded_to_storage": "boolean" + }, "values": { "id": "codeNode_630", - "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst fileBase64 = {{triggerNode_1.output.file_base64}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst fileName = {{triggerNode_1.output.file_name}} || \"document.pdf\";\nconst mimeType = {{triggerNode_1.output.mime_type}} || \"application/pdf\";\n\n// If a URL was provided directly, use it — no storage upload needed\nif (!fileBase64 && fileUrl) {\n output = { resolved_url: fileUrl, file_name: fileName, uploaded_to_storage: false };\n return;\n}\n\nif (!fileBase64) {\n throw new Error(\"No file_base64 or file_url provided\");\n}\n\n// Upload base64 to Supabase Storage\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}}; // from Lamatic secret\nconst serviceKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}}; // from Lamatic secret\n\n// Convert base64 to binary\n// base64 may arrive as a plain string or as data:mime;base64,... URI\nlet b64 = fileBase64;\nif (b64.includes(\",\")) b64 = b64.split(\",\")[1];\n\nconst binaryStr = atob(b64);\nconst bytes = new Uint8Array(binaryStr.length);\nfor (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n}\n\n// Unique storage path\nconst safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\nconst storagePath = `${Date.now()}_${safeName}`;\n\nconst uploadResp = await fetch(\n `${supabaseUrl}/storage/v1/object/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": mimeType,\n \"x-upsert\": \"false\",\n },\n body: bytes,\n }\n);\n\nif (!uploadResp.ok) {\n const errText = await uploadResp.text();\n throw new Error(`Storage upload failed: ${uploadResp.status} — ${errText}`);\n}\n\nconst signResp = await fetch(\n `${supabaseUrl}/storage/v1/object/sign/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ expiresIn: 3600 })\n }\n);\nif (!signResp.ok) throw new Error(`Signed URL failed: ${signResp.status}`);\nconst signData = await signResp.json();\nconst signedUrl = `${supabaseUrl}/storage/v1${signData.signedURL}`;\n\noutput = {\n resolved_url: signedUrl,\n file_name: fileName,\n uploaded_to_storage: true,\n};", + "code": "// Assign the value you want to return from this code node to `output`. \n// The `output` variable is already declared.\nconst fileBase64 = {{triggerNode_1.output.file_base64}};\nconst fileUrl = {{triggerNode_1.output.file_url}};\nconst fileName = {{triggerNode_1.output.file_name}} || \"document.pdf\";\nconst mimeType = {{triggerNode_1.output.mime_type}} || \"application/pdf\";\n\n// If a URL was provided directly, use it — no storage upload needed\nif (!fileBase64 && fileUrl) {\n output = { resolved_url: fileUrl, file_name: fileName, uploaded_to_storage: false };\n return output;\n}\n\nif (!fileBase64) {\n throw new Error(\"No file_base64 or file_url provided\");\n}\n\n// Upload base64 to Supabase Storage\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}}; // from Lamatic secret\nconst serviceKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}}; // from Lamatic secret\n\n// Convert base64 to binary\n// base64 may arrive as a plain string or as data:mime;base64,... URI\nlet b64 = fileBase64;\nif (b64.includes(\",\")) b64 = b64.split(\",\")[1];\n\nconst binaryStr = atob(b64);\nconst bytes = new Uint8Array(binaryStr.length);\nfor (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n}\n\n// Unique storage path\nconst safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\nconst storagePath = `${Date.now()}_${safeName}`;\n\nconst uploadResp = await fetch(\n `${supabaseUrl}/storage/v1/object/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": mimeType,\n \"x-upsert\": \"false\",\n },\n body: bytes,\n }\n);\n\nif (!uploadResp.ok) {\n const errText = await uploadResp.text();\n throw new Error(`Storage upload failed: ${uploadResp.status} — ${errText}`);\n}\n\nconst signResp = await fetch(\n `${supabaseUrl}/storage/v1/object/sign/pdfs/${storagePath}`,\n {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ expiresIn: 3600 }) // Valid for 1 hour\n }\n);\nif (!signResp.ok) {\n throw new Error(`Signed URL failed: ${signResp.status}`);\n}\nconst signData = await signResp.json();\nconst signedUrl = `${supabaseUrl}/storage/v1${signData.signedURL}`;\nconsole.log(\"Processing complete\");\n\noutput = {\n resolved_url: signedUrl,\n file_name: fileName,\n uploaded_to_storage: true,\n};\n\nconsole.log(\"✅ Code node complete:\", output.resolved_url);", "nodeName": "Code" } }, @@ -49,7 +54,7 @@ "x": 0, "y": 130 }, - "selected": false + "selected": true }, { "id": "extractFromFileNode_1", @@ -63,7 +68,7 @@ "values": { "id": "extractFromFileNode_1", "format": "pdf", - "fileUrl": "[\"{{codeNode_630.output.resolved_url}}\"]", + "fileUrl": "{{codeNode_630.output.resolved_url}}", "nodeName": "Extract PDF", "joinPages": false, "operation": "extractFromPDF" @@ -158,7 +163,7 @@ "schema": {}, "values": { "id": "variablesNode_617", - "mapping": "{\n \"file_name\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_name}}\"\n },\n \"file_url\": {\n \"type\": \"string\",\n \"value\": \"{{codeNode_630.output.resolved_url}}\"\n },\n \"tree\": {\n \"type\": \"string\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree}}\"\n },\n \"raw_data\": {\n \"type\": \"string\",\n \"value\": \"{{extractFromFileNode_1.output.files}}\"\n },\n \"tree_node_count\": {\n \"type\": \"number\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree_node_count}}\"\n }\n}", + "mapping": "{\n \"file_name\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_name}}\"\n },\n \"file_url\": {\n \"type\": \"string\",\n \"value\": \"{{triggerNode_1.output.file_url}}\"\n },\n \"tree\": {\n \"type\": \"string\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree}}\"\n },\n \"raw_data\": {\n \"type\": \"string\",\n \"value\": \"{{extractFromFileNode_1.output.files}}\"\n },\n \"tree_node_count\": {\n \"type\": \"number\",\n \"value\": \"{{InstructorLLMNode_tree.output.tree_node_count}}\"\n }\n}", "nodeName": "Variables" } }, @@ -191,7 +196,7 @@ }, "values": { "id": "codeNode_save", - "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{codeNode_format.output.raw_text}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{codeNode_630.output.resolved_url}};\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\nconst sanitizedRawText = sanitize(rawText);\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: sanitizedRawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\nif (!response.ok) {\n const errorDetail = result?.message || result?.error || result?.hint || response.statusText;\n output = {\n success: false,\n status_code: response.status,\n response_text: response.statusText,\n error: errorDetail,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"failed\"\n };\n throw new Error(`Supabase write failed [${response.status}]: ${errorDetail}`);\n}\n\noutput = {\n success: true,\n status_code: response.status,\n response_text: response.statusText,\n error: null,\n doc_id: result[0]?.doc_id || docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};", + "code": "const tree = {{InstructorLLMNode_tree.output.tree}};\nconst rawText = {{codeNode_format.output.raw_text}};\nconst fileName = {{triggerNode_1.output.file_name}};\nconst fileUrl = {{codeNode_630.output.resolved_url}};\nconst supabaseUrl = {{secrets.project.SUPABASE_URL}};\nconst supabaseKey = {{secrets.project.SUPABASE_SERVICE_ROLE_KEY}};\n\nfunction sanitize(str) {\n if (typeof str !== \"string\") return str;\n return str.replace(/\\u0000/g, \"\").replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, \"\");\n}\n\nconst sanitizedRawText = sanitize(rawText);\n\nconst docId = \"pi-\" + Math.random().toString(36).slice(2, 18);\n\nconst payload = {\n doc_id: docId,\n file_name: fileName,\n file_url: fileUrl,\n raw_text: rawText,\n tree: tree,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n\nconst response = await fetch(supabaseUrl + \"/rest/v1/documents\", {\n method: \"POST\",\n headers: {\n \"apikey\": supabaseKey,\n \"Authorization\": \"Bearer \" + supabaseKey,\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\"\n },\n body: JSON.stringify(payload)\n});\n\nconst result = await response.json();\n\nif (!response.ok) {\n const errorDetail = result?.message || result?.error || result?.hint || response.statusText;\n output = {\n success: false,\n status_code: response.status,\n response_text: response.statusText,\n error: errorDetail,\n doc_id: docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"failed\"\n };\n throw new Error(`Supabase write failed [${response.status}]: ${errorDetail}`);\n}\n\noutput = {\n success: true,\n status_code: response.status,\n response_text: response.statusText,\n error: null,\n doc_id: result[0]?.doc_id || docId,\n file_name: fileName,\n tree_node_count: tree.length,\n status: \"completed\"\n};\n", "nodeName": "Save to Supabase" }, "disabled": false @@ -205,7 +210,7 @@ "x": 0, "y": 780 }, - "selected": true, + "selected": false, "draggable": false }, {