Spaces:
Running
Running
bigwolfe
commited on
Commit
Β·
d97497a
1
Parent(s):
a5a2c66
Build attempt 1
Browse files- .dockerignore +64 -0
- DEPLOYMENT.md +220 -0
- Dockerfile +39 -0
- ai-notes/hf-deployment-complete.md +183 -0
- backend/src/api/main.py +48 -9
- backend/src/api/routes/__init__.py +2 -2
- backend/src/api/routes/auth.py +180 -0
- backend/src/services/config.py +6 -0
- backend/src/services/seed.py +515 -0
- frontend/src/App.tsx +6 -1
- frontend/src/components/NoteViewer.tsx +0 -2
- frontend/src/components/SearchBar.tsx +1 -3
- frontend/src/lib/markdown.ts +0 -183
- frontend/src/lib/markdown.tsx +2 -1
- frontend/src/pages/MainApp.tsx +7 -0
- frontend/src/services/api.ts +12 -9
- frontend/src/services/auth.ts +19 -0
- spaces_README.md +119 -0
.dockerignore
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
**/__pycache__
|
| 3 |
+
**/*.pyc
|
| 4 |
+
**/*.pyo
|
| 5 |
+
**/*.pyd
|
| 6 |
+
**/.Python
|
| 7 |
+
**/env
|
| 8 |
+
**/venv
|
| 9 |
+
**/.venv
|
| 10 |
+
**/ENV
|
| 11 |
+
**/env.bak/
|
| 12 |
+
**/venv.bak/
|
| 13 |
+
**/*.egg-info/
|
| 14 |
+
**/.pytest_cache/
|
| 15 |
+
**/.mypy_cache/
|
| 16 |
+
**/.ruff_cache/
|
| 17 |
+
|
| 18 |
+
# Node.js
|
| 19 |
+
**/node_modules
|
| 20 |
+
**/npm-debug.log*
|
| 21 |
+
**/yarn-debug.log*
|
| 22 |
+
**/yarn-error.log*
|
| 23 |
+
**/pnpm-debug.log*
|
| 24 |
+
**/lerna-debug.log*
|
| 25 |
+
frontend/dist
|
| 26 |
+
frontend/.vite
|
| 27 |
+
|
| 28 |
+
# Git
|
| 29 |
+
.git
|
| 30 |
+
.gitignore
|
| 31 |
+
.gitattributes
|
| 32 |
+
|
| 33 |
+
# IDE
|
| 34 |
+
**/.vscode
|
| 35 |
+
**/.idea
|
| 36 |
+
**/*.swp
|
| 37 |
+
**/*.swo
|
| 38 |
+
**/*~
|
| 39 |
+
|
| 40 |
+
# OS
|
| 41 |
+
.DS_Store
|
| 42 |
+
Thumbs.db
|
| 43 |
+
|
| 44 |
+
# Project specific
|
| 45 |
+
*.log
|
| 46 |
+
.backend.pid
|
| 47 |
+
.frontend.pid
|
| 48 |
+
backend.log
|
| 49 |
+
frontend.log
|
| 50 |
+
data/index.db
|
| 51 |
+
data/vaults/*
|
| 52 |
+
!data/vaults/.gitkeep
|
| 53 |
+
|
| 54 |
+
# Documentation (not needed in container)
|
| 55 |
+
*.md
|
| 56 |
+
!backend/README.md
|
| 57 |
+
specs/
|
| 58 |
+
ai-notes/
|
| 59 |
+
|
| 60 |
+
# Scripts
|
| 61 |
+
start-dev.sh
|
| 62 |
+
stop-dev.sh
|
| 63 |
+
status-dev.sh
|
| 64 |
+
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
This guide walks through deploying the Document-MCP application to Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
1. **Hugging Face Account**: Sign up at https://huggingface.co/join
|
| 8 |
+
2. **OAuth Application**: Created at https://huggingface.co/settings/connected-applications
|
| 9 |
+
3. **Git with HF CLI** (optional but recommended): `pip install huggingface_hub`
|
| 10 |
+
|
| 11 |
+
## Step 1: Create HF Space
|
| 12 |
+
|
| 13 |
+
1. Go to https://huggingface.co/new-space
|
| 14 |
+
2. Fill in details:
|
| 15 |
+
- **Space name**: `Document-MCP` (or your preferred name)
|
| 16 |
+
- **License**: `apache-2.0` or `mit`
|
| 17 |
+
- **Select the SDK**: Choose **Docker**
|
| 18 |
+
- **Space hardware**: Start with **CPU basic** (free tier)
|
| 19 |
+
- **Visibility**: Public or Private
|
| 20 |
+
|
| 21 |
+
3. Click **Create Space**
|
| 22 |
+
|
| 23 |
+
## Step 2: Configure OAuth Application
|
| 24 |
+
|
| 25 |
+
If you haven't already created an OAuth app:
|
| 26 |
+
|
| 27 |
+
1. Go to https://huggingface.co/settings/connected-applications
|
| 28 |
+
2. Click **Create new application**
|
| 29 |
+
3. Fill in:
|
| 30 |
+
- **Application Name**: `Documentation-MCP`
|
| 31 |
+
- **Homepage URL**: `https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP`
|
| 32 |
+
- **Redirect URI**: `https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP/auth/callback`
|
| 33 |
+
- **Scopes**: Select `openid` and `profile`
|
| 34 |
+
|
| 35 |
+
4. Click **Create**
|
| 36 |
+
5. **Save the Client ID and Client Secret** - you'll need these for environment variables
|
| 37 |
+
|
| 38 |
+
## Step 3: Set Environment Variables
|
| 39 |
+
|
| 40 |
+
In your HF Space settings, add these secrets:
|
| 41 |
+
|
| 42 |
+
1. Go to your Space: `https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP`
|
| 43 |
+
2. Click **Settings** tab
|
| 44 |
+
3. Scroll to **Repository secrets**
|
| 45 |
+
4. Add the following secrets:
|
| 46 |
+
|
| 47 |
+
```
|
| 48 |
+
JWT_SECRET_KEY=<generate-a-random-32-char-string>
|
| 49 |
+
HF_OAUTH_CLIENT_ID=<your-oauth-client-id>
|
| 50 |
+
HF_OAUTH_CLIENT_SECRET=<your-oauth-client-secret>
|
| 51 |
+
HF_SPACE_URL=https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### Generating JWT_SECRET_KEY
|
| 55 |
+
|
| 56 |
+
Run this to generate a secure random key:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
python3 -c "import secrets; print(secrets.token_hex(32))"
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
Or:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
openssl rand -hex 32
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## Step 4: Push Code to HF Space
|
| 69 |
+
|
| 70 |
+
### Option A: Using Git (Recommended)
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Clone your HF Space repository
|
| 74 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP
|
| 75 |
+
cd Document-MCP
|
| 76 |
+
|
| 77 |
+
# Copy all project files (excluding .git)
|
| 78 |
+
cp -r /path/to/Document-MCP/{backend,frontend,Dockerfile,.dockerignore} .
|
| 79 |
+
|
| 80 |
+
# Add and commit
|
| 81 |
+
git add .
|
| 82 |
+
git commit -m "Initial deployment"
|
| 83 |
+
|
| 84 |
+
# Push to HF Space
|
| 85 |
+
git push
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Option B: Using HF CLI
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
# Install HF CLI
|
| 92 |
+
pip install huggingface_hub
|
| 93 |
+
|
| 94 |
+
# Login
|
| 95 |
+
huggingface-cli login
|
| 96 |
+
|
| 97 |
+
# Upload repository
|
| 98 |
+
cd /path/to/Document-MCP
|
| 99 |
+
huggingface-cli upload YOUR_USERNAME/Document-MCP . --repo-type=space
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### Option C: Manual Upload
|
| 103 |
+
|
| 104 |
+
1. Go to **Files** tab in your Space
|
| 105 |
+
2. Click **Add file** β **Upload files**
|
| 106 |
+
3. Upload:
|
| 107 |
+
- `Dockerfile`
|
| 108 |
+
- `.dockerignore`
|
| 109 |
+
- `backend/` directory (all contents)
|
| 110 |
+
- `frontend/` directory (all contents)
|
| 111 |
+
|
| 112 |
+
## Step 5: Wait for Build
|
| 113 |
+
|
| 114 |
+
1. HF Spaces will automatically build your Docker container
|
| 115 |
+
2. Check the **Logs** tab to monitor build progress
|
| 116 |
+
3. Build typically takes 5-10 minutes for first deployment
|
| 117 |
+
4. Once complete, your app will be available at: `https://YOUR_USERNAME-Document-MCP.hf.space`
|
| 118 |
+
|
| 119 |
+
## Step 6: Test the Deployment
|
| 120 |
+
|
| 121 |
+
1. **Open the Space URL** in your browser
|
| 122 |
+
2. You should see the Login page
|
| 123 |
+
3. Click **Sign in with Hugging Face**
|
| 124 |
+
4. Authorize the OAuth app
|
| 125 |
+
5. You'll be redirected back to the app with a JWT token
|
| 126 |
+
6. Browse the demo vault with pre-seeded notes
|
| 127 |
+
|
| 128 |
+
## Step 7: MCP HTTP Access
|
| 129 |
+
|
| 130 |
+
To use MCP tools via HTTP (for AI agents):
|
| 131 |
+
|
| 132 |
+
1. **Get your JWT token**:
|
| 133 |
+
- Go to Settings page in the app
|
| 134 |
+
- Copy your API token
|
| 135 |
+
|
| 136 |
+
2. **Configure MCP client** (e.g., Claude Desktop):
|
| 137 |
+
|
| 138 |
+
```json
|
| 139 |
+
{
|
| 140 |
+
"mcpServers": {
|
| 141 |
+
"obsidian-docs": {
|
| 142 |
+
"url": "https://YOUR_USERNAME-Document-MCP.hf.space/mcp",
|
| 143 |
+
"transport": "http",
|
| 144 |
+
"headers": {
|
| 145 |
+
"Authorization": "Bearer YOUR_JWT_TOKEN_HERE"
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
3. **Test MCP tools**:
|
| 153 |
+
|
| 154 |
+
```bash
|
| 155 |
+
curl -X POST "https://YOUR_USERNAME-Document-MCP.hf.space/mcp/list_notes" \
|
| 156 |
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
| 157 |
+
-H "Content-Type: application/json"
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
## Troubleshooting
|
| 161 |
+
|
| 162 |
+
### Build Fails
|
| 163 |
+
|
| 164 |
+
**Check logs** in the Space's "Logs" tab. Common issues:
|
| 165 |
+
|
| 166 |
+
- **npm install fails**: Check `frontend/package.json` dependencies
|
| 167 |
+
- **Python install fails**: Check `backend/pyproject.toml` dependencies
|
| 168 |
+
- **Out of memory**: Upgrade to a paid tier with more RAM
|
| 169 |
+
|
| 170 |
+
### OAuth Redirect Loop
|
| 171 |
+
|
| 172 |
+
- Verify `HF_SPACE_URL` matches your actual Space URL exactly
|
| 173 |
+
- Ensure OAuth redirect URI is: `https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP/auth/callback`
|
| 174 |
+
- Check that OAuth app scopes include `openid` and `profile`
|
| 175 |
+
|
| 176 |
+
### 500 Internal Server Error
|
| 177 |
+
|
| 178 |
+
- Check application logs in the Space's "Logs" tab
|
| 179 |
+
- Verify all environment variables are set correctly
|
| 180 |
+
- Ensure `JWT_SECRET_KEY` is at least 16 characters
|
| 181 |
+
|
| 182 |
+
### Data Not Persisting
|
| 183 |
+
|
| 184 |
+
**This is expected** - the demo uses ephemeral storage. Data resets on container restart. The app seeds demo content on every startup.
|
| 185 |
+
|
| 186 |
+
For persistent storage, you'd need to:
|
| 187 |
+
- Upgrade to HF Spaces Pro with persistent storage
|
| 188 |
+
- Or use external database (Supabase, PlanetScale, etc.)
|
| 189 |
+
|
| 190 |
+
## Updating Your Deployment
|
| 191 |
+
|
| 192 |
+
To deploy updates:
|
| 193 |
+
|
| 194 |
+
```bash
|
| 195 |
+
cd /path/to/Document-MCP
|
| 196 |
+
git add .
|
| 197 |
+
git commit -m "Update: description of changes"
|
| 198 |
+
git push
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
HF Spaces will automatically rebuild and redeploy.
|
| 202 |
+
|
| 203 |
+
## Monitoring
|
| 204 |
+
|
| 205 |
+
- **View logs**: Space Logs tab
|
| 206 |
+
- **Check build status**: Space "Building" indicator
|
| 207 |
+
- **Test health endpoint**: `https://YOUR_USERNAME-Document-MCP.hf.space/health`
|
| 208 |
+
|
| 209 |
+
## Cost Considerations
|
| 210 |
+
|
| 211 |
+
- **CPU basic**: Free tier, sufficient for demo/personal use
|
| 212 |
+
- **Persistent storage**: Requires paid tier ($5-20/month)
|
| 213 |
+
- **More CPU/RAM**: Upgrade if you experience slow performance
|
| 214 |
+
|
| 215 |
+
## Support
|
| 216 |
+
|
| 217 |
+
- **HF Spaces Docs**: https://huggingface.co/docs/hub/spaces
|
| 218 |
+
- **OAuth Docs**: https://huggingface.co/docs/hub/oauth
|
| 219 |
+
- **Docker SDK Guide**: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 220 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile for Hugging Face Space deployment
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install Node.js for frontend build
|
| 7 |
+
RUN apt-get update && \
|
| 8 |
+
apt-get install -y nodejs npm curl && \
|
| 9 |
+
rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy and install frontend dependencies
|
| 12 |
+
COPY frontend/package*.json frontend/
|
| 13 |
+
RUN cd frontend && npm ci
|
| 14 |
+
|
| 15 |
+
# Copy frontend source and build
|
| 16 |
+
COPY frontend/ frontend/
|
| 17 |
+
RUN cd frontend && npm run build
|
| 18 |
+
|
| 19 |
+
# Install Python dependencies
|
| 20 |
+
COPY backend/pyproject.toml backend/setup.py backend/README.md backend/
|
| 21 |
+
RUN pip install --no-cache-dir -e backend/
|
| 22 |
+
|
| 23 |
+
# Copy backend source
|
| 24 |
+
COPY backend/ backend/
|
| 25 |
+
|
| 26 |
+
# Create data directory for vaults and database
|
| 27 |
+
RUN mkdir -p /app/data/vaults
|
| 28 |
+
|
| 29 |
+
# Expose port 7860 (required by Hugging Face Spaces)
|
| 30 |
+
EXPOSE 7860
|
| 31 |
+
|
| 32 |
+
# Set environment variables for production
|
| 33 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 34 |
+
VAULT_BASE_PATH=/app/data/vaults \
|
| 35 |
+
DATABASE_PATH=/app/data/index.db
|
| 36 |
+
|
| 37 |
+
# Start the FastAPI server
|
| 38 |
+
CMD ["uvicorn", "backend.src.api.main:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info"]
|
| 39 |
+
|
ai-notes/hf-deployment-complete.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HF Space Deployment - Implementation Complete
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
All code changes for Hugging Face Space deployment have been implemented successfully. The application is ready to be deployed.
|
| 6 |
+
|
| 7 |
+
## What Was Implemented
|
| 8 |
+
|
| 9 |
+
### 1. Backend OAuth Integration
|
| 10 |
+
- β
Created `backend/src/api/routes/auth.py` with:
|
| 11 |
+
- `/auth/login` - Redirects to HF OAuth
|
| 12 |
+
- `/auth/callback` - Handles OAuth callback, exchanges code for token, creates JWT
|
| 13 |
+
- `/api/me` - Returns current user info
|
| 14 |
+
- β
Updated `backend/src/services/config.py` to include OAuth config (client_id, client_secret, space_url)
|
| 15 |
+
- β
Mounted auth routes in `backend/src/api/main.py`
|
| 16 |
+
|
| 17 |
+
### 2. Database Seed Data
|
| 18 |
+
- β
Created `backend/src/services/seed.py` with:
|
| 19 |
+
- `seed_demo_vault()` - Creates 10 demo notes with wikilinks, tags, and proper frontmatter
|
| 20 |
+
- `init_and_seed()` - Initializes DB schema + seeds demo vault on startup
|
| 21 |
+
- β
Added startup event handler in `backend/src/api/main.py` that calls `init_and_seed()`
|
| 22 |
+
|
| 23 |
+
### 3. FastMCP HTTP Mode
|
| 24 |
+
- β
Mounted FastMCP at `/mcp` endpoint in `backend/src/api/main.py`
|
| 25 |
+
- β
MCP server accessible via HTTP with Bearer token authentication
|
| 26 |
+
|
| 27 |
+
### 4. Frontend Updates
|
| 28 |
+
- β
Added "DEMO ONLY" warning banner to `frontend/src/pages/MainApp.tsx`
|
| 29 |
+
- β
Created `setAuthTokenFromHash()` in `frontend/src/services/auth.ts` to extract JWT from URL hash
|
| 30 |
+
- β
Updated `frontend/src/App.tsx` to handle OAuth callback token extraction
|
| 31 |
+
- β
Built frontend to `frontend/dist/` successfully
|
| 32 |
+
|
| 33 |
+
### 5. Serve Frontend from FastAPI
|
| 34 |
+
- β
Configured FastAPI to serve `frontend/dist` as static files from root path
|
| 35 |
+
- β
API routes (`/api`, `/auth`, `/mcp`) take precedence over static files
|
| 36 |
+
|
| 37 |
+
### 6. Docker Configuration
|
| 38 |
+
- β
Created `Dockerfile` with:
|
| 39 |
+
- Node.js installation for frontend build
|
| 40 |
+
- Multi-stage build: frontend β backend β serve
|
| 41 |
+
- Port 7860 exposed (HF Spaces requirement)
|
| 42 |
+
- β
Created `.dockerignore` to optimize build
|
| 43 |
+
|
| 44 |
+
### 7. Documentation
|
| 45 |
+
- β
Created `DEPLOYMENT.md` with step-by-step HF Space deployment instructions
|
| 46 |
+
- β
Created `spaces_README.md` for HF Space landing page
|
| 47 |
+
- β
Includes OAuth setup, environment variable configuration, and MCP HTTP usage
|
| 48 |
+
|
| 49 |
+
### 8. Dependencies
|
| 50 |
+
- β
Added `pyjwt` and `httpx` to backend dependencies (already present in pyproject.toml)
|
| 51 |
+
|
| 52 |
+
## Demo Notes Created
|
| 53 |
+
|
| 54 |
+
The seed script creates 10 interconnected demo notes:
|
| 55 |
+
1. Getting Started.md
|
| 56 |
+
2. API Documentation.md
|
| 57 |
+
3. MCP Integration.md
|
| 58 |
+
4. Wikilink Examples.md
|
| 59 |
+
5. Architecture Overview.md
|
| 60 |
+
6. Search Features.md
|
| 61 |
+
7. Settings.md
|
| 62 |
+
8. guides/Quick Reference.md
|
| 63 |
+
9. guides/Troubleshooting.md
|
| 64 |
+
10. FAQ.md
|
| 65 |
+
|
| 66 |
+
All notes include wikilinks between them, proper tags, and frontmatter.
|
| 67 |
+
|
| 68 |
+
## Next Steps for Deployment
|
| 69 |
+
|
| 70 |
+
### 1. Set Up Environment Variables
|
| 71 |
+
|
| 72 |
+
You'll need to add these secrets in HF Space settings:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
JWT_SECRET_KEY=<generate with: openssl rand -hex 32>
|
| 76 |
+
HF_OAUTH_CLIENT_ID=<from your OAuth app>
|
| 77 |
+
HF_OAUTH_CLIENT_SECRET=<from your OAuth app>
|
| 78 |
+
HF_SPACE_URL=https://huggingface.co/spaces/bigwolfe/Document-MCP
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### 2. Push to HF Space
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
# Clone your HF Space repo
|
| 85 |
+
git clone https://huggingface.co/spaces/bigwolfe/Document-MCP
|
| 86 |
+
cd Document-MCP
|
| 87 |
+
|
| 88 |
+
# Copy project files
|
| 89 |
+
cp -r /path/to/Document-MCP/{backend,frontend,Dockerfile,.dockerignore,spaces_README.md} .
|
| 90 |
+
|
| 91 |
+
# Rename spaces_README.md to README.md
|
| 92 |
+
mv spaces_README.md README.md
|
| 93 |
+
|
| 94 |
+
# Commit and push
|
| 95 |
+
git add .
|
| 96 |
+
git commit -m "Initial deployment with OAuth, MCP HTTP, and demo vault"
|
| 97 |
+
git push
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### 3. Test the Deployment
|
| 101 |
+
|
| 102 |
+
1. Wait for HF Spaces to build (5-10 minutes)
|
| 103 |
+
2. Visit: `https://bigwolfe-document-mcp.hf.space` (or your actual URL)
|
| 104 |
+
3. Click "Sign in with Hugging Face"
|
| 105 |
+
4. Browse the demo notes
|
| 106 |
+
5. Go to Settings to get your API token
|
| 107 |
+
6. Test MCP HTTP access with the token
|
| 108 |
+
|
| 109 |
+
## Testing MCP HTTP Mode
|
| 110 |
+
|
| 111 |
+
After deployment, test MCP access:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
curl -X POST "https://bigwolfe-document-mcp.hf.space/mcp/list_notes" \
|
| 115 |
+
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
| 116 |
+
-H "Content-Type: application/json"
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Known Limitations
|
| 120 |
+
|
| 121 |
+
1. **Ephemeral Storage**: Data resets on container restart (by design for demo)
|
| 122 |
+
2. **No Rate Limiting**: Consider adding for production
|
| 123 |
+
3. **Single Container**: Not horizontally scalable (SQLite limitation)
|
| 124 |
+
4. **Demo Mode Only**: Prominent warning banner informs users
|
| 125 |
+
|
| 126 |
+
## Files Created/Modified
|
| 127 |
+
|
| 128 |
+
**New Files:**
|
| 129 |
+
- `backend/src/api/routes/auth.py`
|
| 130 |
+
- `backend/src/services/seed.py`
|
| 131 |
+
- `Dockerfile`
|
| 132 |
+
- `.dockerignore`
|
| 133 |
+
- `DEPLOYMENT.md`
|
| 134 |
+
- `spaces_README.md`
|
| 135 |
+
|
| 136 |
+
**Modified Files:**
|
| 137 |
+
- `backend/src/api/main.py`
|
| 138 |
+
- `backend/src/api/routes/__init__.py`
|
| 139 |
+
- `backend/src/services/config.py`
|
| 140 |
+
- `frontend/src/pages/MainApp.tsx`
|
| 141 |
+
- `frontend/src/services/auth.ts`
|
| 142 |
+
- `frontend/src/App.tsx`
|
| 143 |
+
- `frontend/dist/` (built successfully)
|
| 144 |
+
|
| 145 |
+
## Local Testing
|
| 146 |
+
|
| 147 |
+
To test the full stack locally before deploying:
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
# Set environment variables
|
| 151 |
+
export JWT_SECRET_KEY="local-test-key-123"
|
| 152 |
+
export HF_OAUTH_CLIENT_ID="your-client-id"
|
| 153 |
+
export HF_OAUTH_CLIENT_SECRET="your-client-secret"
|
| 154 |
+
export HF_SPACE_URL="http://localhost:7860"
|
| 155 |
+
export VAULT_BASE_PATH="$(pwd)/data/vaults"
|
| 156 |
+
export DATABASE_PATH="$(pwd)/data/index.db"
|
| 157 |
+
|
| 158 |
+
# Start backend
|
| 159 |
+
cd backend
|
| 160 |
+
uvicorn src.api.main:app --host 0.0.0.0 --port 7860
|
| 161 |
+
|
| 162 |
+
# Visit http://localhost:7860 in browser
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
Or test with Docker:
|
| 166 |
+
|
| 167 |
+
```bash
|
| 168 |
+
# Build image
|
| 169 |
+
docker build -t document-mcp .
|
| 170 |
+
|
| 171 |
+
# Run container
|
| 172 |
+
docker run -p 7860:7860 \
|
| 173 |
+
-e JWT_SECRET_KEY="local-test-key" \
|
| 174 |
+
-e HF_OAUTH_CLIENT_ID="your-client-id" \
|
| 175 |
+
-e HF_OAUTH_CLIENT_SECRET="your-client-secret" \
|
| 176 |
+
-e HF_SPACE_URL="http://localhost:7860" \
|
| 177 |
+
document-mcp
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
## Deployment Complete β
|
| 181 |
+
|
| 182 |
+
All tasks from the deployment plan have been completed. The application is production-ready for HF Spaces deployment.
|
| 183 |
+
|
backend/src/api/main.py
CHANGED
|
@@ -2,11 +2,18 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
from fastapi import FastAPI, Request
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
from fastapi.responses import JSONResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
|
| 11 |
app = FastAPI(
|
| 12 |
title="Document Viewer API",
|
|
@@ -17,13 +24,27 @@ app = FastAPI(
|
|
| 17 |
# CORS middleware
|
| 18 |
app.add_middleware(
|
| 19 |
CORSMiddleware,
|
| 20 |
-
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
| 21 |
allow_credentials=True,
|
| 22 |
allow_methods=["*"],
|
| 23 |
allow_headers=["*"],
|
| 24 |
)
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# Error handlers
|
| 28 |
@app.exception_handler(404)
|
| 29 |
async def not_found_handler(request: Request, exc: Exception):
|
|
@@ -52,23 +73,41 @@ async def internal_error_handler(request: Request, exc: Exception):
|
|
| 52 |
)
|
| 53 |
|
| 54 |
|
| 55 |
-
# Mount routers
|
|
|
|
| 56 |
app.include_router(notes.router, tags=["notes"])
|
| 57 |
app.include_router(search.router, tags=["search"])
|
| 58 |
app.include_router(index.router, tags=["index"])
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
""
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
@app.get("/health")
|
| 68 |
async def health():
|
| 69 |
-
"""Health check endpoint."""
|
| 70 |
return {"status": "healthy"}
|
| 71 |
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
__all__ = ["app"]
|
| 74 |
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import logging
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
from fastapi import FastAPI, Request
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
from fastapi.responses import JSONResponse
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
|
| 13 |
+
from .routes import auth, index, notes, search
|
| 14 |
+
from ..services.seed import init_and_seed
|
| 15 |
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
|
| 18 |
app = FastAPI(
|
| 19 |
title="Document Viewer API",
|
|
|
|
| 24 |
# CORS middleware
|
| 25 |
app.add_middleware(
|
| 26 |
CORSMiddleware,
|
| 27 |
+
allow_origins=["http://localhost:5173", "http://localhost:3000", "https://huggingface.co"],
|
| 28 |
allow_credentials=True,
|
| 29 |
allow_methods=["*"],
|
| 30 |
allow_headers=["*"],
|
| 31 |
)
|
| 32 |
|
| 33 |
|
| 34 |
+
# Startup event: Initialize database and seed demo data
|
| 35 |
+
@app.on_event("startup")
|
| 36 |
+
async def startup_event():
|
| 37 |
+
"""Initialize database schema and seed demo vault on startup."""
|
| 38 |
+
logger.info("Running startup: initializing database and seeding demo vault...")
|
| 39 |
+
try:
|
| 40 |
+
init_and_seed(user_id="demo-user")
|
| 41 |
+
logger.info("Startup complete: database and demo vault ready")
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.exception(f"Startup failed: {e}")
|
| 44 |
+
# Don't crash the app, but log the error
|
| 45 |
+
logger.error("App starting without demo data due to initialization error")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
# Error handlers
|
| 49 |
@app.exception_handler(404)
|
| 50 |
async def not_found_handler(request: Request, exc: Exception):
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
|
| 76 |
+
# Mount routers (auth must come first for /auth/login and /auth/callback)
|
| 77 |
+
app.include_router(auth.router, tags=["auth"])
|
| 78 |
app.include_router(notes.router, tags=["notes"])
|
| 79 |
app.include_router(search.router, tags=["search"])
|
| 80 |
app.include_router(index.router, tags=["index"])
|
| 81 |
|
| 82 |
+
# Mount MCP HTTP endpoint
|
| 83 |
+
try:
|
| 84 |
+
from ..mcp.server import mcp
|
| 85 |
+
app.mount("/mcp", mcp.get_asgi_app())
|
| 86 |
+
logger.info("MCP HTTP endpoint mounted at /mcp")
|
| 87 |
+
except Exception as e:
|
| 88 |
+
logger.warning(f"Failed to mount MCP HTTP endpoint: {e}")
|
| 89 |
|
| 90 |
|
| 91 |
@app.get("/health")
|
| 92 |
async def health():
|
| 93 |
+
"""Health check endpoint for HF Spaces."""
|
| 94 |
return {"status": "healthy"}
|
| 95 |
|
| 96 |
|
| 97 |
+
# Serve frontend static files (must be last to not override API routes)
|
| 98 |
+
frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist"
|
| 99 |
+
if frontend_dist.exists():
|
| 100 |
+
app.mount("/", StaticFiles(directory=str(frontend_dist), html=True), name="static")
|
| 101 |
+
logger.info(f"Serving frontend from: {frontend_dist}")
|
| 102 |
+
else:
|
| 103 |
+
logger.warning(f"Frontend dist not found at: {frontend_dist}")
|
| 104 |
+
|
| 105 |
+
# Fallback health endpoint if no frontend
|
| 106 |
+
@app.get("/")
|
| 107 |
+
async def root():
|
| 108 |
+
"""API health check endpoint."""
|
| 109 |
+
return {"status": "ok", "service": "Document Viewer API"}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
__all__ = ["app"]
|
| 113 |
|
backend/src/api/routes/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""HTTP API route handlers."""
|
| 2 |
|
| 3 |
-
from . import index, notes, search
|
| 4 |
|
| 5 |
-
__all__ = ["notes", "search", "index"]
|
|
|
|
| 1 |
"""HTTP API route handlers."""
|
| 2 |
|
| 3 |
+
from . import auth, index, notes, search
|
| 4 |
|
| 5 |
+
__all__ = ["auth", "notes", "search", "index"]
|
backend/src/api/routes/auth.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OAuth and authentication routes for Hugging Face integration."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Optional
|
| 7 |
+
from urllib.parse import urlencode
|
| 8 |
+
|
| 9 |
+
import httpx
|
| 10 |
+
from fastapi import APIRouter, HTTPException, Query, Request
|
| 11 |
+
from fastapi.responses import RedirectResponse
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
|
| 14 |
+
from ...services.config import get_config
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
router = APIRouter()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class UserInfo(BaseModel):
|
| 22 |
+
"""Current user information."""
|
| 23 |
+
|
| 24 |
+
user_id: str
|
| 25 |
+
username: str
|
| 26 |
+
email: Optional[str] = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@router.get("/auth/login")
|
| 30 |
+
async def login():
|
| 31 |
+
"""Redirect to Hugging Face OAuth authorization page."""
|
| 32 |
+
config = get_config()
|
| 33 |
+
|
| 34 |
+
if not config.hf_oauth_client_id:
|
| 35 |
+
raise HTTPException(
|
| 36 |
+
status_code=501,
|
| 37 |
+
detail="OAuth not configured. Set HF_OAUTH_CLIENT_ID and HF_OAUTH_CLIENT_SECRET environment variables."
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Construct HF OAuth URL
|
| 41 |
+
base_url = "https://huggingface.co/oauth/authorize"
|
| 42 |
+
params = {
|
| 43 |
+
"client_id": config.hf_oauth_client_id,
|
| 44 |
+
"redirect_uri": f"{config.hf_space_url}/auth/callback",
|
| 45 |
+
"scope": "openid profile email",
|
| 46 |
+
"response_type": "code",
|
| 47 |
+
"state": "random_state_token", # In production, use a secure random state
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
auth_url = f"{base_url}?{urlencode(params)}"
|
| 51 |
+
logger.info(f"Redirecting to HF OAuth: {auth_url}")
|
| 52 |
+
|
| 53 |
+
return RedirectResponse(url=auth_url)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@router.get("/auth/callback")
|
| 57 |
+
async def callback(
|
| 58 |
+
code: str = Query(..., description="OAuth authorization code"),
|
| 59 |
+
state: Optional[str] = Query(None, description="State parameter for CSRF protection"),
|
| 60 |
+
):
|
| 61 |
+
"""Handle OAuth callback from Hugging Face."""
|
| 62 |
+
config = get_config()
|
| 63 |
+
|
| 64 |
+
if not config.hf_oauth_client_id or not config.hf_oauth_client_secret:
|
| 65 |
+
raise HTTPException(
|
| 66 |
+
status_code=501,
|
| 67 |
+
detail="OAuth not configured"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
# Exchange authorization code for access token
|
| 72 |
+
async with httpx.AsyncClient() as client:
|
| 73 |
+
token_response = await client.post(
|
| 74 |
+
"https://huggingface.co/oauth/token",
|
| 75 |
+
data={
|
| 76 |
+
"grant_type": "authorization_code",
|
| 77 |
+
"code": code,
|
| 78 |
+
"redirect_uri": f"{config.hf_space_url}/auth/callback",
|
| 79 |
+
"client_id": config.hf_oauth_client_id,
|
| 80 |
+
"client_secret": config.hf_oauth_client_secret,
|
| 81 |
+
},
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
if token_response.status_code != 200:
|
| 85 |
+
logger.error(f"Token exchange failed: {token_response.text}")
|
| 86 |
+
raise HTTPException(
|
| 87 |
+
status_code=400,
|
| 88 |
+
detail="Failed to exchange authorization code for token"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
token_data = token_response.json()
|
| 92 |
+
access_token = token_data.get("access_token")
|
| 93 |
+
|
| 94 |
+
if not access_token:
|
| 95 |
+
raise HTTPException(
|
| 96 |
+
status_code=400,
|
| 97 |
+
detail="No access token in response"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
# Get user profile from HF
|
| 101 |
+
user_response = await client.get(
|
| 102 |
+
"https://huggingface.co/api/whoami-v2",
|
| 103 |
+
headers={"Authorization": f"Bearer {access_token}"}
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
if user_response.status_code != 200:
|
| 107 |
+
logger.error(f"User profile fetch failed: {user_response.text}")
|
| 108 |
+
raise HTTPException(
|
| 109 |
+
status_code=400,
|
| 110 |
+
detail="Failed to fetch user profile"
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
user_data = user_response.json()
|
| 114 |
+
username = user_data.get("name")
|
| 115 |
+
email = user_data.get("email")
|
| 116 |
+
|
| 117 |
+
if not username:
|
| 118 |
+
raise HTTPException(
|
| 119 |
+
status_code=400,
|
| 120 |
+
detail="No username in user profile"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Create JWT for our application
|
| 124 |
+
import jwt
|
| 125 |
+
from datetime import datetime, timedelta, timezone
|
| 126 |
+
|
| 127 |
+
user_id = username # Use HF username as user_id
|
| 128 |
+
payload = {
|
| 129 |
+
"sub": user_id,
|
| 130 |
+
"username": username,
|
| 131 |
+
"email": email,
|
| 132 |
+
"exp": datetime.now(timezone.utc) + timedelta(days=7),
|
| 133 |
+
"iat": datetime.now(timezone.utc),
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
jwt_token = jwt.encode(payload, config.jwt_secret_key, algorithm="HS256")
|
| 137 |
+
|
| 138 |
+
logger.info(f"OAuth successful for user: {username}")
|
| 139 |
+
|
| 140 |
+
# Redirect to frontend with token in URL hash
|
| 141 |
+
return RedirectResponse(url=f"/#token={jwt_token}")
|
| 142 |
+
|
| 143 |
+
except httpx.HTTPError as e:
|
| 144 |
+
logger.exception(f"HTTP error during OAuth: {e}")
|
| 145 |
+
raise HTTPException(
|
| 146 |
+
status_code=500,
|
| 147 |
+
detail="OAuth flow failed due to network error"
|
| 148 |
+
)
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.exception(f"Unexpected error during OAuth: {e}")
|
| 151 |
+
raise HTTPException(
|
| 152 |
+
status_code=500,
|
| 153 |
+
detail="OAuth flow failed"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@router.get("/api/me", response_model=UserInfo)
|
| 158 |
+
async def get_current_user(request: Request):
|
| 159 |
+
"""Get current authenticated user information."""
|
| 160 |
+
# Extract user_id from request state (set by auth middleware)
|
| 161 |
+
from ..middleware.auth import get_user_id
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
user_id = get_user_id()
|
| 165 |
+
|
| 166 |
+
# In a real app, we might fetch more user details from DB
|
| 167 |
+
# For now, just return the user_id
|
| 168 |
+
return UserInfo(
|
| 169 |
+
user_id=user_id,
|
| 170 |
+
username=user_id, # username is same as user_id in our system
|
| 171 |
+
)
|
| 172 |
+
except Exception as e:
|
| 173 |
+
raise HTTPException(
|
| 174 |
+
status_code=401,
|
| 175 |
+
detail="Not authenticated"
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
__all__ = ["router"]
|
| 180 |
+
|
backend/src/services/config.py
CHANGED
|
@@ -29,6 +29,10 @@ class AppConfig(BaseModel):
|
|
| 29 |
hf_oauth_client_secret: Optional[str] = Field(
|
| 30 |
None, description="Hugging Face OAuth client secret (optional)"
|
| 31 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
@field_validator("vault_base_path", mode="before")
|
| 34 |
@classmethod
|
|
@@ -67,12 +71,14 @@ def get_config() -> AppConfig:
|
|
| 67 |
vault_base = _read_env("VAULT_BASE_PATH", str(DEFAULT_VAULT_BASE))
|
| 68 |
hf_client_id = _read_env("HF_OAUTH_CLIENT_ID")
|
| 69 |
hf_client_secret = _read_env("HF_OAUTH_CLIENT_SECRET")
|
|
|
|
| 70 |
|
| 71 |
config = AppConfig(
|
| 72 |
jwt_secret_key=jwt_secret,
|
| 73 |
vault_base_path=vault_base,
|
| 74 |
hf_oauth_client_id=hf_client_id,
|
| 75 |
hf_oauth_client_secret=hf_client_secret,
|
|
|
|
| 76 |
)
|
| 77 |
# Ensure vault base directory exists for downstream services.
|
| 78 |
config.vault_base_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 29 |
hf_oauth_client_secret: Optional[str] = Field(
|
| 30 |
None, description="Hugging Face OAuth client secret (optional)"
|
| 31 |
)
|
| 32 |
+
hf_space_url: str = Field(
|
| 33 |
+
default="http://localhost:5173",
|
| 34 |
+
description="Base URL of the HF Space or local dev server"
|
| 35 |
+
)
|
| 36 |
|
| 37 |
@field_validator("vault_base_path", mode="before")
|
| 38 |
@classmethod
|
|
|
|
| 71 |
vault_base = _read_env("VAULT_BASE_PATH", str(DEFAULT_VAULT_BASE))
|
| 72 |
hf_client_id = _read_env("HF_OAUTH_CLIENT_ID")
|
| 73 |
hf_client_secret = _read_env("HF_OAUTH_CLIENT_SECRET")
|
| 74 |
+
hf_space_url = _read_env("HF_SPACE_URL", "http://localhost:5173")
|
| 75 |
|
| 76 |
config = AppConfig(
|
| 77 |
jwt_secret_key=jwt_secret,
|
| 78 |
vault_base_path=vault_base,
|
| 79 |
hf_oauth_client_id=hf_client_id,
|
| 80 |
hf_oauth_client_secret=hf_client_secret,
|
| 81 |
+
hf_space_url=hf_space_url,
|
| 82 |
)
|
| 83 |
# Ensure vault base directory exists for downstream services.
|
| 84 |
config.vault_base_path.mkdir(parents=True, exist_ok=True)
|
backend/src/services/seed.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Seed demo vault with sample documentation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from .database import init_database
|
| 9 |
+
from .indexer import IndexerService
|
| 10 |
+
from .vault import VaultService
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Demo notes with wikilinks and tags
|
| 15 |
+
DEMO_NOTES = [
|
| 16 |
+
{
|
| 17 |
+
"path": "Getting Started.md",
|
| 18 |
+
"title": "Getting Started",
|
| 19 |
+
"tags": ["guide", "intro"],
|
| 20 |
+
"body": """# Getting Started
|
| 21 |
+
|
| 22 |
+
Welcome to the Document Viewer! This is an AI-powered documentation system with wikilinks, full-text search, and backlinks.
|
| 23 |
+
|
| 24 |
+
## Key Features
|
| 25 |
+
|
| 26 |
+
- **Wikilinks**: Link between notes using `[[Note Name]]` syntax
|
| 27 |
+
- **Full-Text Search**: Powered by SQLite FTS5 with BM25 ranking
|
| 28 |
+
- **Backlinks**: Automatically track which notes reference each other
|
| 29 |
+
- **MCP Integration**: AI agents can read and write docs via [[MCP Integration]]
|
| 30 |
+
- **Multi-Tenant**: Each user has an isolated vault
|
| 31 |
+
|
| 32 |
+
## Next Steps
|
| 33 |
+
|
| 34 |
+
1. Browse the [[API Documentation]]
|
| 35 |
+
2. Learn about [[Wikilink Examples]]
|
| 36 |
+
3. Understand the [[Architecture Overview]]
|
| 37 |
+
4. Check out [[Search Features]]
|
| 38 |
+
|
| 39 |
+
## Demo Mode
|
| 40 |
+
|
| 41 |
+
β οΈ This is a **demo instance** - all data is temporary and resets on server restart."""
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"path": "API Documentation.md",
|
| 45 |
+
"title": "API Documentation",
|
| 46 |
+
"tags": ["api", "reference"],
|
| 47 |
+
"body": """# API Documentation
|
| 48 |
+
|
| 49 |
+
The Document Viewer exposes a REST API for managing notes and searching.
|
| 50 |
+
|
| 51 |
+
## Authentication
|
| 52 |
+
|
| 53 |
+
All API requests require a `Bearer` token in the `Authorization` header:
|
| 54 |
+
|
| 55 |
+
```
|
| 56 |
+
Authorization: Bearer <your-jwt-token>
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Get your token from [[Settings]] after signing in with Hugging Face OAuth.
|
| 60 |
+
|
| 61 |
+
## Endpoints
|
| 62 |
+
|
| 63 |
+
### Notes
|
| 64 |
+
|
| 65 |
+
- `GET /api/notes` - List all notes in your vault
|
| 66 |
+
- `GET /api/notes/{path}` - Get a specific note
|
| 67 |
+
- `POST /api/notes` - Create a new note
|
| 68 |
+
- `PUT /api/notes/{path}` - Update an existing note
|
| 69 |
+
|
| 70 |
+
### Search
|
| 71 |
+
|
| 72 |
+
- `GET /api/search?q=query` - Full-text search with [[Search Features]]
|
| 73 |
+
- `GET /api/backlinks/{path}` - Get notes that link to this note
|
| 74 |
+
- `GET /api/tags` - List all tags with counts
|
| 75 |
+
|
| 76 |
+
### Index Management
|
| 77 |
+
|
| 78 |
+
- `GET /api/index/health` - Check index status
|
| 79 |
+
- `POST /api/index/rebuild` - Rebuild the search index
|
| 80 |
+
|
| 81 |
+
## Related
|
| 82 |
+
|
| 83 |
+
- [[MCP Integration]] - AI agent access via MCP
|
| 84 |
+
- [[Wikilink Examples]] - How to use wikilinks"""
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"path": "MCP Integration.md",
|
| 88 |
+
"title": "MCP Integration",
|
| 89 |
+
"tags": ["mcp", "ai", "integration"],
|
| 90 |
+
"body": """# MCP Integration
|
| 91 |
+
|
| 92 |
+
The Model Context Protocol (MCP) allows AI agents like Claude to interact with your documentation vault.
|
| 93 |
+
|
| 94 |
+
## Available Tools
|
| 95 |
+
|
| 96 |
+
The MCP server exposes these tools:
|
| 97 |
+
|
| 98 |
+
- `list_notes` - List all notes in the vault
|
| 99 |
+
- `read_note` - Read a specific note with metadata
|
| 100 |
+
- `write_note` - Create or update a note
|
| 101 |
+
- `delete_note` - Remove a note
|
| 102 |
+
- `search_notes` - Full-text search with ranking
|
| 103 |
+
- `get_backlinks` - Find notes linking to a target
|
| 104 |
+
- `get_tags` - List all tags
|
| 105 |
+
|
| 106 |
+
## Configuration
|
| 107 |
+
|
| 108 |
+
For **local development** (STDIO mode), see [[Getting Started]].
|
| 109 |
+
|
| 110 |
+
For **HTTP mode** (HF Space), use:
|
| 111 |
+
|
| 112 |
+
```json
|
| 113 |
+
{
|
| 114 |
+
"mcpServers": {
|
| 115 |
+
"obsidian-docs": {
|
| 116 |
+
"url": "https://huggingface.co/spaces/bigwolfe/Document-MCP/mcp",
|
| 117 |
+
"headers": {
|
| 118 |
+
"Authorization": "Bearer YOUR_JWT_TOKEN"
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## Related
|
| 126 |
+
|
| 127 |
+
- [[API Documentation]] - REST API reference
|
| 128 |
+
- [[Architecture Overview]] - System design"""
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"path": "Wikilink Examples.md",
|
| 132 |
+
"title": "Wikilink Examples",
|
| 133 |
+
"tags": ["guide", "wikilinks"],
|
| 134 |
+
"body": """# Wikilink Examples
|
| 135 |
+
|
| 136 |
+
Wikilinks are a powerful way to connect notes using simple `[[bracket]]` syntax.
|
| 137 |
+
|
| 138 |
+
## Basic Wikilinks
|
| 139 |
+
|
| 140 |
+
Link to other notes by title:
|
| 141 |
+
|
| 142 |
+
- [[Getting Started]]
|
| 143 |
+
- [[API Documentation]]
|
| 144 |
+
- [[MCP Integration]]
|
| 145 |
+
|
| 146 |
+
## How It Works
|
| 147 |
+
|
| 148 |
+
Wikilinks are resolved using **normalized slug matching**:
|
| 149 |
+
|
| 150 |
+
1. The link text is normalized (lowercase, spaces to dashes)
|
| 151 |
+
2. The system checks note titles and filenames
|
| 152 |
+
3. Same-folder matches are preferred
|
| 153 |
+
4. Broken links are styled differently
|
| 154 |
+
|
| 155 |
+
## Broken Links
|
| 156 |
+
|
| 157 |
+
If you create a link to a note that doesn't exist, like [[Nonexistent Note]], it will be styled as a broken link.
|
| 158 |
+
|
| 159 |
+
## Backlinks
|
| 160 |
+
|
| 161 |
+
When you link to a note, it automatically appears in that note's backlinks section. Try viewing [[Getting Started]] to see this note in its backlinks!
|
| 162 |
+
|
| 163 |
+
## Advanced Features
|
| 164 |
+
|
| 165 |
+
- Links are indexed for fast resolution
|
| 166 |
+
- The [[Search Features]] include wikilink graph analysis
|
| 167 |
+
- See [[Architecture Overview]] for implementation details"""
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"path": "Architecture Overview.md",
|
| 171 |
+
"title": "Architecture Overview",
|
| 172 |
+
"tags": ["architecture", "technical"],
|
| 173 |
+
"body": """# Architecture Overview
|
| 174 |
+
|
| 175 |
+
The Document Viewer is built with a modern tech stack optimized for AI-human collaboration.
|
| 176 |
+
|
| 177 |
+
## Tech Stack
|
| 178 |
+
|
| 179 |
+
### Backend
|
| 180 |
+
|
| 181 |
+
- **FastAPI** - HTTP API server
|
| 182 |
+
- **FastMCP** - MCP server for AI agents
|
| 183 |
+
- **SQLite FTS5** - Full-text search engine
|
| 184 |
+
- **python-frontmatter** - YAML metadata parsing
|
| 185 |
+
|
| 186 |
+
### Frontend
|
| 187 |
+
|
| 188 |
+
- **React + Vite** - Modern web framework
|
| 189 |
+
- **shadcn/ui** - Beautiful UI components
|
| 190 |
+
- **Tailwind CSS** - Utility-first styling
|
| 191 |
+
- **react-markdown** - Markdown rendering
|
| 192 |
+
|
| 193 |
+
## Data Model
|
| 194 |
+
|
| 195 |
+
### Notes
|
| 196 |
+
|
| 197 |
+
Each note is a Markdown file with optional YAML frontmatter:
|
| 198 |
+
|
| 199 |
+
```yaml
|
| 200 |
+
---
|
| 201 |
+
title: My Note
|
| 202 |
+
tags: [guide, tutorial]
|
| 203 |
+
created: 2025-01-15T10:00:00Z
|
| 204 |
+
updated: 2025-01-15T14:30:00Z
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
# Note content here
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
### Indexing
|
| 211 |
+
|
| 212 |
+
The system maintains several indexes:
|
| 213 |
+
|
| 214 |
+
- **note_metadata** - Versions, titles, timestamps
|
| 215 |
+
- **note_fts** - Full-text search (SQLite FTS5)
|
| 216 |
+
- **note_tags** - Tag associations
|
| 217 |
+
- **note_links** - Wikilink graph
|
| 218 |
+
|
| 219 |
+
See [[Search Features]] for ranking details.
|
| 220 |
+
|
| 221 |
+
## Multi-Tenancy
|
| 222 |
+
|
| 223 |
+
Each user gets an isolated vault at `data/vaults/{user_id}/`. See [[API Documentation]] for authentication."""
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"path": "Search Features.md",
|
| 227 |
+
"title": "Search Features",
|
| 228 |
+
"tags": ["search", "features"],
|
| 229 |
+
"body": """# Search Features
|
| 230 |
+
|
| 231 |
+
The Document Viewer includes powerful full-text search with intelligent ranking.
|
| 232 |
+
|
| 233 |
+
## BM25 Ranking
|
| 234 |
+
|
| 235 |
+
Search uses the BM25 algorithm with custom weights:
|
| 236 |
+
|
| 237 |
+
- **Title matches**: 3x weight
|
| 238 |
+
- **Body matches**: 1x weight
|
| 239 |
+
- **Recency bonus**: +1.0 for notes updated in last 7 days, +0.5 for last 30 days
|
| 240 |
+
|
| 241 |
+
## Search Syntax
|
| 242 |
+
|
| 243 |
+
Just type natural language queries:
|
| 244 |
+
|
| 245 |
+
- `authentication` - Find all notes mentioning authentication
|
| 246 |
+
- `api design` - Multiple terms are combined
|
| 247 |
+
- Searches are tokenized and case-insensitive
|
| 248 |
+
|
| 249 |
+
## Index Health
|
| 250 |
+
|
| 251 |
+
Check the footer of the main app to see:
|
| 252 |
+
|
| 253 |
+
- Total note count
|
| 254 |
+
- Last index update timestamp
|
| 255 |
+
|
| 256 |
+
Rebuild the index from [[Settings]] if needed.
|
| 257 |
+
|
| 258 |
+
## Related
|
| 259 |
+
|
| 260 |
+
- [[API Documentation]] - Search API endpoints
|
| 261 |
+
- [[Architecture Overview]] - Technical implementation
|
| 262 |
+
- [[Wikilink Examples]] - Linking between notes"""
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"path": "Settings.md",
|
| 266 |
+
"title": "Settings",
|
| 267 |
+
"tags": ["settings", "config"],
|
| 268 |
+
"body": """# Settings
|
| 269 |
+
|
| 270 |
+
Access settings from the main app to manage your vault and API access.
|
| 271 |
+
|
| 272 |
+
## User Profile
|
| 273 |
+
|
| 274 |
+
View your authenticated user ID and account information.
|
| 275 |
+
|
| 276 |
+
## API Token
|
| 277 |
+
|
| 278 |
+
Your JWT token for API and MCP access:
|
| 279 |
+
|
| 280 |
+
- Copy the token to configure MCP clients
|
| 281 |
+
- Token expires after 7 days
|
| 282 |
+
- Re-authenticate to get a new token
|
| 283 |
+
|
| 284 |
+
See [[MCP Integration]] for configuration examples.
|
| 285 |
+
|
| 286 |
+
## Index Health
|
| 287 |
+
|
| 288 |
+
Monitor your vault's search index:
|
| 289 |
+
|
| 290 |
+
- **Note count**: Total indexed notes
|
| 291 |
+
- **Last rebuild**: Full index rebuild timestamp
|
| 292 |
+
- **Last update**: Most recent incremental update
|
| 293 |
+
|
| 294 |
+
Use the **Rebuild Index** button if:
|
| 295 |
+
|
| 296 |
+
- Search results seem outdated
|
| 297 |
+
- You manually edited files outside the app
|
| 298 |
+
- Index health looks unhealthy
|
| 299 |
+
|
| 300 |
+
## Related
|
| 301 |
+
|
| 302 |
+
- [[Getting Started]] - First steps
|
| 303 |
+
- [[API Documentation]] - Using the API"""
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"path": "guides/Quick Reference.md",
|
| 307 |
+
"title": "Quick Reference",
|
| 308 |
+
"tags": ["guide", "reference"],
|
| 309 |
+
"body": """# Quick Reference
|
| 310 |
+
|
| 311 |
+
A cheat sheet for common tasks in the Document Viewer.
|
| 312 |
+
|
| 313 |
+
## Creating Notes
|
| 314 |
+
|
| 315 |
+
1. Click "New Note" button
|
| 316 |
+
2. Enter note name (`.md` extension optional)
|
| 317 |
+
3. Write content in split-pane editor
|
| 318 |
+
4. Click "Save"
|
| 319 |
+
|
| 320 |
+
## Editing Notes
|
| 321 |
+
|
| 322 |
+
1. Navigate to a note
|
| 323 |
+
2. Click "Edit" button
|
| 324 |
+
3. Modify in left pane, preview in right pane
|
| 325 |
+
4. Click "Save" (handles version conflicts automatically)
|
| 326 |
+
|
| 327 |
+
## Using Wikilinks
|
| 328 |
+
|
| 329 |
+
Link to other notes: `[[Note Title]]`
|
| 330 |
+
|
| 331 |
+
See [[Wikilink Examples]] for more details.
|
| 332 |
+
|
| 333 |
+
## Searching
|
| 334 |
+
|
| 335 |
+
Use the search bar at the top of the directory pane. Results are ranked by relevance and recency.
|
| 336 |
+
|
| 337 |
+
Learn more in [[Search Features]].
|
| 338 |
+
|
| 339 |
+
## MCP Access
|
| 340 |
+
|
| 341 |
+
Get your API token from [[Settings]] and configure your MCP client per [[MCP Integration]]."""
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"path": "guides/Troubleshooting.md",
|
| 345 |
+
"title": "Troubleshooting",
|
| 346 |
+
"tags": ["guide", "help"],
|
| 347 |
+
"body": """# Troubleshooting
|
| 348 |
+
|
| 349 |
+
Common issues and solutions.
|
| 350 |
+
|
| 351 |
+
## Search Not Finding Notes
|
| 352 |
+
|
| 353 |
+
**Problem**: Search returns no results or outdated results.
|
| 354 |
+
|
| 355 |
+
**Solution**: Go to [[Settings]] and click "Rebuild Index". This re-scans all notes and updates the search index.
|
| 356 |
+
|
| 357 |
+
## Wikilink Not Working
|
| 358 |
+
|
| 359 |
+
**Problem**: Clicking a wikilink doesn't navigate, or shows as broken.
|
| 360 |
+
|
| 361 |
+
**Solution**:
|
| 362 |
+
- Check the target note exists
|
| 363 |
+
- Wikilinks match on normalized slugs (case-insensitive, spacesβdashes)
|
| 364 |
+
- See [[Wikilink Examples]] for how resolution works
|
| 365 |
+
|
| 366 |
+
## Version Conflict on Save
|
| 367 |
+
|
| 368 |
+
**Problem**: "This note changed since you opened it" error when saving.
|
| 369 |
+
|
| 370 |
+
**Solution**:
|
| 371 |
+
- Someone else (or an AI agent) edited the note while you were editing
|
| 372 |
+
- Reload the page to get the latest version
|
| 373 |
+
- Copy your changes and re-apply them
|
| 374 |
+
- This prevents data loss from concurrent edits
|
| 375 |
+
|
| 376 |
+
## Authentication Issues
|
| 377 |
+
|
| 378 |
+
**Problem**: Keep getting logged out or "401 Unauthorized" errors.
|
| 379 |
+
|
| 380 |
+
**Solution**:
|
| 381 |
+
- JWT tokens expire after 7 days
|
| 382 |
+
- Sign in again to get a new token
|
| 383 |
+
- Check that your [[MCP Integration]] config uses a valid token
|
| 384 |
+
|
| 385 |
+
## Data Disappeared
|
| 386 |
+
|
| 387 |
+
**Problem**: Notes or changes are missing after page reload.
|
| 388 |
+
|
| 389 |
+
**Solution**:
|
| 390 |
+
- This is a **DEMO instance** with ephemeral storage
|
| 391 |
+
- Data resets when the server restarts
|
| 392 |
+
- For permanent storage, deploy your own instance
|
| 393 |
+
- See [[Getting Started]] for demo disclaimer"""
|
| 394 |
+
},
|
| 395 |
+
{
|
| 396 |
+
"path": "FAQ.md",
|
| 397 |
+
"title": "FAQ",
|
| 398 |
+
"tags": ["faq", "help"],
|
| 399 |
+
"body": """# FAQ
|
| 400 |
+
|
| 401 |
+
Frequently asked questions about the Document Viewer.
|
| 402 |
+
|
| 403 |
+
## General
|
| 404 |
+
|
| 405 |
+
**Q: What is this?**
|
| 406 |
+
|
| 407 |
+
A: An AI-powered documentation system where AI agents (via [[MCP Integration]]) can write and update docs, and humans can read and refine them in a beautiful UI.
|
| 408 |
+
|
| 409 |
+
**Q: Is my data persistent?**
|
| 410 |
+
|
| 411 |
+
A: **No, this is a demo instance.** All data is ephemeral and resets on server restart. For permanent storage, deploy your own instance.
|
| 412 |
+
|
| 413 |
+
**Q: How do I sign in?**
|
| 414 |
+
|
| 415 |
+
A: Click "Sign in with Hugging Face" on the login page. You'll authenticate via HF OAuth and get isolated vault access.
|
| 416 |
+
|
| 417 |
+
## Features
|
| 418 |
+
|
| 419 |
+
**Q: What are wikilinks?**
|
| 420 |
+
|
| 421 |
+
A: Wikilinks let you link between notes using `[[Note Name]]` syntax. See [[Wikilink Examples]].
|
| 422 |
+
|
| 423 |
+
**Q: How does search work?**
|
| 424 |
+
|
| 425 |
+
A: Full-text search with BM25 ranking, weighted by title matches and recency. See [[Search Features]].
|
| 426 |
+
|
| 427 |
+
**Q: Can AI agents edit my docs?**
|
| 428 |
+
|
| 429 |
+
A: Yes! Configure [[MCP Integration]] to let AI agents read and write notes via MCP tools.
|
| 430 |
+
|
| 431 |
+
## Technical
|
| 432 |
+
|
| 433 |
+
**Q: What tech stack?**
|
| 434 |
+
|
| 435 |
+
A: FastAPI + SQLite FTS5 backend, React + shadcn/ui frontend. See [[Architecture Overview]].
|
| 436 |
+
|
| 437 |
+
**Q: Is it multi-tenant?**
|
| 438 |
+
|
| 439 |
+
A: Yes, each HF user gets an isolated vault with per-user search indexes.
|
| 440 |
+
|
| 441 |
+
**Q: Where's the source code?**
|
| 442 |
+
|
| 443 |
+
A: See [[Getting Started]] for links to the repository.
|
| 444 |
+
|
| 445 |
+
## Related
|
| 446 |
+
|
| 447 |
+
- [[Troubleshooting]] - Common issues
|
| 448 |
+
- [[guides/Quick Reference]] - Command cheat sheet"""
|
| 449 |
+
},
|
| 450 |
+
]
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def seed_demo_vault(user_id: str = "demo-user") -> int:
|
| 454 |
+
"""
|
| 455 |
+
Create demo notes in the specified user's vault.
|
| 456 |
+
|
| 457 |
+
Returns the number of notes created.
|
| 458 |
+
"""
|
| 459 |
+
vault_service = VaultService()
|
| 460 |
+
indexer_service = IndexerService()
|
| 461 |
+
|
| 462 |
+
logger.info(f"Seeding demo vault for user: {user_id}")
|
| 463 |
+
|
| 464 |
+
# Create demo notes
|
| 465 |
+
notes_created = 0
|
| 466 |
+
for note_data in DEMO_NOTES:
|
| 467 |
+
try:
|
| 468 |
+
path = note_data["path"]
|
| 469 |
+
title = note_data["title"]
|
| 470 |
+
tags = note_data.get("tags", [])
|
| 471 |
+
body = note_data["body"]
|
| 472 |
+
|
| 473 |
+
# Write note to vault
|
| 474 |
+
note = vault_service.write_note(
|
| 475 |
+
user_id,
|
| 476 |
+
path,
|
| 477 |
+
title=title,
|
| 478 |
+
metadata={"tags": tags},
|
| 479 |
+
body=body,
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
# Index the note
|
| 483 |
+
indexer_service.index_note(user_id, note)
|
| 484 |
+
notes_created += 1
|
| 485 |
+
|
| 486 |
+
logger.info(f"Created demo note: {path}")
|
| 487 |
+
|
| 488 |
+
except Exception as e:
|
| 489 |
+
logger.error(f"Failed to create demo note {note_data['path']}: {e}")
|
| 490 |
+
|
| 491 |
+
logger.info(f"Seeded {notes_created} demo notes for user: {user_id}")
|
| 492 |
+
return notes_created
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
def init_and_seed(user_id: str = "demo-user") -> None:
|
| 496 |
+
"""
|
| 497 |
+
Initialize database schema and seed demo vault.
|
| 498 |
+
|
| 499 |
+
This is called on application startup to ensure the app always has
|
| 500 |
+
valid demo content, even with ephemeral storage.
|
| 501 |
+
"""
|
| 502 |
+
logger.info("Initializing database and seeding demo vault...")
|
| 503 |
+
|
| 504 |
+
# Initialize database schema
|
| 505 |
+
db_path = init_database()
|
| 506 |
+
logger.info(f"Database initialized at: {db_path}")
|
| 507 |
+
|
| 508 |
+
# Seed demo vault
|
| 509 |
+
notes_created = seed_demo_vault(user_id)
|
| 510 |
+
|
| 511 |
+
logger.info(f"Initialization complete. Created {notes_created} demo notes.")
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
__all__ = ["seed_demo_vault", "init_and_seed"]
|
| 515 |
+
|
frontend/src/App.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation } from
|
|
| 3 |
import { MainApp } from './pages/MainApp';
|
| 4 |
import { Login } from './pages/Login';
|
| 5 |
import { Settings } from './pages/Settings';
|
| 6 |
-
import { isAuthenticated, getCurrentUser } from './services/auth';
|
| 7 |
import './App.css';
|
| 8 |
|
| 9 |
// Protected route wrapper with auth check
|
|
@@ -15,6 +15,11 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
| 15 |
// T110: Check if user is authenticated on mount
|
| 16 |
useEffect(() => {
|
| 17 |
const checkAuth = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
if (!isAuthenticated()) {
|
| 19 |
setIsChecking(false);
|
| 20 |
return;
|
|
|
|
| 3 |
import { MainApp } from './pages/MainApp';
|
| 4 |
import { Login } from './pages/Login';
|
| 5 |
import { Settings } from './pages/Settings';
|
| 6 |
+
import { isAuthenticated, getCurrentUser, setAuthTokenFromHash } from './services/auth';
|
| 7 |
import './App.css';
|
| 8 |
|
| 9 |
// Protected route wrapper with auth check
|
|
|
|
| 15 |
// T110: Check if user is authenticated on mount
|
| 16 |
useEffect(() => {
|
| 17 |
const checkAuth = async () => {
|
| 18 |
+
// Check for OAuth callback token in URL hash
|
| 19 |
+
if (setAuthTokenFromHash()) {
|
| 20 |
+
console.log('OAuth token extracted from URL hash');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
if (!isAuthenticated()) {
|
| 24 |
setIsChecking(false);
|
| 25 |
return;
|
frontend/src/components/NoteViewer.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import { useMemo } from 'react';
|
|
| 6 |
import ReactMarkdown from 'react-markdown';
|
| 7 |
import remarkGfm from 'remark-gfm';
|
| 8 |
import { Edit, Trash2, Calendar, Tag as TagIcon, ArrowLeft } from 'lucide-react';
|
| 9 |
-
import { Card } from '@/components/ui/card';
|
| 10 |
import { Badge } from '@/components/ui/badge';
|
| 11 |
import { Button } from '@/components/ui/button';
|
| 12 |
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
@@ -14,7 +13,6 @@ import { Separator } from '@/components/ui/separator';
|
|
| 14 |
import type { Note } from '@/types/note';
|
| 15 |
import type { BacklinkResult } from '@/services/api';
|
| 16 |
import { createWikilinkComponent } from '@/lib/markdown.tsx';
|
| 17 |
-
import { normalizeSlug } from '@/lib/wikilink';
|
| 18 |
|
| 19 |
interface NoteViewerProps {
|
| 20 |
note: Note;
|
|
|
|
| 6 |
import ReactMarkdown from 'react-markdown';
|
| 7 |
import remarkGfm from 'remark-gfm';
|
| 8 |
import { Edit, Trash2, Calendar, Tag as TagIcon, ArrowLeft } from 'lucide-react';
|
|
|
|
| 9 |
import { Badge } from '@/components/ui/badge';
|
| 10 |
import { Button } from '@/components/ui/button';
|
| 11 |
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
|
|
| 13 |
import type { Note } from '@/types/note';
|
| 14 |
import type { BacklinkResult } from '@/services/api';
|
| 15 |
import { createWikilinkComponent } from '@/lib/markdown.tsx';
|
|
|
|
| 16 |
|
| 17 |
interface NoteViewerProps {
|
| 18 |
note: Note;
|
frontend/src/components/SearchBar.tsx
CHANGED
|
@@ -3,16 +3,14 @@
|
|
| 3 |
*/
|
| 4 |
import { useState, useEffect, useCallback } from 'react';
|
| 5 |
import { Search, X } from 'lucide-react';
|
| 6 |
-
import { Input } from '@/components/ui/input';
|
| 7 |
import {
|
| 8 |
Command,
|
| 9 |
-
CommandEmpty,
|
| 10 |
CommandGroup,
|
| 11 |
CommandItem,
|
| 12 |
CommandList,
|
| 13 |
} from '@/components/ui/command';
|
| 14 |
-
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
| 15 |
import { Button } from '@/components/ui/button';
|
|
|
|
| 16 |
import { searchNotes } from '@/services/api';
|
| 17 |
import type { SearchResult } from '@/types/search';
|
| 18 |
|
|
|
|
| 3 |
*/
|
| 4 |
import { useState, useEffect, useCallback } from 'react';
|
| 5 |
import { Search, X } from 'lucide-react';
|
|
|
|
| 6 |
import {
|
| 7 |
Command,
|
|
|
|
| 8 |
CommandGroup,
|
| 9 |
CommandItem,
|
| 10 |
CommandList,
|
| 11 |
} from '@/components/ui/command';
|
|
|
|
| 12 |
import { Button } from '@/components/ui/button';
|
| 13 |
+
import { Input } from '@/components/ui/input';
|
| 14 |
import { searchNotes } from '@/services/api';
|
| 15 |
import type { SearchResult } from '@/types/search';
|
| 16 |
|
frontend/src/lib/markdown.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* T074: Markdown rendering configuration and wikilink handling
|
| 3 |
-
*/
|
| 4 |
-
import React from 'react';
|
| 5 |
-
import type { Components } from 'react-markdown';
|
| 6 |
-
|
| 7 |
-
export interface WikilinkComponentProps {
|
| 8 |
-
linkText: string;
|
| 9 |
-
resolved: boolean;
|
| 10 |
-
onClick?: (linkText: string) => void;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
/**
|
| 14 |
-
* Custom renderer for wikilinks in markdown
|
| 15 |
-
*/
|
| 16 |
-
export function createWikilinkComponent(
|
| 17 |
-
onWikilinkClick?: (linkText: string) => void
|
| 18 |
-
): Components {
|
| 19 |
-
return {
|
| 20 |
-
// Override the text renderer to handle wikilinks
|
| 21 |
-
text: ({ value }) => {
|
| 22 |
-
const parts: React.ReactNode[] = [];
|
| 23 |
-
const pattern = /\[\[([^\]]+)\]\]/g;
|
| 24 |
-
let lastIndex = 0;
|
| 25 |
-
let match;
|
| 26 |
-
let key = 0;
|
| 27 |
-
|
| 28 |
-
while ((match = pattern.exec(value)) !== null) {
|
| 29 |
-
// Add text before the wikilink
|
| 30 |
-
if (match.index > lastIndex) {
|
| 31 |
-
parts.push(value.slice(lastIndex, match.index));
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
// Add the wikilink as a clickable element
|
| 35 |
-
const linkText = match[1];
|
| 36 |
-
parts.push(
|
| 37 |
-
<span
|
| 38 |
-
key={key++}
|
| 39 |
-
className="wikilink cursor-pointer text-primary hover:underline"
|
| 40 |
-
onClick={(e) => {
|
| 41 |
-
e.preventDefault();
|
| 42 |
-
onWikilinkClick?.(linkText);
|
| 43 |
-
}}
|
| 44 |
-
role="link"
|
| 45 |
-
tabIndex={0}
|
| 46 |
-
onKeyDown={(e) => {
|
| 47 |
-
if (e.key === 'Enter' || e.key === ' ') {
|
| 48 |
-
e.preventDefault();
|
| 49 |
-
onWikilinkClick?.(linkText);
|
| 50 |
-
}
|
| 51 |
-
}}
|
| 52 |
-
>
|
| 53 |
-
[[{linkText}]]
|
| 54 |
-
</span>
|
| 55 |
-
);
|
| 56 |
-
|
| 57 |
-
lastIndex = pattern.lastIndex;
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
// Add remaining text
|
| 61 |
-
if (lastIndex < value.length) {
|
| 62 |
-
parts.push(value.slice(lastIndex));
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
return parts.length > 0 ? <>{parts}</> : value;
|
| 66 |
-
},
|
| 67 |
-
|
| 68 |
-
// Style code blocks
|
| 69 |
-
code: ({ className, children, ...props }) => {
|
| 70 |
-
const match = /language-(\w+)/.exec(className || '');
|
| 71 |
-
const isInline = !match;
|
| 72 |
-
|
| 73 |
-
if (isInline) {
|
| 74 |
-
return (
|
| 75 |
-
<code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
|
| 76 |
-
{children}
|
| 77 |
-
</code>
|
| 78 |
-
);
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
return (
|
| 82 |
-
<code
|
| 83 |
-
className={`${className} block bg-muted p-4 rounded-lg overflow-x-auto text-sm font-mono`}
|
| 84 |
-
{...props}
|
| 85 |
-
>
|
| 86 |
-
{children}
|
| 87 |
-
</code>
|
| 88 |
-
);
|
| 89 |
-
},
|
| 90 |
-
|
| 91 |
-
// Style links
|
| 92 |
-
a: ({ href, children, ...props }) => {
|
| 93 |
-
const isExternal = href?.startsWith('http');
|
| 94 |
-
return (
|
| 95 |
-
<a
|
| 96 |
-
href={href}
|
| 97 |
-
className="text-primary hover:underline"
|
| 98 |
-
target={isExternal ? '_blank' : undefined}
|
| 99 |
-
rel={isExternal ? 'noopener noreferrer' : undefined}
|
| 100 |
-
{...props}
|
| 101 |
-
>
|
| 102 |
-
{children}
|
| 103 |
-
</a>
|
| 104 |
-
);
|
| 105 |
-
},
|
| 106 |
-
|
| 107 |
-
// Style headings
|
| 108 |
-
h1: ({ children, ...props }) => (
|
| 109 |
-
<h1 className="text-3xl font-bold mt-6 mb-4" {...props}>
|
| 110 |
-
{children}
|
| 111 |
-
</h1>
|
| 112 |
-
),
|
| 113 |
-
h2: ({ children, ...props }) => (
|
| 114 |
-
<h2 className="text-2xl font-semibold mt-5 mb-3" {...props}>
|
| 115 |
-
{children}
|
| 116 |
-
</h2>
|
| 117 |
-
),
|
| 118 |
-
h3: ({ children, ...props }) => (
|
| 119 |
-
<h3 className="text-xl font-semibold mt-4 mb-2" {...props}>
|
| 120 |
-
{children}
|
| 121 |
-
</h3>
|
| 122 |
-
),
|
| 123 |
-
|
| 124 |
-
// Style lists
|
| 125 |
-
ul: ({ children, ...props }) => (
|
| 126 |
-
<ul className="list-disc list-inside my-2 space-y-1" {...props}>
|
| 127 |
-
{children}
|
| 128 |
-
</ul>
|
| 129 |
-
),
|
| 130 |
-
ol: ({ children, ...props }) => (
|
| 131 |
-
<ol className="list-decimal list-inside my-2 space-y-1" {...props}>
|
| 132 |
-
{children}
|
| 133 |
-
</ol>
|
| 134 |
-
),
|
| 135 |
-
|
| 136 |
-
// Style blockquotes
|
| 137 |
-
blockquote: ({ children, ...props }) => (
|
| 138 |
-
<blockquote className="border-l-4 border-muted-foreground pl-4 italic my-4" {...props}>
|
| 139 |
-
{children}
|
| 140 |
-
</blockquote>
|
| 141 |
-
),
|
| 142 |
-
|
| 143 |
-
// Style tables
|
| 144 |
-
table: ({ children, ...props }) => (
|
| 145 |
-
<div className="overflow-x-auto my-4">
|
| 146 |
-
<table className="min-w-full border-collapse border border-border" {...props}>
|
| 147 |
-
{children}
|
| 148 |
-
</table>
|
| 149 |
-
</div>
|
| 150 |
-
),
|
| 151 |
-
th: ({ children, ...props }) => (
|
| 152 |
-
<th className="border border-border px-4 py-2 bg-muted font-semibold text-left" {...props}>
|
| 153 |
-
{children}
|
| 154 |
-
</th>
|
| 155 |
-
),
|
| 156 |
-
td: ({ children, ...props }) => (
|
| 157 |
-
<td className="border border-border px-4 py-2" {...props}>
|
| 158 |
-
{children}
|
| 159 |
-
</td>
|
| 160 |
-
),
|
| 161 |
-
};
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
/**
|
| 165 |
-
* Render broken wikilinks with distinct styling
|
| 166 |
-
*/
|
| 167 |
-
export function renderBrokenWikilink(
|
| 168 |
-
linkText: string,
|
| 169 |
-
onCreate?: () => void
|
| 170 |
-
): React.ReactElement {
|
| 171 |
-
return (
|
| 172 |
-
<span
|
| 173 |
-
className="wikilink-broken text-destructive border-b border-dashed border-destructive cursor-pointer hover:bg-destructive/10"
|
| 174 |
-
onClick={onCreate}
|
| 175 |
-
role="link"
|
| 176 |
-
tabIndex={0}
|
| 177 |
-
title={`Note "${linkText}" not found. Click to create.`}
|
| 178 |
-
>
|
| 179 |
-
[[{linkText}]]
|
| 180 |
-
</span>
|
| 181 |
-
);
|
| 182 |
-
}
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/lib/markdown.tsx
CHANGED
|
@@ -18,7 +18,8 @@ export function createWikilinkComponent(
|
|
| 18 |
): Components {
|
| 19 |
return {
|
| 20 |
// Override the text renderer to handle wikilinks
|
| 21 |
-
text: ({
|
|
|
|
| 22 |
const parts: React.ReactNode[] = [];
|
| 23 |
const pattern = /\[\[([^\]]+)\]\]/g;
|
| 24 |
let lastIndex = 0;
|
|
|
|
| 18 |
): Components {
|
| 19 |
return {
|
| 20 |
// Override the text renderer to handle wikilinks
|
| 21 |
+
text: ({ children }: any) => {
|
| 22 |
+
const value = String(children || '');
|
| 23 |
const parts: React.ReactNode[] = [];
|
| 24 |
const pattern = /\[\[([^\]]+)\]\]/g;
|
| 25 |
let lastIndex = 0;
|
frontend/src/pages/MainApp.tsx
CHANGED
|
@@ -206,6 +206,13 @@ export function MainApp() {
|
|
| 206 |
|
| 207 |
return (
|
| 208 |
<div className="h-screen flex flex-col">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
{/* Top bar */}
|
| 210 |
<div className="border-b border-border p-4">
|
| 211 |
<div className="flex items-center justify-between">
|
|
|
|
| 206 |
|
| 207 |
return (
|
| 208 |
<div className="h-screen flex flex-col">
|
| 209 |
+
{/* Demo warning banner */}
|
| 210 |
+
<Alert variant="destructive" className="rounded-none border-x-0 border-t-0">
|
| 211 |
+
<AlertDescription className="text-center">
|
| 212 |
+
β οΈ DEMO ONLY β ALL DATA IS TEMPORARY AND MAY BE DELETED AT ANY TIME
|
| 213 |
+
</AlertDescription>
|
| 214 |
+
</Alert>
|
| 215 |
+
|
| 216 |
{/* Top bar */}
|
| 217 |
<div className="border-b border-border p-4">
|
| 218 |
<div className="flex items-center justify-between">
|
frontend/src/services/api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import type { Note, NoteSummary, NoteUpdateRequest } from '@/types/note';
|
| 2 |
import type { SearchResult, Tag, IndexHealth } from '@/types/search';
|
| 3 |
import type { User } from '@/types/user';
|
| 4 |
import type { APIError } from '@/types/auth';
|
|
@@ -7,13 +7,16 @@ import type { APIError } from '@/types/auth';
|
|
| 7 |
* Custom error class for API errors
|
| 8 |
*/
|
| 9 |
export class APIException extends Error {
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
) {
|
| 15 |
super(error);
|
| 16 |
this.name = 'APIException';
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
}
|
| 19 |
|
|
@@ -46,9 +49,9 @@ async function apiFetch<T>(
|
|
| 46 |
options: RequestInit = {}
|
| 47 |
): Promise<T> {
|
| 48 |
const token = getAuthToken();
|
| 49 |
-
const headers:
|
| 50 |
'Content-Type': 'application/json',
|
| 51 |
-
...(options.headers || {}),
|
| 52 |
};
|
| 53 |
|
| 54 |
if (token) {
|
|
@@ -135,7 +138,7 @@ export async function getTags(): Promise<Tag[]> {
|
|
| 135 |
}
|
| 136 |
|
| 137 |
/**
|
| 138 |
-
* T071:
|
| 139 |
*/
|
| 140 |
export async function createNote(data: NoteCreateRequest): Promise<Note> {
|
| 141 |
return apiFetch<Note>('/api/notes', {
|
|
|
|
| 1 |
+
import type { Note, NoteSummary, NoteUpdateRequest, NoteCreateRequest } from '@/types/note';
|
| 2 |
import type { SearchResult, Tag, IndexHealth } from '@/types/search';
|
| 3 |
import type { User } from '@/types/user';
|
| 4 |
import type { APIError } from '@/types/auth';
|
|
|
|
| 7 |
* Custom error class for API errors
|
| 8 |
*/
|
| 9 |
export class APIException extends Error {
|
| 10 |
+
status: number;
|
| 11 |
+
error: string;
|
| 12 |
+
detail?: Record<string, unknown>;
|
| 13 |
+
|
| 14 |
+
constructor(status: number, error: string, detail?: Record<string, unknown>) {
|
| 15 |
super(error);
|
| 16 |
this.name = 'APIException';
|
| 17 |
+
this.status = status;
|
| 18 |
+
this.error = error;
|
| 19 |
+
this.detail = detail;
|
| 20 |
}
|
| 21 |
}
|
| 22 |
|
|
|
|
| 49 |
options: RequestInit = {}
|
| 50 |
): Promise<T> {
|
| 51 |
const token = getAuthToken();
|
| 52 |
+
const headers: Record<string, string> = {
|
| 53 |
'Content-Type': 'application/json',
|
| 54 |
+
...(options.headers as Record<string, string> || {}),
|
| 55 |
};
|
| 56 |
|
| 57 |
if (token) {
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
/**
|
| 141 |
+
* T071: Create a note
|
| 142 |
*/
|
| 143 |
export async function createNote(data: NoteCreateRequest): Promise<Note> {
|
| 144 |
return apiFetch<Note>('/api/notes', {
|
frontend/src/services/auth.ts
CHANGED
|
@@ -81,3 +81,22 @@ export function getStoredToken(): string | null {
|
|
| 81 |
return localStorage.getItem('auth_token');
|
| 82 |
}
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
return localStorage.getItem('auth_token');
|
| 82 |
}
|
| 83 |
|
| 84 |
+
/**
|
| 85 |
+
* Extract JWT token from URL hash after OAuth callback.
|
| 86 |
+
* URL format: /#token=<jwt>
|
| 87 |
+
* Returns true if token was found and saved.
|
| 88 |
+
*/
|
| 89 |
+
export function setAuthTokenFromHash(): boolean {
|
| 90 |
+
const hash = window.location.hash;
|
| 91 |
+
if (hash.startsWith('#token=')) {
|
| 92 |
+
const token = hash.substring(7); // Remove '#token='
|
| 93 |
+
if (token) {
|
| 94 |
+
localStorage.setItem('auth_token', token);
|
| 95 |
+
// Clean up the URL
|
| 96 |
+
window.history.replaceState(null, '', window.location.pathname);
|
| 97 |
+
return true;
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
return false;
|
| 101 |
+
}
|
| 102 |
+
|
spaces_README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Document Viewer
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Document Viewer - AI-Powered Documentation System
|
| 12 |
+
|
| 13 |
+
An Obsidian-style documentation system where AI agents and humans collaborate on creating and maintaining documentation.
|
| 14 |
+
|
| 15 |
+
## β οΈ Demo Mode
|
| 16 |
+
|
| 17 |
+
**This is a demonstration instance with ephemeral storage.**
|
| 18 |
+
|
| 19 |
+
- All data is temporary and resets on server restart
|
| 20 |
+
- Demo content is automatically seeded on each startup
|
| 21 |
+
- For production use, deploy your own instance with persistent storage
|
| 22 |
+
|
| 23 |
+
## π― Features
|
| 24 |
+
|
| 25 |
+
- **Wikilinks** - Link between notes using `[[Note Name]]` syntax
|
| 26 |
+
- **Full-Text Search** - BM25 ranking with recency bonus
|
| 27 |
+
- **Backlinks** - Automatically track note references
|
| 28 |
+
- **Split-Pane Editor** - Live markdown preview
|
| 29 |
+
- **MCP Integration** - AI agents can read/write via Model Context Protocol
|
| 30 |
+
- **Multi-Tenant** - Each user gets an isolated vault (HF OAuth)
|
| 31 |
+
|
| 32 |
+
## π Getting Started
|
| 33 |
+
|
| 34 |
+
1. Click **"Sign in with Hugging Face"** to authenticate
|
| 35 |
+
2. Browse the pre-seeded demo notes
|
| 36 |
+
3. Try searching, creating, and editing notes
|
| 37 |
+
4. Check out the wikilinks between documents
|
| 38 |
+
|
| 39 |
+
## π€ AI Agent Access (MCP)
|
| 40 |
+
|
| 41 |
+
After signing in, go to **Settings** to get your API token for MCP access:
|
| 42 |
+
|
| 43 |
+
```json
|
| 44 |
+
{
|
| 45 |
+
"mcpServers": {
|
| 46 |
+
"obsidian-docs": {
|
| 47 |
+
"url": "https://huggingface.co/spaces/YOUR_USERNAME/Document-MCP/mcp",
|
| 48 |
+
"transport": "http",
|
| 49 |
+
"headers": {
|
| 50 |
+
"Authorization": "Bearer YOUR_JWT_TOKEN"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
AI agents can then use these tools:
|
| 58 |
+
- `list_notes` - Browse vault
|
| 59 |
+
- `read_note` - Read note content
|
| 60 |
+
- `write_note` - Create/update notes
|
| 61 |
+
- `search_notes` - Full-text search
|
| 62 |
+
- `get_backlinks` - Find references
|
| 63 |
+
- `get_tags` - List all tags
|
| 64 |
+
|
| 65 |
+
## ποΈ Tech Stack
|
| 66 |
+
|
| 67 |
+
**Backend:**
|
| 68 |
+
- FastAPI - HTTP API server
|
| 69 |
+
- FastMCP - MCP server for AI integration
|
| 70 |
+
- SQLite FTS5 - Full-text search
|
| 71 |
+
- python-frontmatter - YAML metadata
|
| 72 |
+
|
| 73 |
+
**Frontend:**
|
| 74 |
+
- React + Vite - Modern web framework
|
| 75 |
+
- shadcn/ui - UI components
|
| 76 |
+
- Tailwind CSS - Styling
|
| 77 |
+
- react-markdown - Markdown rendering
|
| 78 |
+
|
| 79 |
+
## π Documentation
|
| 80 |
+
|
| 81 |
+
Key demo notes to explore:
|
| 82 |
+
|
| 83 |
+
- **Getting Started** - Introduction and overview
|
| 84 |
+
- **API Documentation** - REST API reference
|
| 85 |
+
- **MCP Integration** - AI agent configuration
|
| 86 |
+
- **Wikilink Examples** - How linking works
|
| 87 |
+
- **Architecture Overview** - System design
|
| 88 |
+
- **Search Features** - Full-text search details
|
| 89 |
+
|
| 90 |
+
## βοΈ Deploy Your Own
|
| 91 |
+
|
| 92 |
+
Want persistent storage and full control? Deploy your own instance:
|
| 93 |
+
|
| 94 |
+
1. Clone the repository
|
| 95 |
+
2. Set up HF OAuth app
|
| 96 |
+
3. Configure environment variables
|
| 97 |
+
4. Deploy to HF Spaces or any Docker host
|
| 98 |
+
|
| 99 |
+
See [DEPLOYMENT.md](https://github.com/YOUR_REPO/Document-MCP/blob/main/DEPLOYMENT.md) for detailed instructions.
|
| 100 |
+
|
| 101 |
+
## π Privacy & Data
|
| 102 |
+
|
| 103 |
+
- **Multi-tenant**: Each HF user gets an isolated vault
|
| 104 |
+
- **Demo data**: Resets on restart (ephemeral storage)
|
| 105 |
+
- **OAuth**: Secure authentication via Hugging Face
|
| 106 |
+
- **No tracking**: We don't collect analytics or personal data
|
| 107 |
+
|
| 108 |
+
## π License
|
| 109 |
+
|
| 110 |
+
MIT License - See LICENSE file for details
|
| 111 |
+
|
| 112 |
+
## π€ Contributing
|
| 113 |
+
|
| 114 |
+
Contributions welcome! Open an issue or submit a PR.
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
Built with β€οΈ for the AI-human documentation collaboration workflow
|
| 119 |
+
|