From 54f3c5ac172ecd40a15a1d7915ad2f345b2f1722 Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 13:33:27 -0600 Subject: [PATCH 1/9] merge --- frontend/.vite/deps/_metadata.json | 8 ++++++++ frontend/.vite/deps/package.json | 3 +++ 2 files changed, 11 insertions(+) create mode 100644 frontend/.vite/deps/_metadata.json create mode 100644 frontend/.vite/deps/package.json diff --git a/frontend/.vite/deps/_metadata.json b/frontend/.vite/deps/_metadata.json new file mode 100644 index 0000000..766c1f6 --- /dev/null +++ b/frontend/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "a27d90aa", + "configHash": "acf43111", + "lockfileHash": "7dd1e565", + "browserHash": "f059b2e5", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/frontend/.vite/deps/package.json b/frontend/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/frontend/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} From d254f5b0f2c9a39d85b31602387870a6d01a86e1 Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 13:55:11 -0600 Subject: [PATCH 2/9] Fix HF Spaces README configuration --- .gitignore | 1 + README.md | 391 +++++++++++------------------------------------------ 2 files changed, 78 insertions(+), 314 deletions(-) diff --git a/.gitignore b/.gitignore index 95c65fc..9c0e677 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ data/ reproduce_auth_500.py debug_list_notes.py +DEPLOY_TO_HF.md diff --git a/README.md b/README.md index 5b00932..2e21b74 100644 --- a/README.md +++ b/README.md @@ -1,358 +1,121 @@ -# Document Viewer - -A multi-tenant Obsidian-like documentation system with AI agent integration via Model Context Protocol (MCP). - -## 🎯 Overview - -Document Viewer enables both humans and AI agents to create, browse, and search documentation with powerful features like: - -- πŸ“ **Markdown Notes** with YAML frontmatter -- πŸ”— **Wikilinks** - `[[Note Name]]` style internal linking with auto-resolution -- πŸ” **Full-Text Search** - BM25 ranking with recency bonus -- ↩️ **Backlinks** - Automatic tracking of which notes reference each other -- 🏷️ **Tags** - Organize notes with frontmatter tags -- ✏️ **Split-Pane Editor** - Live markdown preview with optimistic concurrency -- πŸ€– **MCP Integration** - AI agents can read/write docs via FastMCP -- πŸ‘₯ **Multi-Tenant** - Isolated vaults per user (production ready with HF OAuth) - -## πŸ—οΈ Tech Stack - -### Backend -- **FastAPI** - HTTP API server -- **FastMCP** - MCP server for AI agent integration -- **SQLite FTS5** - Full-text search with BM25 ranking -- **python-frontmatter** - YAML frontmatter parsing -- **PyJWT** - Token-based authentication - -### Frontend -- **React + Vite** - Modern web framework -- **shadcn/ui** - Beautiful UI components -- **Tailwind CSS** - Utility-first styling -- **react-markdown** - Markdown rendering with custom wikilink support -- **TypeScript** - Type-safe frontend code - -## πŸ“¦ Local Setup - -### Prerequisites -- Python 3.11+ -- Node.js 18+ -- `uv` (Python package manager) or `pip` - -### 1. Clone Repository - -```bash -git clone -cd Document-MCP -``` - -### 2. Backend Setup - -```bash -cd backend - -# Create virtual environment -uv venv -# or: python -m venv .venv - -# Install dependencies -uv pip install -e . -# or: .venv/bin/pip install -e . - -# Initialize database -cd .. -VIRTUAL_ENV=backend/.venv backend/.venv/bin/python -c "from backend.src.services.database import init_database; init_database()" -``` - -### 3. Frontend Setup - -```bash -cd frontend - -# Install dependencies -npm install -``` - -### 4. Environment Configuration - -The project includes development scripts that set environment variables automatically. For manual configuration, create a `.env` file in the backend directory: - -```bash -# backend/.env -JWT_SECRET_KEY=your-secret-key-here -VAULT_BASE_PATH=/path/to/Document-MCP/data/vaults -``` - -See `.env.example` for all available options. - -## πŸš€ Running the Application - -### Easy Start (Recommended) - -Use the provided scripts to start both servers: - -```bash -# Start frontend and backend -./start-dev.sh - -# Check status -./status-dev.sh - -# Stop servers -./stop-dev.sh - -# View logs -tail -f backend.log frontend.log -``` - -### Manual Start - -#### Running Backend - -Start the HTTP API server: - -```bash -cd backend -JWT_SECRET_KEY="local-dev-secret-key-123" \ -VAULT_BASE_PATH="$(pwd)/../data/vaults" \ -.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --reload -``` - -Backend will be available at: `http://localhost:8000` +--- +title: Document Viewer +emoji: πŸ“š +colorFrom: blue +colorTo: purple +sdk: docker +pinned: false +license: mit +--- -API docs (Swagger): `http://localhost:8000/docs` +# Document Viewer - AI-Powered Documentation System -#### Running MCP Server (STDIO Mode) +An Obsidian-style documentation system where AI agents and humans collaborate on creating and maintaining documentation. -For AI agent integration via MCP: +## ⚠️ Demo Mode -```bash -cd backend -JWT_SECRET_KEY="local-dev-secret-key-123" \ -VAULT_BASE_PATH="$(pwd)/../data/vaults" \ -.venv/bin/python -m src.mcp.server -``` +**This is a demonstration instance with ephemeral storage.** -#### Running Frontend +- All data is temporary and resets on server restart +- Demo content is automatically seeded on each startup +- For production use, deploy your own instance with persistent storage -```bash -cd frontend -npm run dev -``` +## 🎯 Features -Frontend will be available at: `http://localhost:5173` +- **Wikilinks** - Link between notes using `[[Note Name]]` syntax +- **Full-Text Search** - BM25 ranking with recency bonus +- **Backlinks** - Automatically track note references +- **Split-Pane Editor** - Live markdown preview +- **MCP Integration** - AI agents can read/write via Model Context Protocol +- **Multi-Tenant** - Each user gets an isolated vault (HF OAuth) -## πŸ€– MCP Client Configuration +## πŸš€ Getting Started -To use the Document Viewer with AI agents (Claude Desktop, Cline, etc.), add this to your MCP configuration: +1. Click **"Sign in with Hugging Face"** to authenticate +2. Browse the pre-seeded demo notes +3. Try searching, creating, and editing notes +4. Check out the wikilinks between documents -### Claude Desktop / Cline +## πŸ€– AI Agent Access (MCP) -Add to `~/.cursor/mcp.json` (or Claude Desktop settings): +After signing in, go to **Settings** to get your API token for MCP access: ```json { "mcpServers": { "obsidian-docs": { - "command": "python", - "args": ["-m", "backend.src.mcp.server"], - "cwd": "/path/to/Document-MCP", - "env": { - "BEARER_TOKEN": "local-dev-token", - "FASTMCP_SHOW_CLI_BANNER": "false", - "PYTHONPATH": "/path/to/Document-MCP", - "JWT_SECRET_KEY": "local-dev-secret-key-123", - "VAULT_BASE_PATH": "/path/to/Document-MCP/data/vaults" + "url": "https://YOUR_USERNAME-Document-MCP.hf.space/mcp", + "transport": "http", + "headers": { + "Authorization": "Bearer YOUR_JWT_TOKEN" } } } } ``` -**Note:** In production, use actual JWT tokens instead of `local-dev-token`. - -### Available MCP Tools - -AI agents can use these tools: +For local experiments you can still run the MCP server via STDIOβ€”use the "Local Development" snippet shown in Settings. -- `list_notes` - List all notes in vault -- `read_note` - Read a specific note -- `write_note` - Create or update a note -- `delete_note` - Remove a note -- `search_notes` - Full-text search with BM25 ranking -- `get_backlinks` - Find notes linking to a target -- `get_tags` - List all tags with usage counts +AI agents can then use these tools: +- `list_notes` - Browse vault +- `read_note` - Read note content +- `write_note` - Create/update notes +- `search_notes` - Full-text search +- `get_backlinks` - Find references +- `get_tags` - List all tags -## πŸ›οΈ Architecture - -### Data Model - -**Note Structure:** -```yaml ---- -title: My Note -tags: [guide, tutorial] -created: 2025-01-15T10:00:00Z -updated: 2025-01-15T14:30:00Z ---- - -# My Note - -Content with [[Wikilinks]] to other notes. -``` - -**Vault Structure:** -``` -data/vaults/ -β”œβ”€β”€ local-dev/ # Development user vault -β”‚ β”œβ”€β”€ Getting Started.md -β”‚ β”œβ”€β”€ API Documentation.md -β”‚ └── ... -└── {user_id}/ # Production user vaults - └── *.md -``` +## πŸ—οΈ Tech Stack -**Index Tables (SQLite):** -- `note_metadata` - Note versions, titles, timestamps -- `note_fts` - FTS5 full-text search index -- `note_tags` - Tag associations -- `note_links` - Wikilink graph (resolved/unresolved) -- `index_health` - Index statistics per user - -### Key Features - -**Wikilink Resolution:** -- Normalizes titles to slugs: `[[Getting Started]]` β†’ `getting-started` -- Matches against both title and filename -- Prefers same-folder matches -- Tracks broken links for UI styling - -**Search Ranking:** -- BM25 algorithm with title-weighted scoring (3x title, 1x body) -- Recency bonus: +1.0 for notes updated in last 7 days, +0.5 for last 30 days -- Returns highlighted snippets with `` tags - -**Optimistic Concurrency:** -- Version-based conflict detection for note edits -- Prevents data loss from concurrent edits -- Returns 409 Conflict with helpful message - -## πŸ”’ Authentication - -### Local Development -Uses a static token: `local-dev-token` - -### Production (Hugging Face OAuth) -- Multi-tenant with per-user isolated vaults -- JWT tokens with user_id claims -- Automatic vault initialization on first login - -See deployment documentation for HF OAuth setup. - -## πŸ“Š Performance Considerations - -**SQLite Optimizations:** -- FTS5 with prefix indexes (`prefix='2 3'`) for fast autocomplete and substring matching -- Recommended: Enable WAL mode for concurrent reads/writes: - ```sql - PRAGMA journal_mode=WAL; - PRAGMA synchronous=NORMAL; - ``` -- Normalized slug indexes (`normalized_title_slug`, `normalized_path_slug`) for O(1) wikilink resolution -- BM25 ranking weights: 3.0 for title matches, 1.0 for body matches - -**Rate Limiting:** -- ⚠️ **Production Recommendation**: Add per-user rate limits to prevent abuse -- API endpoints currently have no rate limiting -- Consider implementing: - - `/api/notes` (POST): 100 requests/hour per user - - `/api/index/rebuild` (POST): 10 requests/day per user - - `/api/search`: 1000 requests/hour per user -- Use libraries like `slowapi` or Redis-based rate limiting - -**Scaling:** -- **Single-server**: SQLite handles 100K+ notes efficiently -- **Multi-server**: Migrate to PostgreSQL with `pg_trgm` or `pgvector` for FTS -- **Caching**: Add Redis for: - - Session tokens (reduce DB lookups) - - Frequently accessed notes - - Search result caching (TTL: 5 minutes) -- **CDN**: Serve frontend assets via CDN for global performance - -## πŸ§ͺ Development - -### Project Structure +**Backend:** +- FastAPI - HTTP API server +- FastMCP - MCP server for AI integration +- SQLite FTS5 - Full-text search +- python-frontmatter - YAML metadata -``` -Document-MCP/ -β”œβ”€β”€ backend/ -β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ api/ # FastAPI routes & middleware -β”‚ β”‚ β”œβ”€β”€ mcp/ # FastMCP server -β”‚ β”‚ β”œβ”€β”€ models/ # Pydantic models -β”‚ β”‚ └── services/ # Business logic -β”‚ └── tests/ # Backend tests -β”œβ”€β”€ frontend/ -β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ components/ # React components -β”‚ β”‚ β”œβ”€β”€ pages/ # Page components -β”‚ β”‚ β”œβ”€β”€ services/ # API client -β”‚ β”‚ └── types/ # TypeScript types -β”‚ └── tests/ # Frontend tests -β”œβ”€β”€ data/ -β”‚ β”œβ”€β”€ vaults/ # User markdown files -β”‚ └── index.db # SQLite database -β”œβ”€β”€ specs/ # Feature specifications -└── start-dev.sh # Development startup script -``` +**Frontend:** +- React + Vite - Modern web framework +- shadcn/ui - UI components +- Tailwind CSS - Styling +- react-markdown - Markdown rendering -### Adding a New Note (via UI) +## πŸ“– Documentation -1. Click "New Note" button -2. Enter note name (`.md` extension optional) -3. Edit in split-pane editor -4. Save with Cmd/Ctrl+S +Key demo notes to explore: -### Adding a New Note (via MCP) +- **Getting Started** - Introduction and overview +- **API Documentation** - REST API reference +- **MCP Integration** - AI agent configuration +- **Wikilink Examples** - How linking works +- **Architecture Overview** - System design +- **Search Features** - Full-text search details -```python -# AI agent writes a note -write_note( - path="guides/my-guide.md", - body="# My Guide\n\nContent here with [[links]]", - title="My Guide", - metadata={"tags": ["guide", "tutorial"]} -) -``` +## βš™οΈ Deploy Your Own -## πŸ› Troubleshooting +Want persistent storage and full control? Deploy your own instance: -**Backend won't start:** -- Ensure virtual environment is activated -- Check environment variables are set -- Verify database is initialized +1. Clone the repository +2. Set up HF OAuth app +3. Configure environment variables +4. Deploy to HF Spaces or any Docker host -**Frontend shows connection errors:** -- Ensure backend is running on port 8000 -- Check Vite proxy configuration in `frontend/vite.config.ts` +See [DEPLOYMENT.md](https://github.com/YOUR_REPO/Document-MCP/blob/main/DEPLOYMENT.md) for detailed instructions. -**Search returns no results:** -- Verify notes are indexed (check Settings β†’ Index Health) -- Try rebuilding the index via Settings page +## πŸ”’ Privacy & Data -**MCP tools not showing in Claude:** -- Verify MCP configuration path is correct -- Check `PYTHONPATH` includes project root -- Restart Claude Desktop after config changes +- **Multi-tenant**: Each HF user gets an isolated vault +- **Demo data**: Resets on restart (ephemeral storage) +- **OAuth**: Secure authentication via Hugging Face +- **No tracking**: We don't collect analytics or personal data ## πŸ“ License -[Add license information] +MIT License - See LICENSE file for details ## 🀝 Contributing -[Add contributing guidelines] +Contributions welcome! Open an issue or submit a PR. -## πŸ“§ Contact +--- -[Add contact information] +Built with ❀️ for the AI-human documentation collaboration workflow From 0aeab8ec94b41f76f2f82df393352794ffde80f1 Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 13:58:18 -0600 Subject: [PATCH 3/9] Fix Dockerfile: use Node.js 20.x for React 19 compatibility --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c2fc652..d3da5b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,11 @@ FROM python:3.11-slim WORKDIR /app -# Install Node.js for frontend build +# Install Node.js 20.x for frontend build (required for React 19, Vite 7) RUN apt-get update && \ - apt-get install -y nodejs npm curl && \ + apt-get install -y curl gnupg && \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs && \ rm -rf /var/lib/apt/lists/* # Copy and install frontend dependencies From a356bff14a217dc462f9b85e57fad9e5086889df Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 14:04:25 -0600 Subject: [PATCH 4/9] Fix merge conflict marker in Settings.tsx --- frontend/src/pages/Settings.tsx | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index a8f78d8..dfc46be 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -265,28 +265,6 @@ export function Settings() {
Last Updated
{formatDate(indexHealth.last_incremental_update)}
-======= - - {rebuildResult && ( - - - Index rebuilt successfully! Indexed {rebuildResult.notes_indexed} notes in {rebuildResult.duration_ms}ms - - - )} - - - -
- Rebuilding the index will re-scan all notes and update the full-text search database. - This may take a few seconds for large vaults.
From bb3b6b3e36bf05d0f2f1c00dc3615c47782f2519 Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 14:10:19 -0600 Subject: [PATCH 5/9] Fix TypeScript errors: add @types/d3-force, remove unused imports/variables --- frontend/package-lock.json | 8 ++++++ frontend/package.json | 1 + frontend/src/components/GraphView.tsx | 4 +-- frontend/src/components/NoteViewer.tsx | 2 +- frontend/src/pages/MainApp.tsx | 38 +------------------------- 5 files changed, 13 insertions(+), 40 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3ede11b..4de582e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,6 +36,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/d3-force": "^3.0.10", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", @@ -3272,6 +3273,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", diff --git a/frontend/package.json b/frontend/package.json index b433578..4dcbd5d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/d3-force": "^3.0.10", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", diff --git a/frontend/src/components/GraphView.tsx b/frontend/src/components/GraphView.tsx index a589bb9..b0329a2 100644 --- a/frontend/src/components/GraphView.tsx +++ b/frontend/src/components/GraphView.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState, useMemo } from 'react'; import ForceGraph2D, { type ForceGraphMethods } from 'react-force-graph-2d'; import { forceRadial } from 'd3-force'; -import type { GraphData, GraphNode } from '@/types/graph'; +import type { GraphData } from '@/types/graph'; import { getGraphData } from '@/services/api'; import { Loader2, AlertCircle } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; @@ -14,7 +14,7 @@ export function GraphView({ onSelectNote }: GraphViewProps) { const [data, setData] = useState({ nodes: [], links: [] }); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const graphRef = useRef(); + const graphRef = useRef(undefined); // Theme detection would go here, simplified for MVP const isDark = document.documentElement.classList.contains('dark'); diff --git a/frontend/src/components/NoteViewer.tsx b/frontend/src/components/NoteViewer.tsx index 97af246..9ca4f51 100644 --- a/frontend/src/components/NoteViewer.tsx +++ b/frontend/src/components/NoteViewer.tsx @@ -40,7 +40,7 @@ export function NoteViewer({ // [[Link|Alias]] -> [Alias](wikilink:Link) const processedBody = useMemo(() => { if (!note.body) return ''; - const processed = note.body.replace(/\[\[([^\]]+)\]\]/g, (match, content) => { + const processed = note.body.replace(/\[\[([^\]]+)\]\]/g, (_match, content) => { const [link, alias] = content.split('|'); const displayText = alias || link; const href = `wikilink:${encodeURIComponent(link)}`; diff --git a/frontend/src/pages/MainApp.tsx b/frontend/src/pages/MainApp.tsx index 7cdd667..c14c276 100644 --- a/frontend/src/pages/MainApp.tsx +++ b/frontend/src/pages/MainApp.tsx @@ -29,7 +29,6 @@ import { } from '@/services/api'; import { Dialog, - DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -281,7 +280,7 @@ export function MainApp() { const folderPath = newFolderName.replace(/\/$/, ''); // Remove trailing slash if present const placeholderPath = `${folderPath}/.placeholder.md`; - const note = await createNote({ + await createNote({ note_path: placeholderPath, title: 'Folder', body: `# ${folderPath}\n\nThis folder was created.`, @@ -308,41 +307,6 @@ export function MainApp() { } }; - // Handle rename note - const handleRenameNote = async (oldPath: string, newPath: string) => { - if (!newPath.trim()) { - toast.error('New path cannot be empty'); - return; - } - - try { - // Ensure new path has .md extension - const finalNewPath = newPath.endsWith('.md') ? newPath : `${newPath}.md`; - - await moveNote(oldPath, finalNewPath); - - // Refresh notes list - const notesList = await listNotes(); - setNotes(notesList); - - // If renaming currently selected note, update selection - if (selectedPath === oldPath) { - setSelectedPath(finalNewPath); - } - - toast.success(`Note renamed successfully`); - } catch (err) { - let errorMessage = 'Failed to rename note'; - if (err instanceof APIException) { - errorMessage = err.message || err.error; - } else if (err instanceof Error) { - errorMessage = err.message; - } - toast.error(errorMessage); - console.error('Error renaming note:', err); - } - }; - // Handle dragging file to folder const handleMoveNoteToFolder = async (oldPath: string, targetFolderPath: string) => { try { From 80afb65dfeeeb7dbf0e4658e3a734626c4473668 Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 14:56:48 -0600 Subject: [PATCH 6/9] spec --- .../checklists/requirements.md | 34 ++++++++ .../contracts/chat-api.yaml | 60 +++++++++++++ specs/003-ai-chat-window/data-model.md | 51 +++++++++++ specs/003-ai-chat-window/plan.md | 83 ++++++++++++++++++ specs/003-ai-chat-window/quickstart.md | 32 +++++++ specs/003-ai-chat-window/research.md | 48 +++++++++++ specs/003-ai-chat-window/spec.md | 85 +++++++++++++++++++ specs/003-ai-chat-window/tasks.md | 74 ++++++++++++++++ 8 files changed, 467 insertions(+) create mode 100644 specs/003-ai-chat-window/checklists/requirements.md create mode 100644 specs/003-ai-chat-window/contracts/chat-api.yaml create mode 100644 specs/003-ai-chat-window/data-model.md create mode 100644 specs/003-ai-chat-window/plan.md create mode 100644 specs/003-ai-chat-window/quickstart.md create mode 100644 specs/003-ai-chat-window/research.md create mode 100644 specs/003-ai-chat-window/spec.md create mode 100644 specs/003-ai-chat-window/tasks.md diff --git a/specs/003-ai-chat-window/checklists/requirements.md b/specs/003-ai-chat-window/checklists/requirements.md new file mode 100644 index 0000000..77d5599 --- /dev/null +++ b/specs/003-ai-chat-window/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: AI Chat Window + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-11-26 +**Feature**: [Link to spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` \ No newline at end of file diff --git a/specs/003-ai-chat-window/contracts/chat-api.yaml b/specs/003-ai-chat-window/contracts/chat-api.yaml new file mode 100644 index 0000000..1baf205 --- /dev/null +++ b/specs/003-ai-chat-window/contracts/chat-api.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.0 +info: + title: Document MCP Chat API + version: 1.0.0 +paths: + /api/chat: + post: + summary: Send a message to the AI agent + description: Streams the response using Server-Sent Events (SSE). + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + message: + type: string + history: + type: array + items: + type: object + properties: + role: + type: string + enum: [user, assistant, system] + content: + type: string + persona: + type: string + default: default + model: + type: string + responses: + '200': + description: Stream of tokens + content: + text/event-stream: + schema: + type: string + example: "data: {\"type\": \"token\", \"content\": \"Hello\"}\n\n" + /api/chat/personas: + get: + summary: List available personas + responses: + '200': + description: List of personas + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string diff --git a/specs/003-ai-chat-window/data-model.md b/specs/003-ai-chat-window/data-model.md new file mode 100644 index 0000000..1933470 --- /dev/null +++ b/specs/003-ai-chat-window/data-model.md @@ -0,0 +1,51 @@ +# Data Model: AI Chat Window + +## Entities + +### ChatMessage +Represents a single message in the conversation history. + +| Field | Type | Description | +|-------|------|-------------| +| `role` | `enum` | `user`, `assistant`, `system` | +| `content` | `string` | The text content of the message | +| `timestamp` | `datetime` | ISO 8601 timestamp of creation | + +### ChatRequest +The payload sent from Frontend to Backend to initiate/continue a chat. + +| Field | Type | Description | +|-------|------|-------------| +| `message` | `string` | The new user message | +| `history` | `List[ChatMessage]` | Previous conversation context | +| `persona` | `string` | ID of the selected persona (e.g., "default", "auto-linker") | +| `model` | `string` | Optional: Specific OpenRouter model ID | + +### ChatResponseChunk (SSE) +The streaming data format received by the frontend. + +| Field | Type | Description | +|-------|------|-------------| +| `type` | `enum` | `token` (text chunk) or `tool_call` (tool execution status) | +| `content` | `string` | The text fragment or status message | +| `done` | `boolean` | True if generation is complete | + +## Persistence (Markdown Format) +Saved in `data/vaults/{user_id}/Chat Logs/{timestamp}.md` + +```markdown +--- +title: Chat Session - {timestamp} +date: {date} +tags: [chat-log, {persona}] +model: {model_id} +--- + +# Chat Session + +## User ({time}) +What is the summary of... + +## Assistant ({time}) +Based on your notes... +``` diff --git a/specs/003-ai-chat-window/plan.md b/specs/003-ai-chat-window/plan.md new file mode 100644 index 0000000..7088bd6 --- /dev/null +++ b/specs/003-ai-chat-window/plan.md @@ -0,0 +1,83 @@ +# Implementation Plan: AI Chat Window + +**Branch**: `003-ai-chat-window` | **Date**: 2025-11-26 | **Spec**: [specs/003-ai-chat-window/spec.md](spec.md) +**Input**: Feature specification from `/specs/003-ai-chat-window/spec.md` + +## Summary + +Implement an integrated AI Chat Window powered by OpenRouter. This involves a new backend `POST /api/chat` endpoint that uses the `openai` client to communicate with LLMs, exposing internal `VaultService` methods as tools. The frontend will receive a new `ChatWindow` component with streaming support (SSE) and persona selection. Chat history will be persisted as Markdown files in the vault. + +## Technical Context + +**Language/Version**: Python 3.11+ (Backend), TypeScript/React 18 (Frontend) +**Primary Dependencies**: +- Backend: `openai` (for OpenRouter), `fastapi` (StreamingResponse) +- Frontend: `fetch` (Streaming body reading), Tailwind CSS +**Storage**: +- Active Session: In-memory (or transient SQLite) +- Persistence: Markdown files in `Chat Logs/` folder +**Testing**: `pytest` (Backend), Manual/E2E (Frontend) +**Target Platform**: Web Application (Linux Dev Environment) +**Project Type**: Full-stack (FastAPI + React) +**Performance Goals**: <3s time-to-first-token +**Constraints**: Must reuse existing `VaultService` logic; no new database services (keep it lightweight). + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [x] **Brownfield Integration**: Reuses `VaultService` and `IndexerService`. Matches `backend/src` and `frontend/src` structure. +- [x] **Test-Backed Development**: Backend logic will be unit tested. +- [x] **Incremental Delivery**: New API route and independent UI component. +- [x] **Specification-Driven**: All features map to `spec.md` requirements. + +## Project Structure + +### Documentation (this feature) + +```text +specs/003-ai-chat-window/ +β”œβ”€β”€ plan.md # This file +β”œβ”€β”€ research.md # Phase 0 output +β”œβ”€β”€ data-model.md # Phase 1 output +β”œβ”€β”€ quickstart.md # Phase 1 output +β”œβ”€β”€ contracts/ # Phase 1 output +└── tasks.md # Phase 2 output +``` + +### Source Code (repository root) + +```text +backend/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ └── routes/ +β”‚ β”‚ └── chat.py # NEW: Chat endpoint logic +β”‚ β”œβ”€β”€ services/ +β”‚ β”‚ β”œβ”€β”€ chat.py # NEW: Chat orchestration service (OpenAI wrapper) +β”‚ β”‚ └── prompts.py # NEW: System prompts/personas definitions +β”‚ └── models/ +β”‚ └── chat.py # NEW: Pydantic models for Chat requests/responses +└── tests/ + └── unit/ + └── test_chat_service.py # NEW: Tests for chat logic + +frontend/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ chat/ # NEW: Chat UI Components +β”‚ β”‚ β”‚ β”œβ”€β”€ ChatWindow.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ ChatMessage.tsx +β”‚ β”‚ β”‚ └── PersonaSelector.tsx +β”‚ └── services/ +β”‚ └── api.ts # UPDATE: Add chat endpoints +└── tests/ +``` + +**Structure Decision**: Standard Full-stack separation. Backend adds a dedicated `chat` service and route to isolate LLM logic from core data services. Frontend adds a self-contained `chat/` directory for UI components. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| N/A | | | \ No newline at end of file diff --git a/specs/003-ai-chat-window/quickstart.md b/specs/003-ai-chat-window/quickstart.md new file mode 100644 index 0000000..992ad3c --- /dev/null +++ b/specs/003-ai-chat-window/quickstart.md @@ -0,0 +1,32 @@ +# Quickstart: AI Chat Window + +## Prerequisites +1. **OpenRouter Key**: Get an API key from [openrouter.ai](https://openrouter.ai). +2. **Environment**: Set `OPENROUTER_API_KEY` in `backend/.env`. + +## Testing the Backend +1. **Start Server**: + ```bash + cd backend + source .venv/bin/activate + uvicorn src.api.main:app --reload + ``` +2. **Test Endpoint**: + ```bash + curl -X POST http://localhost:8000/api/chat \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello", "history": []}' + ``` + *Note: This will output raw SSE stream data.* + +## Testing the Frontend +1. **Start Client**: + ```bash + cd frontend + npm run dev + ``` +2. **Open UI**: Go to `http://localhost:5173`. +3. **Chat**: Click the "Chat" button in the sidebar. Select a persona and send a message. + +## Verification +1. **Check Logs**: After a chat, check `data/vaults/{user}/Chat Logs/` to see the saved Markdown file. diff --git a/specs/003-ai-chat-window/research.md b/specs/003-ai-chat-window/research.md new file mode 100644 index 0000000..993d32a --- /dev/null +++ b/specs/003-ai-chat-window/research.md @@ -0,0 +1,48 @@ +# Phase 0: Research & Design Decisions + +**Feature**: AI Chat Window (`003-ai-chat-window`) + +## 1. OpenRouter Integration +**Question**: What is the best way to integrate OpenRouter in Python? +**Finding**: OpenRouter is API-compatible with OpenAI. The standard `openai` Python client library is recommended, configured with `base_url="https://openrouter.ai/api/v1"` and the OpenRouter API key. +**Decision**: Use `openai` Python package. +**Rationale**: Industry standard, robust, async support. +**Alternatives**: `requests` (too manual), `langchain` (too heavy/complex for this specific need). + +## 2. Real-time Streaming +**Question**: How to stream LLM tokens from FastAPI to React? +**Finding**: Server-Sent Events (SSE) is the standard for unidirectional text streaming. FastAPI supports this via `StreamingResponse`. +**Decision**: Use `StreamingResponse` with a generator that yields SSE-formatted data (`data: ...\n\n`). +**Rationale**: Simpler than WebSockets, works well through proxies/firewalls, native support in modern browsers (`EventSource` or `fetch` with readable streams). + +## 3. Tool Execution Strategy +**Question**: How to invoke existing MCP tools (`list_notes`, `read_note`) from the chat endpoint? +**Finding**: The tools are defined as decorated functions in `backend/src/mcp/server.py`. We can import them directly. However, `FastMCP` wraps them. We might need to access the underlying function or just call the wrapper if it allows direct invocation. +**Decision**: Import the `mcp` object from `backend/src/mcp/server.py`. Use `mcp.list_tools()` to dynamically get tool definitions for the system prompt. Call the underlying functions directly if exposed, or use the `mcp.call_tool()` API if available. *Fallback*: Re-import the service functions (`vault_service.read_note`) directly if the MCP wrapper adds too much overhead/complexity for internal calls. +**Refinement**: The `server.py` defines tools using `@mcp.tool`. The most robust way is to import the `vault_service` and `indexer_service` instances directly from `server.py` (or a shared module) and wrap them in a simple "Agent Tool" registry for the LLM, mirroring the MCP definitions. This avoids "fake" network calls to localhost. + +## 4. Frontend UI Components +**Question**: What UI library to use for the chat interface? +**Finding**: Project uses Tailwind + generic React. +**Decision**: Build a custom `ChatWindow` component using Tailwind. Use a scrollable container for messages and a sticky footer for the input. +**Rationale**: Lightweight, full control over styling. + +## 5. Chat History Persistence +**Question**: How to store chat history? +**Finding**: Spec requires saving to Markdown files in the vault. +**Decision**: +1. **In-Memory/Database**: Use a simple `sqlite` table (or just in-memory if stateless) to hold the *active* conversation state for the UI. +2. **Persistence**: On "End Session" or auto-save (debounced), dump the conversation to `Chat Logs/{timestamp}-{title}.md`. +**Rationale**: Markdown is the source of truth. The database is just for the "hot" state to avoid parsing MD files on every new message. + +## 6. System Prompts & Personas +**Question**: How to manage prompts? +**Decision**: Store prompts in a simple dictionary or JSON file in `backend/src/services/prompts.py`. +**Structure**: +```python +PERSONAS = { + "default": "You are a helpful assistant...", + "auto-linker": "You are an expert editor. Your goal is to densely connect notes...", +} +``` + diff --git a/specs/003-ai-chat-window/spec.md b/specs/003-ai-chat-window/spec.md new file mode 100644 index 0000000..2482e0e --- /dev/null +++ b/specs/003-ai-chat-window/spec.md @@ -0,0 +1,85 @@ +# Feature Specification: AI Chat Window + +**Feature Branch**: `003-ai-chat-window` +**Created**: 2025-11-26 +**Status**: Draft +**Input**: User description: "Add an AI Chat Window using OpenRouter as the LLM provider. The system should reuse existing MCP tools (backend agent) to manage the vault. Include a 'Persona/Mode' selector to allow users to choose specialized system prompts for tasks like reindexing, cross-linking, and summarization. Chat history should be persisted to the vault." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - General Q&A with Vault Context (Priority: P1) + +As a user, I want to ask questions about my notes so that I can quickly find information or synthesize concepts without manually searching. + +**Why this priority**: This is the core value propositionβ€”enabling natural language interaction with the knowledge base. + +**Independent Test**: Can be tested by asking a question about a known note and verifying the answer cites the correct information. + +**Acceptance Scenarios**: + +1. **Given** the chat window is open, **When** I ask "What is the summary of project X?", **Then** the agent searches the vault and returns a summary based on the note content. +2. **Given** a specific note is open, **When** I ask "Summarize this", **Then** the agent reads the current note context and provides a summary. + +--- + +### User Story 2 - Vault Management via Personas (Priority: P2) + +As a power user, I want to select specialized "Personas" (e.g., Auto-Linker, Tag Gardener) so that I can perform complex maintenance tasks with optimized prompts. + +**Why this priority**: Distinguishes this from a simple "chatbot" by adding workflow automation capabilities. + +**Independent Test**: Select a persona, give a relevant command, and verify the specific tool (write/update) is called. + +**Acceptance Scenarios**: + +1. **Given** the "Auto-Linker" persona is selected, **When** I ask "Fix links in Note A", **Then** the agent identifies unlinked concepts and updates the note with `[[WikiLinks]]`. +2. **Given** the "Tag Gardener" persona is selected, **When** I ask "Clean up tags", **Then** the agent identifies synonymous tags and standardizes them across affected notes. + +--- + +### User Story 3 - Chat History Persistence (Priority: P3) + +As a user, I want my chat conversations to be saved in the vault so that I can reference past insights or continue working later. + +**Why this priority**: Ensures work isn't lost and integrates chat logs as first-class citizens in the vault. + +**Independent Test**: Refresh the browser and verify the previous conversation is still visible. + +**Acceptance Scenarios**: + +1. **Given** I have had a conversation, **When** I refresh the page, **Then** the chat history is restored. +2. **Given** a conversation is finished, **When** I look in the vault file explorer, **Then** I see a new Markdown file (e.g., in `Chat Logs/`) containing the transcript. + +--- + +### Edge Cases + +- **Network Failure**: What happens if OpenRouter is down? (System should show error and allow retry). +- **Large Context**: What happens if the vault search returns too much text? (Agent should truncate or summarize input). +- **Invalid Tool Use**: What happens if the agent tries to write a file with invalid characters? (System should catch error and ask agent to retry). + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST provide a UI interface for Chat (floating or sidebar) that persists across navigation. +- **FR-002**: System MUST allow users to configure an OpenRouter API Key (via env vars or UI settings). +- **FR-003**: System MUST expose existing internal MCP tools (`read_note`, `write_note`, `search_notes`, etc.) to the LLM. +- **FR-004**: System MUST support selecting "Personas" that inject specific system prompts (Auto-Linker, Tag Gardener, etc.) into the context. +- **FR-005**: Chat sessions MUST be automatically saved to the vault as Markdown files (e.g., in a `Chat Logs` folder). +- **FR-006**: System MUST stream LLM responses to the UI for real-time feedback. +- **FR-007**: System MUST support creating new chat sessions and switching between past sessions. + +### Key Entities + +- **Chat Session**: Represents a conversation thread. Properties: ID, Title, Created Date, Messages (User/Assistant/Tool). +- **Persona**: A preset configuration of System Prompt + available Tools. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Agent responses start streaming within 3 seconds of user input. +- **SC-002**: 95% of "Auto-Linker" requests result in valid WikiLinks being added without syntax errors. +- **SC-003**: Users can switch between active chat and past history (refresh/reload) with zero data loss. +- **SC-004**: System can handle a context window of at least 16k tokens (supporting moderate-sized note analysis). \ No newline at end of file diff --git a/specs/003-ai-chat-window/tasks.md b/specs/003-ai-chat-window/tasks.md new file mode 100644 index 0000000..5daef6b --- /dev/null +++ b/specs/003-ai-chat-window/tasks.md @@ -0,0 +1,74 @@ +# Tasks: AI Chat Window + +**Feature Branch**: `003-ai-chat-window` +**Spec**: [specs/003-ai-chat-window/spec.md](spec.md) +**Plan**: [specs/003-ai-chat-window/plan.md](plan.md) + +## Phase 1: Setup +*Goal: Initialize project structure and install dependencies.* + +- [ ] T001 Create contracts directory and API spec at specs/003-ai-chat-window/contracts/chat-api.yaml +- [ ] T002 [P] Create directory backend/src/services/chat +- [ ] T003 [P] Create directory frontend/src/components/chat +- [ ] T004 Add openai dependency to backend/requirements.txt + +## Phase 2: Foundational +*Goal: Core backend logic for Chat (Service Layer).* + +- [ ] T005 [US1] Define ChatMessage and ChatRequest models in backend/src/models/chat.py +- [ ] T006 [US1] Define Persona and Prompt models in backend/src/models/chat.py +- [ ] T007 [US2] Implement prompt storage (dictionary of personas) in backend/src/services/prompts.py +- [ ] T008 [US1] Create ChatService class skeleton in backend/src/services/chat.py + +## Phase 3: User Story 1 - General Q&A with Vault Context +*Goal: Enable basic chat interactions with streaming and tool use.* +*Test Criteria: Can ask a question and get a streaming response citing vault notes.* + +- [ ] T009 [US1] Implement OpenAI client initialization in backend/src/services/chat.py +- [ ] T010 [US1] Implement tool registry (wrap VaultService/IndexerService) in backend/src/services/chat.py +- [ ] T011 [US1] Implement stream_chat method with SSE generator in backend/src/services/chat.py +- [ ] T012 [US1] Create unit tests for ChatService in backend/tests/unit/test_chat_service.py +- [ ] T013 [US1] Implement POST /api/chat endpoint in backend/src/api/routes/chat.py +- [ ] T014 [US1] Register chat router in backend/src/api/main.py +- [ ] T015 [P] [US1] Create ChatMessage component in frontend/src/components/chat/ChatMessage.tsx +- [ ] T016 [US1] Create ChatWindow component skeleton in frontend/src/components/chat/ChatWindow.tsx +- [ ] T017 [US1] Implement streaming fetch logic in frontend/src/services/api.ts +- [ ] T018 [US1] Connect ChatWindow to API and handle SSE stream in frontend/src/components/chat/ChatWindow.tsx + +## Phase 4: User Story 2 - Vault Management via Personas +*Goal: Allow users to select specialized agents for maintenance tasks.* +*Test Criteria: Selecting "Auto-Linker" injects the correct system prompt and tools.* + +- [ ] T019 [US2] Add GET /api/chat/personas endpoint to backend/src/api/routes/chat.py +- [ ] T020 [P] [US2] Create PersonaSelector component in frontend/src/components/chat/PersonaSelector.tsx +- [ ] T021 [US2] Add persona selection state to frontend/src/components/chat/ChatWindow.tsx +- [ ] T022 [US2] Update ChatService to accept and apply persona ID in backend/src/services/chat.py + +## Phase 5: User Story 3 - Chat History Persistence +*Goal: Save conversation logs to the vault.* +*Test Criteria: Chat logs appear as Markdown files in "Chat Logs/" folder.* + +- [ ] T023 [US3] Implement save_chat_log method in backend/src/services/chat.py (Markdown formatting) +- [ ] T024 [US3] Update POST /api/chat to auto-save on completion (or session end) in backend/src/api/routes/chat.py +- [ ] T025 [US3] Add logic to restore history from ChatRequest.history in backend/src/services/chat.py +- [ ] T026 [US3] Add "Clear History" or "New Chat" button in frontend/src/components/chat/ChatWindow.tsx + +## Phase 6: Polish & Cross-Cutting Concerns +*Goal: Final UI touches and error handling.* + +- [ ] T027 [P] Style ChatWindow with Tailwind (responsive sidebar/floating) in frontend/src/components/chat/ChatWindow.tsx +- [ ] T028 Implement error handling for OpenRouter failures in backend/src/services/chat.py +- [ ] T029 Add tool execution status messages to UI stream in frontend/src/components/chat/ChatMessage.tsx + +## Dependencies + +- **US1** depends on Setup & Foundational tasks. +- **US2** extends US1 (can be parallelized after US1 backend is stable). +- **US3** extends US1 backend logic. + +## Implementation Strategy + +1. **MVP (US1)**: Get the chat bubble working with a hardcoded "Hello World" stream, then hook up OpenRouter. +2. **Tools**: Enable `read_note` and `search_notes` so the agent isn't blind. +3. **Personas (US2)**: Add the dropdown and the specialized prompts. +4. **Persistence (US3)**: Add the file writing logic last. From 45117b7c6ac848979132188ca349faae2f86f597 Mon Sep 17 00:00:00 2001 From: bigwolfe Date: Wed, 26 Nov 2025 15:08:49 -0600 Subject: [PATCH 7/9] Add demo token endpoint and read-only frontend handling --- backend/src/api/main.py | 3 +- backend/src/api/routes/__init__.py | 4 +- backend/src/api/routes/demo.py | 39 +++++++++++++ backend/src/api/routes/index.py | 14 +++++ backend/src/api/routes/notes.py | 24 +++++++- frontend/src/App.tsx | 16 +++-- frontend/src/pages/MainApp.tsx | 68 ++++++++++++++++++++-- frontend/src/pages/Settings.tsx | 28 ++++++++- frontend/src/services/api.ts | 11 ++++ frontend/src/services/auth.ts | 93 +++++++++++++++++++++++++++--- 10 files changed, 277 insertions(+), 23 deletions(-) create mode 100644 backend/src/api/routes/demo.py diff --git a/backend/src/api/main.py b/backend/src/api/main.py index 97052b1..aa17848 100644 --- a/backend/src/api/main.py +++ b/backend/src/api/main.py @@ -21,7 +21,7 @@ from fastmcp.server.http import StreamableHTTPSessionManager from fastapi.responses import FileResponse -from .routes import auth, index, notes, search, graph +from .routes import auth, index, notes, search, graph, demo from ..mcp.server import mcp from ..services.seed import init_and_seed @@ -95,6 +95,7 @@ async def internal_error_handler(request: Request, exc: Exception): app.include_router(search.router, tags=["search"]) app.include_router(index.router, tags=["index"]) app.include_router(graph.router, tags=["graph"]) +app.include_router(demo.router, tags=["demo"]) # Hosted MCP HTTP endpoint (mounted Starlette app) diff --git a/backend/src/api/routes/__init__.py b/backend/src/api/routes/__init__.py index 1977ebc..23f1cec 100644 --- a/backend/src/api/routes/__init__.py +++ b/backend/src/api/routes/__init__.py @@ -1,5 +1,5 @@ """HTTP API route handlers.""" -from . import auth, index, notes, search +from . import auth, index, notes, search, graph, demo -__all__ = ["auth", "notes", "search", "index"] +__all__ = ["auth", "notes", "search", "index", "graph", "demo"] diff --git a/backend/src/api/routes/demo.py b/backend/src/api/routes/demo.py new file mode 100644 index 0000000..3035380 --- /dev/null +++ b/backend/src/api/routes/demo.py @@ -0,0 +1,39 @@ +"""Public endpoints for demo mode access.""" + +from __future__ import annotations + +from datetime import timedelta + +from fastapi import APIRouter + +from ...services.auth import AuthService +from ...services.seed import ensure_welcome_note + +DEMO_USER_ID = "demo-user" +DEMO_TOKEN_TTL_HOURS = 12 + +router = APIRouter() +auth_service = AuthService() + + +@router.get("/api/demo/token") +async def issue_demo_token(): + """ + Issue a short-lived JWT for the shared demo vault. + + The caller can use this token to explore the application in read-only mode. + """ + ensure_welcome_note(DEMO_USER_ID) + token, expires_at = auth_service.issue_token_response( + DEMO_USER_ID, expires_in=timedelta(hours=DEMO_TOKEN_TTL_HOURS) + ) + return { + "token": token, + "token_type": "bearer", + "expires_at": expires_at.isoformat(), + "user_id": DEMO_USER_ID, + } + + +__all__ = ["router", "DEMO_USER_ID"] + diff --git a/backend/src/api/routes/index.py b/backend/src/api/routes/index.py index 482cead..14a934b 100644 --- a/backend/src/api/routes/index.py +++ b/backend/src/api/routes/index.py @@ -14,6 +14,19 @@ from ...services.vault import VaultService from ..middleware import AuthContext, get_auth_context +DEMO_USER_ID = "demo-user" + + +def _ensure_index_mutation_allowed(user_id: str) -> None: + if user_id == DEMO_USER_ID: + raise HTTPException( + status_code=403, + detail={ + "error": "demo_read_only", + "message": "Demo mode does not allow index rebuilds. Sign in to manage the index.", + }, + ) + router = APIRouter() @@ -79,6 +92,7 @@ async def rebuild_index(auth: AuthContext = Depends(get_auth_context)): """Rebuild the entire index from scratch.""" start_time = time.time() user_id = auth.user_id + _ensure_index_mutation_allowed(user_id) vault_service = VaultService() indexer_service = IndexerService() diff --git a/backend/src/api/routes/notes.py b/backend/src/api/routes/notes.py index 5bfae26..3e2d71a 100644 --- a/backend/src/api/routes/notes.py +++ b/backend/src/api/routes/notes.py @@ -16,6 +16,19 @@ router = APIRouter() +DEMO_USER_ID = "demo-user" + + +def _ensure_write_allowed(user_id: str) -> None: + if user_id == DEMO_USER_ID: + raise HTTPException( + status_code=403, + detail={ + "error": "demo_read_only", + "message": "Demo mode is read-only. Sign in with Hugging Face to make changes.", + }, + ) + class ConflictError(Exception): """Raised when optimistic concurrency check fails.""" @@ -60,6 +73,7 @@ async def list_notes( async def create_note(create: NoteCreate, auth: AuthContext = Depends(get_auth_context)): """Create a new note.""" user_id = auth.user_id + _ensure_write_allowed(user_id) vault_service = VaultService() indexer_service = IndexerService() db_service = DatabaseService() @@ -230,6 +244,7 @@ async def update_note( ): """Update a note with optimistic concurrency control.""" user_id = auth.user_id + _ensure_write_allowed(user_id) vault_service = VaultService() indexer_service = IndexerService() db_service = DatabaseService() @@ -337,9 +352,14 @@ class NoteMoveRequest(BaseModel): @router.patch("/api/notes/{path:path}", response_model=Note) -async def move_note(path: str, move_request: NoteMoveRequest): +async def move_note( + path: str, + move_request: NoteMoveRequest, + auth: AuthContext = Depends(get_auth_context), +): """Move or rename a note to a new path.""" - user_id = get_user_id() + user_id = auth.user_id + _ensure_write_allowed(user_id) vault_service = VaultService() indexer_service = IndexerService() db_service = DatabaseService() diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4197f57..251c64b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,7 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation } from import { MainApp } from './pages/MainApp'; import { Login } from './pages/Login'; import { Settings } from './pages/Settings'; -import { isAuthenticated, getCurrentUser, setAuthTokenFromHash } from './services/auth'; +import { isAuthenticated, getCurrentUser, setAuthTokenFromHash, ensureDemoToken, isDemoSession } from './services/auth'; import { AuthLoadingSkeleton } from './components/AuthLoadingSkeleton'; import { Toaster } from './components/ui/toaster'; import './App.css'; @@ -28,13 +28,21 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { } if (!isAuthenticated()) { - setHasToken(false); - setIsChecking(false); - return; + const demoReady = await ensureDemoToken(); + if (!demoReady) { + setHasToken(false); + setIsChecking(false); + return; + } } setHasToken(true); + if (isDemoSession()) { + setIsChecking(false); + return; + } + const token = localStorage.getItem('auth_token'); // Skip validation for local dev token if (token === 'local-dev-token') { diff --git a/frontend/src/pages/MainApp.tsx b/frontend/src/pages/MainApp.tsx index c14c276..cf88660 100644 --- a/frontend/src/pages/MainApp.tsx +++ b/frontend/src/pages/MainApp.tsx @@ -41,6 +41,7 @@ import type { IndexHealth } from '@/types/search'; import type { Note, NoteSummary } from '@/types/note'; import { normalizeSlug } from '@/lib/wikilink'; import { Network } from 'lucide-react'; +import { AUTH_TOKEN_CHANGED_EVENT, isDemoSession, login } from '@/services/auth'; export function MainApp() { const navigate = useNavigate(); @@ -61,6 +62,21 @@ export function MainApp() { const [isNewFolderDialogOpen, setIsNewFolderDialogOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [isDemoMode, setIsDemoMode] = useState(isDemoSession()); + + useEffect(() => { + const handleAuthChange = () => { + const demo = isDemoSession(); + setIsDemoMode(demo); + if (demo) { + setIsEditMode(false); + } + }; + window.addEventListener(AUTH_TOKEN_CHANGED_EVENT, handleAuthChange); + return () => { + window.removeEventListener(AUTH_TOKEN_CHANGED_EVENT, handleAuthChange); + }; + }, []); // T083: Load directory tree on mount // T119: Load index health @@ -169,6 +185,10 @@ export function MainApp() { // T093: Handle edit button click const handleEdit = () => { + if (isDemoMode) { + toast.error('Demo mode is read-only. Sign in with Hugging Face to edit notes.'); + return; + } setIsEditMode(true); }; @@ -188,6 +208,10 @@ export function MainApp() { // Handle note dialog open change const handleDialogOpenChange = (open: boolean) => { + if (open && isDemoMode) { + toast.error('Demo mode is read-only. Sign in with Hugging Face to create notes.'); + return; + } setIsNewNoteDialogOpen(open); if (!open) { // Clear input when dialog closes @@ -197,6 +221,10 @@ export function MainApp() { // Handle folder dialog open change const handleFolderDialogOpenChange = (open: boolean) => { + if (open && isDemoMode) { + toast.error('Demo mode is read-only. Sign in with Hugging Face to create folders.'); + return; + } setIsNewFolderDialogOpen(open); if (!open) { // Clear input when dialog closes @@ -206,6 +234,10 @@ export function MainApp() { // Handle create new note const handleCreateNote = async () => { + if (isDemoMode) { + toast.error('Demo mode is read-only. Sign in to create notes.'); + return; + } if (!newNoteName.trim() || isCreatingNote) return; setIsCreatingNote(true); @@ -270,6 +302,10 @@ export function MainApp() { // Handle create new folder const handleCreateFolder = async () => { + if (isDemoMode) { + toast.error('Demo mode is read-only. Sign in to create folders.'); + return; + } if (!newFolderName.trim() || isCreatingFolder) return; setIsCreatingFolder(true); @@ -309,6 +345,10 @@ export function MainApp() { // Handle dragging file to folder const handleMoveNoteToFolder = async (oldPath: string, targetFolderPath: string) => { + if (isDemoMode) { + toast.error('Demo mode is read-only. Sign in to move notes.'); + return; + } try { // Get the filename from the old path const fileName = oldPath.split('/').pop(); @@ -360,9 +400,19 @@ export function MainApp() { {/* Top bar */}
-
+

πŸ“š Document Viewer

+ {isDemoMode && ( + + )} +
+ )} {/* Left sidebar */} @@ -390,7 +450,7 @@ export function MainApp() { onOpenChange={handleDialogOpenChange} > - @@ -440,7 +500,7 @@ export function MainApp() { onOpenChange={handleFolderDialogOpenChange} > - @@ -536,7 +596,7 @@ export function MainApp() { ) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index dfc46be..4825326 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -11,7 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; import { SettingsSectionSkeleton } from '@/components/SettingsSectionSkeleton'; -import { getCurrentUser, getToken, logout, getStoredToken } from '@/services/auth'; +import { getCurrentUser, getToken, logout, getStoredToken, isDemoSession, AUTH_TOKEN_CHANGED_EVENT } from '@/services/auth'; import { getIndexHealth, rebuildIndex, type RebuildResponse } from '@/services/api'; import type { User } from '@/types/user'; import type { IndexHealth } from '@/types/search'; @@ -25,11 +25,18 @@ export function Settings() { const [isRebuilding, setIsRebuilding] = useState(false); const [rebuildResult, setRebuildResult] = useState(null); const [error, setError] = useState(null); + const [isDemoMode, setIsDemoMode] = useState(isDemoSession()); useEffect(() => { loadData(); }, []); + useEffect(() => { + const handler = () => setIsDemoMode(isDemoSession()); + window.addEventListener(AUTH_TOKEN_CHANGED_EVENT, handler); + return () => window.removeEventListener(AUTH_TOKEN_CHANGED_EVENT, handler); + }, []); + const loadData = async () => { try { const token = getStoredToken(); @@ -60,6 +67,10 @@ export function Settings() { }; const handleGenerateToken = async () => { + if (isDemoMode) { + setError('Demo mode is read-only. Sign in to generate new tokens.'); + return; + } try { setError(null); const tokenResponse = await getToken(); @@ -81,6 +92,10 @@ export function Settings() { }; const handleRebuildIndex = async () => { + if (isDemoMode) { + setError('Demo mode is read-only. Sign in to rebuild the index.'); + return; + } setIsRebuilding(true); setError(null); setRebuildResult(null); @@ -125,6 +140,13 @@ export function Settings() { {/* Content */}
+ {isDemoMode && ( + + + You are viewing the shared demo vault. Sign in with Hugging Face from the main app to enable token generation and index management. + + + )} {error && ( {error} @@ -203,7 +225,7 @@ export function Settings() {
- @@ -285,7 +307,7 @@ export function Settings() { + + + + + ); +} diff --git a/frontend/src/pages/MainApp.tsx b/frontend/src/pages/MainApp.tsx index 6bec96a..8146534 100644 --- a/frontend/src/pages/MainApp.tsx +++ b/frontend/src/pages/MainApp.tsx @@ -23,6 +23,7 @@ import { getIndexHealth, createNote, moveNote, + deleteNote, type BacklinkResult, APIException, } from '@/services/api'; @@ -37,6 +38,7 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; +import { DeleteConfirmationDialog } from '@/components/DeleteConfirmationDialog'; import type { IndexHealth } from '@/types/search'; import type { Note, NoteSummary } from '@/types/note'; import { normalizeSlug } from '@/lib/wikilink'; @@ -59,6 +61,8 @@ export function MainApp() { const [isNewFolderDialogOpen, setIsNewFolderDialogOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); // T083: Load directory tree on mount // T119: Load index health @@ -379,6 +383,49 @@ export function MainApp() { } }; + // Handle delete note + const handleDeleteNote = async () => { + if (!currentNote) return; + + setIsDeleting(true); + try { + await deleteNote(currentNote.note_path); + + // Refresh notes list + const notesList = await listNotes(); + setNotes(notesList); + + // Clear current note selection + setSelectedPath(null); + setCurrentNote(null); + setIsDeleteDialogOpen(false); + + // Select first note if available + if (notesList.length > 0) { + setSelectedPath(notesList[0].note_path); + } + + const displayName = currentNote.note_path.replace(/\.md$/, '').split('/').pop() || currentNote.note_path; + toast.success(`Note "${displayName}" deleted successfully`); + } catch (err) { + let errorMessage = 'Failed to delete note'; + if (err instanceof APIException) { + // Use message first, then error, then fallback + errorMessage = err.message || err.error || `Error ${err.status}`; + // Handle case where message might be undefined + if (!errorMessage || errorMessage === 'undefined') { + errorMessage = `Failed to delete note (HTTP ${err.status})`; + } + } else if (err instanceof Error) { + errorMessage = err.message || 'Unknown error occurred'; + } + toast.error(errorMessage); + console.error('Error deleting note:', err); + } finally { + setIsDeleting(false); + } + }; + return (
{/* Top bar */} @@ -544,6 +591,7 @@ export function MainApp() { note={currentNote} backlinks={backlinks} onEdit={handleEdit} + onDelete={() => setIsDeleteDialogOpen(true)} onWikilinkClick={handleWikilinkClick} /> ) @@ -593,6 +641,15 @@ export function MainApp() { )}
+ + {/* Delete Confirmation Dialog */} + setIsDeleteDialogOpen(false)} + />
); } diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 13b6078..7e323b4 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -63,7 +63,19 @@ async function apiFetch( if (!response.ok) { let errorData: APIError; try { - errorData = await response.json(); + const jsonData = await response.json(); + // Handle both standard APIError format and FastAPI HTTPException format + if ('detail' in jsonData && typeof jsonData.detail === 'string') { + // FastAPI HTTPException with detail as string + errorData = { + error: jsonData.error || 'Error', + message: jsonData.detail, + detail: jsonData.detail as any, + }; + } else { + // Standard APIError format + errorData = jsonData as APIError; + } } catch { errorData = { error: 'Unknown error', @@ -72,7 +84,7 @@ async function apiFetch( } throw new APIException( response.status, - errorData.message, + errorData.message || errorData.error, errorData.detail ); } @@ -195,3 +207,13 @@ export async function moveNote(oldPath: string, newPath: string): Promise }); } +/** + * Delete a note + */ +export async function deleteNote(path: string): Promise { + const encodedPath = encodeURIComponent(path); + return apiFetch(`/api/notes/${encodedPath}`, { + method: 'DELETE', + }); +} + From 6e73d0a4857cb2dd63c3f0e2adeb229b12c0e5dc Mon Sep 17 00:00:00 2001 From: Abel P Date: Wed, 26 Nov 2025 22:25:41 -0500 Subject: [PATCH 9/9] Install react-force-graph-2d dependency for GraphView component --- frontend/package-lock.json | 67 ++++++++++---------------------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4de582e..346d572 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -122,7 +122,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -714,7 +713,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1740,7 +1738,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3341,7 +3338,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3351,7 +3347,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3362,7 +3357,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3426,7 +3420,6 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -3708,7 +3701,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3976,24 +3968,28 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -4039,7 +4035,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4739,7 +4734,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5157,7 +5151,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5426,7 +5419,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6215,9 +6207,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6225,6 +6217,10 @@ }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -6548,7 +6544,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8418,7 +8413,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8729,29 +8723,11 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8761,7 +8737,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9852,7 +9827,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -9990,7 +9964,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10170,7 +10143,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10573,7 +10545,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10764,7 +10735,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -10891,7 +10861,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }