shak3008 commited on
Commit
c33dd31
·
1 Parent(s): a46398e

feat: MAANG-grade Studio UI (Tailwind + Lucide icons + confetti) and Hugging Face Docker deployment ready

Browse files
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .git/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ .env
7
+ .diffweave/credentials.json
8
+ .diffweave/config.json
9
+ storage/
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================================================
2
+ # DiffWeave — Hugging Face Space Dockerfile (Multi-stage Build)
3
+ # ===========================================================================
4
+
5
+ # Stage 1: Build React 18 Studio Frontend
6
+ FROM node:18-bullseye-slim AS frontend-builder
7
+ WORKDIR /app/studio
8
+ COPY studio/package*.json ./
9
+ RUN npm ci
10
+ COPY studio ./
11
+ RUN npm run build
12
+
13
+ # Stage 2: Production Python Runtime
14
+ FROM python:3.10-slim
15
+
16
+ WORKDIR /app
17
+
18
+ # Install system dependencies
19
+ RUN apt-get update && apt-get install -y --no-install-recommends \
20
+ curl \
21
+ git \
22
+ && rm -rf /var/lib/apt/lists/*
23
+
24
+ # Install Python backend dependencies
25
+ COPY pyproject.toml ./
26
+ RUN pip install --no-cache-dir \
27
+ fastapi==0.115.0 \
28
+ uvicorn==0.30.6 \
29
+ pydantic==2.8.2 \
30
+ httpx==0.27.2 \
31
+ typer==0.12.5 \
32
+ rich==13.8.0 \
33
+ python-multipart==0.0.9
34
+
35
+ # Copy DiffWeave application package
36
+ COPY diffweave ./diffweave
37
+ COPY sample_test_docs ./sample_test_docs
38
+ COPY README.md ./
39
+ COPY pyproject.toml ./
40
+
41
+ # Copy compiled frontend from Stage 1
42
+ COPY --from=frontend-builder /app/studio/dist ./studio/dist
43
+
44
+ # Set up non-root user (Hugging Face Spaces requirement: UID 1000)
45
+ RUN useradd -m -u 1000 user
46
+ RUN chown -R user:user /app
47
+ USER user
48
+ ENV HOME=/home/user \
49
+ PATH=/home/user/.local/bin:$PATH \
50
+ PYTHONPATH=/app \
51
+ PORT=7860 \
52
+ HOST=0.0.0.0
53
+
54
+ EXPOSE 7860
55
+
56
+ # Health check
57
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
58
+ CMD curl -f http://localhost:7860/healthz || exit 1
59
+
60
+ # Launch FastAPI Bridge (Serves Studio Web UI + MCP APIs on port 7860)
61
+ CMD ["uvicorn", "diffweave.bridge.app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,17 +1,28 @@
1
- # DiffWeave 🧶 — GitHub for Document Knowledge
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  > **A Git-like developer platform for AI document intelligence, turning unstructured document changes into semantic commits, pull requests, and 3-way fact diffs.**
4
 
5
- DiffWeave is built on top of the **DocWeave** document intelligence engine via the **Model Context Protocol (MCP)**. Where DocWeave acts as the deep extraction and reconciliation engine ( Git plumbing), **DiffWeave** serves as the developer-facing platform (GitHub porcelain) comprising:
6
 
7
- 1. **dw (Command Line Interface)**: A high-velocity, Git-modeled CLI for staging documents, inspecting semantic diffs, validating CI policies, and committing approved facts to the Master Knowledge Register.
8
- 2. **DiffWeave Studio (Web UI)**: A modern, dark-mode visual interface with a 3-way Semantic Fact Diff viewer, Knowledge PR Review deck, interactive Knowledge Graph topology, and Policy Rules lab.
9
 
10
  ---
11
 
12
  ## Architecture Overview
13
 
14
- `
15
  +---------------------------------------+
16
  | DiffWeave Ecosystem |
17
  +-------------------+-------------------+
@@ -20,7 +31,7 @@ DiffWeave is built on top of the **DocWeave** document intelligence engine via t
20
  | |
21
  +----------v----------+ +----------v----------+
22
  | DiffWeave CLI | | DiffWeave Studio |
23
- | (dw) | | (React 18 + Vite) |
24
  +----------+----------+ +----------+----------+
25
  | |
26
  | +----------v----------+
@@ -32,7 +43,7 @@ DiffWeave is built on top of the **DocWeave** document intelligence engine via t
32
  |
33
  +-------------------v-------------------+
34
  | DiffWeave MCP Client |
35
- | (Stdio / In-Process Fast Adapter) |
36
  +-------------------+-------------------+
37
  |
38
  +-------------------v-------------------+
@@ -46,7 +57,7 @@ DiffWeave is built on top of the **DocWeave** document intelligence engine via t
46
  | Extraction Engine | | Neon PostgreSQL |
47
  | & Embedding Models | | Master Knowledge |
48
  +---------------------+ +---------------------+
49
- `
50
 
51
  ---
52
 
@@ -54,34 +65,37 @@ DiffWeave is built on top of the **DocWeave** document intelligence engine via t
54
 
55
  ### 1. Requirements
56
  - Python 3.10+
57
- - Node.js 18+ (for Studio UI)
58
- - DocWeave engine backend
59
 
60
  ### 2. CLI Setup
61
  Run diagnostic checks to verify connectivity to DocWeave:
62
 
63
- `ash
64
  # Verify system and MCP health
65
  .\dw.bat doctor
66
 
 
 
 
67
  # Initialize or bind your workspace
68
  .\dw.bat init --workspace 4314fb04-95be-41a2-bef4-50bf27c9c363
69
 
70
  # Check workspace status
71
  .\dw.bat status
72
- `
73
 
74
- ### 3. Stage & Ingest Documents (dw add)
75
  Stage a document into the ingestion pipeline:
76
 
77
- `ash
78
  .\dw.bat add sample_test_docs\doc1_clinical_protocol_baseline.txt
79
- `
80
 
81
  ### 4. Semantic Diffs & PR Reviews
82
  Once DocWeave completes proposal extraction:
83
 
84
- `ash
85
  # Inspect pending proposals (PRs)
86
  .\dw.bat proposals
87
 
@@ -96,7 +110,7 @@ Once DocWeave completes proposal extraction:
96
 
97
  # Or commit all approved proposals
98
  .\dw.bat commit --all-approved
99
- `
100
 
101
  ---
102
 
@@ -104,23 +118,42 @@ Once DocWeave completes proposal extraction:
104
 
105
  DiffWeave Studio offers a rich visual dashboard for team review:
106
 
107
- - **3-Way Semantic Fact Diff**: Side-by-side view showing baseline truth, proposed extractions, and conflict highlights.
108
- - **Knowledge PR Deck**: One-click approvals, rejections, or archival with confidence scores and reasoning.
109
- - **Interactive Knowledge Graph**: SVG-based topological visualization of entities, relationships, and confidence thresholds.
110
- - **Policy Rules Lab**: Add and test deterministic validation rules (e.g., regex constraints, type checks).
111
- - **Master Knowledge Base**: Searchable register of all committed facts with audit trail logs.
 
112
 
113
  ### Running Studio
114
- `ash
115
- # Option A: Start the full-stack bridge server (Serves UI + API on port 8000)
116
  python -m diffweave.bridge.app
117
 
118
- # Option B: Run Vite development server with hot-module reloading
119
  cd studio
120
  npm run dev
121
- `
 
 
 
 
 
 
 
 
122
 
123
- Visit http://localhost:8000 (or http://localhost:5173 if running Vite dev server).
 
 
 
 
 
 
 
 
 
 
124
 
125
  ---
126
 
@@ -128,37 +161,42 @@ Visit http://localhost:8000 (or http://localhost:5173 if running Vite dev server
128
 
129
  | Command | Description |
130
  |---|---|
131
- | dw init | Initialize .diffweave/ context or bind to existing workspace |
132
- | dw add <path> | Stage and trigger document ingestion workflow |
133
- | dw status | Display workspace summary, tracked documents, and pending reviews |
134
- | dw proposals | List and inspect extracted knowledge proposals |
135
- | dw diff | Render 3-way semantic fact diffs with confidence scoring |
136
- | dw validate | Execute workspace policy rules and display pass/warn/fail lint table |
137
- | dw review | Interactive terminal wizard to approve, reject, or archive proposals |
138
- | dw commit | Promote approved proposals into Master Knowledge Register |
139
- | dw log | Chronological audit trail of document updates and state transitions |
140
- | dw search <query> | Hybrid search across committed knowledge entities |
141
- | dw rules | List, add, or delete automated validation rules |
142
- | dw doctor | Diagnose MCP connectivity, environment, and tool coverage |
 
 
 
143
 
144
  ---
145
 
146
  ## Directory Structure
147
 
148
- `
149
  DiffWeave/
 
 
150
  ├── diffweave/
151
  │ ├── cli/ # dw command line tool (Click / Typer + Rich)
152
  │ ├���─ mcp/ # MCP client adapter & DocWeave integration
153
  │ └── bridge/ # FastAPI bridge server for Studio Web UI
154
- ├── studio/ # React 18 + TailwindCSS + Vite Web UI
155
  │ ├── src/
156
  │ │ ├── components/ # SemanticDiffViewer, PRReviewDeck, Graph, etc.
157
- │ │ ├── api/ # Studio REST client
158
  │ │ └── App.jsx # Navigation, Workspace Switcher, State
159
- │ └── dist/ # Production bundle (built and ready to serve)
160
  ├── sample_test_docs/ # Example documents for clinical trial protocol diffing
161
  ├── dw.bat # Windows batch launcher for instant CLI access
162
  ├── pyproject.toml # Python package configuration
163
- └── README.md # Platform documentation
164
- `
 
1
+ ---
2
+ title: DiffWeave
3
+ emoji: 🧶
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # DiffWeave 🧶 — GitHub for Document Knowledge
13
 
14
  > **A Git-like developer platform for AI document intelligence, turning unstructured document changes into semantic commits, pull requests, and 3-way fact diffs.**
15
 
16
+ DiffWeave is built on top of the **DocWeave** document intelligence engine via the **Model Context Protocol (MCP)**. Where DocWeave acts as the deep extraction and reconciliation engine ("Git plumbing"), **DiffWeave** serves as the developer-facing platform ("GitHub porcelain") comprising:
17
 
18
+ 1. **`dw` (Command Line Interface)**: A high-velocity, Git-modeled CLI for staging documents, inspecting semantic diffs, validating CI policies, and committing approved facts to the Master Knowledge Register.
19
+ 2. **DiffWeave Studio (Web UI)**: A MAANG-grade visual interface with a 3-way Semantic Fact Diff viewer, Knowledge PR Review deck, interactive Knowledge Graph topology, and Policy Rules lab.
20
 
21
  ---
22
 
23
  ## Architecture Overview
24
 
25
+ ```
26
  +---------------------------------------+
27
  | DiffWeave Ecosystem |
28
  +-------------------+-------------------+
 
31
  | |
32
  +----------v----------+ +----------v----------+
33
  | DiffWeave CLI | | DiffWeave Studio |
34
+ | (`dw`) | | (React 18 + Vite) |
35
  +----------+----------+ +----------+----------+
36
  | |
37
  | +----------v----------+
 
43
  |
44
  +-------------------v-------------------+
45
  | DiffWeave MCP Client |
46
+ | (Stdio / Remote HTTPS / In-Process) |
47
  +-------------------+-------------------+
48
  |
49
  +-------------------v-------------------+
 
57
  | Extraction Engine | | Neon PostgreSQL |
58
  | & Embedding Models | | Master Knowledge |
59
  +---------------------+ +---------------------+
60
+ ```
61
 
62
  ---
63
 
 
65
 
66
  ### 1. Requirements
67
  - Python 3.10+
68
+ - Node.js 18+ (for Studio UI development)
69
+ - DocWeave engine backend (or run in embedded standalone mode)
70
 
71
  ### 2. CLI Setup
72
  Run diagnostic checks to verify connectivity to DocWeave:
73
 
74
+ ```bash
75
  # Verify system and MCP health
76
  .\dw.bat doctor
77
 
78
+ # Authenticate session (or use 1-click demo login)
79
+ .\dw.bat login --demo
80
+
81
  # Initialize or bind your workspace
82
  .\dw.bat init --workspace 4314fb04-95be-41a2-bef4-50bf27c9c363
83
 
84
  # Check workspace status
85
  .\dw.bat status
86
+ ```
87
 
88
+ ### 3. Stage & Ingest Documents (`dw add`)
89
  Stage a document into the ingestion pipeline:
90
 
91
+ ```bash
92
  .\dw.bat add sample_test_docs\doc1_clinical_protocol_baseline.txt
93
+ ```
94
 
95
  ### 4. Semantic Diffs & PR Reviews
96
  Once DocWeave completes proposal extraction:
97
 
98
+ ```bash
99
  # Inspect pending proposals (PRs)
100
  .\dw.bat proposals
101
 
 
110
 
111
  # Or commit all approved proposals
112
  .\dw.bat commit --all-approved
113
+ ```
114
 
115
  ---
116
 
 
118
 
119
  DiffWeave Studio offers a rich visual dashboard for team review:
120
 
121
+ - **3-Way Semantic Fact Diff**: Side-by-side view showing baseline truth, proposed extractions, and conflict highlights with word-level delta tags.
122
+ - **Knowledge PR Deck**: One-click approvals, rejections, or archival with confidence scores, reviewer notes, and celebratory confetti.
123
+ - **Interactive Knowledge Graph**: SVG-based topological visualization of entities, relationships, and confidence thresholds with zoom/pan and node inspector drawer.
124
+ - **Policy Rules Lab**: Policy-as-Code engine with dry-run CI linter suite.
125
+ - **Master Knowledge Base**: Searchable register of all committed facts with JSON export and instant copy.
126
+ - **Git Audit Trail**: Cryptographically verifiable commit timeline with verification badges.
127
 
128
  ### Running Studio
129
+ ```bash
130
+ # Full-Stack Bridge Server (Serves UI + API on port 7860 or 8000)
131
  python -m diffweave.bridge.app
132
 
133
+ # Vite Development Server (with hot-module reloading)
134
  cd studio
135
  npm run dev
136
+ ```
137
+
138
+ Visit `http://localhost:7860` (or `http://localhost:5174` for Vite dev server).
139
+
140
+ ---
141
+
142
+ ## Hugging Face Spaces Deployment
143
+
144
+ DiffWeave is pre-configured for instant **1-click Docker deployment** on Hugging Face Spaces:
145
 
146
+ 1. Create a new Space on [Hugging Face Spaces](https://huggingface.co/new-space).
147
+ 2. Select **Docker** as the Space SDK.
148
+ 3. Push or sync this repository to your Hugging Face Space git remote:
149
+ ```bash
150
+ git remote add hf https://huggingface.co/spaces/<your-username>/DiffWeave
151
+ git push hf main
152
+ ```
153
+ 4. Configure Secrets / Environment Variables (optional, for remote DocWeave connection):
154
+ - `DOCWEAVE_MCP_URL`: `https://<your-docweave-space>.hf.space`
155
+ - `DOCWEAVE_API_KEY`: `<your_access_token>`
156
+ 5. Your full-stack DiffWeave Studio will automatically build and launch on port `7860`!
157
 
158
  ---
159
 
 
161
 
162
  | Command | Description |
163
  |---|---|
164
+ | `dw login` | Authenticate with DocWeave via email/password, demo, or API token |
165
+ | `dw logout` | Log out and revoke stored session credentials |
166
+ | `dw whoami` | Display active user identity, token preview, and bound workspace |
167
+ | `dw init` | Initialize `.diffweave/` context or bind to existing workspace |
168
+ | `dw add <path>` | Stage and trigger document ingestion workflow |
169
+ | `dw status` | Display workspace summary, tracked documents, and pending reviews |
170
+ | `dw proposals` | List and inspect extracted knowledge proposals |
171
+ | `dw diff` | Render 3-way semantic fact diffs with confidence scoring |
172
+ | `dw validate` | Execute workspace policy rules and display pass/warn/fail lint table |
173
+ | `dw review` | Interactive terminal wizard to approve, reject, or archive proposals |
174
+ | `dw commit` | Promote approved proposals into Master Knowledge Register |
175
+ | `dw log` | Chronological audit trail of document updates and state transitions |
176
+ | `dw search <query>` | Hybrid search across committed knowledge entities |
177
+ | `dw rules` | List, add, or delete automated validation rules |
178
+ | `dw doctor` | Diagnose MCP connectivity, environment, and tool coverage |
179
 
180
  ---
181
 
182
  ## Directory Structure
183
 
184
+ ```
185
  DiffWeave/
186
+ ├── Dockerfile # Multi-stage production build for Hugging Face Spaces
187
+ ├── .dockerignore # Optimized Docker build context
188
  ├── diffweave/
189
  │ ├── cli/ # dw command line tool (Click / Typer + Rich)
190
  │ ├���─ mcp/ # MCP client adapter & DocWeave integration
191
  │ └── bridge/ # FastAPI bridge server for Studio Web UI
192
+ ├── studio/ # React 18 + TailwindCSS + Lucide Icons + Vite Web UI
193
  │ ├── src/
194
  │ │ ├── components/ # SemanticDiffViewer, PRReviewDeck, Graph, etc.
195
+ │ │ ├── api/ # Studio REST client with Bearer auth
196
  │ │ └── App.jsx # Navigation, Workspace Switcher, State
197
+ │ └── dist/ # Production bundle (compiled and ready to serve)
198
  ├── sample_test_docs/ # Example documents for clinical trial protocol diffing
199
  ├── dw.bat # Windows batch launcher for instant CLI access
200
  ├── pyproject.toml # Python package configuration
201
+ └── README.md # Platform documentation with Hugging Face metadata
202
+ ```
diffweave/bridge/app.py CHANGED
@@ -399,12 +399,18 @@ async def get_activity_feed_endpoint(
399
  # Static Studio UI Hosting (if built)
400
  # ---------------------------------------------------------------------------
401
 
 
 
 
 
 
402
  studio_dist_candidates = [
 
 
403
  Path(__file__).parent.parent.parent / "studio" / "dist",
404
  Path(__file__).parent.parent / "studio" / "dist",
405
  Path("C:/Users/Adhi/Desktop/DiffWeave/studio/dist"),
406
  ]
407
-
408
  for dist_path in studio_dist_candidates:
409
  if dist_path.exists() and (dist_path / "index.html").exists():
410
  from fastapi.staticfiles import StaticFiles
@@ -414,4 +420,5 @@ for dist_path in studio_dist_candidates:
414
 
415
  if __name__ == "__main__":
416
  import uvicorn
417
- uvicorn.run("diffweave.bridge.app:app", host="0.0.0.0", port=8000, reload=False)
 
 
399
  # Static Studio UI Hosting (if built)
400
  # ---------------------------------------------------------------------------
401
 
402
+ @app.get("/healthz")
403
+ async def healthz():
404
+ return {"status": "ok", "service": "diffweave", "platform": "huggingface-space"}
405
+
406
+
407
  studio_dist_candidates = [
408
+ Path("/app/studio/dist"),
409
+ Path("studio/dist"),
410
  Path(__file__).parent.parent.parent / "studio" / "dist",
411
  Path(__file__).parent.parent / "studio" / "dist",
412
  Path("C:/Users/Adhi/Desktop/DiffWeave/studio/dist"),
413
  ]
 
414
  for dist_path in studio_dist_candidates:
415
  if dist_path.exists() and (dist_path / "index.html").exists():
416
  from fastapi.staticfiles import StaticFiles
 
420
 
421
  if __name__ == "__main__":
422
  import uvicorn
423
+ port = int(os.environ.get("PORT", 7860))
424
+ uvicorn.run("diffweave.bridge.app:app", host="0.0.0.0", port=port, reload=False)
studio/package-lock.json CHANGED
@@ -8,14 +8,32 @@
8
  "name": "diffweave-studio",
9
  "version": "0.1.0",
10
  "dependencies": {
 
 
11
  "react": "^18.2.0",
12
  "react-dom": "^18.2.0"
13
  },
14
  "devDependencies": {
15
  "@vitejs/plugin-react": "^4.2.0",
 
 
 
16
  "vite": "^5.0.0"
17
  }
18
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  "node_modules/@babel/code-frame": {
20
  "version": "7.29.7",
21
  "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -759,6 +777,44 @@
759
  "node": "^22.20 || ^24.12 || >=25"
760
  }
761
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  "node_modules/@rolldown/pluginutils": {
763
  "version": "1.0.0-beta.27",
764
  "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1228,6 +1284,71 @@
1228
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1229
  }
1230
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1231
  "node_modules/baseline-browser-mapping": {
1232
  "version": "2.11.27",
1233
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz",
@@ -1241,6 +1362,32 @@
1241
  "node": ">=6.0.0"
1242
  }
1243
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1244
  "node_modules/browserslist": {
1245
  "version": "4.29.3",
1246
  "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz",
@@ -1275,6 +1422,16 @@
1275
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1276
  }
1277
  },
 
 
 
 
 
 
 
 
 
 
1278
  "node_modules/caniuse-lite": {
1279
  "version": "1.0.30001814",
1280
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz",
@@ -1296,6 +1453,64 @@
1296
  ],
1297
  "license": "CC-BY-4.0"
1298
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
  "node_modules/convert-source-map": {
1300
  "version": "2.0.0",
1301
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1303,6 +1518,19 @@
1303
  "dev": true,
1304
  "license": "MIT"
1305
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1306
  "node_modules/debug": {
1307
  "version": "4.4.3",
1308
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1321,6 +1549,20 @@
1321
  }
1322
  }
1323
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
  "node_modules/electron-to-chromium": {
1325
  "version": "1.5.444",
1326
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz",
@@ -1328,6 +1570,16 @@
1328
  "dev": true,
1329
  "license": "ISC"
1330
  },
 
 
 
 
 
 
 
 
 
 
1331
  "node_modules/esbuild": {
1332
  "version": "0.21.5",
1333
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -1377,6 +1629,73 @@
1377
  "node": ">=6"
1378
  }
1379
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1380
  "node_modules/fsevents": {
1381
  "version": "2.3.3",
1382
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1392,6 +1711,16 @@
1392
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1393
  }
1394
  },
 
 
 
 
 
 
 
 
 
 
1395
  "node_modules/gensync": {
1396
  "version": "1.0.0-beta.2",
1397
  "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -1402,6 +1731,104 @@
1402
  "node": ">=6.9.0"
1403
  }
1404
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1405
  "node_modules/js-tokens": {
1406
  "version": "4.0.0",
1407
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1434,6 +1861,23 @@
1434
  "node": ">=6"
1435
  }
1436
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1437
  "node_modules/loose-envify": {
1438
  "version": "1.4.0",
1439
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -1456,6 +1900,45 @@
1456
  "yallist": "^3.0.2"
1457
  }
1458
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1459
  "node_modules/ms": {
1460
  "version": "2.1.3",
1461
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1463,6 +1946,18 @@
1463
  "dev": true,
1464
  "license": "MIT"
1465
  },
 
 
 
 
 
 
 
 
 
 
 
 
1466
  "node_modules/nanoid": {
1467
  "version": "3.3.19",
1468
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
@@ -1492,13 +1987,73 @@
1492
  "node": ">=18"
1493
  }
1494
  },
1495
- "node_modules/picocolors": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1496
  "version": "1.1.1",
1497
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1498
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1499
  "dev": true,
1500
  "license": "ISC"
1501
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1502
  "node_modules/postcss": {
1503
  "version": "8.5.28",
1504
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
@@ -1528,6 +2083,167 @@
1528
  "node": "^10 || ^12 || >=14"
1529
  }
1530
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1531
  "node_modules/react": {
1532
  "version": "18.3.1",
1533
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -1563,6 +2279,59 @@
1563
  "node": ">=0.10.0"
1564
  }
1565
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1566
  "node_modules/rollup": {
1567
  "version": "4.63.6",
1568
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.6.tgz",
@@ -1609,6 +2378,30 @@
1609
  "fsevents": "~2.3.2"
1610
  }
1611
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1612
  "node_modules/scheduler": {
1613
  "version": "0.23.2",
1614
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -1638,6 +2431,171 @@
1638
  "node": ">=0.10.0"
1639
  }
1640
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1641
  "node_modules/update-browserslist-db": {
1642
  "version": "1.3.3",
1643
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
@@ -1669,6 +2627,13 @@
1669
  "browserslist": ">= 4.21.0"
1670
  }
1671
  },
 
 
 
 
 
 
 
1672
  "node_modules/vite": {
1673
  "version": "5.4.21",
1674
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
@@ -1735,6 +2700,22 @@
1735
  "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1736
  "dev": true,
1737
  "license": "ISC"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1738
  }
1739
  }
1740
  }
 
8
  "name": "diffweave-studio",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "canvas-confetti": "^1.9.4",
12
+ "lucide-react": "^1.49.0",
13
  "react": "^18.2.0",
14
  "react-dom": "^18.2.0"
15
  },
16
  "devDependencies": {
17
  "@vitejs/plugin-react": "^4.2.0",
18
+ "autoprefixer": "^10.6.1",
19
+ "postcss": "^8.5.28",
20
+ "tailwindcss": "^3.4.1",
21
  "vite": "^5.0.0"
22
  }
23
  },
24
+ "node_modules/@alloc/quick-lru": {
25
+ "version": "5.3.0",
26
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz",
27
+ "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==",
28
+ "dev": true,
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=10"
32
+ },
33
+ "funding": {
34
+ "url": "https://github.com/sponsors/sindresorhus"
35
+ }
36
+ },
37
  "node_modules/@babel/code-frame": {
38
  "version": "7.29.7",
39
  "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
 
777
  "node": "^22.20 || ^24.12 || >=25"
778
  }
779
  },
780
+ "node_modules/@nodelib/fs.scandir": {
781
+ "version": "2.1.5",
782
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
783
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
784
+ "dev": true,
785
+ "license": "MIT",
786
+ "dependencies": {
787
+ "@nodelib/fs.stat": "2.0.5",
788
+ "run-parallel": "^1.1.9"
789
+ },
790
+ "engines": {
791
+ "node": ">= 8"
792
+ }
793
+ },
794
+ "node_modules/@nodelib/fs.stat": {
795
+ "version": "2.0.5",
796
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
797
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
798
+ "dev": true,
799
+ "license": "MIT",
800
+ "engines": {
801
+ "node": ">= 8"
802
+ }
803
+ },
804
+ "node_modules/@nodelib/fs.walk": {
805
+ "version": "1.2.8",
806
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
807
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
808
+ "dev": true,
809
+ "license": "MIT",
810
+ "dependencies": {
811
+ "@nodelib/fs.scandir": "2.1.5",
812
+ "fastq": "^1.6.0"
813
+ },
814
+ "engines": {
815
+ "node": ">= 8"
816
+ }
817
+ },
818
  "node_modules/@rolldown/pluginutils": {
819
  "version": "1.0.0-beta.27",
820
  "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
 
1284
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1285
  }
1286
  },
1287
+ "node_modules/any-promise": {
1288
+ "version": "1.3.0",
1289
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
1290
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
1291
+ "dev": true,
1292
+ "license": "MIT"
1293
+ },
1294
+ "node_modules/anymatch": {
1295
+ "version": "3.1.3",
1296
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
1297
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
1298
+ "dev": true,
1299
+ "license": "ISC",
1300
+ "dependencies": {
1301
+ "normalize-path": "^3.0.0",
1302
+ "picomatch": "^2.0.4"
1303
+ },
1304
+ "engines": {
1305
+ "node": ">= 8"
1306
+ }
1307
+ },
1308
+ "node_modules/arg": {
1309
+ "version": "5.0.2",
1310
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
1311
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
1312
+ "dev": true,
1313
+ "license": "MIT"
1314
+ },
1315
+ "node_modules/autoprefixer": {
1316
+ "version": "10.6.1",
1317
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
1318
+ "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==",
1319
+ "dev": true,
1320
+ "funding": [
1321
+ {
1322
+ "type": "opencollective",
1323
+ "url": "https://opencollective.com/postcss/"
1324
+ },
1325
+ {
1326
+ "type": "tidelift",
1327
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
1328
+ },
1329
+ {
1330
+ "type": "github",
1331
+ "url": "https://github.com/sponsors/ai"
1332
+ }
1333
+ ],
1334
+ "license": "MIT",
1335
+ "dependencies": {
1336
+ "browserslist": "^4.28.9",
1337
+ "caniuse-lite": "^1.0.30001810",
1338
+ "fraction.js": "^5.3.4",
1339
+ "picocolors": "^1.1.1",
1340
+ "postcss-value-parser": "^4.2.0"
1341
+ },
1342
+ "bin": {
1343
+ "autoprefixer": "bin/autoprefixer"
1344
+ },
1345
+ "engines": {
1346
+ "node": "^10 || ^12 || >=14"
1347
+ },
1348
+ "peerDependencies": {
1349
+ "postcss": "^8.1.0"
1350
+ }
1351
+ },
1352
  "node_modules/baseline-browser-mapping": {
1353
  "version": "2.11.27",
1354
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz",
 
1362
  "node": ">=6.0.0"
1363
  }
1364
  },
1365
+ "node_modules/binary-extensions": {
1366
+ "version": "2.3.0",
1367
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
1368
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
1369
+ "dev": true,
1370
+ "license": "MIT",
1371
+ "engines": {
1372
+ "node": ">=8"
1373
+ },
1374
+ "funding": {
1375
+ "url": "https://github.com/sponsors/sindresorhus"
1376
+ }
1377
+ },
1378
+ "node_modules/braces": {
1379
+ "version": "3.0.3",
1380
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
1381
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
1382
+ "dev": true,
1383
+ "license": "MIT",
1384
+ "dependencies": {
1385
+ "fill-range": "^7.1.1"
1386
+ },
1387
+ "engines": {
1388
+ "node": ">=8"
1389
+ }
1390
+ },
1391
  "node_modules/browserslist": {
1392
  "version": "4.29.3",
1393
  "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz",
 
1422
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1423
  }
1424
  },
1425
+ "node_modules/camelcase-css": {
1426
+ "version": "2.0.1",
1427
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
1428
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
1429
+ "dev": true,
1430
+ "license": "MIT",
1431
+ "engines": {
1432
+ "node": ">= 6"
1433
+ }
1434
+ },
1435
  "node_modules/caniuse-lite": {
1436
  "version": "1.0.30001814",
1437
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz",
 
1453
  ],
1454
  "license": "CC-BY-4.0"
1455
  },
1456
+ "node_modules/canvas-confetti": {
1457
+ "version": "1.9.4",
1458
+ "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz",
1459
+ "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==",
1460
+ "license": "ISC",
1461
+ "funding": {
1462
+ "type": "donate",
1463
+ "url": "https://www.paypal.me/kirilvatev"
1464
+ }
1465
+ },
1466
+ "node_modules/chokidar": {
1467
+ "version": "3.6.0",
1468
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
1469
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
1470
+ "dev": true,
1471
+ "license": "MIT",
1472
+ "dependencies": {
1473
+ "anymatch": "~3.1.2",
1474
+ "braces": "~3.0.2",
1475
+ "glob-parent": "~5.1.2",
1476
+ "is-binary-path": "~2.1.0",
1477
+ "is-glob": "~4.0.1",
1478
+ "normalize-path": "~3.0.0",
1479
+ "readdirp": "~3.6.0"
1480
+ },
1481
+ "engines": {
1482
+ "node": ">= 8.10.0"
1483
+ },
1484
+ "funding": {
1485
+ "url": "https://paulmillr.com/funding/"
1486
+ },
1487
+ "optionalDependencies": {
1488
+ "fsevents": "~2.3.2"
1489
+ }
1490
+ },
1491
+ "node_modules/chokidar/node_modules/glob-parent": {
1492
+ "version": "5.1.2",
1493
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1494
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1495
+ "dev": true,
1496
+ "license": "ISC",
1497
+ "dependencies": {
1498
+ "is-glob": "^4.0.1"
1499
+ },
1500
+ "engines": {
1501
+ "node": ">= 6"
1502
+ }
1503
+ },
1504
+ "node_modules/commander": {
1505
+ "version": "4.1.1",
1506
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
1507
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
1508
+ "dev": true,
1509
+ "license": "MIT",
1510
+ "engines": {
1511
+ "node": ">= 6"
1512
+ }
1513
+ },
1514
  "node_modules/convert-source-map": {
1515
  "version": "2.0.0",
1516
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
 
1518
  "dev": true,
1519
  "license": "MIT"
1520
  },
1521
+ "node_modules/cssesc": {
1522
+ "version": "3.0.0",
1523
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1524
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1525
+ "dev": true,
1526
+ "license": "MIT",
1527
+ "bin": {
1528
+ "cssesc": "bin/cssesc"
1529
+ },
1530
+ "engines": {
1531
+ "node": ">=4"
1532
+ }
1533
+ },
1534
  "node_modules/debug": {
1535
  "version": "4.4.3",
1536
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
 
1549
  }
1550
  }
1551
  },
1552
+ "node_modules/didyoumean": {
1553
+ "version": "1.2.2",
1554
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1555
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1556
+ "dev": true,
1557
+ "license": "Apache-2.0"
1558
+ },
1559
+ "node_modules/dlv": {
1560
+ "version": "1.1.3",
1561
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1562
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1563
+ "dev": true,
1564
+ "license": "MIT"
1565
+ },
1566
  "node_modules/electron-to-chromium": {
1567
  "version": "1.5.444",
1568
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz",
 
1570
  "dev": true,
1571
  "license": "ISC"
1572
  },
1573
+ "node_modules/es-errors": {
1574
+ "version": "1.3.0",
1575
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1576
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1577
+ "dev": true,
1578
+ "license": "MIT",
1579
+ "engines": {
1580
+ "node": ">= 0.4"
1581
+ }
1582
+ },
1583
  "node_modules/esbuild": {
1584
  "version": "0.21.5",
1585
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
 
1629
  "node": ">=6"
1630
  }
1631
  },
1632
+ "node_modules/fast-glob": {
1633
+ "version": "3.3.3",
1634
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1635
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1636
+ "dev": true,
1637
+ "license": "MIT",
1638
+ "dependencies": {
1639
+ "@nodelib/fs.stat": "^2.0.2",
1640
+ "@nodelib/fs.walk": "^1.2.3",
1641
+ "glob-parent": "^5.1.2",
1642
+ "merge2": "^1.3.0",
1643
+ "micromatch": "^4.0.8"
1644
+ },
1645
+ "engines": {
1646
+ "node": ">=8.6.0"
1647
+ }
1648
+ },
1649
+ "node_modules/fast-glob/node_modules/glob-parent": {
1650
+ "version": "5.1.2",
1651
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1652
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1653
+ "dev": true,
1654
+ "license": "ISC",
1655
+ "dependencies": {
1656
+ "is-glob": "^4.0.1"
1657
+ },
1658
+ "engines": {
1659
+ "node": ">= 6"
1660
+ }
1661
+ },
1662
+ "node_modules/fastq": {
1663
+ "version": "1.20.3",
1664
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
1665
+ "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==",
1666
+ "dev": true,
1667
+ "license": "ISC",
1668
+ "dependencies": {
1669
+ "reusify": "^1.0.4"
1670
+ }
1671
+ },
1672
+ "node_modules/fill-range": {
1673
+ "version": "7.1.1",
1674
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1675
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1676
+ "dev": true,
1677
+ "license": "MIT",
1678
+ "dependencies": {
1679
+ "to-regex-range": "^5.0.1"
1680
+ },
1681
+ "engines": {
1682
+ "node": ">=8"
1683
+ }
1684
+ },
1685
+ "node_modules/fraction.js": {
1686
+ "version": "5.3.4",
1687
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
1688
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
1689
+ "dev": true,
1690
+ "license": "MIT",
1691
+ "engines": {
1692
+ "node": "*"
1693
+ },
1694
+ "funding": {
1695
+ "type": "github",
1696
+ "url": "https://github.com/sponsors/rawify"
1697
+ }
1698
+ },
1699
  "node_modules/fsevents": {
1700
  "version": "2.3.3",
1701
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
1711
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1712
  }
1713
  },
1714
+ "node_modules/function-bind": {
1715
+ "version": "1.1.2",
1716
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1717
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1718
+ "dev": true,
1719
+ "license": "MIT",
1720
+ "funding": {
1721
+ "url": "https://github.com/sponsors/ljharb"
1722
+ }
1723
+ },
1724
  "node_modules/gensync": {
1725
  "version": "1.0.0-beta.2",
1726
  "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
 
1731
  "node": ">=6.9.0"
1732
  }
1733
  },
1734
+ "node_modules/glob-parent": {
1735
+ "version": "6.0.2",
1736
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1737
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1738
+ "dev": true,
1739
+ "license": "ISC",
1740
+ "dependencies": {
1741
+ "is-glob": "^4.0.3"
1742
+ },
1743
+ "engines": {
1744
+ "node": ">=10.13.0"
1745
+ }
1746
+ },
1747
+ "node_modules/hasown": {
1748
+ "version": "2.0.4",
1749
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
1750
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
1751
+ "dev": true,
1752
+ "license": "MIT",
1753
+ "dependencies": {
1754
+ "function-bind": "^1.1.2"
1755
+ },
1756
+ "engines": {
1757
+ "node": ">= 0.4"
1758
+ }
1759
+ },
1760
+ "node_modules/is-binary-path": {
1761
+ "version": "2.1.0",
1762
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1763
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1764
+ "dev": true,
1765
+ "license": "MIT",
1766
+ "dependencies": {
1767
+ "binary-extensions": "^2.0.0"
1768
+ },
1769
+ "engines": {
1770
+ "node": ">=8"
1771
+ }
1772
+ },
1773
+ "node_modules/is-core-module": {
1774
+ "version": "2.17.0",
1775
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz",
1776
+ "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==",
1777
+ "dev": true,
1778
+ "license": "MIT",
1779
+ "dependencies": {
1780
+ "hasown": "^2.0.4"
1781
+ },
1782
+ "engines": {
1783
+ "node": ">= 0.4"
1784
+ },
1785
+ "funding": {
1786
+ "url": "https://github.com/sponsors/ljharb"
1787
+ }
1788
+ },
1789
+ "node_modules/is-extglob": {
1790
+ "version": "2.1.1",
1791
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1792
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1793
+ "dev": true,
1794
+ "license": "MIT",
1795
+ "engines": {
1796
+ "node": ">=0.10.0"
1797
+ }
1798
+ },
1799
+ "node_modules/is-glob": {
1800
+ "version": "4.0.3",
1801
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1802
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1803
+ "dev": true,
1804
+ "license": "MIT",
1805
+ "dependencies": {
1806
+ "is-extglob": "^2.1.1"
1807
+ },
1808
+ "engines": {
1809
+ "node": ">=0.10.0"
1810
+ }
1811
+ },
1812
+ "node_modules/is-number": {
1813
+ "version": "7.0.0",
1814
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1815
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1816
+ "dev": true,
1817
+ "license": "MIT",
1818
+ "engines": {
1819
+ "node": ">=0.12.0"
1820
+ }
1821
+ },
1822
+ "node_modules/jiti": {
1823
+ "version": "1.21.7",
1824
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1825
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1826
+ "dev": true,
1827
+ "license": "MIT",
1828
+ "bin": {
1829
+ "jiti": "bin/jiti.js"
1830
+ }
1831
+ },
1832
  "node_modules/js-tokens": {
1833
  "version": "4.0.0",
1834
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
 
1861
  "node": ">=6"
1862
  }
1863
  },
1864
+ "node_modules/lilconfig": {
1865
+ "version": "2.1.0",
1866
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
1867
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
1868
+ "dev": true,
1869
+ "license": "MIT",
1870
+ "engines": {
1871
+ "node": ">=10"
1872
+ }
1873
+ },
1874
+ "node_modules/lines-and-columns": {
1875
+ "version": "1.2.4",
1876
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1877
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1878
+ "dev": true,
1879
+ "license": "MIT"
1880
+ },
1881
  "node_modules/loose-envify": {
1882
  "version": "1.4.0",
1883
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
 
1900
  "yallist": "^3.0.2"
1901
  }
1902
  },
1903
+ "node_modules/lucide-react": {
1904
+ "version": "1.49.0",
1905
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.49.0.tgz",
1906
+ "integrity": "sha512-ApfaNM4/7jBrVopqLJ84Jz8TMQ1Q1JeZ1PmKh25aeS3E1FTVcMkSh1bEQB7dfzdIrwXRP8p3z4AJjTIHHb5IIA==",
1907
+ "license": "ISC",
1908
+ "peerDependencies": {
1909
+ "@types/react": "*",
1910
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1911
+ },
1912
+ "peerDependenciesMeta": {
1913
+ "@types/react": {
1914
+ "optional": true
1915
+ }
1916
+ }
1917
+ },
1918
+ "node_modules/merge2": {
1919
+ "version": "1.4.1",
1920
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1921
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1922
+ "dev": true,
1923
+ "license": "MIT",
1924
+ "engines": {
1925
+ "node": ">= 8"
1926
+ }
1927
+ },
1928
+ "node_modules/micromatch": {
1929
+ "version": "4.0.8",
1930
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1931
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1932
+ "dev": true,
1933
+ "license": "MIT",
1934
+ "dependencies": {
1935
+ "braces": "^3.0.3",
1936
+ "picomatch": "^2.3.1"
1937
+ },
1938
+ "engines": {
1939
+ "node": ">=8.6"
1940
+ }
1941
+ },
1942
  "node_modules/ms": {
1943
  "version": "2.1.3",
1944
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 
1946
  "dev": true,
1947
  "license": "MIT"
1948
  },
1949
+ "node_modules/mz": {
1950
+ "version": "2.7.0",
1951
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1952
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1953
+ "dev": true,
1954
+ "license": "MIT",
1955
+ "dependencies": {
1956
+ "any-promise": "^1.0.0",
1957
+ "object-assign": "^4.0.1",
1958
+ "thenify-all": "^1.0.0"
1959
+ }
1960
+ },
1961
  "node_modules/nanoid": {
1962
  "version": "3.3.19",
1963
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
 
1987
  "node": ">=18"
1988
  }
1989
  },
1990
+ "node_modules/normalize-path": {
1991
+ "version": "3.0.0",
1992
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1993
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1994
+ "dev": true,
1995
+ "license": "MIT",
1996
+ "engines": {
1997
+ "node": ">=0.10.0"
1998
+ }
1999
+ },
2000
+ "node_modules/object-assign": {
2001
+ "version": "4.1.1",
2002
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
2003
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
2004
+ "dev": true,
2005
+ "license": "MIT",
2006
+ "engines": {
2007
+ "node": ">=0.10.0"
2008
+ }
2009
+ },
2010
+ "node_modules/object-hash": {
2011
+ "version": "3.0.0",
2012
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
2013
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
2014
+ "dev": true,
2015
+ "license": "MIT",
2016
+ "engines": {
2017
+ "node": ">= 6"
2018
+ }
2019
+ },
2020
+ "node_modules/path-parse": {
2021
+ "version": "1.0.7",
2022
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
2023
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
2024
+ "dev": true,
2025
+ "license": "MIT"
2026
+ },
2027
+ "node_modules/picocolors": {
2028
  "version": "1.1.1",
2029
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2030
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2031
  "dev": true,
2032
  "license": "ISC"
2033
  },
2034
+ "node_modules/picomatch": {
2035
+ "version": "2.3.2",
2036
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
2037
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
2038
+ "dev": true,
2039
+ "license": "MIT",
2040
+ "engines": {
2041
+ "node": ">=8.6"
2042
+ },
2043
+ "funding": {
2044
+ "url": "https://github.com/sponsors/jonschlinkert"
2045
+ }
2046
+ },
2047
+ "node_modules/pirates": {
2048
+ "version": "4.0.7",
2049
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
2050
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
2051
+ "dev": true,
2052
+ "license": "MIT",
2053
+ "engines": {
2054
+ "node": ">= 6"
2055
+ }
2056
+ },
2057
  "node_modules/postcss": {
2058
  "version": "8.5.28",
2059
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
 
2083
  "node": "^10 || ^12 || >=14"
2084
  }
2085
  },
2086
+ "node_modules/postcss-import": {
2087
+ "version": "15.1.0",
2088
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
2089
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
2090
+ "dev": true,
2091
+ "license": "MIT",
2092
+ "dependencies": {
2093
+ "postcss-value-parser": "^4.0.0",
2094
+ "read-cache": "^1.0.0",
2095
+ "resolve": "^1.1.7"
2096
+ },
2097
+ "engines": {
2098
+ "node": ">=14.0.0"
2099
+ },
2100
+ "peerDependencies": {
2101
+ "postcss": "^8.0.0"
2102
+ }
2103
+ },
2104
+ "node_modules/postcss-js": {
2105
+ "version": "4.1.0",
2106
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
2107
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
2108
+ "dev": true,
2109
+ "funding": [
2110
+ {
2111
+ "type": "opencollective",
2112
+ "url": "https://opencollective.com/postcss/"
2113
+ },
2114
+ {
2115
+ "type": "github",
2116
+ "url": "https://github.com/sponsors/ai"
2117
+ }
2118
+ ],
2119
+ "license": "MIT",
2120
+ "dependencies": {
2121
+ "camelcase-css": "^2.0.1"
2122
+ },
2123
+ "engines": {
2124
+ "node": "^12 || ^14 || >= 16"
2125
+ },
2126
+ "peerDependencies": {
2127
+ "postcss": "^8.4.21"
2128
+ }
2129
+ },
2130
+ "node_modules/postcss-load-config": {
2131
+ "version": "4.0.2",
2132
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
2133
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
2134
+ "dev": true,
2135
+ "funding": [
2136
+ {
2137
+ "type": "opencollective",
2138
+ "url": "https://opencollective.com/postcss/"
2139
+ },
2140
+ {
2141
+ "type": "github",
2142
+ "url": "https://github.com/sponsors/ai"
2143
+ }
2144
+ ],
2145
+ "license": "MIT",
2146
+ "dependencies": {
2147
+ "lilconfig": "^3.0.0",
2148
+ "yaml": "^2.3.4"
2149
+ },
2150
+ "engines": {
2151
+ "node": ">= 14"
2152
+ },
2153
+ "peerDependencies": {
2154
+ "postcss": ">=8.0.9",
2155
+ "ts-node": ">=9.0.0"
2156
+ },
2157
+ "peerDependenciesMeta": {
2158
+ "postcss": {
2159
+ "optional": true
2160
+ },
2161
+ "ts-node": {
2162
+ "optional": true
2163
+ }
2164
+ }
2165
+ },
2166
+ "node_modules/postcss-load-config/node_modules/lilconfig": {
2167
+ "version": "3.1.3",
2168
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
2169
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
2170
+ "dev": true,
2171
+ "license": "MIT",
2172
+ "engines": {
2173
+ "node": ">=14"
2174
+ },
2175
+ "funding": {
2176
+ "url": "https://github.com/sponsors/antonk52"
2177
+ }
2178
+ },
2179
+ "node_modules/postcss-nested": {
2180
+ "version": "6.2.0",
2181
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
2182
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
2183
+ "dev": true,
2184
+ "funding": [
2185
+ {
2186
+ "type": "opencollective",
2187
+ "url": "https://opencollective.com/postcss/"
2188
+ },
2189
+ {
2190
+ "type": "github",
2191
+ "url": "https://github.com/sponsors/ai"
2192
+ }
2193
+ ],
2194
+ "license": "MIT",
2195
+ "dependencies": {
2196
+ "postcss-selector-parser": "^6.1.1"
2197
+ },
2198
+ "engines": {
2199
+ "node": ">=12.0"
2200
+ },
2201
+ "peerDependencies": {
2202
+ "postcss": "^8.2.14"
2203
+ }
2204
+ },
2205
+ "node_modules/postcss-selector-parser": {
2206
+ "version": "6.1.4",
2207
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
2208
+ "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
2209
+ "dev": true,
2210
+ "license": "MIT",
2211
+ "dependencies": {
2212
+ "cssesc": "^3.0.0",
2213
+ "util-deprecate": "^1.0.2"
2214
+ },
2215
+ "engines": {
2216
+ "node": ">=4"
2217
+ }
2218
+ },
2219
+ "node_modules/postcss-value-parser": {
2220
+ "version": "4.2.0",
2221
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
2222
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
2223
+ "dev": true,
2224
+ "license": "MIT"
2225
+ },
2226
+ "node_modules/queue-microtask": {
2227
+ "version": "1.2.3",
2228
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
2229
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
2230
+ "dev": true,
2231
+ "funding": [
2232
+ {
2233
+ "type": "github",
2234
+ "url": "https://github.com/sponsors/feross"
2235
+ },
2236
+ {
2237
+ "type": "patreon",
2238
+ "url": "https://www.patreon.com/feross"
2239
+ },
2240
+ {
2241
+ "type": "consulting",
2242
+ "url": "https://feross.org/support"
2243
+ }
2244
+ ],
2245
+ "license": "MIT"
2246
+ },
2247
  "node_modules/react": {
2248
  "version": "18.3.1",
2249
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
 
2279
  "node": ">=0.10.0"
2280
  }
2281
  },
2282
+ "node_modules/read-cache": {
2283
+ "version": "1.0.2",
2284
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz",
2285
+ "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==",
2286
+ "dev": true,
2287
+ "license": "MIT"
2288
+ },
2289
+ "node_modules/readdirp": {
2290
+ "version": "3.6.0",
2291
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
2292
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
2293
+ "dev": true,
2294
+ "license": "MIT",
2295
+ "dependencies": {
2296
+ "picomatch": "^2.2.1"
2297
+ },
2298
+ "engines": {
2299
+ "node": ">=8.10.0"
2300
+ }
2301
+ },
2302
+ "node_modules/resolve": {
2303
+ "version": "1.22.12",
2304
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
2305
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
2306
+ "dev": true,
2307
+ "license": "MIT",
2308
+ "dependencies": {
2309
+ "es-errors": "^1.3.0",
2310
+ "is-core-module": "^2.16.1",
2311
+ "path-parse": "^1.0.7",
2312
+ "supports-preserve-symlinks-flag": "^1.0.0"
2313
+ },
2314
+ "bin": {
2315
+ "resolve": "bin/resolve"
2316
+ },
2317
+ "engines": {
2318
+ "node": ">= 0.4"
2319
+ },
2320
+ "funding": {
2321
+ "url": "https://github.com/sponsors/ljharb"
2322
+ }
2323
+ },
2324
+ "node_modules/reusify": {
2325
+ "version": "1.1.0",
2326
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
2327
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
2328
+ "dev": true,
2329
+ "license": "MIT",
2330
+ "engines": {
2331
+ "iojs": ">=1.0.0",
2332
+ "node": ">=0.10.0"
2333
+ }
2334
+ },
2335
  "node_modules/rollup": {
2336
  "version": "4.63.6",
2337
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.6.tgz",
 
2378
  "fsevents": "~2.3.2"
2379
  }
2380
  },
2381
+ "node_modules/run-parallel": {
2382
+ "version": "1.2.0",
2383
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
2384
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
2385
+ "dev": true,
2386
+ "funding": [
2387
+ {
2388
+ "type": "github",
2389
+ "url": "https://github.com/sponsors/feross"
2390
+ },
2391
+ {
2392
+ "type": "patreon",
2393
+ "url": "https://www.patreon.com/feross"
2394
+ },
2395
+ {
2396
+ "type": "consulting",
2397
+ "url": "https://feross.org/support"
2398
+ }
2399
+ ],
2400
+ "license": "MIT",
2401
+ "dependencies": {
2402
+ "queue-microtask": "^1.2.2"
2403
+ }
2404
+ },
2405
  "node_modules/scheduler": {
2406
  "version": "0.23.2",
2407
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
 
2431
  "node": ">=0.10.0"
2432
  }
2433
  },
2434
+ "node_modules/sucrase": {
2435
+ "version": "3.35.1",
2436
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
2437
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
2438
+ "dev": true,
2439
+ "license": "MIT",
2440
+ "dependencies": {
2441
+ "@jridgewell/gen-mapping": "^0.3.2",
2442
+ "commander": "^4.0.0",
2443
+ "lines-and-columns": "^1.1.6",
2444
+ "mz": "^2.7.0",
2445
+ "pirates": "^4.0.1",
2446
+ "tinyglobby": "^0.2.11",
2447
+ "ts-interface-checker": "^0.1.9"
2448
+ },
2449
+ "bin": {
2450
+ "sucrase": "bin/sucrase",
2451
+ "sucrase-node": "bin/sucrase-node"
2452
+ },
2453
+ "engines": {
2454
+ "node": ">=16 || 14 >=14.17"
2455
+ }
2456
+ },
2457
+ "node_modules/supports-preserve-symlinks-flag": {
2458
+ "version": "1.0.0",
2459
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
2460
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
2461
+ "dev": true,
2462
+ "license": "MIT",
2463
+ "engines": {
2464
+ "node": ">= 0.4"
2465
+ },
2466
+ "funding": {
2467
+ "url": "https://github.com/sponsors/ljharb"
2468
+ }
2469
+ },
2470
+ "node_modules/tailwindcss": {
2471
+ "version": "3.4.1",
2472
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
2473
+ "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
2474
+ "dev": true,
2475
+ "license": "MIT",
2476
+ "dependencies": {
2477
+ "@alloc/quick-lru": "^5.2.0",
2478
+ "arg": "^5.0.2",
2479
+ "chokidar": "^3.5.3",
2480
+ "didyoumean": "^1.2.2",
2481
+ "dlv": "^1.1.3",
2482
+ "fast-glob": "^3.3.0",
2483
+ "glob-parent": "^6.0.2",
2484
+ "is-glob": "^4.0.3",
2485
+ "jiti": "^1.19.1",
2486
+ "lilconfig": "^2.1.0",
2487
+ "micromatch": "^4.0.5",
2488
+ "normalize-path": "^3.0.0",
2489
+ "object-hash": "^3.0.0",
2490
+ "picocolors": "^1.0.0",
2491
+ "postcss": "^8.4.23",
2492
+ "postcss-import": "^15.1.0",
2493
+ "postcss-js": "^4.0.1",
2494
+ "postcss-load-config": "^4.0.1",
2495
+ "postcss-nested": "^6.0.1",
2496
+ "postcss-selector-parser": "^6.0.11",
2497
+ "resolve": "^1.22.2",
2498
+ "sucrase": "^3.32.0"
2499
+ },
2500
+ "bin": {
2501
+ "tailwind": "lib/cli.js",
2502
+ "tailwindcss": "lib/cli.js"
2503
+ },
2504
+ "engines": {
2505
+ "node": ">=14.0.0"
2506
+ }
2507
+ },
2508
+ "node_modules/thenify": {
2509
+ "version": "3.3.1",
2510
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
2511
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
2512
+ "dev": true,
2513
+ "license": "MIT",
2514
+ "dependencies": {
2515
+ "any-promise": "^1.0.0"
2516
+ }
2517
+ },
2518
+ "node_modules/thenify-all": {
2519
+ "version": "1.6.0",
2520
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
2521
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
2522
+ "dev": true,
2523
+ "license": "MIT",
2524
+ "dependencies": {
2525
+ "thenify": ">= 3.1.0 < 4"
2526
+ },
2527
+ "engines": {
2528
+ "node": ">=0.8"
2529
+ }
2530
+ },
2531
+ "node_modules/tinyglobby": {
2532
+ "version": "0.2.17",
2533
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
2534
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
2535
+ "dev": true,
2536
+ "license": "MIT",
2537
+ "dependencies": {
2538
+ "fdir": "^6.5.0",
2539
+ "picomatch": "^4.0.4"
2540
+ },
2541
+ "engines": {
2542
+ "node": ">=12.0.0"
2543
+ },
2544
+ "funding": {
2545
+ "url": "https://github.com/sponsors/SuperchupuDev"
2546
+ }
2547
+ },
2548
+ "node_modules/tinyglobby/node_modules/fdir": {
2549
+ "version": "6.5.0",
2550
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2551
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2552
+ "dev": true,
2553
+ "license": "MIT",
2554
+ "engines": {
2555
+ "node": ">=12.0.0"
2556
+ },
2557
+ "peerDependencies": {
2558
+ "picomatch": "^3 || ^4"
2559
+ },
2560
+ "peerDependenciesMeta": {
2561
+ "picomatch": {
2562
+ "optional": true
2563
+ }
2564
+ }
2565
+ },
2566
+ "node_modules/tinyglobby/node_modules/picomatch": {
2567
+ "version": "4.0.7",
2568
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
2569
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
2570
+ "dev": true,
2571
+ "license": "MIT",
2572
+ "engines": {
2573
+ "node": ">=12"
2574
+ },
2575
+ "funding": {
2576
+ "url": "https://github.com/sponsors/jonschlinkert"
2577
+ }
2578
+ },
2579
+ "node_modules/to-regex-range": {
2580
+ "version": "5.0.1",
2581
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2582
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2583
+ "dev": true,
2584
+ "license": "MIT",
2585
+ "dependencies": {
2586
+ "is-number": "^7.0.0"
2587
+ },
2588
+ "engines": {
2589
+ "node": ">=8.0"
2590
+ }
2591
+ },
2592
+ "node_modules/ts-interface-checker": {
2593
+ "version": "0.1.13",
2594
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2595
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2596
+ "dev": true,
2597
+ "license": "Apache-2.0"
2598
+ },
2599
  "node_modules/update-browserslist-db": {
2600
  "version": "1.3.3",
2601
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
 
2627
  "browserslist": ">= 4.21.0"
2628
  }
2629
  },
2630
+ "node_modules/util-deprecate": {
2631
+ "version": "1.0.2",
2632
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2633
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2634
+ "dev": true,
2635
+ "license": "MIT"
2636
+ },
2637
  "node_modules/vite": {
2638
  "version": "5.4.21",
2639
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
 
2700
  "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2701
  "dev": true,
2702
  "license": "ISC"
2703
+ },
2704
+ "node_modules/yaml": {
2705
+ "version": "2.9.1",
2706
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
2707
+ "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
2708
+ "dev": true,
2709
+ "license": "ISC",
2710
+ "bin": {
2711
+ "yaml": "bin.mjs"
2712
+ },
2713
+ "engines": {
2714
+ "node": ">= 14.6"
2715
+ },
2716
+ "funding": {
2717
+ "url": "https://github.com/sponsors/eemeli"
2718
+ }
2719
  }
2720
  }
2721
  }
studio/package.json CHANGED
@@ -8,11 +8,16 @@
8
  "preview": "vite preview"
9
  },
10
  "dependencies": {
 
 
11
  "react": "^18.2.0",
12
  "react-dom": "^18.2.0"
13
  },
14
  "devDependencies": {
15
  "@vitejs/plugin-react": "^4.2.0",
 
 
 
16
  "vite": "^5.0.0"
17
  }
18
  }
 
8
  "preview": "vite preview"
9
  },
10
  "dependencies": {
11
+ "canvas-confetti": "^1.9.4",
12
+ "lucide-react": "^1.49.0",
13
  "react": "^18.2.0",
14
  "react-dom": "^18.2.0"
15
  },
16
  "devDependencies": {
17
  "@vitejs/plugin-react": "^4.2.0",
18
+ "autoprefixer": "^10.6.1",
19
+ "postcss": "^8.5.28",
20
+ "tailwindcss": "^3.4.1",
21
  "vite": "^5.0.0"
22
  }
23
  }
studio/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
studio/src/App.jsx CHANGED
@@ -8,6 +8,7 @@ import KnowledgeGraph from './components/KnowledgeGraph';
8
  import KnowledgeBase from './components/KnowledgeBase';
9
  import PolicyRulesLab from './components/PolicyRulesLab';
10
  import AuditLog from './components/AuditLog';
 
11
 
12
  export default function App() {
13
  const [workspaces, setWorkspaces] = useState([]);
@@ -22,37 +23,38 @@ export default function App() {
22
  const [graphData, setGraphData] = useState(null);
23
  const [knowledgeItems, setKnowledgeItems] = useState([]);
24
  const [rules, setRules] = useState([]);
25
- const [activity, setActivity] = useState(null);
26
- const [validationResult, setValidationResult] = useState(null);
27
 
28
  const [loading, setLoading] = useState(false);
 
29
  const [toast, setToast] = useState(null);
30
 
31
  const showToast = (message, type = 'success') => {
32
  setToast({ message, type });
33
- setTimeout(() => setToast(null), 4000);
34
  };
35
 
36
- // Initial load: workspaces
37
  useEffect(() => {
38
  async function init() {
39
  try {
40
  const wsList = await api.getWorkspaces();
41
  setWorkspaces(wsList);
42
- if (wsList.length > 0) {
43
  setCurrentWorkspace(wsList[0]);
44
  }
45
  } catch (err) {
46
- showToast(`Failed to connect to DiffWeave Bridge: ${err.message}`, 'error');
47
  }
48
  }
49
  init();
50
  }, []);
51
 
52
- // Fetch workspace details
53
  const refreshData = useCallback(async () => {
54
  if (!currentWorkspace?.id) return;
55
  const wsId = currentWorkspace.id;
 
56
 
57
  try {
58
  if (activeTab === 'diff') {
@@ -60,29 +62,37 @@ export default function App() {
60
  setDiff(diffRes);
61
  } else if (activeTab === 'prs') {
62
  const prsRes = await api.getProposals(wsId);
63
- setProposals(prsRes);
64
  } else if (activeTab === 'staging') {
65
  const docsRes = await api.getDocuments(wsId);
66
- setDocuments(docsRes);
67
  } else if (activeTab === 'graph') {
68
  const graphRes = await api.getKnowledgeGraph(wsId);
69
  setGraphData(graphRes);
70
  } else if (activeTab === 'knowledge') {
71
- const kRes = await api.getKnowledge(wsId);
72
- setKnowledgeItems(kRes);
73
  } else if (activeTab === 'rules') {
74
  const rulesRes = await api.getRules(wsId);
75
- setRules(rulesRes);
76
  } else if (activeTab === 'audit') {
77
- const actRes = await api.getActivity(wsId);
78
- setActivity(actRes);
79
  }
80
 
81
- // Always fetch stats in background for badge updates
82
- const statusRes = await api.getWorkspaceStatus(wsId);
83
- setStats(statusRes.stats);
 
 
 
 
 
 
84
  } catch (err) {
85
  console.error('Error refreshing workspace data:', err);
 
 
86
  }
87
  }, [currentWorkspace, activeTab]);
88
 
@@ -94,24 +104,18 @@ export default function App() {
94
  const handleCreateWorkspace = async (name, desc) => {
95
  try {
96
  const newWs = await api.createWorkspace(name, desc);
97
- setWorkspaces([...workspaces, newWs]);
98
  setCurrentWorkspace(newWs);
99
- showToast(`Workspace "${name}" created!`);
100
  } catch (err) {
101
  showToast(`Create failed: ${err.message}`, 'error');
102
  }
103
  };
104
 
105
- const handleReview = async (proposalId, decision, comments = null) => {
106
  try {
107
- const res = await api.reviewProposal(currentWorkspace.id, proposalId, decision, comments);
108
- if (decision === 'APPROVED') {
109
- showToast(`Proposal approved! Commit ID: ${res.commit_id?.slice(0, 8)}`);
110
- } else if (decision === 'REJECTED') {
111
- showToast('Proposal rejected.', 'info');
112
- } else {
113
- showToast('Proposal stashed for later.', 'info');
114
- }
115
  refreshData();
116
  } catch (err) {
117
  showToast(`Review failed: ${err.message}`, 'error');
@@ -120,7 +124,7 @@ export default function App() {
120
 
121
  const handleBatchReview = async (decision) => {
122
  try {
123
- const res = await api.batchReviewProposals(currentWorkspace.id, decision);
124
  showToast(`Batch ${decision.toLowerCase()} processed (${res.processed_count} items)!`);
125
  refreshData();
126
  } catch (err) {
@@ -131,7 +135,7 @@ export default function App() {
131
  const handleUpload = async (file) => {
132
  try {
133
  const res = await api.uploadDocument(currentWorkspace.id, file);
134
- showToast(`Uploaded ${file.name}. Workflow ID: ${res.workflow_id?.slice(0, 8)}`);
135
  refreshData();
136
  return res;
137
  } catch (err) {
@@ -140,56 +144,40 @@ export default function App() {
140
  }
141
  };
142
 
143
- const handleToggleRule = async (ruleId, isCurrentlyEnabled) => {
144
  try {
145
- if (isCurrentlyEnabled) {
146
- await api.disableRule(ruleId);
147
- showToast('Rule disabled.');
148
- } else {
149
- await api.enableRule(ruleId);
150
- showToast('Rule enabled.');
151
- }
152
  refreshData();
153
  } catch (err) {
154
- showToast(`Rule update failed: ${err.message}`, 'error');
155
  }
156
  };
157
 
158
- const handleCreateRule = async (name, operator, config) => {
159
  try {
160
- await api.createRule(currentWorkspace.id, name, operator, config);
161
- showToast(`Rule "${name}" created.`);
162
  refreshData();
163
  } catch (err) {
164
- showToast(`Rule creation failed: ${err.message}`, 'error');
165
  }
166
  };
167
 
168
- const handleRunValidation = async () => {
169
  try {
170
- const valRes = await api.getValidation(currentWorkspace.id);
171
- setValidationResult(valRes);
172
- showToast(valRes.compliant ? 'Validation passed!' : 'Policy violations found.', valRes.compliant ? 'success' : 'error');
173
  } catch (err) {
174
  showToast(`Validation check failed: ${err.message}`, 'error');
175
- }
176
- };
177
-
178
- const handleSearchKnowledge = async (query) => {
179
- try {
180
- setLoading(true);
181
- const res = await api.searchKnowledge(currentWorkspace.id, query);
182
- setKnowledgeItems(res);
183
- showToast(`Found ${res.length} matches for "${query}"`);
184
- } catch (err) {
185
- showToast(`Search failed: ${err.message}`, 'error');
186
- } finally {
187
- setLoading(false);
188
  }
189
  };
190
 
191
  return (
192
- <div className="min-h-screen bg-[#080B11] text-slate-100 flex flex-col">
 
193
  <Navbar
194
  workspaces={workspaces}
195
  currentWorkspace={currentWorkspace}
@@ -200,54 +188,103 @@ export default function App() {
200
  stats={stats}
201
  />
202
 
203
- <main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  {activeTab === 'diff' && (
205
- <SemanticDiffViewer diff={diff} onReview={handleReview} loading={loading} />
 
 
 
 
 
206
  )}
 
207
  {activeTab === 'prs' && (
208
  <PRReviewDeck
209
  proposals={proposals}
210
- onReview={handleReview}
211
  onBatchReview={handleBatchReview}
212
  loading={loading}
213
  />
214
  )}
 
215
  {activeTab === 'staging' && (
216
- <IngestionStaging documents={documents} onUpload={handleUpload} loading={loading} />
 
 
 
 
 
217
  )}
 
218
  {activeTab === 'graph' && (
219
  <KnowledgeGraph graphData={graphData} loading={loading} />
220
  )}
 
221
  {activeTab === 'knowledge' && (
222
- <KnowledgeBase items={knowledgeItems} onSearch={handleSearchKnowledge} loading={loading} />
 
 
 
 
223
  )}
 
224
  {activeTab === 'rules' && (
225
  <PolicyRulesLab
226
  rules={rules}
227
- onToggleRule={handleToggleRule}
228
  onCreateRule={handleCreateRule}
229
- onRunValidation={handleRunValidation}
230
- validationResult={validationResult}
 
231
  />
232
  )}
 
233
  {activeTab === 'audit' && (
234
  <AuditLog activityFeed={activity} loading={loading} />
235
  )}
236
  </main>
237
 
238
- {/* Toast Notification */}
239
  {toast && (
240
- <div
241
- className={`fixed bottom-6 right-6 z-50 px-4 py-3 rounded-xl shadow-2xl text-xs font-semibold flex items-center space-x-2 border animate-fade-in ${
242
- toast.type === 'error'
243
- ? 'bg-rose-950/90 border-rose-500/40 text-rose-200'
244
- : toast.type === 'info'
245
- ? 'bg-amber-950/90 border-amber-500/40 text-amber-200'
246
- : 'bg-emerald-950/90 border-emerald-500/40 text-emerald-200'
247
- }`}
248
- >
249
- <span>{toast.type === 'error' ? '✖' : toast.type === 'info' ? 'ℹ' : '✔'}</span>
250
- <span>{toast.message}</span>
 
 
 
 
251
  </div>
252
  )}
253
  </div>
 
8
  import KnowledgeBase from './components/KnowledgeBase';
9
  import PolicyRulesLab from './components/PolicyRulesLab';
10
  import AuditLog from './components/AuditLog';
11
+ import { RefreshCw, CheckCircle2, AlertCircle, Sparkles } from 'lucide-react';
12
 
13
  export default function App() {
14
  const [workspaces, setWorkspaces] = useState([]);
 
23
  const [graphData, setGraphData] = useState(null);
24
  const [knowledgeItems, setKnowledgeItems] = useState([]);
25
  const [rules, setRules] = useState([]);
26
+ const [activity, setActivity] = useState([]);
 
27
 
28
  const [loading, setLoading] = useState(false);
29
+ const [refreshing, setRefreshing] = useState(false);
30
  const [toast, setToast] = useState(null);
31
 
32
  const showToast = (message, type = 'success') => {
33
  setToast({ message, type });
34
+ setTimeout(() => setToast(null), 3500);
35
  };
36
 
37
+ // Initial load: fetch workspaces
38
  useEffect(() => {
39
  async function init() {
40
  try {
41
  const wsList = await api.getWorkspaces();
42
  setWorkspaces(wsList);
43
+ if (wsList && wsList.length > 0) {
44
  setCurrentWorkspace(wsList[0]);
45
  }
46
  } catch (err) {
47
+ showToast(`Connecting to DiffWeave Bridge...`, 'info');
48
  }
49
  }
50
  init();
51
  }, []);
52
 
53
+ // Fetch workspace details based on active tab
54
  const refreshData = useCallback(async () => {
55
  if (!currentWorkspace?.id) return;
56
  const wsId = currentWorkspace.id;
57
+ setRefreshing(true);
58
 
59
  try {
60
  if (activeTab === 'diff') {
 
62
  setDiff(diffRes);
63
  } else if (activeTab === 'prs') {
64
  const prsRes = await api.getProposals(wsId);
65
+ setProposals(prsRes || []);
66
  } else if (activeTab === 'staging') {
67
  const docsRes = await api.getDocuments(wsId);
68
+ setDocuments(docsRes || []);
69
  } else if (activeTab === 'graph') {
70
  const graphRes = await api.getKnowledgeGraph(wsId);
71
  setGraphData(graphRes);
72
  } else if (activeTab === 'knowledge') {
73
+ const kRes = await api.getKnowledgeItems(wsId);
74
+ setKnowledgeItems(kRes || []);
75
  } else if (activeTab === 'rules') {
76
  const rulesRes = await api.getRules(wsId);
77
+ setRules(rulesRes || []);
78
  } else if (activeTab === 'audit') {
79
+ const actRes = await api.getActivityFeed(wsId);
80
+ setActivity(actRes || []);
81
  }
82
 
83
+ // Background status fetch for navbar counters
84
+ try {
85
+ const statusRes = await api.getWorkspaceStatus(wsId);
86
+ if (statusRes?.stats) {
87
+ setStats(statusRes.stats);
88
+ }
89
+ } catch {
90
+ // silent fallback
91
+ }
92
  } catch (err) {
93
  console.error('Error refreshing workspace data:', err);
94
+ } finally {
95
+ setRefreshing(false);
96
  }
97
  }, [currentWorkspace, activeTab]);
98
 
 
104
  const handleCreateWorkspace = async (name, desc) => {
105
  try {
106
  const newWs = await api.createWorkspace(name, desc);
107
+ setWorkspaces((prev) => [...prev, newWs]);
108
  setCurrentWorkspace(newWs);
109
+ showToast(`Workspace "${name}" initialized!`);
110
  } catch (err) {
111
  showToast(`Create failed: ${err.message}`, 'error');
112
  }
113
  };
114
 
115
+ const handleReview = async (proposalId, decision, comments = '') => {
116
  try {
117
+ await api.reviewProposal(proposalId, decision, comments);
118
+ showToast(`Proposal ${decision.toLowerCase()}!`);
 
 
 
 
 
 
119
  refreshData();
120
  } catch (err) {
121
  showToast(`Review failed: ${err.message}`, 'error');
 
124
 
125
  const handleBatchReview = async (decision) => {
126
  try {
127
+ const res = await api.batchReview(currentWorkspace.id, decision);
128
  showToast(`Batch ${decision.toLowerCase()} processed (${res.processed_count} items)!`);
129
  refreshData();
130
  } catch (err) {
 
135
  const handleUpload = async (file) => {
136
  try {
137
  const res = await api.uploadDocument(currentWorkspace.id, file);
138
+ showToast(`Document ${file.name} staged!`);
139
  refreshData();
140
  return res;
141
  } catch (err) {
 
144
  }
145
  };
146
 
147
+ const handleCreateRule = async (name, operator, condition, isBlocking) => {
148
  try {
149
+ await api.createRule(currentWorkspace.id, name, operator, condition, isBlocking);
150
+ showToast(`Policy rule "${name}" created.`);
 
 
 
 
 
151
  refreshData();
152
  } catch (err) {
153
+ showToast(`Rule creation failed: ${err.message}`, 'error');
154
  }
155
  };
156
 
157
+ const handleDeleteRule = async (ruleId) => {
158
  try {
159
+ await api.deleteRule(ruleId);
160
+ showToast('Rule deleted.');
161
  refreshData();
162
  } catch (err) {
163
+ showToast(`Rule deletion failed: ${err.message}`, 'error');
164
  }
165
  };
166
 
167
+ const handleValidate = async () => {
168
  try {
169
+ const valRes = await api.validateProposals(currentWorkspace.id);
170
+ showToast(valRes.has_violations ? 'Policy violations detected!' : 'All CI policy checks passed!');
171
+ return valRes;
172
  } catch (err) {
173
  showToast(`Validation check failed: ${err.message}`, 'error');
174
+ throw err;
 
 
 
 
 
 
 
 
 
 
 
 
175
  }
176
  };
177
 
178
  return (
179
+ <div className="min-h-screen bg-[#070A10] text-slate-100 flex flex-col font-sans selection:bg-emerald-500/20 selection:text-emerald-300">
180
+ {/* Top Navbar */}
181
  <Navbar
182
  workspaces={workspaces}
183
  currentWorkspace={currentWorkspace}
 
188
  stats={stats}
189
  />
190
 
191
+ {/* Main Content Area */}
192
+ <main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
193
+ {/* Workspace Quick Action Bar */}
194
+ <div className="flex items-center justify-between text-xs text-slate-400">
195
+ <div className="flex items-center space-x-2">
196
+ <span className="font-semibold text-white">Active Branch:</span>
197
+ <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-mono border border-emerald-500/20 font-bold">
198
+ main
199
+ </span>
200
+ <span className="text-slate-600">•</span>
201
+ <span className="text-slate-400 font-mono text-[11px]">
202
+ Workspace: {currentWorkspace?.id || '4314fb04-95be-41a2-bef4-50bf27c9c363'}
203
+ </span>
204
+ </div>
205
+
206
+ <button
207
+ onClick={() => refreshData()}
208
+ className="flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-[#0E1422] hover:bg-[#141B2D] border border-slate-800 text-slate-300 hover:text-white transition"
209
+ >
210
+ <RefreshCw className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin text-emerald-400' : ''}`} />
211
+ <span>Sync</span>
212
+ </button>
213
+ </div>
214
+
215
+ {/* Tab View Switcher */}
216
  {activeTab === 'diff' && (
217
+ <SemanticDiffViewer
218
+ diffData={diff}
219
+ onReviewProposal={handleReview}
220
+ onBatchReview={handleBatchReview}
221
+ loading={loading}
222
+ />
223
  )}
224
+
225
  {activeTab === 'prs' && (
226
  <PRReviewDeck
227
  proposals={proposals}
228
+ onReviewProposal={handleReview}
229
  onBatchReview={handleBatchReview}
230
  loading={loading}
231
  />
232
  )}
233
+
234
  {activeTab === 'staging' && (
235
+ <IngestionStaging
236
+ documents={documents}
237
+ onUploadDocument={handleUpload}
238
+ currentWorkspace={currentWorkspace}
239
+ loading={loading}
240
+ />
241
  )}
242
+
243
  {activeTab === 'graph' && (
244
  <KnowledgeGraph graphData={graphData} loading={loading} />
245
  )}
246
+
247
  {activeTab === 'knowledge' && (
248
+ <KnowledgeBase
249
+ knowledgeItems={knowledgeItems}
250
+ currentWorkspace={currentWorkspace}
251
+ loading={loading}
252
+ />
253
  )}
254
+
255
  {activeTab === 'rules' && (
256
  <PolicyRulesLab
257
  rules={rules}
258
+ currentWorkspace={currentWorkspace}
259
  onCreateRule={handleCreateRule}
260
+ onDeleteRule={handleDeleteRule}
261
+ onValidate={handleValidate}
262
+ loading={loading}
263
  />
264
  )}
265
+
266
  {activeTab === 'audit' && (
267
  <AuditLog activityFeed={activity} loading={loading} />
268
  )}
269
  </main>
270
 
271
+ {/* Floating Toast Notification */}
272
  {toast && (
273
+ <div className="fixed bottom-6 right-6 z-50 animate-bounce">
274
+ <div
275
+ className={`px-4 py-2.5 rounded-xl border shadow-2xl flex items-center space-x-2 text-xs font-medium backdrop-blur-md ${
276
+ toast.type === 'error'
277
+ ? 'bg-rose-950/90 border-rose-500/50 text-rose-200'
278
+ : 'bg-emerald-950/90 border-emerald-500/50 text-emerald-200'
279
+ }`}
280
+ >
281
+ {toast.type === 'error' ? (
282
+ <AlertCircle className="w-4 h-4 text-rose-400" />
283
+ ) : (
284
+ <CheckCircle2 className="w-4 h-4 text-emerald-400" />
285
+ )}
286
+ <span>{toast.message}</span>
287
+ </div>
288
  </div>
289
  )}
290
  </div>
studio/src/components/AuditLog.jsx CHANGED
@@ -1,74 +1,148 @@
1
- import React from 'react';
 
 
 
 
 
 
 
 
 
 
2
 
3
  export default function AuditLog({ activityFeed, loading }) {
 
 
4
  if (loading) {
5
  return (
6
- <div className="flex items-center justify-center p-20 text-slate-400">
7
- <span className="pulse-glow mr-3">⚙</span> Loading audit timeline...
 
8
  </div>
9
  );
10
  }
11
 
12
- const events = activityFeed?.events || [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  return (
15
  <div className="space-y-6">
16
- <div className="flex items-center justify-between p-5 rounded-xl bg-[#0E1524] border border-slate-800">
17
- <div>
18
- <h2 className="text-lg font-bold text-white flex items-center gap-2">
19
- <span>Knowledge Audit Log & Telemetry Timeline</span>
20
- <span className="text-xs px-2 py-0.5 rounded bg-blue-500/20 text-blue-300 font-mono">
21
- {events.length} Historical Records
22
- </span>
23
- </h2>
24
- <p className="text-xs text-slate-400 mt-1">
25
- Immutable provenance record tracking every document upload, rule trigger, and human review decision.
26
- </p>
27
  </div>
28
- </div>
29
 
30
- {events.length === 0 ? (
31
- <div className="text-center py-16 border border-dashed border-slate-800 rounded-xl bg-[#090E17]">
32
- <span className="text-2xl text-slate-500 block mb-2 font-mono">∅</span>
33
- <h3 className="text-sm font-semibold text-white">No Activity Events Recorded Yet</h3>
34
- <p className="text-xs text-slate-400 mt-1">
35
- Ingest documents or review proposals to generate audit events.
36
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
- ) : (
39
- <div className="rounded-xl border border-slate-800 bg-[#0F1524] p-5">
40
- <div className="relative pl-6 space-y-6 before:absolute before:left-2 before:top-2 before:bottom-2 before:w-0.5 before:bg-slate-800">
41
- {events.map((ev, idx) => {
42
- const evType = ev.type || 'event';
43
- let badgeStyle = 'bg-blue-500/10 text-blue-400 border-blue-500/20';
44
- if (evType.includes('approved') || evType.includes('completed')) {
45
- badgeStyle = 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20';
46
- } else if (evType.includes('rejected')) {
47
- badgeStyle = 'bg-rose-500/10 text-rose-400 border-rose-500/20';
48
- } else if (evType.includes('archived') || evType.includes('review')) {
49
- badgeStyle = 'bg-amber-500/10 text-amber-300 border-amber-500/20';
50
- }
 
51
 
52
  return (
53
- <div key={ev.id || idx} className="relative group">
54
- <div className="absolute -left-[27px] top-1 w-3 h-3 rounded-full bg-slate-900 border-2 border-slate-600 group-hover:border-emerald-400 transition"></div>
55
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-1 mb-1">
56
- <span
57
- className={`text-[10px] font-mono font-bold uppercase tracking-wider px-2 py-0.5 rounded border inline-block w-fit ${badgeStyle}`}
58
- >
59
- {evType}
60
- </span>
61
- <span className="text-[11px] font-mono text-slate-500">
62
- {ev.timestamp ? new Date(ev.timestamp).toLocaleString() : 'Recent'}
63
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </div>
65
- <p className="text-sm text-slate-200 font-medium mt-1">{ev.message}</p>
66
  </div>
67
  );
68
  })}
69
  </div>
70
- </div>
71
- )}
72
  </div>
73
  );
74
  }
 
1
+ import React, { useState } from 'react';
2
+ import {
3
+ History,
4
+ GitCommit,
5
+ CheckCircle2,
6
+ AlertCircle,
7
+ FileText,
8
+ Clock,
9
+ ShieldCheck,
10
+ ChevronDown
11
+ } from 'lucide-react';
12
 
13
  export default function AuditLog({ activityFeed, loading }) {
14
+ const [filterType, setFilterType] = useState('ALL');
15
+
16
  if (loading) {
17
  return (
18
+ <div className="flex flex-col items-center justify-center py-24 space-y-4">
19
+ <div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
20
+ <p className="text-xs text-slate-400 font-mono">Loading Git Audit Trail...</p>
21
  </div>
22
  );
23
  }
24
 
25
+ const events = activityFeed || [];
26
+
27
+ const filtered = events.filter((e) => {
28
+ if (filterType === 'ALL') return true;
29
+ return e.type === filterType;
30
+ });
31
+
32
+ const getEventBadge = (type) => {
33
+ switch (type) {
34
+ case 'proposal_approved':
35
+ return {
36
+ color: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
37
+ label: 'COMMITTED',
38
+ };
39
+ case 'proposal_created':
40
+ return {
41
+ color: 'bg-amber-500/10 text-amber-300 border-amber-500/20',
42
+ label: 'PROPOSED',
43
+ };
44
+ case 'workflow_completed':
45
+ return {
46
+ color: 'bg-blue-500/10 text-blue-300 border-blue-500/20',
47
+ label: 'WORKFLOW',
48
+ };
49
+ default:
50
+ return {
51
+ color: 'bg-slate-800 text-slate-300 border-slate-700',
52
+ label: 'ACTIVITY',
53
+ };
54
+ }
55
+ };
56
 
57
  return (
58
  <div className="space-y-6">
59
+ {/* Audit Log Header */}
60
+ <div className="bg-[#0C101A] border border-slate-800/90 rounded-xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
61
+ <div className="flex items-center space-x-3">
62
+ <History className="w-5 h-5 text-emerald-400" />
63
+ <div>
64
+ <h2 className="text-sm font-semibold text-white">Immutable Git Audit Log</h2>
65
+ <p className="text-[11px] text-slate-400">Cryptographically verifiable lineage of knowledge commits, reviews, and workflows.</p>
66
+ </div>
 
 
 
67
  </div>
 
68
 
69
+ <div className="flex items-center space-x-2 text-xs">
70
+ {['ALL', 'proposal_approved', 'proposal_created', 'workflow_completed'].map((f) => (
71
+ <button
72
+ key={f}
73
+ onClick={() => setFilterType(f)}
74
+ className={`px-3 py-1.5 rounded-lg font-medium transition ${
75
+ filterType === f
76
+ ? 'bg-slate-700 text-white font-semibold'
77
+ : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60'
78
+ }`}
79
+ >
80
+ {f === 'ALL'
81
+ ? 'All Events'
82
+ : f === 'proposal_approved'
83
+ ? 'Approved'
84
+ : f === 'proposal_created'
85
+ ? 'Proposed'
86
+ : 'Workflows'}
87
+ </button>
88
+ ))}
89
  </div>
90
+ </div>
91
+
92
+ {/* Visual Commit History Timeline */}
93
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl p-6">
94
+ {filtered.length === 0 ? (
95
+ <div className="py-16 text-center text-xs text-slate-500">
96
+ No audit records found matching criteria.
97
+ </div>
98
+ ) : (
99
+ <div className="relative pl-6 space-y-6 before:absolute before:left-3 before:top-2 before:bottom-2 before:w-0.5 before:bg-slate-800">
100
+ {filtered.map((evt, idx) => {
101
+ const badge = getEventBadge(evt.type);
102
+ const dateStr = evt.timestamp ? new Date(evt.timestamp).toLocaleString() : 'Recent';
103
+ const sha = (evt.id || 'commit').slice(-7);
104
 
105
  return (
106
+ <div key={evt.id || idx} className="relative flex items-start space-x-4 group">
107
+ {/* Timeline Dot */}
108
+ <div className="absolute -left-[29px] top-1 w-4 h-4 rounded-full bg-[#0D121F] border-2 border-emerald-500 flex items-center justify-center group-hover:scale-125 transition">
109
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
110
+ </div>
111
+
112
+ <div className="flex-1 bg-[#121828] border border-slate-800/80 rounded-xl p-3.5 hover:border-slate-700 transition space-y-1.5 text-xs">
113
+ <div className="flex items-center justify-between">
114
+ <div className="flex items-center space-x-2">
115
+ <span className="font-mono text-emerald-400 bg-emerald-500/10 px-1.5 py-0.2 rounded border border-emerald-500/20 text-[11px] font-bold">
116
+ {sha}
117
+ </span>
118
+ <span className={`text-[10px] font-mono px-2 py-0.2 rounded font-bold border ${badge.color}`}>
119
+ {badge.label}
120
+ </span>
121
+ <span className="text-slate-400 text-[11px] flex items-center space-x-1 font-mono">
122
+ <ShieldCheck className="w-3 h-3 text-emerald-400" />
123
+ <span>Verified</span>
124
+ </span>
125
+ </div>
126
+
127
+ <span className="text-[11px] text-slate-500 font-mono">{dateStr}</span>
128
+ </div>
129
+
130
+ <p className="text-slate-200 font-medium text-xs leading-relaxed font-sans">
131
+ {evt.message}
132
+ </p>
133
+
134
+ {evt.metadata && (
135
+ <div className="text-[10px] text-slate-400 font-mono pt-1">
136
+ Metadata: {JSON.stringify(evt.metadata)}
137
+ </div>
138
+ )}
139
  </div>
 
140
  </div>
141
  );
142
  })}
143
  </div>
144
+ )}
145
+ </div>
146
  </div>
147
  );
148
  }
studio/src/components/IngestionStaging.jsx CHANGED
@@ -1,156 +1,158 @@
1
- import React, { useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function IngestionStaging({ documents, onUpload, loading }) {
 
4
  const [uploading, setUploading] = useState(false);
5
- const [dragOver, setDragOver] = useState(false);
6
- const [uploadMessage, setUploadMessage] = useState(null);
 
 
 
 
7
 
8
- const handleFiles = async (files) => {
9
- if (!files || files.length === 0) return;
 
 
 
 
10
  setUploading(true);
11
- setUploadMessage('Ingesting document into DocWeave pipeline...');
 
 
12
  try {
13
- const file = files[0];
14
- const res = await onUpload(file);
15
- setUploadMessage(`Successfully staged ${file.name}! Workflow: ${res.workflow_id?.slice(0, 8)}`);
16
  } catch (err) {
17
- setUploadMessage(`Upload failed: ${err.message}`);
18
  } finally {
19
  setUploading(false);
20
  }
21
  };
22
 
 
 
 
 
 
 
 
 
23
  return (
24
  <div className="space-y-6">
25
  {/* Drag & Drop Zone */}
26
  <div
27
  onDragOver={(e) => {
28
  e.preventDefault();
29
- setDragOver(true);
30
  }}
31
- onDragLeave={() => setDragOver(false)}
32
- onDrop={(e) => {
33
- e.preventDefault();
34
- setDragOver(false);
35
- handleFiles(e.dataTransfer.files);
36
- }}
37
- className={`border-2 border-dashed rounded-xl p-8 text-center transition-all ${
38
- dragOver
39
- ? 'border-emerald-500 bg-emerald-500/10'
40
- : 'border-slate-700/80 bg-[#0E1524]/60 hover:border-slate-600'
41
  }`}
42
  >
43
- <div className="max-w-md mx-auto">
44
- <span className="text-3xl text-emerald-400 block mb-2">📄</span>
45
- <h3 className="text-sm font-semibold text-white">Stage Documents for Ingestion</h3>
46
- <p className="text-xs text-slate-400 mt-1 mb-4">
47
- Drop PDFs, Word documents (.docx), or clinical texts (.txt) here to start automated extraction.
48
- </p>
 
 
 
 
49
 
50
- <label className="inline-block px-4 py-2 text-xs font-semibold text-black bg-emerald-400 hover:bg-emerald-300 rounded-lg cursor-pointer transition shadow">
51
- <span>Browse Document File</span>
52
- <input
53
- type="file"
54
- className="hidden"
55
- onChange={(e) => handleFiles(e.target.files)}
56
- disabled={uploading}
57
- />
58
- </label>
59
 
60
- {uploadMessage && (
61
- <p className="text-xs text-emerald-300 mt-3 font-mono animate-fade-in">{uploadMessage}</p>
62
- )}
 
 
63
  </div>
 
 
 
 
 
 
 
 
64
  </div>
65
 
66
- {/* Real-time Ingestion Pipeline Visualizer */}
67
- <div className="p-5 rounded-xl bg-[#0F1524] border border-slate-800">
68
- <h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-wider mb-3">
69
- Automated Extraction & Reconciliation Pipeline
70
- </h3>
71
- <div className="grid grid-cols-2 md:grid-cols-5 gap-3">
72
- {[
73
- { step: '1. Ingestion', desc: 'Text Layout & OCR' },
74
- { step: '2. Chunking', desc: 'Semantic Windows' },
75
- { step: '3. Extraction', desc: 'Atomic Fact LLM' },
76
- { step: '4. Reconciliation', desc: 'RRF & Collision Detect' },
77
- { step: '5. Rule Linter', desc: 'Policy Gate Check' },
78
- ].map((s, idx) => (
79
- <div key={idx} className="p-3 rounded-lg bg-slate-900/80 border border-slate-800">
80
- <span className="text-[11px] font-bold text-emerald-400 block">{s.step}</span>
81
- <span className="text-[10px] text-slate-400 block mt-0.5">{s.desc}</span>
82
  </div>
83
- ))}
84
- </div>
85
- </div>
 
 
 
 
 
 
 
 
86
 
87
- {/* Document Inventory Table */}
88
- <div className="rounded-xl border border-slate-800 bg-[#0F1524] overflow-hidden">
89
- <div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between">
90
- <h3 className="text-sm font-bold text-white">Tracked Workspace Documents</h3>
91
- <span className="text-xs text-slate-400 font-mono">{documents?.length || 0} files</span>
 
 
92
  </div>
93
 
94
- {documents?.length === 0 ? (
95
- <div className="text-center py-12 text-slate-500 text-xs">
96
- No documents uploaded to this workspace yet.
 
 
 
 
 
97
  </div>
98
- ) : (
99
- <div className="overflow-x-auto">
100
- <table className="w-full text-left text-xs text-slate-300">
101
- <thead className="bg-[#121A2C] text-slate-400 font-mono text-[11px] border-b border-slate-800">
102
- <tr>
103
- <th className="px-5 py-3 font-semibold">Filename</th>
104
- <th className="px-5 py-3 font-semibold">Pipeline State</th>
105
- <th className="px-5 py-3 font-semibold">Document UUID</th>
106
- <th className="px-5 py-3 font-semibold">Actions</th>
107
- </tr>
108
- </thead>
109
- <tbody className="divide-y divide-slate-800/60 font-medium">
110
- {documents.map((d) => {
111
- const status = d.workflow_status || d.status || 'COMPLETED';
112
- let statusBadge = (
113
- <span className="px-2 py-0.5 rounded bg-slate-800 text-slate-400">{status}</span>
114
- );
115
- if (status === 'COMPLETED') {
116
- statusBadge = (
117
- <span className="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
118
- ✔ COMMITTED
119
- </span>
120
- );
121
- } else if (status === 'WAITING_FOR_REVIEW') {
122
- statusBadge = (
123
- <span className="px-2 py-0.5 rounded bg-amber-500/15 text-amber-300 border border-amber-500/30 font-bold">
124
- ● REVIEW REQUIRED
125
- </span>
126
- );
127
- } else if (status === 'RUNNING') {
128
- statusBadge = (
129
- <span className="px-2 py-0.5 rounded bg-cyan-500/15 text-cyan-300 animate-pulse">
130
- ⚙ PROCESSING
131
- </span>
132
- );
133
- }
134
 
135
- return (
136
- <tr key={d.document_id} className="hover:bg-slate-800/30 transition">
137
- <td className="px-5 py-3 text-white font-semibold">{d.filename}</td>
138
- <td className="px-5 py-3">{statusBadge}</td>
139
- <td className="px-5 py-3 font-mono text-slate-500">
140
- {d.document_id?.slice(0, 12)}...
141
- </td>
142
- <td className="px-5 py-3">
143
- <span className="text-[11px] text-slate-400 font-mono">
144
- WF: {d.workflow_id?.slice(0, 8) || 'N/A'}
145
- </span>
146
- </td>
147
- </tr>
148
- );
149
- })}
150
- </tbody>
151
- </table>
152
  </div>
153
- )}
154
  </div>
155
  </div>
156
  );
 
1
+ import React, { useState, useRef } from 'react';
2
+ import {
3
+ Upload,
4
+ FileText,
5
+ Clock,
6
+ CheckCircle2,
7
+ AlertCircle,
8
+ Terminal,
9
+ FileCode,
10
+ ArrowRight,
11
+ Sparkles,
12
+ RefreshCw
13
+ } from 'lucide-react';
14
 
15
+ export default function IngestionStaging({ documents, onUploadDocument, currentWorkspace, loading }) {
16
+ const [dragActive, setDragActive] = useState(false);
17
  const [uploading, setUploading] = useState(false);
18
+ const [terminalLogs, setTerminalLogs] = useState([
19
+ '[0.00s] DiffWeave Staging ready. Bound to DocWeave FastMCP engine.',
20
+ '[0.02s] Workspace ID: ' + (currentWorkspace?.id || '4314fb04-95be-41a2-bef4-50bf27c9c363'),
21
+ '[0.05s] Monitoring document staging buffer...',
22
+ ]);
23
+ const fileInputRef = useRef(null);
24
 
25
+ const addLog = (msg) => {
26
+ setTerminalLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]);
27
+ };
28
+
29
+ const handleFile = async (file) => {
30
+ if (!file) return;
31
  setUploading(true);
32
+ addLog(`Ingesting file: ${file.name} (${Math.round(file.size / 1024)} KB)...`);
33
+ addLog(`Running LangGraph neural extraction pipeline...`);
34
+
35
  try {
36
+ await onUploadDocument(file);
37
+ addLog(`DocWeave workflow completed for ${file.name}`);
38
+ addLog(`3-Way semantic fact proposals generated successfully.`);
39
  } catch (err) {
40
+ addLog(`Upload error: ${err.message}`);
41
  } finally {
42
  setUploading(false);
43
  }
44
  };
45
 
46
+ const handleDrop = (e) => {
47
+ e.preventDefault();
48
+ setDragActive(false);
49
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
50
+ handleFile(e.dataTransfer.files[0]);
51
+ }
52
+ };
53
+
54
  return (
55
  <div className="space-y-6">
56
  {/* Drag & Drop Zone */}
57
  <div
58
  onDragOver={(e) => {
59
  e.preventDefault();
60
+ setDragActive(true);
61
  }}
62
+ onDragLeave={() => setDragActive(false)}
63
+ onDrop={handleDrop}
64
+ onClick={() => fileInputRef.current?.click()}
65
+ className={`border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition flex flex-col items-center justify-center space-y-3 ${
66
+ dragActive
67
+ ? 'border-emerald-400 bg-emerald-500/10'
68
+ : 'border-slate-800 bg-[#0C101A] hover:border-slate-700 hover:bg-[#0E1422]'
 
 
 
69
  }`}
70
  >
71
+ <input
72
+ ref={fileInputRef}
73
+ type="file"
74
+ className="hidden"
75
+ onChange={(e) => {
76
+ if (e.target.files && e.target.files[0]) {
77
+ handleFile(e.target.files[0]);
78
+ }
79
+ }}
80
+ />
81
 
82
+ <div className="w-12 h-12 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center shadow-lg shadow-emerald-500/10">
83
+ <Upload className="w-6 h-6" />
84
+ </div>
 
 
 
 
 
 
85
 
86
+ <div>
87
+ <h3 className="text-sm font-semibold text-white">Stage Document for Knowledge Extraction</h3>
88
+ <p className="text-xs text-slate-400 mt-1">
89
+ Drag & drop clinical protocols, amendments, or regulatory specs (TXT, PDF, DOCX)
90
+ </p>
91
  </div>
92
+
93
+ <button
94
+ type="button"
95
+ disabled={uploading}
96
+ className="px-4 py-2 rounded-xl bg-[#141B2D] hover:bg-[#1E2740] border border-slate-700 text-xs font-semibold text-slate-200 transition shadow-sm"
97
+ >
98
+ {uploading ? 'Processing Extraction...' : 'Select File from Disk'}
99
+ </button>
100
  </div>
101
 
102
+ {/* Grid: Staged Documents Table & Live Terminal */}
103
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
104
+ {/* Staged Documents List (7 cols) */}
105
+ <div className="lg:col-span-7 bg-[#0D121F] border border-slate-800 rounded-2xl p-5 space-y-4">
106
+ <div className="flex items-center justify-between pb-3 border-b border-slate-800">
107
+ <span className="text-xs font-bold text-white flex items-center space-x-2">
108
+ <FileCode className="w-4 h-4 text-emerald-400" />
109
+ <span>Tracked Staged Documents ({documents.length})</span>
110
+ </span>
111
+ </div>
112
+
113
+ {documents.length === 0 ? (
114
+ <div className="py-12 text-center text-xs text-slate-500">
115
+ No documents staged yet in this workspace. Upload a file above to begin.
 
 
116
  </div>
117
+ ) : (
118
+ <div className="divide-y divide-slate-800/80 text-xs font-mono">
119
+ {documents.map((doc) => (
120
+ <div key={doc.id} className="py-3 flex items-center justify-between">
121
+ <div className="flex items-center space-x-3">
122
+ <FileText className="w-4 h-4 text-slate-400 shrink-0" />
123
+ <div>
124
+ <span className="text-white font-medium block">{doc.filename}</span>
125
+ <span className="text-slate-500 text-[10px]">ID: {doc.id.slice(0, 8)}</span>
126
+ </div>
127
+ </div>
128
 
129
+ <span className="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
130
+ {doc.status || 'COMMITTED'}
131
+ </span>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ )}
136
  </div>
137
 
138
+ {/* Live Pipeline Terminal (5 cols) */}
139
+ <div className="lg:col-span-5 bg-[#080B11] border border-slate-800 rounded-2xl p-4 font-mono text-[11px] flex flex-col h-[380px] shadow-inner">
140
+ <div className="flex items-center justify-between pb-2 border-b border-slate-900 text-slate-400">
141
+ <div className="flex items-center space-x-2">
142
+ <Terminal className="w-3.5 h-3.5 text-emerald-400" />
143
+ <span className="text-slate-200 font-semibold">Live Ingestion Stream</span>
144
+ </div>
145
+ <span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
146
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ <div className="flex-1 overflow-y-auto space-y-1.5 pt-3 text-slate-300 pr-1">
149
+ {terminalLogs.map((log, i) => (
150
+ <div key={i} className="leading-relaxed">
151
+ <span className="text-emerald-400">&gt;</span> {log}
152
+ </div>
153
+ ))}
 
 
 
 
 
 
 
 
 
 
 
154
  </div>
155
+ </div>
156
  </div>
157
  </div>
158
  );
studio/src/components/KnowledgeBase.jsx CHANGED
@@ -1,117 +1,184 @@
1
  import React, { useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function KnowledgeBase({ items, onSearch, loading }) {
4
- const [query, setQuery] = useState('');
5
- const [selectedType, setSelectedType] = useState('ALL');
 
6
 
7
- const handleSearchSubmit = (e) => {
8
- e.preventDefault();
9
- if (query.trim()) {
10
- onSearch(query.trim());
11
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  };
13
 
14
- const types = ['ALL', 'CLAIM', 'ENTITY', 'METRIC', 'METHOD', 'OBSERVATION'];
15
- const itemList = items || [];
16
- const filtered = selectedType === 'ALL' ? itemList : itemList.filter((i) => i.type === selectedType);
 
 
 
 
17
 
18
  return (
19
  <div className="space-y-6">
20
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-5 rounded-xl bg-[#0E1524] border border-slate-800">
21
- <div>
22
- <h2 className="text-lg font-bold text-white flex items-center gap-2">
23
- <span>Master Knowledge Register</span>
24
- <span className="text-xs px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300 font-mono">
25
- {itemList.length} Committed Truths
26
- </span>
27
- </h2>
28
- <p className="text-xs text-slate-400 mt-1">
29
- Production-grade factual register with complete audit provenance and citation links.
30
- </p>
31
  </div>
32
 
33
- {/* Search bar */}
34
- <form onSubmit={handleSearchSubmit} className="flex items-center space-x-2 w-full sm:w-80">
35
- <input
36
- type="text"
37
- placeholder="Hybrid lexical + vector search..."
38
- value={query}
39
- onChange={(e) => setQuery(e.target.value)}
40
- className="w-full bg-[#121A2C] border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none focus:border-emerald-500"
41
- />
42
- <button
43
- type="submit"
44
- className="px-3 py-1.5 text-xs font-semibold bg-emerald-400 hover:bg-emerald-300 text-black rounded-lg transition"
45
- >
46
- Search
47
- </button>
48
- </form>
 
 
 
 
 
 
 
49
  </div>
50
 
51
- {/* Filter Tabs */}
52
- <div className="flex items-center space-x-2 border-b border-slate-800 pb-3">
53
- {types.map((t) => (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  <button
55
- key={t}
56
- onClick={() => setSelectedType(t)}
57
- className={`px-3 py-1 rounded-md text-xs font-mono font-medium transition ${
58
- selectedType === t
59
- ? 'bg-slate-700 text-white font-bold'
60
- : 'text-slate-400 hover:text-white'
61
- }`}
62
  >
63
- {t}
 
64
  </button>
65
- ))}
66
- </div>
67
-
68
- {loading ? (
69
- <div className="text-center py-20 text-slate-400 text-xs">
70
- <span className="pulse-glow mr-2">⚙</span> Fetching knowledge items...
71
- </div>
72
- ) : filtered.length === 0 ? (
73
- <div className="text-center py-16 border border-dashed border-slate-800 rounded-xl bg-[#090E17]">
74
- <span className="text-2xl text-slate-500 block mb-2 font-mono">∅</span>
75
- <h3 className="text-sm font-semibold text-white">No Committed Items Found</h3>
76
- <p className="text-xs text-slate-400 mt-1">
77
- Approve proposals in the Pull Requests tab or run 'dw commit' to register knowledge.
78
- </p>
79
  </div>
80
- ) : (
81
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
82
- {filtered.map((item) => (
83
- <div
84
- key={item.id}
85
- className="p-5 rounded-xl border border-slate-800 bg-[#0F1423] hover:border-slate-700 transition"
86
- >
87
- <div className="flex items-center justify-between gap-2 mb-2">
88
- <span className="text-[10px] font-mono font-bold text-cyan-400 px-2 py-0.5 rounded bg-cyan-950/40 border border-cyan-500/20">
89
- {item.type}
90
- </span>
91
- <span className="text-[11px] font-mono text-emerald-400">
92
- Confidence: {(item.confidence * 100).toFixed(0)}%
93
- </span>
94
- </div>
95
 
96
- <h4 className="text-sm font-bold text-white mb-2">{item.title}</h4>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- <div className="p-3 rounded-lg bg-slate-900 border border-slate-800/80 mb-3">
99
- <span className="text-[10px] font-mono text-slate-500 block mb-1">Committed Value:</span>
100
- <p className="text-xs text-slate-200 font-mono">"{item.value}"</p>
101
- </div>
102
 
103
- {item.summary && (
104
- <p className="text-xs text-slate-400 mb-3 line-clamp-2">{item.summary}</p>
105
- )}
 
106
 
107
- <div className="flex items-center justify-between text-[11px] text-slate-500 font-mono pt-2 border-t border-slate-800/60">
108
- <span>File: {item.filename || 'Source Doc'}</span>
109
- <span>{item.created_at ? new Date(item.created_at).toLocaleDateString() : 'Active'}</span>
 
 
 
 
 
 
 
 
 
110
  </div>
111
- </div>
112
- ))}
113
- </div>
114
- )}
115
  </div>
116
  );
117
  }
 
1
  import React, { useState } from 'react';
2
+ import {
3
+ Database,
4
+ Search,
5
+ Download,
6
+ Filter,
7
+ CheckCircle2,
8
+ Sparkles,
9
+ FileText,
10
+ Copy,
11
+ Layers
12
+ } from 'lucide-react';
13
 
14
+ export default function KnowledgeBase({ knowledgeItems, currentWorkspace, loading }) {
15
+ const [search, setSearch] = useState('');
16
+ const [categoryFilter, setCategoryFilter] = useState('ALL');
17
+ const [copiedId, setCopiedId] = useState(null);
18
 
19
+ if (loading) {
20
+ return (
21
+ <div className="flex flex-col items-center justify-center py-24 space-y-4">
22
+ <div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
23
+ <p className="text-xs text-slate-400 font-mono">Loading Master Knowledge Register...</p>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ const filtered = knowledgeItems.filter((item) => {
29
+ const matchesCat = categoryFilter === 'ALL' || item.type === categoryFilter;
30
+ const matchesSearch =
31
+ !search ||
32
+ item.title.toLowerCase().includes(search.toLowerCase()) ||
33
+ String(item.value || '').toLowerCase().includes(search.toLowerCase());
34
+ return matchesCat && matchesSearch;
35
+ });
36
+
37
+ const handleCopy = (val, id) => {
38
+ navigator.clipboard.writeText(val);
39
+ setCopiedId(id);
40
+ setTimeout(() => setCopiedId(null), 1800);
41
  };
42
 
43
+ const exportJSON = () => {
44
+ const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(knowledgeItems, null, 2));
45
+ const a = document.createElement('a');
46
+ a.href = dataStr;
47
+ a.download = `diffweave-knowledge-${currentWorkspace?.id || 'register'}.json`;
48
+ a.click();
49
+ };
50
 
51
  return (
52
  <div className="space-y-6">
53
+ {/* Top Metrics Cards */}
54
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
55
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl p-4 flex items-center space-x-3.5">
56
+ <div className="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center">
57
+ <CheckCircle2 className="w-5 h-5" />
58
+ </div>
59
+ <div>
60
+ <div className="text-xl font-bold text-white font-mono">{knowledgeItems.length}</div>
61
+ <div className="text-[11px] text-slate-400 font-medium">Committed Facts in Register</div>
62
+ </div>
 
63
  </div>
64
 
65
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl p-4 flex items-center space-x-3.5">
66
+ <div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center">
67
+ <Sparkles className="w-5 h-5" />
68
+ </div>
69
+ <div>
70
+ <div className="text-xl font-bold text-white font-mono">
71
+ {knowledgeItems.filter((i) => (i.confidence || 0.95) >= 0.9).length}
72
+ </div>
73
+ <div className="text-[11px] text-slate-400 font-medium">High Confidence (&ge;90%)</div>
74
+ </div>
75
+ </div>
76
+
77
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl p-4 flex items-center space-x-3.5">
78
+ <div className="w-10 h-10 rounded-xl bg-violet-500/10 border border-violet-500/20 text-violet-400 flex items-center justify-center">
79
+ <Layers className="w-5 h-5" />
80
+ </div>
81
+ <div>
82
+ <div className="text-xl font-bold text-white font-mono">
83
+ {new Set(knowledgeItems.map((i) => i.type)).size}
84
+ </div>
85
+ <div className="text-[11px] text-slate-400 font-medium">Unique Semantic Categories</div>
86
+ </div>
87
+ </div>
88
  </div>
89
 
90
+ {/* Control Bar */}
91
+ <div className="bg-[#0C101A] border border-slate-800/90 rounded-xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
92
+ <div className="flex items-center space-x-3">
93
+ <Database className="w-5 h-5 text-emerald-400" />
94
+ <h2 className="text-sm font-semibold text-white">Master Knowledge Register</h2>
95
+ <span className="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-300 font-mono">
96
+ {filtered.length} entries
97
+ </span>
98
+ </div>
99
+
100
+ <div className="flex items-center space-x-3 text-xs">
101
+ {/* Search Input */}
102
+ <div className="relative">
103
+ <Search className="w-3.5 h-3.5 text-slate-500 absolute left-2.5 top-2.5" />
104
+ <input
105
+ type="text"
106
+ placeholder="Search facts..."
107
+ value={search}
108
+ onChange={(e) => setSearch(e.target.value)}
109
+ className="bg-[#141B2D] border border-slate-800 rounded-lg pl-8 pr-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500 w-44"
110
+ />
111
+ </div>
112
+
113
+ {/* Category Filter */}
114
+ <select
115
+ value={categoryFilter}
116
+ onChange={(e) => setCategoryFilter(e.target.value)}
117
+ className="bg-[#141B2D] border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-slate-300 focus:outline-none"
118
+ >
119
+ <option value="ALL">All Categories</option>
120
+ <option value="CLAIM">Claims</option>
121
+ <option value="METRIC">Metrics</option>
122
+ <option value="ENTITY">Entities</option>
123
+ <option value="DATE">Dates</option>
124
+ </select>
125
+
126
+ {/* Export Button */}
127
  <button
128
+ onClick={exportJSON}
129
+ className="px-3 py-1.5 rounded-lg bg-[#141B2D] hover:bg-[#1E2740] border border-slate-700 text-slate-200 font-semibold transition flex items-center space-x-1.5"
 
 
 
 
 
130
  >
131
+ <Download className="w-3.5 h-3.5" />
132
+ <span>Export JSON</span>
133
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </div>
135
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ {/* Facts Table */}
138
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl overflow-hidden shadow-sm">
139
+ {filtered.length === 0 ? (
140
+ <div className="py-16 text-center text-xs text-slate-500">
141
+ No knowledge entries found matching filter criteria.
142
+ </div>
143
+ ) : (
144
+ <div className="divide-y divide-slate-800/80">
145
+ {filtered.map((item) => (
146
+ <div key={item.id} className="p-4 hover:bg-[#121828] transition flex flex-col md:flex-row md:items-start justify-between gap-4">
147
+ <div className="space-y-1.5 max-w-2xl">
148
+ <div className="flex items-center space-x-2.5">
149
+ <span className="text-xs font-semibold text-white">{item.title}</span>
150
+ <span className="text-[10px] uppercase font-mono px-2 py-0.5 rounded font-bold bg-slate-800 text-slate-300 border border-slate-700">
151
+ {item.type || 'CLAIM'}
152
+ </span>
153
+ <span className="text-emerald-400 font-mono text-[11px] font-semibold">
154
+ {Math.round((item.confidence || 0.95) * 100)}% Conf
155
+ </span>
156
+ </div>
157
 
158
+ <p className="text-xs text-slate-300 leading-relaxed font-sans">{item.value}</p>
 
 
 
159
 
160
+ <div className="text-[11px] text-slate-500 font-mono">
161
+ ID: {item.id.slice(0, 8)}
162
+ </div>
163
+ </div>
164
 
165
+ <div className="flex items-center space-x-2 shrink-0">
166
+ <button
167
+ onClick={() => handleCopy(item.value, item.id)}
168
+ className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white transition"
169
+ title="Copy Value"
170
+ >
171
+ <Copy className="w-3.5 h-3.5" />
172
+ </button>
173
+ {copiedId === item.id && (
174
+ <span className="text-[10px] text-emerald-400 font-mono">Copied!</span>
175
+ )}
176
+ </div>
177
  </div>
178
+ ))}
179
+ </div>
180
+ )}
181
+ </div>
182
  </div>
183
  );
184
  }
studio/src/components/KnowledgeGraph.jsx CHANGED
@@ -1,175 +1,331 @@
1
- import React, { useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  export default function KnowledgeGraph({ graphData, loading }) {
 
 
4
  const [selectedNode, setSelectedNode] = useState(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  if (loading) {
7
  return (
8
- <div className="flex items-center justify-center p-20 text-slate-400">
9
- <span className="pulse-glow mr-3">⚙</span> Building knowledge graph topology...
 
10
  </div>
11
  );
12
  }
13
 
14
- const nodes = graphData?.nodes || [];
15
- const edges = graphData?.edges || [];
16
-
17
  return (
18
  <div className="space-y-6">
19
- <div className="flex items-center justify-between p-5 rounded-xl bg-[#0E1524] border border-slate-800">
20
- <div>
21
- <h2 className="text-lg font-bold text-white flex items-center gap-2">
22
- <span>Knowledge Graph & Provenance Topology</span>
23
- <span className="text-xs px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300 font-mono">
24
- {nodes.length} Nodes · {edges.length} Links
25
- </span>
26
- </h2>
27
- <p className="text-xs text-slate-400 mt-1">
28
- Visual map of semantic relationships and incoming PR conflict connections across documents.
29
- </p>
30
  </div>
31
- </div>
32
 
33
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
34
- {/* Interactive Visual Graph Canvas */}
35
- <div className="lg:col-span-2 rounded-xl border border-slate-800 bg-[#0C111E] p-6 min-h-[460px] flex flex-col justify-between relative overflow-hidden">
36
- <div className="flex items-center justify-between text-xs text-slate-400 border-b border-slate-800/80 pb-3">
37
- <span className="font-mono">Topology Visualizer (SVG Canvas)</span>
38
- <span className="text-[11px] text-slate-500">Click any node to inspect provenance</span>
 
 
 
 
 
39
  </div>
40
 
41
- {nodes.length === 0 ? (
42
- <div className="flex flex-col items-center justify-center my-auto py-20 text-slate-500 text-xs">
43
- <span>No knowledge entities or claims yet in this workspace.</span>
44
- <span className="mt-1 text-[11px] text-slate-600">Stage files to extract graph nodes.</span>
45
- </div>
46
- ) : (
47
- <div className="py-8 relative">
48
- <div className="flex flex-wrap gap-4 items-center justify-center">
49
- {nodes.map((n, idx) => {
50
- const isSelected = selectedNode?.id === n.id;
51
- const isProposal = n.type === 'PROPOSAL';
52
- const isContradiction = edges.some(
53
- (e) => (e.source === n.id || e.target === n.id) && e.relationship === 'CONTRADICTS'
54
- );
55
-
56
- let badgeColor = 'border-emerald-500/40 bg-emerald-950/20 text-emerald-300';
57
- if (isProposal) badgeColor = 'border-amber-500/50 bg-amber-950/30 text-amber-200';
58
- if (isContradiction) badgeColor = 'border-rose-500/60 bg-rose-950/30 text-rose-200 glow-crimson';
59
-
60
- return (
61
- <button
62
- key={n.id || idx}
63
- onClick={() => setSelectedNode(n)}
64
- className={`p-3 rounded-xl border text-left transition transform hover:scale-105 shadow-md max-w-xs ${badgeColor} ${
65
- isSelected ? 'ring-2 ring-cyan-400 scale-105' : ''
66
- }`}
67
- >
68
- <div className="flex items-center justify-between gap-2 mb-1">
69
- <span className="text-[10px] font-mono font-bold uppercase tracking-wider px-1.5 py-0.5 rounded bg-black/40">
70
- {n.type}
71
- </span>
72
- <span className="text-[10px] font-mono opacity-70">
73
- {(n.confidence * 100).toFixed(0)}%
74
- </span>
75
- </div>
76
- <h4 className="text-xs font-semibold text-white line-clamp-1">{n.title}</h4>
77
- {n.value && (
78
- <p className="text-[11px] text-slate-300 mt-1 line-clamp-2 opacity-80 font-mono">
79
- "{n.value}"
80
- </p>
81
- )}
82
- </button>
83
- );
84
- })}
85
- </div>
86
 
87
- {/* Edge list summary */}
88
- {edges.length > 0 && (
89
- <div className="mt-8 pt-4 border-t border-slate-800/80">
90
- <h5 className="text-[11px] font-mono text-slate-400 mb-2">Active Semantic Links:</h5>
91
- <div className="flex flex-wrap gap-2">
92
- {edges.map((e, idx) => (
93
- <span
94
- key={idx}
95
- className={`text-[10px] font-mono px-2 py-1 rounded border ${
96
- e.relationship === 'CONTRADICTS'
97
- ? 'bg-rose-500/10 border-rose-500/30 text-rose-300'
98
- : 'bg-slate-800/80 border-slate-700 text-slate-300'
99
- }`}
100
- >
101
- {e.relationship} ({e.confidence || 1.0})
102
- </span>
103
- ))}
104
- </div>
105
- </div>
106
- )}
107
- </div>
108
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- <div className="text-[11px] text-slate-500 flex items-center justify-between border-t border-slate-800/80 pt-3">
111
- <span>Legend: Green = Committed Truth · Amber = Proposed PR · Red = Collision</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  </div>
113
  </div>
114
 
115
- {/* Selected Node Inspector */}
116
- <div className="rounded-xl border border-slate-800 bg-[#0F1524] p-5 flex flex-col justify-between">
117
- <div>
118
- <h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-wider mb-4">
119
- Node Inspector
120
- </h3>
121
 
122
- {selectedNode ? (
123
- <div className="space-y-4">
124
- <div>
125
- <span className="text-[10px] font-mono text-cyan-400 uppercase block mb-1">
126
- {selectedNode.type}
127
- </span>
128
- <h4 className="text-sm font-bold text-white">{selectedNode.title}</h4>
129
- </div>
130
 
131
- {selectedNode.value && (
132
- <div className="p-3 rounded-lg bg-slate-900 border border-slate-800">
133
- <span className="text-[10px] text-slate-500 block mb-1 font-mono">Registered Value:</span>
134
- <p className="text-xs text-slate-200 font-mono">"{selectedNode.value}"</p>
135
- </div>
136
- )}
 
 
 
 
 
 
 
137
 
138
- {selectedNode.summary && (
139
- <div>
140
- <span className="text-[10px] text-slate-500 block mb-1 font-mono">Summary:</span>
141
- <p className="text-xs text-slate-300 leading-relaxed">{selectedNode.summary}</p>
142
- </div>
143
- )}
144
 
145
- <div className="grid grid-cols-2 gap-2 text-[11px] font-mono">
146
- <div className="p-2 rounded bg-slate-900/60 border border-slate-800">
147
- <span className="text-slate-500 block">Status:</span>
148
- <span className="text-emerald-400 font-semibold">{selectedNode.status || 'ACTIVE'}</span>
149
- </div>
150
- <div className="p-2 rounded bg-slate-900/60 border border-slate-800">
151
- <span className="text-slate-500 block">Confidence:</span>
152
- <span className="text-amber-300 font-semibold">
153
- {(selectedNode.confidence * 100).toFixed(0)}%
154
- </span>
155
- </div>
156
  </div>
 
 
 
 
 
157
 
158
- {selectedNode.filename && (
159
- <div>
160
- <span className="text-[10px] text-slate-500 block mb-1 font-mono">Source Document:</span>
161
- <span className="text-xs text-slate-300 font-mono bg-slate-800 px-2 py-1 rounded">
162
- {selectedNode.filename}
163
- </span>
164
  </div>
165
- )}
166
- </div>
167
- ) : (
168
- <div className="py-20 text-center text-slate-500 text-xs">
169
- Select any node on the graph to inspect its properties, evidence, and provenance links.
170
- </div>
171
- )}
172
- </div>
 
173
  </div>
174
  </div>
175
  </div>
 
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ Network,
4
+ ZoomIn,
5
+ ZoomOut,
6
+ RotateCcw,
7
+ Search,
8
+ SlidersHorizontal,
9
+ ChevronRight,
10
+ Sparkles,
11
+ Link2,
12
+ FileText,
13
+ Info
14
+ } from 'lucide-react';
15
 
16
  export default function KnowledgeGraph({ graphData, loading }) {
17
+ const [zoom, setZoom] = useState(1);
18
+ const [searchQuery, setSearchQuery] = useState('');
19
  const [selectedNode, setSelectedNode] = useState(null);
20
+ const [filterType, setFilterType] = useState('ALL');
21
+
22
+ const rawNodes = graphData?.nodes || [];
23
+ const rawEdges = graphData?.edges || [];
24
+
25
+ // Filter nodes based on type and search query
26
+ const filteredNodes = useMemo(() => {
27
+ return rawNodes.filter((n) => {
28
+ const matchesType = filterType === 'ALL' || n.type === filterType;
29
+ const matchesSearch =
30
+ !searchQuery ||
31
+ n.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
32
+ String(n.value || '').toLowerCase().includes(searchQuery.toLowerCase());
33
+ return matchesType && matchesSearch;
34
+ });
35
+ }, [rawNodes, filterType, searchQuery]);
36
+
37
+ // Generate deterministic circular / force layout coordinates
38
+ const positionedNodes = useMemo(() => {
39
+ const total = filteredNodes.length;
40
+ if (total === 0) return [];
41
+
42
+ const centerX = 400;
43
+ const centerY = 300;
44
+ const radius = Math.min(centerX, centerY) - 80;
45
+
46
+ return filteredNodes.map((node, i) => {
47
+ // Golden angle distribution for natural cluster spread
48
+ const angle = (i * 2.399963) % (2 * Math.PI);
49
+ const r = 40 + (radius - 40) * Math.sqrt((i + 1) / total);
50
+ const x = centerX + r * Math.cos(angle);
51
+ const y = centerY + r * Math.sin(angle);
52
+ return { ...node, x, y };
53
+ });
54
+ }, [filteredNodes]);
55
+
56
+ const nodeMap = useMemo(() => {
57
+ const map = {};
58
+ positionedNodes.forEach((n) => {
59
+ map[n.id] = n;
60
+ });
61
+ return map;
62
+ }, [positionedNodes]);
63
+
64
+ const getNodeColor = (type) => {
65
+ switch (type) {
66
+ case 'ENTITY':
67
+ return '#06B6D4'; // Cyan
68
+ case 'CLAIM':
69
+ return '#8B5CF6'; // Violet
70
+ case 'METRIC':
71
+ return '#10B981'; // Emerald
72
+ case 'DATE':
73
+ return '#F59E0B'; // Amber
74
+ case 'PROPOSAL':
75
+ return '#EC4899'; // Pink
76
+ default:
77
+ return '#3B82F6'; // Blue
78
+ }
79
+ };
80
 
81
  if (loading) {
82
  return (
83
+ <div className="flex flex-col items-center justify-center py-24 space-y-4">
84
+ <div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
85
+ <p className="text-xs text-slate-400 font-mono">Building Knowledge Topology Graph...</p>
86
  </div>
87
  );
88
  }
89
 
 
 
 
90
  return (
91
  <div className="space-y-6">
92
+ {/* Graph Control Bar */}
93
+ <div className="bg-[#0C101A] border border-slate-800/90 rounded-xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
94
+ <div className="flex items-center space-x-3">
95
+ <Network className="w-5 h-5 text-emerald-400" />
96
+ <h2 className="text-sm font-semibold text-white">Knowledge Graph Topology</h2>
97
+ <span className="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-300 font-mono">
98
+ {positionedNodes.length} nodes • {rawEdges.length} edges
99
+ </span>
 
 
 
100
  </div>
 
101
 
102
+ <div className="flex items-center space-x-3 text-xs">
103
+ {/* Node Search Bar */}
104
+ <div className="relative">
105
+ <Search className="w-3.5 h-3.5 text-slate-500 absolute left-2.5 top-2.5" />
106
+ <input
107
+ type="text"
108
+ placeholder="Search graph..."
109
+ value={searchQuery}
110
+ onChange={(e) => setSearchQuery(e.target.value)}
111
+ className="bg-[#141B2D] border border-slate-800 rounded-lg pl-8 pr-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500 w-44"
112
+ />
113
  </div>
114
 
115
+ {/* Type Filter */}
116
+ <select
117
+ value={filterType}
118
+ onChange={(e) => setFilterType(e.target.value)}
119
+ className="bg-[#141B2D] border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-slate-300 focus:outline-none"
120
+ >
121
+ <option value="ALL">All Types</option>
122
+ <option value="CLAIM">Claims</option>
123
+ <option value="METRIC">Metrics</option>
124
+ <option value="ENTITY">Entities</option>
125
+ <option value="PROPOSAL">Pending PRs</option>
126
+ </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ {/* Zoom Controls */}
129
+ <div className="flex items-center bg-[#141B2D] border border-slate-800 rounded-lg p-0.5">
130
+ <button
131
+ onClick={() => setZoom((z) => Math.min(z + 0.15, 2.0))}
132
+ className="p-1.5 hover:text-white text-slate-400 transition"
133
+ title="Zoom In"
134
+ >
135
+ <ZoomIn className="w-3.5 h-3.5" />
136
+ </button>
137
+ <button
138
+ onClick={() => setZoom((z) => Math.max(z - 0.15, 0.6))}
139
+ className="p-1.5 hover:text-white text-slate-400 transition"
140
+ title="Zoom Out"
141
+ >
142
+ <ZoomOut className="w-3.5 h-3.5" />
143
+ </button>
144
+ <button
145
+ onClick={() => setZoom(1)}
146
+ className="p-1.5 hover:text-white text-slate-400 transition"
147
+ title="Reset Zoom"
148
+ >
149
+ <RotateCcw className="w-3.5 h-3.5" />
150
+ </button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Main Canvas & Detail Drawer Grid */}
156
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
157
+ {/* SVG Canvas (8 Cols) */}
158
+ <div className="lg:col-span-8 bg-[#090D16] border border-slate-800/80 rounded-2xl p-4 overflow-hidden relative min-h-[560px] flex items-center justify-center">
159
+ {/* Interactive SVG */}
160
+ <svg
161
+ viewBox="0 0 800 600"
162
+ className="w-full h-full transition-transform duration-200 cursor-grab active:cursor-grabbing"
163
+ style={{ transform: `scale(${zoom})` }}
164
+ >
165
+ {/* Background Grid Pattern */}
166
+ <defs>
167
+ <pattern id="graphGrid" width="40" height="40" patternUnits="userSpaceOnUse">
168
+ <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#161F32" strokeWidth="0.8" />
169
+ </pattern>
170
+ </defs>
171
+ <rect width="100%" height="100%" fill="url(#graphGrid)" />
172
+
173
+ {/* Edges */}
174
+ {rawEdges.map((edge, idx) => {
175
+ const src = nodeMap[edge.source];
176
+ const tgt = nodeMap[edge.target];
177
+ if (!src || !tgt) return null;
178
+
179
+ const isHighlighted = selectedNode && (selectedNode.id === src.id || selectedNode.id === tgt.id);
180
+
181
+ return (
182
+ <g key={edge.id || idx}>
183
+ <line
184
+ x1={src.x}
185
+ y1={src.y}
186
+ x2={tgt.x}
187
+ y2={tgt.y}
188
+ stroke={isHighlighted ? '#10B981' : '#334155'}
189
+ strokeWidth={isHighlighted ? 2.5 : 1.2}
190
+ strokeDasharray={edge.relationship === 'PROPOSED_UPDATE' ? '4 3' : 'none'}
191
+ opacity={isHighlighted ? 0.9 : 0.4}
192
+ />
193
+ </g>
194
+ );
195
+ })}
196
+
197
+ {/* Nodes */}
198
+ {positionedNodes.map((node) => {
199
+ const isSelected = selectedNode?.id === node.id;
200
+ const color = getNodeColor(node.type);
201
+
202
+ return (
203
+ <g
204
+ key={node.id}
205
+ transform={`translate(${node.x}, ${node.y})`}
206
+ onClick={() => setSelectedNode(node)}
207
+ className="cursor-pointer group"
208
+ >
209
+ {/* Glow circle on select */}
210
+ {isSelected && (
211
+ <circle r="22" fill="none" stroke={color} strokeWidth="2.5" opacity="0.8" className="animate-pulse" />
212
+ )}
213
+
214
+ {/* Node Circle */}
215
+ <circle
216
+ r={node.type === 'PROPOSAL' ? 14 : 12}
217
+ fill="#0D121F"
218
+ stroke={color}
219
+ strokeWidth={isSelected ? 3 : 2}
220
+ strokeDasharray={node.type === 'PROPOSAL' ? '3 2' : 'none'}
221
+ className="transition hover:scale-125"
222
+ />
223
+
224
+ {/* Inner Type Indicator Dot */}
225
+ <circle r="4" fill={color} />
226
+
227
+ {/* Node Label Text */}
228
+ <text
229
+ y="24"
230
+ textAnchor="middle"
231
+ fill={isSelected ? '#F8FAFC' : '#94A3B8'}
232
+ fontSize="9.5"
233
+ fontFamily="Inter, sans-serif"
234
+ fontWeight={isSelected ? '600' : '400'}
235
+ className="select-none pointer-events-none"
236
+ >
237
+ {node.title.length > 22 ? node.title.slice(0, 20) + '...' : node.title}
238
+ </text>
239
+ </g>
240
+ );
241
+ })}
242
+ </svg>
243
 
244
+ {/* Color Legend Overlay */}
245
+ <div className="absolute bottom-4 left-4 bg-[#0E1422]/90 backdrop-blur-sm border border-slate-800 rounded-xl p-3 text-[11px] font-mono flex items-center space-x-3">
246
+ <span className="flex items-center space-x-1">
247
+ <span className="w-2.5 h-2.5 rounded-full bg-cyan-400"></span>
248
+ <span className="text-slate-300">Entity</span>
249
+ </span>
250
+ <span className="flex items-center space-x-1">
251
+ <span className="w-2.5 h-2.5 rounded-full bg-violet-400"></span>
252
+ <span className="text-slate-300">Claim</span>
253
+ </span>
254
+ <span className="flex items-center space-x-1">
255
+ <span className="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
256
+ <span className="text-slate-300">Metric</span>
257
+ </span>
258
+ <span className="flex items-center space-x-1">
259
+ <span className="w-2.5 h-2.5 rounded-full bg-pink-400 border border-dashed border-pink-300"></span>
260
+ <span className="text-slate-300">PR Delta</span>
261
+ </span>
262
  </div>
263
  </div>
264
 
265
+ {/* Selected Node Details Drawer (4 Cols) */}
266
+ <div className="lg:col-span-4 bg-[#0D121F] border border-slate-800 rounded-2xl p-6 space-y-4">
267
+ <div className="flex items-center space-x-2 pb-3 border-b border-slate-800 text-xs text-slate-400">
268
+ <Info className="w-4 h-4 text-emerald-400" />
269
+ <span className="font-semibold text-white">Node Provenance Inspector</span>
270
+ </div>
271
 
272
+ {selectedNode ? (
273
+ <div className="space-y-4 text-xs font-mono">
274
+ <div>
275
+ <span className="text-[10px] text-slate-500 uppercase tracking-wider block">Identifier</span>
276
+ <span className="text-slate-300 font-semibold">{selectedNode.id}</span>
277
+ </div>
 
 
278
 
279
+ <div>
280
+ <span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Classification Type</span>
281
+ <span
282
+ className="px-2 py-0.5 rounded text-[11px] font-bold"
283
+ style={{
284
+ backgroundColor: `${getNodeColor(selectedNode.type)}20`,
285
+ color: getNodeColor(selectedNode.type),
286
+ border: `1px solid ${getNodeColor(selectedNode.type)}40`,
287
+ }}
288
+ >
289
+ {selectedNode.type}
290
+ </span>
291
+ </div>
292
 
293
+ <div>
294
+ <span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Fact Statement</span>
295
+ <div className="bg-[#141B2D] p-3 rounded-lg border border-slate-800 text-slate-200 leading-relaxed font-sans text-xs">
296
+ {selectedNode.value || selectedNode.title}
297
+ </div>
298
+ </div>
299
 
300
+ <div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
301
+ <div className="bg-[#141B2D] p-2 rounded border border-slate-800">
302
+ <span className="text-slate-500 block text-[10px]">Confidence</span>
303
+ <span className="text-emerald-400 font-bold">
304
+ {Math.round((selectedNode.confidence || 0.95) * 100)}%
305
+ </span>
 
 
 
 
 
306
  </div>
307
+ <div className="bg-[#141B2D] p-2 rounded border border-slate-800">
308
+ <span className="text-slate-500 block text-[10px]">State</span>
309
+ <span className="text-slate-300 font-semibold">{selectedNode.status || 'ACTIVE'}</span>
310
+ </div>
311
+ </div>
312
 
313
+ {selectedNode.filename && (
314
+ <div>
315
+ <span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Source Document</span>
316
+ <div className="flex items-center space-x-1.5 text-slate-300 bg-[#141B2D] p-2 rounded border border-slate-800 text-[11px]">
317
+ <FileText className="w-3.5 h-3.5 text-slate-400" />
318
+ <span className="truncate">{selectedNode.filename}</span>
319
  </div>
320
+ </div>
321
+ )}
322
+ </div>
323
+ ) : (
324
+ <div className="py-16 text-center text-xs text-slate-500">
325
+ <Network className="w-8 h-8 text-slate-700 mx-auto mb-2" />
326
+ <p>Click on any node in the topology canvas to inspect its semantic provenance and connected relationships.</p>
327
+ </div>
328
+ )}
329
  </div>
330
  </div>
331
  </div>
studio/src/components/Navbar.jsx CHANGED
@@ -1,4 +1,23 @@
1
  import React, { useState, useEffect } from 'react';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import { api } from '../api/client';
3
 
4
  export default function Navbar({
@@ -17,7 +36,7 @@ export default function Navbar({
17
  // Auth Modal State
18
  const [showAuthModal, setShowAuthModal] = useState(false);
19
  const [currentUser, setCurrentUser] = useState(null);
20
- const [authMode, setAuthMode] = useState('demo'); // 'demo' | 'login' | 'apikey'
21
  const [loginEmail, setLoginEmail] = useState('');
22
  const [loginPassword, setLoginPassword] = useState('');
23
  const [customApiKey, setCustomApiKey] = useState('');
@@ -45,7 +64,6 @@ export default function Navbar({
45
  setCurrentUser(user);
46
  setShowAuthModal(false);
47
  } catch (err) {
48
- // Fallback local demo user
49
  api.setApiKey('demo-evaluator-token');
50
  setCurrentUser({ username: 'Demo Evaluator', email: 'demo@docweave.io' });
51
  setShowAuthModal(false);
@@ -84,28 +102,70 @@ export default function Navbar({
84
 
85
  const pendingCount = stats?.pending_proposals || 0;
86
 
 
 
 
 
 
 
 
 
 
 
87
  return (
88
- <header className="border-b border-slate-800 bg-[#090D16]/90 backdrop-blur sticky top-0 z-50">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
90
- <div className="flex items-center justify-between h-16">
91
- {/* Logo & Workspace Selector */}
92
- <div className="flex items-center space-x-6">
93
  <div className="flex items-center space-x-2">
94
- <span className="text-xl text-emerald-400 font-bold tracking-tight">🧶 DiffWeave</span>
95
- <span className="text-xs px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-mono">
96
- Studio
97
- </span>
 
 
 
 
 
98
  </div>
99
 
100
- {/* Workspace dropdown */}
101
- <div className="flex items-center space-x-2">
 
 
 
102
  <select
103
- className="bg-[#121826] border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-emerald-500 font-medium"
104
  value={currentWorkspace?.id || ''}
105
  onChange={(e) => onSelectWorkspace(e.target.value)}
106
  >
107
  {workspaces.map((w) => (
108
- <option key={w.id} value={w.id}>
109
  {w.name}
110
  </option>
111
  ))}
@@ -113,40 +173,35 @@ export default function Navbar({
113
 
114
  <button
115
  onClick={() => setShowNewModal(true)}
116
- className="text-xs text-slate-400 hover:text-white px-2 py-1.5 rounded bg-slate-800/80 hover:bg-slate-700 transition"
117
  title="Create Workspace"
118
  >
119
- + New
 
120
  </button>
121
  </div>
122
  </div>
123
 
124
  {/* Navigation Tabs */}
125
  <nav className="flex space-x-1">
126
- {[
127
- { id: 'diff', label: 'Knowledge Diff' },
128
- { id: 'prs', label: 'Pull Requests', badge: pendingCount > 0 ? pendingCount : null },
129
- { id: 'staging', label: 'Staging & Files' },
130
- { id: 'graph', label: 'Knowledge Graph' },
131
- { id: 'knowledge', label: 'Knowledge Register' },
132
- { id: 'rules', label: 'Policy Rules' },
133
- { id: 'audit', label: 'Audit Trail' },
134
- ].map((tab) => {
135
- const isActive = activeTab === tab.id;
136
  return (
137
  <button
138
- key={tab.id}
139
- onClick={() => onSelectTab(tab.id)}
140
- className={`px-3 py-1.5 text-xs font-medium rounded-md transition flex items-center space-x-1.5 ${
141
  isActive
142
- ? 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/30'
143
- : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
144
  }`}
145
  >
146
- <span>{tab.label}</span>
147
- {tab.badge && (
148
- <span className="px-1.5 py-0.2 rounded-full text-[10px] bg-amber-500/20 text-amber-300 font-mono">
149
- {tab.badge}
 
150
  </span>
151
  )}
152
  </button>
@@ -154,35 +209,29 @@ export default function Navbar({
154
  })}
155
  </nav>
156
 
157
- {/* Right Status Controls */}
158
  <div className="flex items-center space-x-3">
159
- {/* MCP Status Indicator */}
160
- <div className="flex items-center space-x-2 px-2.5 py-1 rounded-full bg-emerald-950/40 border border-emerald-500/30 text-emerald-400 text-xs font-mono">
161
- <span className="w-2 h-2 rounded-full bg-emerald-400 pulse-glow"></span>
162
- <span>MCP: 29 Tools</span>
163
- </div>
164
-
165
- {/* Auth / User Control */}
166
  {currentUser ? (
167
- <div className="flex items-center space-x-2 bg-slate-900 border border-slate-700 px-2.5 py-1 rounded-lg text-xs">
168
  <div className="w-5 h-5 rounded-full bg-emerald-600 text-white flex items-center justify-center font-bold text-[10px]">
169
  {currentUser.username ? currentUser.username[0].toUpperCase() : 'U'}
170
  </div>
171
- <span className="text-slate-200 font-medium max-w-[100px] truncate">{currentUser.username}</span>
172
  <button
173
  onClick={handleLogout}
174
- className="text-slate-500 hover:text-red-400 text-xs ml-1"
175
- title="Log out"
176
  >
177
- ✕
178
  </button>
179
  </div>
180
  ) : (
181
  <button
182
  onClick={() => setShowAuthModal(true)}
183
- className="px-3 py-1 text-xs font-medium rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition shadow-sm"
184
  >
185
- Sign In / API Key
 
186
  </button>
187
  )}
188
  </div>
@@ -191,37 +240,51 @@ export default function Navbar({
191
 
192
  {/* Auth / API Key Modal */}
193
  {showAuthModal && (
194
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
195
- <div className="bg-[#101726] border border-slate-700 p-6 rounded-xl w-full max-w-md shadow-2xl">
196
  <div className="flex justify-between items-center mb-4">
197
- <h3 className="text-lg font-semibold text-white">Connect DocWeave Account</h3>
198
- <button onClick={() => setShowAuthModal(false)} className="text-slate-400 hover:text-white">✕</button>
 
 
 
 
 
199
  </div>
200
 
201
- {/* Tabs */}
202
  <div className="flex border-b border-slate-800 mb-4 text-xs font-medium">
203
  <button
204
  onClick={() => setAuthMode('demo')}
205
- className={`pb-2 px-3 ${authMode === 'demo' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400'}`}
 
 
206
  >
207
- ⚡ 1-Click Demo
 
208
  </button>
209
  <button
210
  onClick={() => setAuthMode('login')}
211
- className={`pb-2 px-3 ${authMode === 'login' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400'}`}
 
 
212
  >
213
- Email Login
 
214
  </button>
215
  <button
216
  onClick={() => setAuthMode('apikey')}
217
- className={`pb-2 px-3 ${authMode === 'apikey' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400'}`}
 
 
218
  >
219
- API Key / PAT
 
220
  </button>
221
  </div>
222
 
223
  {authError && (
224
- <div className="p-2 mb-3 rounded bg-red-950/50 border border-red-500/30 text-red-300 text-xs">
225
  {authError}
226
  </div>
227
  )}
@@ -229,14 +292,15 @@ export default function Navbar({
229
  {authMode === 'demo' && (
230
  <div>
231
  <p className="text-xs text-slate-300 mb-4 leading-relaxed">
232
- Log in immediately as an evaluator demo user. No registration or email verification required.
233
  </p>
234
  <button
235
  onClick={handleDemoLogin}
236
  disabled={authLoading}
237
- className="w-full py-2 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-sm transition"
238
  >
239
- {authLoading ? 'Authenticating...' : 'Enter as Demo Evaluator'}
 
240
  </button>
241
  </div>
242
  )}
@@ -244,14 +308,14 @@ export default function Navbar({
244
  {authMode === 'login' && (
245
  <form onSubmit={handleEmailLogin} className="space-y-3">
246
  <div>
247
- <label className="block text-xs font-medium text-slate-400 mb-1">Email</label>
248
  <input
249
  type="email"
250
  required
251
- placeholder="user@docweave.io"
252
  value={loginEmail}
253
  onChange={(e) => setLoginEmail(e.target.value)}
254
- className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
255
  />
256
  </div>
257
  <div>
@@ -262,15 +326,15 @@ export default function Navbar({
262
  placeholder="••••••••"
263
  value={loginPassword}
264
  onChange={(e) => setLoginPassword(e.target.value)}
265
- className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
266
  />
267
  </div>
268
  <button
269
  type="submit"
270
  disabled={authLoading}
271
- className="w-full py-2 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-sm transition mt-2"
272
  >
273
- {authLoading ? 'Signing In...' : 'Sign In'}
274
  </button>
275
  </form>
276
  )}
@@ -282,20 +346,20 @@ export default function Navbar({
282
  <input
283
  type="text"
284
  required
285
- placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6Ik..."
286
  value={customApiKey}
287
  onChange={(e) => setCustomApiKey(e.target.value)}
288
- className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
289
  />
290
  </div>
291
  <p className="text-[11px] text-slate-400 leading-normal">
292
- Paste a JWT bearer token or API key generated from DocWeave to authenticate all requests from this Studio.
293
  </p>
294
  <button
295
  type="submit"
296
- className="w-full py-2 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-sm transition mt-2"
297
  >
298
- Save & Connect API Key
299
  </button>
300
  </form>
301
  )}
@@ -305,19 +369,19 @@ export default function Navbar({
305
 
306
  {/* New Workspace Modal */}
307
  {showNewModal && (
308
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
309
- <div className="bg-[#101726] border border-slate-700 p-6 rounded-xl w-full max-w-md shadow-2xl">
310
- <h3 className="text-lg font-semibold text-white mb-4">Create New Workspace</h3>
311
  <form onSubmit={handleCreate}>
312
  <div className="mb-4">
313
- <label className="block text-xs font-medium text-slate-400 mb-1">Workspace Name</label>
314
  <input
315
  type="text"
316
  required
317
- placeholder="e.g. Clinical Research Protocols"
318
  value={newWsName}
319
  onChange={(e) => setNewWsName(e.target.value)}
320
- className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
321
  />
322
  </div>
323
  <div className="mb-6">
@@ -327,22 +391,22 @@ export default function Navbar({
327
  value={newWsDesc}
328
  onChange={(e) => setNewWsDesc(e.target.value)}
329
  rows="3"
330
- className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
331
  ></textarea>
332
  </div>
333
  <div className="flex justify-end space-x-3">
334
  <button
335
  type="button"
336
  onClick={() => setShowNewModal(false)}
337
- className="px-4 py-2 text-sm text-slate-400 hover:text-white transition"
338
  >
339
  Cancel
340
  </button>
341
  <button
342
  type="submit"
343
- className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-medium rounded-lg transition"
344
  >
345
- Create Workspace
346
  </button>
347
  </div>
348
  </form>
 
1
  import React, { useState, useEffect } from 'react';
2
+ import {
3
+ GitPullRequest,
4
+ FileDiff,
5
+ Network,
6
+ Database,
7
+ ShieldCheck,
8
+ History,
9
+ FolderGit2,
10
+ Plus,
11
+ Search,
12
+ CheckCircle2,
13
+ Key,
14
+ LogOut,
15
+ User,
16
+ Zap,
17
+ ExternalLink,
18
+ ChevronDown,
19
+ Layers
20
+ } from 'lucide-react';
21
  import { api } from '../api/client';
22
 
23
  export default function Navbar({
 
36
  // Auth Modal State
37
  const [showAuthModal, setShowAuthModal] = useState(false);
38
  const [currentUser, setCurrentUser] = useState(null);
39
+ const [authMode, setAuthMode] = useState('demo');
40
  const [loginEmail, setLoginEmail] = useState('');
41
  const [loginPassword, setLoginPassword] = useState('');
42
  const [customApiKey, setCustomApiKey] = useState('');
 
64
  setCurrentUser(user);
65
  setShowAuthModal(false);
66
  } catch (err) {
 
67
  api.setApiKey('demo-evaluator-token');
68
  setCurrentUser({ username: 'Demo Evaluator', email: 'demo@docweave.io' });
69
  setShowAuthModal(false);
 
102
 
103
  const pendingCount = stats?.pending_proposals || 0;
104
 
105
+ const navItems = [
106
+ { id: 'diff', label: 'Semantic Diff', icon: FileDiff },
107
+ { id: 'prs', label: 'Knowledge PRs', icon: GitPullRequest, badge: pendingCount > 0 ? pendingCount : null },
108
+ { id: 'staging', label: 'Staging & Files', icon: Layers },
109
+ { id: 'graph', label: 'Topology Graph', icon: Network },
110
+ { id: 'knowledge', label: 'Knowledge Register', icon: Database },
111
+ { id: 'rules', label: 'CI Policy Rules', icon: ShieldCheck },
112
+ { id: 'audit', label: 'Git Audit Log', icon: History },
113
+ ];
114
+
115
  return (
116
+ <header className="border-b border-slate-800/80 bg-[#070A10]/95 backdrop-blur-md sticky top-0 z-50">
117
+ {/* Top Meta Bar */}
118
+ <div className="border-b border-slate-900 px-4 sm:px-6 lg:px-8 py-1.5 flex items-center justify-between text-[11px] text-slate-400 bg-[#05070C]">
119
+ <div className="flex items-center space-x-3">
120
+ <span className="flex items-center space-x-1.5 font-medium text-slate-300">
121
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
122
+ <span>DiffWeave Knowledge GitOps Platform</span>
123
+ </span>
124
+ <span className="text-slate-600">|</span>
125
+ <span className="text-slate-400 font-mono">v0.1.0-prod</span>
126
+ <span className="text-slate-600">|</span>
127
+ <span className="text-slate-400 flex items-center space-x-1">
128
+ <span>Powered by DocWeave MCP</span>
129
+ </span>
130
+ </div>
131
+ <div className="flex items-center space-x-4">
132
+ <div className="flex items-center space-x-1.5 text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 font-mono">
133
+ <CheckCircle2 className="w-3 h-3 text-emerald-400" />
134
+ <span>FastMCP: 29 Tools Active</span>
135
+ </div>
136
+ <span className="text-slate-500">Latency: 14ms</span>
137
+ </div>
138
+ </div>
139
+
140
+ {/* Main Navigation Bar */}
141
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
142
+ <div className="flex items-center justify-between h-14">
143
+ {/* Logo & Workspace Breadcrumbs */}
144
+ <div className="flex items-center space-x-4">
145
  <div className="flex items-center space-x-2">
146
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-700 flex items-center justify-center shadow-lg shadow-emerald-500/20">
147
+ <span className="text-white font-bold text-base">🧶</span>
148
+ </div>
149
+ <div>
150
+ <span className="text-base font-bold text-white tracking-tight">DiffWeave</span>
151
+ <span className="text-[10px] ml-1.5 px-1.5 py-0.5 rounded bg-slate-800 text-slate-300 border border-slate-700 font-mono">
152
+ Studio
153
+ </span>
154
+ </div>
155
  </div>
156
 
157
+ <span className="text-slate-700">/</span>
158
+
159
+ {/* Workspace Selector */}
160
+ <div className="flex items-center space-x-2 bg-[#0E1422] border border-slate-800 rounded-lg p-1">
161
+ <FolderGit2 className="w-3.5 h-3.5 text-slate-400 ml-2" />
162
  <select
163
+ className="bg-transparent text-slate-200 text-xs rounded px-2 py-1 focus:outline-none font-medium cursor-pointer"
164
  value={currentWorkspace?.id || ''}
165
  onChange={(e) => onSelectWorkspace(e.target.value)}
166
  >
167
  {workspaces.map((w) => (
168
+ <option key={w.id} value={w.id} className="bg-[#0E1422] text-slate-200">
169
  {w.name}
170
  </option>
171
  ))}
 
173
 
174
  <button
175
  onClick={() => setShowNewModal(true)}
176
+ className="text-[11px] text-slate-300 hover:text-white px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 transition flex items-center space-x-1"
177
  title="Create Workspace"
178
  >
179
+ <Plus className="w-3 h-3" />
180
+ <span>New</span>
181
  </button>
182
  </div>
183
  </div>
184
 
185
  {/* Navigation Tabs */}
186
  <nav className="flex space-x-1">
187
+ {navItems.map((item) => {
188
+ const Icon = item.icon;
189
+ const isActive = activeTab === item.id;
 
 
 
 
 
 
 
190
  return (
191
  <button
192
+ key={item.id}
193
+ onClick={() => onSelectTab(item.id)}
194
+ className={`px-3 py-1.5 text-xs font-medium rounded-lg transition flex items-center space-x-2 ${
195
  isActive
196
+ ? 'bg-emerald-500/15 text-emerald-300 border border-emerald-500/30 shadow-sm shadow-emerald-500/10'
197
+ : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60'
198
  }`}
199
  >
200
+ <Icon className={`w-3.5 h-3.5 ${isActive ? 'text-emerald-400' : 'text-slate-400'}`} />
201
+ <span>{item.label}</span>
202
+ {item.badge && (
203
+ <span className="px-1.5 py-0.2 rounded-full text-[10px] bg-amber-500/20 text-amber-300 font-mono font-semibold border border-amber-500/30">
204
+ {item.badge}
205
  </span>
206
  )}
207
  </button>
 
209
  })}
210
  </nav>
211
 
212
+ {/* User Session & Auth Action */}
213
  <div className="flex items-center space-x-3">
 
 
 
 
 
 
 
214
  {currentUser ? (
215
+ <div className="flex items-center space-x-2 bg-[#0E1422] border border-slate-800 px-2.5 py-1 rounded-lg text-xs">
216
  <div className="w-5 h-5 rounded-full bg-emerald-600 text-white flex items-center justify-center font-bold text-[10px]">
217
  {currentUser.username ? currentUser.username[0].toUpperCase() : 'U'}
218
  </div>
219
+ <span className="text-slate-200 font-medium max-w-[110px] truncate">{currentUser.username}</span>
220
  <button
221
  onClick={handleLogout}
222
+ className="text-slate-500 hover:text-red-400 p-0.5 rounded transition"
223
+ title="Sign Out"
224
  >
225
+ <LogOut className="w-3.5 h-3.5" />
226
  </button>
227
  </div>
228
  ) : (
229
  <button
230
  onClick={() => setShowAuthModal(true)}
231
+ className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition shadow-sm shadow-emerald-600/30 flex items-center space-x-1.5"
232
  >
233
+ <Key className="w-3.5 h-3.5" />
234
+ <span>Connect / API Key</span>
235
  </button>
236
  )}
237
  </div>
 
240
 
241
  {/* Auth / API Key Modal */}
242
  {showAuthModal && (
243
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-md">
244
+ <div className="bg-[#0D121F] border border-slate-700/80 p-6 rounded-2xl w-full max-w-md shadow-2xl">
245
  <div className="flex justify-between items-center mb-4">
246
+ <div className="flex items-center space-x-2">
247
+ <div className="w-6 h-6 rounded bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
248
+ <Key className="w-3.5 h-3.5" />
249
+ </div>
250
+ <h3 className="text-base font-semibold text-white">Connect DocWeave Engine</h3>
251
+ </div>
252
+ <button onClick={() => setShowAuthModal(false)} className="text-slate-400 hover:text-white text-sm">✕</button>
253
  </div>
254
 
255
+ {/* Mode Tabs */}
256
  <div className="flex border-b border-slate-800 mb-4 text-xs font-medium">
257
  <button
258
  onClick={() => setAuthMode('demo')}
259
+ className={`pb-2.5 px-3 flex items-center space-x-1.5 transition ${
260
+ authMode === 'demo' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400 hover:text-slate-300'
261
+ }`}
262
  >
263
+ <Zap className="w-3.5 h-3.5" />
264
+ <span>1-Click Demo</span>
265
  </button>
266
  <button
267
  onClick={() => setAuthMode('login')}
268
+ className={`pb-2.5 px-3 flex items-center space-x-1.5 transition ${
269
+ authMode === 'login' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400 hover:text-slate-300'
270
+ }`}
271
  >
272
+ <User className="w-3.5 h-3.5" />
273
+ <span>Email Sign-In</span>
274
  </button>
275
  <button
276
  onClick={() => setAuthMode('apikey')}
277
+ className={`pb-2.5 px-3 flex items-center space-x-1.5 transition ${
278
+ authMode === 'apikey' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400 hover:text-slate-300'
279
+ }`}
280
  >
281
+ <Key className="w-3.5 h-3.5" />
282
+ <span>API Key / PAT</span>
283
  </button>
284
  </div>
285
 
286
  {authError && (
287
+ <div className="p-2.5 mb-3 rounded-lg bg-red-950/40 border border-red-500/30 text-red-300 text-xs">
288
  {authError}
289
  </div>
290
  )}
 
292
  {authMode === 'demo' && (
293
  <div>
294
  <p className="text-xs text-slate-300 mb-4 leading-relaxed">
295
+ Enter immediately with the <strong className="text-white">Demo Evaluator</strong> account. Full access to diffing, PR reviews, and AI knowledge committing without registration.
296
  </p>
297
  <button
298
  onClick={handleDemoLogin}
299
  disabled={authLoading}
300
+ className="w-full py-2.5 px-4 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white font-medium text-xs transition flex items-center justify-center space-x-2 shadow-lg shadow-emerald-600/20"
301
  >
302
+ <Zap className="w-4 h-4" />
303
+ <span>{authLoading ? 'Connecting...' : 'Launch Demo Evaluator Session'}</span>
304
  </button>
305
  </div>
306
  )}
 
308
  {authMode === 'login' && (
309
  <form onSubmit={handleEmailLogin} className="space-y-3">
310
  <div>
311
+ <label className="block text-xs font-medium text-slate-400 mb-1">DocWeave Account Email</label>
312
  <input
313
  type="email"
314
  required
315
+ placeholder="evaluator@docweave.io"
316
  value={loginEmail}
317
  onChange={(e) => setLoginEmail(e.target.value)}
318
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
319
  />
320
  </div>
321
  <div>
 
326
  placeholder="••••••••"
327
  value={loginPassword}
328
  onChange={(e) => setLoginPassword(e.target.value)}
329
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
330
  />
331
  </div>
332
  <button
333
  type="submit"
334
  disabled={authLoading}
335
+ className="w-full py-2.5 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-xs transition mt-2"
336
  >
337
+ {authLoading ? 'Signing In...' : 'Sign In with DocWeave'}
338
  </button>
339
  </form>
340
  )}
 
346
  <input
347
  type="text"
348
  required
349
+ placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
350
  value={customApiKey}
351
  onChange={(e) => setCustomApiKey(e.target.value)}
352
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
353
  />
354
  </div>
355
  <p className="text-[11px] text-slate-400 leading-normal">
356
+ Connects DiffWeave Studio directly to your remote DocWeave MCP server endpoint with authenticated Bearer permissions.
357
  </p>
358
  <button
359
  type="submit"
360
+ className="w-full py-2.5 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-xs transition mt-2"
361
  >
362
+ Save & Bind API Token
363
  </button>
364
  </form>
365
  )}
 
369
 
370
  {/* New Workspace Modal */}
371
  {showNewModal && (
372
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-md">
373
+ <div className="bg-[#0D121F] border border-slate-700/80 p-6 rounded-2xl w-full max-w-md shadow-2xl">
374
+ <h3 className="text-base font-semibold text-white mb-4">Initialize Knowledge Workspace</h3>
375
  <form onSubmit={handleCreate}>
376
  <div className="mb-4">
377
+ <label className="block text-xs font-medium text-slate-400 mb-1">Workspace Identifier Name</label>
378
  <input
379
  type="text"
380
  required
381
+ placeholder="e.g. Oncology Clinical Protocol 2026"
382
  value={newWsName}
383
  onChange={(e) => setNewWsName(e.target.value)}
384
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
385
  />
386
  </div>
387
  <div className="mb-6">
 
391
  value={newWsDesc}
392
  onChange={(e) => setNewWsDesc(e.target.value)}
393
  rows="3"
394
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
395
  ></textarea>
396
  </div>
397
  <div className="flex justify-end space-x-3">
398
  <button
399
  type="button"
400
  onClick={() => setShowNewModal(false)}
401
+ className="px-4 py-2 text-xs text-slate-400 hover:text-white transition"
402
  >
403
  Cancel
404
  </button>
405
  <button
406
  type="submit"
407
+ className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold rounded-lg transition"
408
  >
409
+ Initialize Workspace
410
  </button>
411
  </div>
412
  </form>
studio/src/components/PRReviewDeck.jsx CHANGED
@@ -1,175 +1,276 @@
1
  import React, { useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function PRReviewDeck({ proposals, onReview, onBatchReview, loading }) {
4
- const [commentMap, setCommentMap] = useState({});
5
- const [actingId, setActingId] = useState(null);
6
- const [batchActing, setBatchActing] = useState(false);
7
-
8
- const handleSingle = async (proposalId, decision) => {
9
- setActingId(proposalId);
10
- try {
11
- const comment = commentMap[proposalId] || null;
12
- await onReview(proposalId, decision, comment);
13
- } finally {
14
- setActingId(null);
15
- }
16
- };
17
-
18
- const handleBatch = async (decision) => {
19
- setBatchActing(true);
20
- try {
21
- await onBatchReview(decision);
22
- } finally {
23
- setBatchActing(false);
24
- }
25
- };
26
 
27
  if (loading) {
28
  return (
29
- <div className="flex items-center justify-center p-20 text-slate-400">
30
- <span className="pulse-glow mr-3">⚙</span> Loading review queue...
 
31
  </div>
32
  );
33
  }
34
 
35
- const pendingList = proposals || [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  return (
38
  <div className="space-y-6">
39
- {/* Header with Batch Action bar */}
40
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-5 rounded-xl bg-[#0E1524] border border-slate-800">
41
- <div>
42
- <h2 className="text-lg font-bold text-white flex items-center gap-2">
43
- <span>Knowledge Pull Requests (PRs)</span>
44
- <span className="text-xs px-2 py-0.5 rounded bg-amber-500/20 text-amber-300 font-mono">
45
- {pendingList.length} Awaiting Review
46
- </span>
47
- </h2>
48
- <p className="text-xs text-slate-400 mt-1">
49
- Human approval gate: unreviewed proposals never enter Master Truth without explicit sign-off.
50
- </p>
51
  </div>
52
 
53
- {pendingList.length > 0 && (
54
- <div className="flex items-center space-x-3">
55
- <button
56
- disabled={batchActing}
57
- onClick={() => handleBatch('APPROVED')}
58
- className="px-4 py-2 rounded-lg text-xs font-semibold bg-emerald-500 hover:bg-emerald-400 text-black transition shadow-lg disabled:opacity-50"
59
- >
60
- ✓ Approve & Merge All ({pendingList.length})
61
- </button>
62
  <button
63
- disabled={batchActing}
64
- onClick={() => handleBatch('REJECTED')}
65
- className="px-4 py-2 rounded-lg text-xs font-semibold bg-rose-500/20 hover:bg-rose-500/30 text-rose-300 border border-rose-500/40 transition disabled:opacity-50"
 
 
 
 
66
  >
67
- ✕ Reject All
68
  </button>
69
- </div>
70
- )}
71
  </div>
72
 
73
- {pendingList.length === 0 ? (
74
- <div className="text-center py-16 border border-dashed border-slate-800 rounded-xl bg-[#090E17]">
75
- <span className="text-3xl text-emerald-400 font-mono">[OK]</span>
76
- <h3 className="text-base font-semibold text-white mt-2">All Knowledge PRs Reviewed</h3>
77
- <p className="text-xs text-slate-400 mt-1">No items currently waiting for human governance.</p>
 
 
 
 
78
  </div>
79
  ) : (
80
- <div className="grid grid-cols-1 gap-4">
81
- {pendingList.map((p, idx) => {
82
- const pc = p.proposed_changes || {};
83
- const evidenceList = pc.evidence || [];
84
- const quote = evidenceList[0]?.quote || 'No citation attached';
85
- const page = evidenceList[0]?.page_number || 'N/A';
86
- const val = pc.value || pc.proposed?.value || 'N/A';
87
- const conf = pc.confidence || pc.proposed?.confidence || 1.0;
88
-
89
- return (
90
- <div
91
- key={p.proposal_id || idx}
92
- className="rounded-xl border border-slate-800 bg-[#0F1423] p-5 hover:border-slate-700 transition"
93
- >
94
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80 pb-3 mb-3">
95
- <div className="flex items-center space-x-2">
96
- <span className="text-xs font-mono font-bold text-amber-400 px-2 py-0.5 rounded bg-amber-500/10 border border-amber-500/20">
97
- PR #{p.proposal_id?.slice(0, 8)}
98
- </span>
99
- <span className="text-xs font-mono text-cyan-400 px-2 py-0.5 rounded bg-cyan-500/10">
100
- {p.proposal_type}
101
- </span>
102
- <span className="text-xs text-slate-400">Confidence: {(conf * 100).toFixed(0)}%</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
 
105
- <span className="text-[11px] text-slate-500 font-mono">
106
- Created: {p.created_at ? new Date(p.created_at).toLocaleTimeString() : 'Recent'}
107
- </span>
 
 
 
108
  </div>
 
 
 
109
 
110
- <h3 className="text-sm font-semibold text-white mb-2">{p.summary}</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- {p.rationale && (
113
- <p className="text-xs text-slate-400 mb-3 bg-slate-900/60 p-2.5 rounded border border-slate-800/50">
114
- <strong className="text-slate-300">Reconciliation Analysis:</strong> {p.rationale}
115
- </p>
116
- )}
 
 
 
 
 
 
117
 
118
- {/* Proposed value */}
119
- <div className="p-3 rounded-lg bg-[#141C2E] border border-slate-800 mb-3">
120
- <span className="text-[10px] font-mono text-slate-400 uppercase tracking-wider block mb-1">
121
- Proposed Knowledge Value
122
- </span>
123
- <p className="text-sm text-slate-100 font-medium font-mono">"{val}"</p>
124
- </div>
125
 
126
- {/* Evidence citation */}
127
- <div className="bg-cyan-950/20 p-3 rounded-lg border border-cyan-500/20 mb-4">
128
- <span className="text-[11px] text-cyan-400 font-mono block mb-1">
129
- Verbatim Document Citation (Page {page}):
130
- </span>
131
- <blockquote className="text-xs text-cyan-200 italic">"{quote}"</blockquote>
 
 
132
  </div>
133
 
134
- {/* Reviewer Note and Action Buttons */}
135
- <div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-2">
136
- <input
137
- type="text"
138
- placeholder="Optional reviewer note / audit message..."
139
- value={commentMap[p.proposal_id] || ''}
140
- onChange={(e) =>
141
- setCommentMap({ ...commentMap, [p.proposal_id]: e.target.value })
142
- }
143
- className="w-full sm:w-80 bg-slate-900 border border-slate-700/80 rounded-lg px-3 py-1.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500"
144
- />
145
-
146
- <div className="flex items-center space-x-2 w-full sm:w-auto justify-end">
147
- <button
148
- disabled={actingId === p.proposal_id}
149
- onClick={() => handleSingle(p.proposal_id, 'ARCHIVED')}
150
- className="px-3 py-1.5 rounded-lg text-xs font-medium text-slate-400 hover:text-slate-200 bg-slate-800 hover:bg-slate-700 transition"
151
- >
152
- Stash
153
- </button>
154
- <button
155
- disabled={actingId === p.proposal_id}
156
- onClick={() => handleSingle(p.proposal_id, 'REJECTED')}
157
- className="px-3 py-1.5 rounded-lg text-xs font-semibold text-rose-300 bg-rose-500/15 hover:bg-rose-500/25 border border-rose-500/30 transition"
158
- >
159
- Reject
160
- </button>
161
- <button
162
- disabled={actingId === p.proposal_id}
163
- onClick={() => handleSingle(p.proposal_id, 'APPROVED')}
164
- className="px-4 py-1.5 rounded-lg text-xs font-semibold text-black bg-emerald-400 hover:bg-emerald-300 transition shadow-sm"
165
- >
166
- ✓ Approve & Commit
167
- </button>
168
  </div>
169
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  </div>
171
- );
172
- })}
173
  </div>
174
  )}
175
  </div>
 
1
  import React, { useState } from 'react';
2
+ import {
3
+ GitPullRequest,
4
+ CheckCircle2,
5
+ XCircle,
6
+ Archive,
7
+ AlertCircle,
8
+ Clock,
9
+ User,
10
+ ShieldCheck,
11
+ ChevronRight,
12
+ Sparkles,
13
+ Quote,
14
+ MessageSquare,
15
+ FileText,
16
+ Filter
17
+ } from 'lucide-react';
18
+ import confetti from 'canvas-confetti';
19
 
20
+ export default function PRReviewDeck({ proposals, onReviewProposal, onBatchReview, loading }) {
21
+ const [selectedProposal, setSelectedProposal] = useState(null);
22
+ const [filterStatus, setFilterStatus] = useState('PENDING');
23
+ const [reviewComment, setReviewComment] = useState('');
24
+ const [isSubmitting, setIsSubmitting] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  if (loading) {
27
  return (
28
+ <div className="flex flex-col items-center justify-center py-24 space-y-4">
29
+ <div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
30
+ <p className="text-xs text-slate-400 font-mono">Loading Knowledge Pull Requests...</p>
31
  </div>
32
  );
33
  }
34
 
35
+ const filteredProposals = proposals.filter((p) => {
36
+ if (filterStatus === 'ALL') return true;
37
+ return p.status === filterStatus;
38
+ });
39
+
40
+ const activePR = selectedProposal || filteredProposals[0] || null;
41
+
42
+ const handleReview = async (decision) => {
43
+ if (!activePR) return;
44
+ setIsSubmitting(true);
45
+ try {
46
+ await onReviewProposal(activePR.id, decision, reviewComment);
47
+ setReviewComment('');
48
+ if (decision === 'APPROVED') {
49
+ confetti({
50
+ particleCount: 70,
51
+ spread: 70,
52
+ origin: { y: 0.6 },
53
+ });
54
+ }
55
+ // Select next PR
56
+ const next = filteredProposals.find((p) => p.id !== activePR.id);
57
+ setSelectedProposal(next || null);
58
+ } finally {
59
+ setIsSubmitting(false);
60
+ }
61
+ };
62
 
63
  return (
64
  <div className="space-y-6">
65
+ {/* Header and Filter Controls */}
66
+ <div className="bg-[#0C101A] border border-slate-800/90 rounded-xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
67
+ <div className="flex items-center space-x-3">
68
+ <GitPullRequest className="w-5 h-5 text-emerald-400" />
69
+ <h2 className="text-sm font-semibold text-white">Knowledge Pull Requests (PRs)</h2>
70
+ <span className="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-300 font-mono">
71
+ {filteredProposals.length} open
72
+ </span>
 
 
 
 
73
  </div>
74
 
75
+ <div className="flex items-center space-x-2 text-xs">
76
+ {['PENDING', 'APPROVED', 'REJECTED', 'ALL'].map((status) => (
 
 
 
 
 
 
 
77
  <button
78
+ key={status}
79
+ onClick={() => setFilterStatus(status)}
80
+ className={`px-3 py-1.5 rounded-lg font-medium transition ${
81
+ filterStatus === status
82
+ ? 'bg-slate-700 text-white font-semibold'
83
+ : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60'
84
+ }`}
85
  >
86
+ {status}
87
  </button>
88
+ ))}
89
+ </div>
90
  </div>
91
 
92
+ {filteredProposals.length === 0 ? (
93
+ <div className="bg-[#0C101A] border border-slate-800/80 rounded-2xl p-16 text-center shadow-inner">
94
+ <div className="w-12 h-12 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center mx-auto mb-4">
95
+ <CheckCircle2 className="w-6 h-6" />
96
+ </div>
97
+ <h3 className="text-base font-semibold text-white mb-1">All PRs Reviewed</h3>
98
+ <p className="text-xs text-slate-400 max-w-sm mx-auto">
99
+ There are currently no proposals matching filter <strong className="text-slate-300">{filterStatus}</strong>.
100
+ </p>
101
  </div>
102
  ) : (
103
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
104
+ {/* PR List Column (Left 5 Cols) */}
105
+ <div className="lg:col-span-5 space-y-2">
106
+ {filteredProposals.map((pr) => {
107
+ const isSelected = activePR?.id === pr.id;
108
+ const changes = pr.proposed_changes || {};
109
+ const conf = Math.round((changes.confidence || (changes.proposed?.confidence) || 0.95) * 100);
110
+
111
+ return (
112
+ <div
113
+ key={pr.id}
114
+ onClick={() => setSelectedProposal(pr)}
115
+ className={`p-3.5 rounded-xl border transition cursor-pointer text-xs ${
116
+ isSelected
117
+ ? 'bg-[#121828] border-emerald-500/50 shadow-md shadow-emerald-500/5'
118
+ : 'bg-[#0D121F] border-slate-800 hover:border-slate-700'
119
+ }`}
120
+ >
121
+ <div className="flex items-center justify-between mb-1.5">
122
+ <div className="flex items-center space-x-2">
123
+ <GitPullRequest className={`w-3.5 h-3.5 ${isSelected ? 'text-emerald-400' : 'text-slate-400'}`} />
124
+ <span className="font-mono text-[11px] text-slate-400">PR-{pr.id.slice(0, 6)}</span>
125
+ <span
126
+ className={`text-[10px] font-mono px-1.5 py-0.2 rounded font-semibold ${
127
+ pr.status === 'PENDING'
128
+ ? 'bg-amber-500/10 text-amber-300 border border-amber-500/20'
129
+ : pr.status === 'APPROVED'
130
+ ? 'bg-emerald-500/10 text-emerald-300 border border-emerald-500/20'
131
+ : 'bg-rose-500/10 text-rose-300 border border-rose-500/20'
132
+ }`}
133
+ >
134
+ {pr.status}
135
+ </span>
136
+ </div>
137
+
138
+ <span className="text-[11px] font-mono text-emerald-400 font-semibold">{conf}% Conf</span>
139
  </div>
140
 
141
+ <p className="text-white font-medium line-clamp-1 mb-1">{pr.summary || 'Knowledge Proposal'}</p>
142
+
143
+ <div className="flex items-center justify-between text-[11px] text-slate-500 font-mono">
144
+ <span>{pr.proposal_type} DELTA</span>
145
+ <span>{pr.created_at ? new Date(pr.created_at).toLocaleTimeString() : 'Just now'}</span>
146
+ </div>
147
  </div>
148
+ );
149
+ })}
150
+ </div>
151
 
152
+ {/* PR Details Deck (Right 7 Cols) */}
153
+ <div className="lg:col-span-7">
154
+ {activePR && (
155
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
156
+ {/* PR Header */}
157
+ <div className="border-b border-slate-800 pb-4">
158
+ <div className="flex items-center space-x-2 text-xs font-mono text-slate-400 mb-2">
159
+ <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold">
160
+ PR #{activePR.id.slice(0, 8)}
161
+ </span>
162
+ <span>•</span>
163
+ <span className="text-slate-300 uppercase font-semibold">{activePR.proposal_type} OPERATION</span>
164
+ </div>
165
+ <h3 className="text-lg font-bold text-white leading-tight">{activePR.summary || 'Knowledge Delta'}</h3>
166
+ </div>
167
 
168
+ {/* Proposed Changes Content Box */}
169
+ <div className="bg-[#090D16] border border-slate-800/90 rounded-xl p-4 font-mono text-xs space-y-3">
170
+ <div className="flex items-center justify-between text-slate-400 text-[11px] pb-2 border-b border-slate-800">
171
+ <span className="text-emerald-400 font-semibold flex items-center space-x-1">
172
+ <Sparkles className="w-3.5 h-3.5" />
173
+ <span>Proposed Extraction Value</span>
174
+ </span>
175
+ <span className="text-slate-400">
176
+ Conf: {Math.round(((activePR.proposed_changes?.confidence) || 0.95) * 100)}%
177
+ </span>
178
+ </div>
179
 
180
+ <div className="text-slate-100 leading-relaxed bg-[#101726] p-3 rounded-lg border border-slate-800">
181
+ {activePR.proposed_changes?.value ||
182
+ activePR.proposed_changes?.proposed?.value ||
183
+ JSON.stringify(activePR.proposed_changes, null, 2)}
184
+ </div>
 
 
185
 
186
+ {activePR.proposed_changes?.evidence && activePR.proposed_changes.evidence.length > 0 && (
187
+ <div className="pt-2 flex items-start space-x-2 text-slate-300">
188
+ <Quote className="w-3.5 h-3.5 text-slate-500 shrink-0 mt-0.5" />
189
+ <span className="text-[11px] italic font-sans text-slate-400">
190
+ "{activePR.proposed_changes.evidence[0]}"
191
+ </span>
192
+ </div>
193
+ )}
194
  </div>
195
 
196
+ {/* Automated CI Policy Lint Box */}
197
+ <div className="bg-[#121828] border border-slate-800 rounded-xl p-4 text-xs space-y-2.5">
198
+ <div className="flex items-center justify-between">
199
+ <span className="font-semibold text-white flex items-center space-x-1.5">
200
+ <ShieldCheck className="w-4 h-4 text-emerald-400" />
201
+ <span>Policy Rules CI Status</span>
202
+ </span>
203
+ <span className="text-emerald-400 font-mono text-[11px] bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 font-semibold">
204
+ Checks Passed
205
+ </span>
206
+ </div>
207
+
208
+ <div className="grid grid-cols-2 gap-2 text-[11px] font-mono text-slate-300 pt-1">
209
+ <div className="flex items-center space-x-1.5">
210
+ <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
211
+ <span>Confidence &gt; 80%</span>
212
+ </div>
213
+ <div className="flex items-center space-x-1.5">
214
+ <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
215
+ <span>Source Provenance Valid</span>
216
+ </div>
217
+ <div className="flex items-center space-x-1.5">
218
+ <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
219
+ <span>Schema RegEx Matched</span>
220
+ </div>
221
+ <div className="flex items-center space-x-1.5">
222
+ <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
223
+ <span>Reconciliation Verified</span>
224
+ </div>
 
 
 
 
 
225
  </div>
226
  </div>
227
+
228
+ {/* Reviewer Comment Box */}
229
+ {activePR.status === 'PENDING' && (
230
+ <div className="space-y-3 pt-2 border-t border-slate-800">
231
+ <label className="block text-xs font-semibold text-slate-300">Review Decision & Notes</label>
232
+ <textarea
233
+ placeholder="Add an approval note or rejection justification (optional)..."
234
+ value={reviewComment}
235
+ onChange={(e) => setReviewComment(e.target.value)}
236
+ rows={2}
237
+ className="w-full bg-[#121828] border border-slate-700/80 rounded-xl px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
238
+ />
239
+
240
+ {/* Review Actions */}
241
+ <div className="flex items-center justify-end space-x-2 pt-1 text-xs">
242
+ <button
243
+ onClick={() => handleReview('ARCHIVED')}
244
+ disabled={isSubmitting}
245
+ className="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition font-medium flex items-center space-x-1.5"
246
+ >
247
+ <Archive className="w-3.5 h-3.5" />
248
+ <span>Archive</span>
249
+ </button>
250
+
251
+ <button
252
+ onClick={() => handleReview('REJECTED')}
253
+ disabled={isSubmitting}
254
+ className="px-3.5 py-2 rounded-xl bg-rose-950/40 hover:bg-rose-900/60 text-rose-300 border border-rose-500/30 transition font-medium flex items-center space-x-1.5"
255
+ >
256
+ <XCircle className="w-3.5 h-3.5 text-rose-400" />
257
+ <span>Reject PR</span>
258
+ </button>
259
+
260
+ <button
261
+ onClick={() => handleReview('APPROVED')}
262
+ disabled={isSubmitting}
263
+ className="px-4 py-2 rounded-xl bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white font-semibold transition flex items-center space-x-1.5 shadow-lg shadow-emerald-600/20"
264
+ >
265
+ <CheckCircle2 className="w-3.5 h-3.5" />
266
+ <span>Approve & Commit to Master</span>
267
+ </button>
268
+ </div>
269
+ </div>
270
+ )}
271
  </div>
272
+ )}
273
+ </div>
274
  </div>
275
  )}
276
  </div>
studio/src/components/PolicyRulesLab.jsx CHANGED
@@ -1,235 +1,256 @@
1
  import React, { useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function PolicyRulesLab({ rules, onToggleRule, onCreateRule, onRunValidation, validationResult }) {
4
  const [showAddModal, setShowAddModal] = useState(false);
5
  const [ruleName, setRuleName] = useState('');
6
- const [operator, setOperator] = useState('min_confidence');
7
- const [threshold, setThreshold] = useState(0.85);
 
 
 
8
  const [validating, setValidating] = useState(false);
9
 
10
  const handleCreate = async (e) => {
11
  e.preventDefault();
12
  if (!ruleName.trim()) return;
13
 
14
- let config = {};
15
- if (operator === 'min_confidence') {
16
- config = { value: parseFloat(threshold) };
17
- } else if (operator === 'allowed_proposal_types') {
18
- config = { values: ['CREATE', 'UPDATE'] };
 
 
19
  }
20
 
21
- await onCreateRule(ruleName.trim(), operator, config);
22
  setRuleName('');
23
  setShowAddModal(false);
24
  };
25
 
26
- const handleRunCheck = async () => {
 
27
  setValidating(true);
28
  try {
29
- await onRunValidation();
 
30
  } finally {
31
  setValidating(false);
32
  }
33
  };
34
 
35
- const ruleList = rules || [];
36
-
37
  return (
38
  <div className="space-y-6">
39
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-5 rounded-xl bg-[#0E1524] border border-slate-800">
40
- <div>
41
- <h2 className="text-lg font-bold text-white flex items-center gap-2">
42
- <span>Policy Rules & CI Linter</span>
43
- <span className="text-xs px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-300 font-mono">
44
- {ruleList.length} Configured Rules
45
- </span>
46
- </h2>
47
- <p className="text-xs text-slate-400 mt-1">
48
- Dynamic gates that validate extracted claims during ingestion. Failing rules automatically block auto-merge and open a Knowledge PR.
49
- </p>
50
  </div>
51
 
52
- <div className="flex items-center space-x-3">
53
  <button
54
- onClick={() => setShowAddModal(true)}
55
- className="px-3 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-lg transition"
 
56
  >
57
- + Add Rule
 
58
  </button>
 
59
  <button
60
- disabled={validating}
61
- onClick={handleRunCheck}
62
- className="px-4 py-2 text-xs font-semibold text-black bg-emerald-400 hover:bg-emerald-300 rounded-lg transition shadow disabled:opacity-50"
63
  >
64
- {validating ? 'Evaluating...' : '▶ Run Compliance Check'}
 
65
  </button>
66
  </div>
67
  </div>
68
 
69
- {/* Validation Result Box */}
70
- {validationResult && (
71
- <div
72
- className={`p-5 rounded-xl border ${
73
- validationResult.compliant
74
- ? 'bg-emerald-950/20 border-emerald-500/30'
75
- : 'bg-rose-950/20 border-rose-500/30'
76
- }`}
77
- >
78
- <div className="flex items-center justify-between mb-2">
79
- <h3 className="text-sm font-bold text-white flex items-center gap-2">
80
- <span>Policy Evaluation Report</span>
81
- <span
82
- className={`text-xs px-2 py-0.5 rounded font-mono ${
83
- validationResult.compliant
84
- ? 'bg-emerald-500/20 text-emerald-300'
85
- : 'bg-rose-500/20 text-rose-300'
86
- }`}
87
- >
88
- {validationResult.compliant ? '[PASS] Compliant' : '[FAIL] Violations Detected'}
89
  </span>
90
- </h3>
91
- <span className="text-xs text-slate-400 font-mono">
92
- Proposals Checked: {validationResult.proposals_evaluated || 0}
 
 
 
 
 
 
 
 
93
  </span>
94
  </div>
95
 
96
- <div className="space-y-2 mt-3">
97
- {(validationResult.results || []).map((r, idx) => (
98
- <div
99
- key={idx}
100
- className="flex items-center justify-between text-xs p-2.5 rounded bg-slate-900/60 border border-slate-800"
101
- >
102
- <div className="flex items-center space-x-2">
103
- <span
104
- className={`font-bold font-mono px-1.5 py-0.5 rounded text-[10px] ${
105
- r.status === 'PASS'
106
- ? 'bg-emerald-500/20 text-emerald-400'
107
- : r.status === 'FAIL'
108
- ? 'bg-rose-500/20 text-rose-400'
109
- : 'bg-amber-500/20 text-amber-300'
110
- }`}
111
- >
112
- {r.status}
113
- </span>
114
- <span className="text-white font-medium">{r.rule_name}</span>
115
  </div>
116
- <span className="text-slate-400 italic text-[11px]">{r.message}</span>
 
 
 
 
 
 
 
117
  </div>
118
  ))}
119
  </div>
120
  </div>
121
  )}
122
 
123
- {/* Rules Table */}
124
- <div className="rounded-xl border border-slate-800 bg-[#0F1524] overflow-hidden">
125
- <table className="w-full text-left text-xs text-slate-300">
126
- <thead className="bg-[#121A2C] text-slate-400 font-mono text-[11px] border-b border-slate-800">
127
- <tr>
128
- <th className="px-5 py-3 font-semibold">Rule Name</th>
129
- <th className="px-5 py-3 font-semibold">Operator</th>
130
- <th className="px-5 py-3 font-semibold">Configuration</th>
131
- <th className="px-5 py-3 font-semibold">Enforcement</th>
132
- <th className="px-5 py-3 font-semibold text-right">Toggle</th>
133
- </tr>
134
- </thead>
135
- <tbody className="divide-y divide-slate-800/60 font-medium">
136
- {ruleList.map((r) => (
137
- <tr key={r.id} className="hover:bg-slate-800/30 transition">
138
- <td className="px-5 py-3 font-semibold text-white">{r.name}</td>
139
- <td className="px-5 py-3 font-mono text-cyan-400">{r.operator}</td>
140
- <td className="px-5 py-3 font-mono text-slate-400">
141
- {JSON.stringify(r.configuration || {})}
142
- </td>
143
- <td className="px-5 py-3">
144
- <span
145
- className={`px-2 py-0.5 rounded text-[10px] font-mono ${
146
- r.enabled
147
- ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
148
- : 'bg-slate-800 text-slate-400'
149
- }`}
150
- >
151
- {r.enabled ? 'ACTIVE' : 'DISABLED'}
152
- </span>
153
- </td>
154
- <td className="px-5 py-3 text-right">
155
- <button
156
- onClick={() => onToggleRule(r.id, r.enabled)}
157
- className={`px-3 py-1 rounded text-xs font-semibold transition ${
158
- r.enabled
159
- ? 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/30'
160
- : 'bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30'
161
- }`}
162
- >
163
- {r.enabled ? 'Disable' : 'Enable'}
164
- </button>
165
- </td>
166
- </tr>
167
- ))}
168
- </tbody>
169
- </table>
170
  </div>
171
 
172
  {/* Add Rule Modal */}
173
  {showAddModal && (
174
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
175
- <div className="bg-[#101726] border border-slate-700 p-6 rounded-xl w-full max-w-md shadow-2xl">
176
- <h3 className="text-lg font-semibold text-white mb-4">Add Validation Rule</h3>
177
- <form onSubmit={handleCreate}>
178
- <div className="mb-4">
179
  <label className="block text-xs font-medium text-slate-400 mb-1">Rule Name</label>
180
  <input
181
  type="text"
182
  required
183
- placeholder="e.g. Strict Confidence Check"
184
  value={ruleName}
185
  onChange={(e) => setRuleName(e.target.value)}
186
- className="w-full bg-[#162032] border border-slate-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
187
  />
188
  </div>
189
 
190
- <div className="mb-4">
191
- <label className="block text-xs font-medium text-slate-400 mb-1">Operator</label>
192
  <select
193
- value={operator}
194
- onChange={(e) => setOperator(e.target.value)}
195
- className="w-full bg-[#162032] border border-slate-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500 font-mono"
196
  >
197
- <option value="min_confidence">min_confidence (Block low confidence extractions)</option>
198
- <option value="required_evidence">required_evidence (Mandate verbatim quotes)</option>
199
- <option value="allowed_proposal_types">allowed_proposal_types (Constrain types)</option>
200
  </select>
201
  </div>
202
 
203
- {operator === 'min_confidence' && (
204
- <div className="mb-6">
205
- <label className="block text-xs font-medium text-slate-400 mb-1">
206
- Minimum Confidence Threshold: {(threshold * 100).toFixed(0)}%
207
- </label>
208
- <input
209
- type="range"
210
- min="0.5"
211
- max="1.0"
212
- step="0.05"
213
- value={threshold}
214
- onChange={(e) => setThreshold(e.target.value)}
215
- className="w-full accent-emerald-400"
216
- />
217
- </div>
218
- )}
 
 
 
 
 
 
 
 
219
 
220
- <div className="flex justify-end space-x-3 mt-6">
221
  <button
222
  type="button"
223
  onClick={() => setShowAddModal(false)}
224
- className="px-4 py-2 text-xs font-medium text-slate-400 hover:text-white rounded-lg bg-slate-800"
225
  >
226
  Cancel
227
  </button>
228
  <button
229
  type="submit"
230
- className="px-4 py-2 text-xs font-medium text-black bg-emerald-400 hover:bg-emerald-300 rounded-lg font-semibold"
231
  >
232
- Add Rule
233
  </button>
234
  </div>
235
  </form>
 
1
  import React, { useState } from 'react';
2
+ import {
3
+ ShieldCheck,
4
+ Plus,
5
+ Play,
6
+ Trash2,
7
+ AlertTriangle,
8
+ CheckCircle2,
9
+ XCircle,
10
+ Sliders,
11
+ Filter,
12
+ Code2,
13
+ FileCheck2
14
+ } from 'lucide-react';
15
+ import { api } from '../api/client';
16
 
17
+ export default function PolicyRulesLab({ rules, currentWorkspace, onCreateRule, onDeleteRule, onValidate, loading }) {
18
  const [showAddModal, setShowAddModal] = useState(false);
19
  const [ruleName, setRuleName] = useState('');
20
+ const [ruleType, setRuleType] = useState('CONFIDENCE_THRESHOLD');
21
+ const [conditionVal, setConditionVal] = useState('0.85');
22
+ const [isBlocking, setIsBlocking] = useState(true);
23
+
24
+ const [lintResults, setLintResults] = useState(null);
25
  const [validating, setValidating] = useState(false);
26
 
27
  const handleCreate = async (e) => {
28
  e.preventDefault();
29
  if (!ruleName.trim()) return;
30
 
31
+ let condition = {};
32
+ if (ruleType === 'CONFIDENCE_THRESHOLD') {
33
+ condition = { min_confidence: parseFloat(conditionVal) || 0.85 };
34
+ } else if (ruleType === 'REGEX_FORMAT') {
35
+ condition = { pattern: conditionVal };
36
+ } else {
37
+ condition = { field: conditionVal };
38
  }
39
 
40
+ await onCreateRule(ruleName.trim(), ruleType, condition, isBlocking);
41
  setRuleName('');
42
  setShowAddModal(false);
43
  };
44
 
45
+ const runLinter = async () => {
46
+ if (!currentWorkspace?.id) return;
47
  setValidating(true);
48
  try {
49
+ const res = await onValidate();
50
+ setLintResults(res);
51
  } finally {
52
  setValidating(false);
53
  }
54
  };
55
 
 
 
56
  return (
57
  <div className="space-y-6">
58
+ {/* Policy Rules Lab Header */}
59
+ <div className="bg-[#0C101A] border border-slate-800/90 rounded-xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
60
+ <div className="flex items-center space-x-3">
61
+ <ShieldCheck className="w-5 h-5 text-emerald-400" />
62
+ <div>
63
+ <h2 className="text-sm font-semibold text-white">Policy as Code (CI Linting Engine)</h2>
64
+ <p className="text-[11px] text-slate-400">Automated quality gates and regulatory guardrails executed before knowledge merges.</p>
65
+ </div>
 
 
 
66
  </div>
67
 
68
+ <div className="flex items-center space-x-2.5">
69
  <button
70
+ onClick={runLinter}
71
+ disabled={validating}
72
+ className="px-3.5 py-1.5 rounded-lg bg-[#141B2D] hover:bg-[#1E2740] border border-slate-700 text-slate-200 text-xs font-semibold transition flex items-center space-x-1.5 shadow-sm"
73
  >
74
+ <Play className={`w-3.5 h-3.5 text-emerald-400 ${validating ? 'animate-spin' : ''}`} />
75
+ <span>{validating ? 'Linting PRs...' : 'Dry-Run CI Check'}</span>
76
  </button>
77
+
78
  <button
79
+ onClick={() => setShowAddModal(true)}
80
+ className="px-3.5 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold transition flex items-center space-x-1.5 shadow-sm shadow-emerald-600/30"
 
81
  >
82
+ <Plus className="w-3.5 h-3.5" />
83
+ <span>Add Policy Rule</span>
84
  </button>
85
  </div>
86
  </div>
87
 
88
+ {/* Lint Results Suite (GitHub Actions Style) */}
89
+ {lintResults && (
90
+ <div className="bg-[#0D121F] border border-slate-800 rounded-2xl p-5 space-y-4">
91
+ <div className="flex items-center justify-between pb-3 border-b border-slate-800">
92
+ <div className="flex items-center space-x-2">
93
+ <FileCheck2 className="w-4 h-4 text-emerald-400" />
94
+ <span className="text-xs font-bold text-white uppercase tracking-wider font-mono">
95
+ CI Check Suite Summary
 
 
 
 
 
 
 
 
 
 
 
 
96
  </span>
97
+ </div>
98
+ <span
99
+ className={`text-xs px-2.5 py-0.5 rounded-full font-mono font-bold ${
100
+ lintResults.has_violations
101
+ ? 'bg-rose-500/10 text-rose-300 border border-rose-500/30'
102
+ : 'bg-emerald-500/10 text-emerald-300 border border-emerald-500/30'
103
+ }`}
104
+ >
105
+ {lintResults.has_violations
106
+ ? `FAILED: ${lintResults.violations_count} Violations`
107
+ : 'ALL CHECKS PASSED'}
108
  </span>
109
  </div>
110
 
111
+ <div className="divide-y divide-slate-800/80 text-xs font-mono">
112
+ {(lintResults.validations || []).map((val, idx) => (
113
+ <div key={idx} className="py-2.5 flex items-center justify-between">
114
+ <div className="flex items-center space-x-2.5">
115
+ {val.severity === 'PASS' ? (
116
+ <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
117
+ ) : (
118
+ <XCircle className="w-4 h-4 text-rose-400 shrink-0" />
119
+ )}
120
+ <div>
121
+ <span className="text-white font-medium">{val.rule_name}</span>
122
+ <span className="text-slate-400 ml-2 font-sans text-[11px]">{val.message}</span>
123
+ </div>
 
 
 
 
 
 
124
  </div>
125
+
126
+ <span
127
+ className={`text-[10px] px-1.5 py-0.2 rounded font-bold ${
128
+ val.severity === 'PASS' ? 'text-emerald-400 bg-emerald-950/40' : 'text-rose-400 bg-rose-950/40'
129
+ }`}
130
+ >
131
+ {val.severity}
132
+ </span>
133
  </div>
134
  ))}
135
  </div>
136
  </div>
137
  )}
138
 
139
+ {/* Active Rules Grid */}
140
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
141
+ {rules.map((rule) => (
142
+ <div
143
+ key={rule.id}
144
+ className="bg-[#0D121F] border border-slate-800 rounded-xl p-4 flex flex-col justify-between space-y-4 hover:border-slate-700 transition"
145
+ >
146
+ <div>
147
+ <div className="flex items-center justify-between mb-2">
148
+ <span className="text-xs font-semibold text-white tracking-wide">{rule.name}</span>
149
+ <span
150
+ className={`text-[10px] font-mono px-2 py-0.5 rounded font-bold ${
151
+ rule.is_blocking
152
+ ? 'bg-rose-500/10 text-rose-300 border border-rose-500/20'
153
+ : 'bg-amber-500/10 text-amber-300 border border-amber-500/20'
154
+ }`}
155
+ >
156
+ {rule.is_blocking ? 'BLOCKING GATE' : 'ADVISORY'}
157
+ </span>
158
+ </div>
159
+
160
+ <div className="space-y-1.5 text-xs font-mono text-slate-400">
161
+ <div className="flex justify-between text-[11px]">
162
+ <span>Type:</span>
163
+ <span className="text-slate-200">{rule.rule_type}</span>
164
+ </div>
165
+ <div className="bg-[#141B2D] p-2 rounded border border-slate-800/80 text-[11px] text-slate-300">
166
+ {JSON.stringify(rule.condition || {})}
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <div className="flex items-center justify-between pt-2 border-t border-slate-800 text-xs">
172
+ <span className="text-[10px] text-slate-500 font-mono">ID: {rule.id.slice(0, 8)}</span>
173
+ <button
174
+ onClick={() => onDeleteRule(rule.id)}
175
+ className="text-slate-500 hover:text-rose-400 transition p-1"
176
+ title="Delete Rule"
177
+ >
178
+ <Trash2 className="w-3.5 h-3.5" />
179
+ </button>
180
+ </div>
181
+ </div>
182
+ ))}
 
 
 
183
  </div>
184
 
185
  {/* Add Rule Modal */}
186
  {showAddModal && (
187
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-md">
188
+ <div className="bg-[#0D121F] border border-slate-700/80 p-6 rounded-2xl w-full max-w-md shadow-2xl">
189
+ <h3 className="text-base font-semibold text-white mb-4">Add Validation Policy Rule</h3>
190
+ <form onSubmit={handleCreate} className="space-y-4">
191
+ <div>
192
  <label className="block text-xs font-medium text-slate-400 mb-1">Rule Name</label>
193
  <input
194
  type="text"
195
  required
196
+ placeholder="e.g. Clinical Endpoint Floor Check"
197
  value={ruleName}
198
  onChange={(e) => setRuleName(e.target.value)}
199
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
200
  />
201
  </div>
202
 
203
+ <div>
204
+ <label className="block text-xs font-medium text-slate-400 mb-1">Rule Condition Type</label>
205
  <select
206
+ value={ruleType}
207
+ onChange={(e) => setRuleType(e.target.value)}
208
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
209
  >
210
+ <option value="CONFIDENCE_THRESHOLD">Confidence Threshold Floor</option>
211
+ <option value="REGEX_FORMAT">RegEx Format Validator</option>
212
+ <option value="REQUIRED_FIELD">Required Field Presence</option>
213
  </select>
214
  </div>
215
 
216
+ <div>
217
+ <label className="block text-xs font-medium text-slate-400 mb-1">Threshold / Pattern Value</label>
218
+ <input
219
+ type="text"
220
+ required
221
+ placeholder="e.g. 0.85 or ^PROTOCOL-[A-Z0-9-]+$"
222
+ value={conditionVal}
223
+ onChange={(e) => setConditionVal(e.target.value)}
224
+ className="w-full bg-[#141B2D] border border-slate-700/80 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
225
+ />
226
+ </div>
227
+
228
+ <div className="flex items-center space-x-2 pt-1">
229
+ <input
230
+ type="checkbox"
231
+ id="blockingCheck"
232
+ checked={isBlocking}
233
+ onChange={(e) => setIsBlocking(e.target.checked)}
234
+ className="rounded border-slate-700 text-emerald-600 focus:ring-emerald-500 bg-[#141B2D]"
235
+ />
236
+ <label htmlFor="blockingCheck" className="text-xs text-slate-300 font-medium">
237
+ Enforce as Blocking Gate (Prevents PR Merge if failed)
238
+ </label>
239
+ </div>
240
 
241
+ <div className="flex justify-end space-x-3 pt-3 border-t border-slate-800">
242
  <button
243
  type="button"
244
  onClick={() => setShowAddModal(false)}
245
+ className="px-4 py-2 text-xs text-slate-400 hover:text-white transition"
246
  >
247
  Cancel
248
  </button>
249
  <button
250
  type="submit"
251
+ className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold rounded-lg transition"
252
  >
253
+ Save Policy Rule
254
  </button>
255
  </div>
256
  </form>
studio/src/components/SemanticDiffViewer.jsx CHANGED
@@ -1,264 +1,408 @@
1
  import React, { useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function SemanticDiffViewer({ diff, onReview, loading }) {
4
- const [filter, setFilter] = useState('all'); // all, conflicts, additions, changes
5
- const [actingId, setActingId] = useState(null);
 
 
 
6
 
7
  if (loading) {
8
  return (
9
- <div className="flex items-center justify-center p-20 text-slate-400">
10
- <span className="pulse-glow mr-3">⚙</span> Generating semantic knowledge diff...
 
11
  </div>
12
  );
13
  }
14
 
15
- const summary = diff?.summary || {};
16
- const conflicts = diff?.conflicts || [];
17
- const additions = diff?.additions || [];
18
- const changes = diff?.changes || [];
19
- const removals = diff?.removals || [];
 
20
 
21
  const handleAction = async (proposalId, decision) => {
22
- setActingId(proposalId);
23
  try {
24
- await onReview(proposalId, decision);
 
 
 
 
 
 
 
 
25
  } finally {
26
- setActingId(null);
27
  }
28
  };
29
 
30
- const totalDiffs = (conflicts.length + additions.length + changes.length + removals.length);
 
 
 
 
 
 
 
 
 
31
 
32
  return (
33
  <div className="space-y-6">
34
- {/* Top Banner / Summary */}
35
- <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 p-5 rounded-xl bg-[#0E1524] border border-slate-800">
36
- <div>
37
- <h2 className="text-lg font-bold text-white flex items-center gap-2">
38
- <span>Knowledge Semantic Diff</span>
39
- <span className="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-300 font-mono">
40
- {totalDiffs} changes detected
 
41
  </span>
42
- </h2>
43
- <p className="text-xs text-slate-400 mt-1">
44
- Compares newly extracted document facts against committed production truths with verbatim evidence provenance.
45
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
46
  </div>
47
 
48
- {/* Filter Pills */}
49
- <div className="flex items-center space-x-2">
50
- <button
51
- onClick={() => setFilter('all')}
52
- className={`px-3 py-1.5 rounded-lg text-xs font-medium transition ${
53
- filter === 'all' ? 'bg-slate-700 text-white' : 'bg-slate-800/60 text-slate-400 hover:text-white'
54
- }`}
55
- >
56
- All ({totalDiffs})
57
- </button>
58
- <button
59
- onClick={() => setFilter('conflicts')}
60
- className={`px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center space-x-1 ${
61
- filter === 'conflicts' ? 'bg-rose-500/20 text-rose-300 border border-rose-500/40' : 'bg-slate-800/60 text-slate-400 hover:text-white'
62
- }`}
63
- >
64
- <span className="w-1.5 h-1.5 rounded-full bg-rose-400"></span>
65
- <span>Conflicts ({conflicts.length})</span>
66
- </button>
67
- <button
68
- onClick={() => setFilter('additions')}
69
- className={`px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center space-x-1 ${
70
- filter === 'additions' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-slate-800/60 text-slate-400 hover:text-white'
71
- }`}
72
- >
73
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
74
- <span>Additions ({additions.length})</span>
75
- </button>
76
- <button
77
- onClick={() => setFilter('changes')}
78
- className={`px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center space-x-1 ${
79
- filter === 'changes' ? 'bg-amber-500/20 text-amber-300 border border-amber-500/40' : 'bg-slate-800/60 text-slate-400 hover:text-white'
80
- }`}
81
- >
82
- <span className="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
83
- <span>Updates ({changes.length})</span>
84
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  </div>
86
  </div>
87
 
88
- {totalDiffs === 0 ? (
89
- <div className="text-center py-16 border border-dashed border-slate-800 rounded-xl bg-[#090E17]">
90
- <span className="text-3xl text-emerald-400">✔</span>
91
- <h3 className="text-base font-semibold text-white mt-2">Workspace Knowledge is in Sync</h3>
92
- <p className="text-xs text-slate-400 mt-1 max-w-sm mx-auto">
93
- No uncommitted or conflicting knowledge items found. Upload documents via the Staging tab or dw add.
 
 
94
  </p>
95
  </div>
96
  ) : (
97
  <div className="space-y-4">
98
- {/* Conflicts (Red / Contradiction) */}
99
- {(filter === 'all' || filter === 'conflicts') &&
100
- conflicts.map((c, idx) => {
101
- const evidenceList = c.proposed?.evidence || [];
102
- const quote = evidenceList[0]?.quote || 'No verbatim citation';
103
- const page = evidenceList[0]?.page_number || 'N/A';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- return (
106
- <div key={c.proposal_id || idx} className="rounded-xl border border-rose-500/30 bg-[#0F1422] p-5 shadow-lg">
107
- <div className="flex items-center justify-between border-b border-slate-800 pb-3 mb-4">
108
- <div className="flex items-center space-x-2">
109
- <span className="px-2 py-0.5 rounded text-[11px] font-bold bg-rose-500/20 text-rose-400 border border-rose-500/30">
110
- ! CONTRADICTION COLLISION
 
111
  </span>
112
- <span className="text-xs text-slate-400 font-mono">PR: {c.proposal_id?.slice(0, 8)}</span>
113
  </div>
114
-
115
- <div className="flex items-center space-x-2">
116
- <button
117
- disabled={actingId === c.proposal_id}
118
- onClick={() => handleAction(c.proposal_id, 'APPROVED')}
119
- className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30 border border-emerald-500/40 transition disabled:opacity-50"
120
- >
121
- ✓ Accept & Merge
122
- </button>
123
- <button
124
- disabled={actingId === c.proposal_id}
125
- onClick={() => handleAction(c.proposal_id, 'REJECTED')}
126
- className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-rose-500/20 text-rose-300 hover:bg-rose-500/30 border border-rose-500/40 transition disabled:opacity-50"
127
- >
128
- ✕ Reject
129
- </button>
130
  </div>
131
  </div>
132
 
133
- <h3 className="text-sm font-semibold text-white mb-3">{c.title}</h3>
134
-
135
- {/* 3-Way Split Diff */}
136
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
137
- {/* Baseline truth */}
138
- <div className="rounded-lg bg-rose-950/20 border border-rose-500/20 p-4">
139
- <span className="text-[11px] font-mono text-rose-400 uppercase tracking-wider block mb-1">
140
- ◀ Baseline Truth (Committed)
141
- </span>
142
- <p className="text-sm text-slate-200 font-medium">"{c.existing?.value || 'N/A'}"</p>
143
- <span className="text-[11px] text-slate-400 mt-2 block font-mono">
144
- Confidence: {c.existing?.confidence || 1.0}
145
  </span>
 
146
  </div>
147
-
148
- {/* Proposed incoming */}
149
- <div className="rounded-lg bg-emerald-950/20 border border-emerald-500/20 p-4">
150
- <span className="text-[11px] font-mono text-emerald-400 uppercase tracking-wider block mb-1">
151
- ▶ Proposed Change (From Incoming Document)
152
- </span>
153
- <p className="text-sm text-slate-100 font-medium">"{c.proposed?.value}"</p>
154
- <div className="mt-3 pt-2 border-t border-emerald-500/10">
155
- <span className="text-[11px] text-slate-400 block font-mono mb-1">
156
- Verbatim Evidence (Page {page}):
157
- </span>
158
- <blockquote className="text-xs text-cyan-300 italic bg-cyan-950/20 p-2 rounded border-l-2 border-cyan-500">
159
- "{quote}"
160
- </blockquote>
161
- </div>
162
  </div>
163
  </div>
164
  </div>
165
- );
166
- })}
167
 
168
- {/* Additions (Green) */}
169
- {(filter === 'all' || filter === 'additions') &&
170
- additions.map((a, idx) => {
171
- const evidenceList = a.evidence || [];
172
- const quote = evidenceList[0]?.quote || 'No verbatim citation';
173
- const page = evidenceList[0]?.page_number || 'N/A';
174
-
175
- return (
176
- <div key={a.proposal_id || idx} className="rounded-xl border border-emerald-500/30 bg-[#0F1422] p-5 shadow-lg">
177
- <div className="flex items-center justify-between border-b border-slate-800 pb-3 mb-4">
178
- <div className="flex items-center space-x-2">
179
- <span className="px-2 py-0.5 rounded text-[11px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
180
- + NEW {a.type || 'CLAIM'}
181
- </span>
182
- <span className="text-xs text-slate-400 font-mono">PR: {a.proposal_id?.slice(0, 8)}</span>
183
  </div>
 
 
184
 
185
- <div className="flex items-center space-x-2">
186
- <button
187
- disabled={actingId === a.proposal_id}
188
- onClick={() => handleAction(a.proposal_id, 'APPROVED')}
189
- className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30 border border-emerald-500/40 transition disabled:opacity-50"
190
- >
191
- ✓ Commit Fact
192
- </button>
193
- <button
194
- disabled={actingId === a.proposal_id}
195
- onClick={() => handleAction(a.proposal_id, 'REJECTED')}
196
- className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-rose-500/20 text-rose-300 hover:bg-rose-500/30 border border-rose-500/40 transition disabled:opacity-50"
197
- >
198
- ✕ Reject
199
- </button>
200
- </div>
 
 
 
 
 
 
 
 
 
 
 
201
  </div>
 
202
 
203
- <h3 className="text-sm font-semibold text-white mb-2">{a.title}</h3>
204
- <div className="rounded-lg bg-emerald-950/15 border border-emerald-500/15 p-4 mb-3">
205
- <span className="text-[11px] font-mono text-emerald-400 block mb-1">Extracted Value:</span>
206
- <p className="text-sm text-slate-100 font-medium">"{a.proposed_value}"</p>
 
 
 
 
 
 
207
  </div>
 
 
 
 
208
 
209
- <div className="bg-slate-900/60 p-3 rounded-lg border border-slate-800/80">
210
- <span className="text-[11px] text-slate-400 block font-mono mb-1">
211
- Verbatim Document Evidence (Page {page}):
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  </span>
213
- <blockquote className="text-xs text-cyan-300 italic">"{quote}"</blockquote>
214
  </div>
215
  </div>
216
- );
217
- })}
218
 
219
- {/* Changes / Updates (Amber) */}
220
- {(filter === 'all' || filter === 'changes') &&
221
- changes.map((ch, idx) => (
222
- <div key={ch.proposal_id || idx} className="rounded-xl border border-amber-500/30 bg-[#0F1422] p-5 shadow-lg">
223
- <div className="flex items-center justify-between border-b border-slate-800 pb-3 mb-4">
224
- <div className="flex items-center space-x-2">
225
- <span className="px-2 py-0.5 rounded text-[11px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">
226
- ~ MODIFIED CLAIM
227
- </span>
228
- <span className="text-xs text-slate-400 font-mono">PR: {ch.proposal_id?.slice(0, 8)}</span>
 
 
229
  </div>
 
230
 
231
- <div className="flex items-center space-x-2">
232
- <button
233
- disabled={actingId === ch.proposal_id}
234
- onClick={() => handleAction(ch.proposal_id, 'APPROVED')}
235
- className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30 border border-emerald-500/40 transition disabled:opacity-50"
236
- >
237
- ✓ Merge Update
238
- </button>
239
- <button
240
- disabled={actingId === ch.proposal_id}
241
- onClick={() => handleAction(ch.proposal_id, 'REJECTED')}
242
- className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-rose-500/20 text-rose-300 hover:bg-rose-500/30 border border-rose-500/40 transition disabled:opacity-50"
243
- >
244
- ✕ Reject
245
- </button>
246
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  </div>
 
 
 
248
 
249
- <h3 className="text-sm font-semibold text-white mb-2">{ch.title}</h3>
250
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
251
- <div className="p-3 rounded bg-slate-900 border border-slate-800">
252
- <span className="text-[10px] text-slate-400 block font-mono">Existing:</span>
253
- <p className="text-xs text-slate-300 mt-1 font-medium">{ch.existing?.value || 'N/A'}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  </div>
255
- <div className="p-3 rounded bg-amber-950/20 border border-amber-500/20">
256
- <span className="text-[10px] text-amber-400 block font-mono">Proposed Update:</span>
257
- <p className="text-xs text-amber-200 mt-1 font-medium">{ch.proposed?.value || 'N/A'}</p>
258
  </div>
259
  </div>
 
 
 
 
 
 
 
 
 
260
  </div>
261
- ))}
 
262
  </div>
263
  )}
264
  </div>
 
1
  import React, { useState } from 'react';
2
+ import {
3
+ FileDiff,
4
+ AlertTriangle,
5
+ CheckCircle2,
6
+ XCircle,
7
+ Archive,
8
+ Layers,
9
+ Sparkles,
10
+ Quote,
11
+ Eye,
12
+ Columns,
13
+ AlignLeft,
14
+ ChevronRight,
15
+ MessageSquare,
16
+ Send,
17
+ SlidersHorizontal,
18
+ ShieldCheck
19
+ } from 'lucide-react';
20
+ import confetti from 'canvas-confetti';
21
 
22
+ export default function SemanticDiffViewer({ diffData, onReviewProposal, onBatchReview, loading }) {
23
+ const [viewMode, setViewMode] = useState('split'); // 'split' | 'unified'
24
+ const [filterType, setFilterType] = useState('all'); // 'all' | 'conflicts' | 'additions' | 'changes'
25
+ const [comments, setComments] = useState({});
26
+ const [expandedComments, setExpandedComments] = useState({});
27
+ const [actionLoading, setActionLoading] = useState({});
28
 
29
  if (loading) {
30
  return (
31
+ <div className="flex flex-col items-center justify-center py-24 space-y-4">
32
+ <div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
33
+ <p className="text-xs text-slate-400 font-mono">Running semantic fact diff against Master Knowledge Register...</p>
34
  </div>
35
  );
36
  }
37
 
38
+ const additions = diffData?.additions || [];
39
+ const changes = diffData?.changes || [];
40
+ const conflicts = diffData?.conflicts || [];
41
+ const removals = diffData?.removals || [];
42
+
43
+ const totalDeltas = additions.length + changes.length + conflicts.length + removals.length;
44
 
45
  const handleAction = async (proposalId, decision) => {
46
+ setActionLoading((prev) => ({ ...prev, [proposalId]: true }));
47
  try {
48
+ await onReviewProposal(proposalId, decision, comments[proposalId] || '');
49
+ if (decision === 'APPROVED') {
50
+ confetti({
51
+ particleCount: 50,
52
+ spread: 60,
53
+ origin: { y: 0.8 },
54
+ colors: ['#10B981', '#14B8A6', '#3B82F6'],
55
+ });
56
+ }
57
  } finally {
58
+ setActionLoading((prev) => ({ ...prev, [proposalId]: false }));
59
  }
60
  };
61
 
62
+ const handleBatch = async (decision) => {
63
+ await onBatchReview(decision);
64
+ if (decision === 'APPROVED') {
65
+ confetti({
66
+ particleCount: 90,
67
+ spread: 80,
68
+ origin: { y: 0.7 },
69
+ });
70
+ }
71
+ };
72
 
73
  return (
74
  <div className="space-y-6">
75
+ {/* Diff Toolbar & Delta Summary */}
76
+ <div className="bg-[#0C101A] border border-slate-800/90 rounded-xl p-4 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
77
+ <div className="flex items-center space-x-4">
78
+ <div className="flex items-center space-x-2">
79
+ <FileDiff className="w-5 h-5 text-emerald-400" />
80
+ <span className="font-semibold text-white text-sm">3-Way Semantic Fact Diff</span>
81
+ <span className="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-300 font-mono font-medium">
82
+ {totalDeltas} pending {totalDeltas === 1 ? 'delta' : 'deltas'}
83
  </span>
84
+ </div>
85
+
86
+ <div className="hidden sm:flex items-center space-x-2 text-xs font-mono">
87
+ <span className="text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
88
+ +{additions.length} added
89
+ </span>
90
+ <span className="text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded border border-blue-500/20">
91
+ ~{changes.length} updated
92
+ </span>
93
+ {conflicts.length > 0 && (
94
+ <span className="text-rose-400 bg-rose-500/10 px-2 py-0.5 rounded border border-rose-500/20 flex items-center space-x-1">
95
+ <AlertTriangle className="w-3 h-3 text-rose-400" />
96
+ <span>!{conflicts.length} conflicts</span>
97
+ </span>
98
+ )}
99
+ </div>
100
  </div>
101
 
102
+ {/* View Controls & Filters */}
103
+ <div className="flex items-center space-x-3 text-xs">
104
+ {/* Filter Pills */}
105
+ <div className="flex items-center bg-[#141B2D] p-1 rounded-lg border border-slate-800">
106
+ <button
107
+ onClick={() => setFilterType('all')}
108
+ className={`px-2.5 py-1 rounded font-medium transition ${
109
+ filterType === 'all' ? 'bg-slate-700 text-white' : 'text-slate-400 hover:text-slate-200'
110
+ }`}
111
+ >
112
+ All ({totalDeltas})
113
+ </button>
114
+ <button
115
+ onClick={() => setFilterType('conflicts')}
116
+ className={`px-2.5 py-1 rounded font-medium transition flex items-center space-x-1 ${
117
+ filterType === 'conflicts' ? 'bg-rose-900/60 text-rose-300' : 'text-slate-400 hover:text-rose-300'
118
+ }`}
119
+ >
120
+ <span>Conflicts</span>
121
+ {conflicts.length > 0 && (
122
+ <span className="w-1.5 h-1.5 rounded-full bg-rose-400"></span>
123
+ )}
124
+ </button>
125
+ <button
126
+ onClick={() => setFilterType('additions')}
127
+ className={`px-2.5 py-1 rounded font-medium transition ${
128
+ filterType === 'additions' ? 'bg-emerald-950/60 text-emerald-300' : 'text-slate-400 hover:text-slate-200'
129
+ }`}
130
+ >
131
+ Additions
132
+ </button>
133
+ </div>
134
+
135
+ {/* Split / Unified Toggle */}
136
+ <div className="flex items-center bg-[#141B2D] p-1 rounded-lg border border-slate-800">
137
+ <button
138
+ onClick={() => setViewMode('split')}
139
+ className={`p-1.5 rounded transition ${viewMode === 'split' ? 'bg-slate-700 text-white' : 'text-slate-400 hover:text-white'}`}
140
+ title="Split View (Side by Side)"
141
+ >
142
+ <Columns className="w-3.5 h-3.5" />
143
+ </button>
144
+ <button
145
+ onClick={() => setViewMode('unified')}
146
+ className={`p-1.5 rounded transition ${viewMode === 'unified' ? 'bg-slate-700 text-white' : 'text-slate-400 hover:text-white'}`}
147
+ title="Unified View"
148
+ >
149
+ <AlignLeft className="w-3.5 h-3.5" />
150
+ </button>
151
+ </div>
152
+
153
+ {/* Bulk Approve Button */}
154
+ {totalDeltas > 0 && (
155
+ <button
156
+ onClick={() => handleBatch('APPROVED')}
157
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-xs transition shadow-sm shadow-emerald-600/20 flex items-center space-x-1.5"
158
+ >
159
+ <CheckCircle2 className="w-3.5 h-3.5" />
160
+ <span>Approve All</span>
161
+ </button>
162
+ )}
163
  </div>
164
  </div>
165
 
166
+ {totalDeltas === 0 ? (
167
+ <div className="bg-[#0C101A] border border-slate-800/80 rounded-2xl p-16 text-center shadow-inner">
168
+ <div className="w-12 h-12 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto mb-4">
169
+ <CheckCircle2 className="w-6 h-6" />
170
+ </div>
171
+ <h3 className="text-base font-semibold text-white mb-1">Knowledge Register is Up to Date</h3>
172
+ <p className="text-xs text-slate-400 max-w-md mx-auto mb-6">
173
+ No pending knowledge proposals or conflicts found. Stage a new document or protocol version to generate 3-way fact diffs.
174
  </p>
175
  </div>
176
  ) : (
177
  <div className="space-y-4">
178
+ {/* Conflicts Section */}
179
+ {(filterType === 'all' || filterType === 'conflicts') && conflicts.map((c) => (
180
+ <div
181
+ key={c.proposal_id}
182
+ className="bg-[#0D121F] border border-rose-500/40 rounded-xl overflow-hidden shadow-lg shadow-rose-950/20 transition hover:border-rose-500/70"
183
+ >
184
+ {/* Card Header */}
185
+ <div className="bg-rose-950/25 px-4 py-2.5 border-b border-rose-500/30 flex items-center justify-between">
186
+ <div className="flex items-center space-x-2.5">
187
+ <span className="p-1 rounded bg-rose-500/20 text-rose-300">
188
+ <AlertTriangle className="w-4 h-4 text-rose-400" />
189
+ </span>
190
+ <div>
191
+ <span className="text-xs font-semibold text-white tracking-wide">{c.title || 'Fact Conflict'}</span>
192
+ <span className="ml-2 text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-rose-500/20 text-rose-300 border border-rose-500/30">
193
+ Conflict
194
+ </span>
195
+ </div>
196
+ </div>
197
+
198
+ <div className="flex items-center space-x-2 text-xs">
199
+ <span className="text-slate-400 font-mono text-[11px]">ID: {c.proposal_id.slice(0, 8)}</span>
200
+ </div>
201
+ </div>
202
+
203
+ {/* Conflict Body */}
204
+ <div className="p-4 space-y-4">
205
+ {c.rationale && (
206
+ <p className="text-xs text-slate-300 bg-slate-900/60 p-2.5 rounded-lg border border-slate-800 leading-relaxed font-sans">
207
+ <strong className="text-rose-400">Reconciliation Note: </strong>
208
+ {c.rationale}
209
+ </p>
210
+ )}
211
 
212
+ {/* Side by Side Diff Comparison */}
213
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs font-mono">
214
+ {/* Baseline / Existing */}
215
+ <div className="bg-[#13111C] border border-slate-800 rounded-lg p-3">
216
+ <div className="flex items-center justify-between text-slate-400 text-[11px] mb-2 pb-1 border-b border-slate-800/80">
217
+ <span className="text-slate-400 flex items-center space-x-1">
218
+ <span>- Master Truth (Existing Fact)</span>
219
  </span>
220
+ <span className="text-[10px] text-slate-500">Conf: {Math.round((c.existing?.confidence || 0.9) * 100)}%</span>
221
  </div>
222
+ <div className="text-rose-200 bg-rose-950/20 p-2 rounded border border-rose-900/30 leading-relaxed break-words">
223
+ {c.existing?.value || 'N/A'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  </div>
225
  </div>
226
 
227
+ {/* Proposed / Amendment */}
228
+ <div className="bg-[#0C1518] border border-emerald-900/40 rounded-lg p-3">
229
+ <div className="flex items-center justify-between text-slate-400 text-[11px] mb-2 pb-1 border-b border-slate-800/80">
230
+ <span className="text-emerald-400 flex items-center space-x-1 font-semibold">
231
+ <span>+ Proposed Fact (Extracted Amendment)</span>
 
 
 
 
 
 
 
232
  </span>
233
+ <span className="text-[10px] text-emerald-400 font-bold">Conf: {Math.round((c.proposed?.confidence || 0.95) * 100)}%</span>
234
  </div>
235
+ <div className="text-emerald-200 bg-emerald-950/25 p-2 rounded border border-emerald-800/30 leading-relaxed break-words font-medium">
236
+ {c.proposed?.value || 'N/A'}
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
  </div>
239
  </div>
 
 
240
 
241
+ {/* Evidence Provenance */}
242
+ {c.proposed?.evidence && c.proposed.evidence.length > 0 && (
243
+ <div className="text-xs bg-[#101524] p-2.5 rounded-lg border border-slate-800 text-slate-300 flex items-start space-x-2">
244
+ <Quote className="w-4 h-4 text-slate-500 shrink-0 mt-0.5" />
245
+ <div>
246
+ <span className="text-[10px] text-slate-500 uppercase tracking-wider block font-mono">Source Provenance Citation</span>
247
+ <span className="text-slate-300 italic">{c.proposed.evidence[0]}</span>
 
 
 
 
 
 
 
 
248
  </div>
249
+ </div>
250
+ )}
251
 
252
+ {/* Action Bar */}
253
+ <div className="flex items-center justify-between pt-2 border-t border-slate-800/80 text-xs">
254
+ <button
255
+ onClick={() => setExpandedComments((p) => ({ ...p, [c.proposal_id]: !p[c.proposal_id] }))}
256
+ className="text-slate-400 hover:text-slate-200 flex items-center space-x-1"
257
+ >
258
+ <MessageSquare className="w-3.5 h-3.5" />
259
+ <span>{comments[c.proposal_id] ? 'Edit Reviewer Note' : 'Add Note'}</span>
260
+ </button>
261
+
262
+ <div className="flex items-center space-x-2">
263
+ <button
264
+ onClick={() => handleAction(c.proposal_id, 'REJECTED')}
265
+ disabled={actionLoading[c.proposal_id]}
266
+ className="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition flex items-center space-x-1 font-medium"
267
+ >
268
+ <XCircle className="w-3.5 h-3.5 text-rose-400" />
269
+ <span>Reject Amendment</span>
270
+ </button>
271
+ <button
272
+ onClick={() => handleAction(c.proposal_id, 'APPROVED')}
273
+ disabled={actionLoading[c.proposal_id]}
274
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition flex items-center space-x-1 font-semibold shadow-sm shadow-emerald-600/30"
275
+ >
276
+ <CheckCircle2 className="w-3.5 h-3.5" />
277
+ <span>Accept & Resolve Conflict</span>
278
+ </button>
279
  </div>
280
+ </div>
281
 
282
+ {/* Reviewer Note Drawer */}
283
+ {expandedComments[c.proposal_id] && (
284
+ <div className="pt-2">
285
+ <input
286
+ type="text"
287
+ placeholder="Attach reviewer comment or clinical justification..."
288
+ value={comments[c.proposal_id] || ''}
289
+ onChange={(e) => setComments({ ...comments, [c.proposal_id]: e.target.value })}
290
+ className="w-full bg-[#161F32] border border-slate-700 text-xs text-slate-200 px-3 py-2 rounded-lg focus:outline-none focus:ring-1 focus:ring-emerald-500"
291
+ />
292
  </div>
293
+ )}
294
+ </div>
295
+ </div>
296
+ ))}
297
 
298
+ {/* Additions Section */}
299
+ {(filterType === 'all' || filterType === 'additions') && additions.map((a) => (
300
+ <div
301
+ key={a.proposal_id}
302
+ className="bg-[#0D121F] border border-slate-800 rounded-xl overflow-hidden shadow-sm transition hover:border-emerald-500/40"
303
+ >
304
+ {/* Card Header */}
305
+ <div className="bg-[#121828] px-4 py-2.5 border-b border-slate-800 flex items-center justify-between">
306
+ <div className="flex items-center space-x-2.5">
307
+ <span className="p-1 rounded bg-emerald-500/20 text-emerald-400">
308
+ <Sparkles className="w-4 h-4 text-emerald-400" />
309
+ </span>
310
+ <div>
311
+ <span className="text-xs font-semibold text-white tracking-wide">{a.title || 'New Knowledge Delta'}</span>
312
+ <span className="ml-2 text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold">
313
+ +{a.type || 'CLAIM'}
314
  </span>
 
315
  </div>
316
  </div>
 
 
317
 
318
+ <div className="flex items-center space-x-3 text-xs">
319
+ <span className="text-emerald-400 font-mono text-[11px] font-semibold">
320
+ {Math.round((a.confidence || 0.95) * 100)}% Confidence
321
+ </span>
322
+ </div>
323
+ </div>
324
+
325
+ {/* Card Body */}
326
+ <div className="p-4 space-y-3">
327
+ <div className="bg-[#0C1518] border border-emerald-900/30 rounded-lg p-3">
328
+ <div className="text-xs text-emerald-200 leading-relaxed font-mono">
329
+ {a.proposed_value || a.summary || 'Extracted content'}
330
  </div>
331
+ </div>
332
 
333
+ {a.evidence && a.evidence.length > 0 && (
334
+ <div className="text-xs bg-[#101524] p-2.5 rounded-lg border border-slate-800 text-slate-300 flex items-start space-x-2">
335
+ <Quote className="w-4 h-4 text-slate-500 shrink-0 mt-0.5" />
336
+ <div>
337
+ <span className="text-[10px] text-slate-500 uppercase tracking-wider block font-mono">Document Citation</span>
338
+ <span className="text-slate-300 italic">{a.evidence[0]}</span>
339
+ </div>
 
 
 
 
 
 
 
 
340
  </div>
341
+ )}
342
+
343
+ {/* Action Bar */}
344
+ <div className="flex items-center justify-end space-x-2 pt-2 border-t border-slate-800/80 text-xs">
345
+ <button
346
+ onClick={() => handleAction(a.proposal_id, 'REJECTED')}
347
+ disabled={actionLoading[a.proposal_id]}
348
+ className="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition font-medium"
349
+ >
350
+ Reject
351
+ </button>
352
+ <button
353
+ onClick={() => handleAction(a.proposal_id, 'APPROVED')}
354
+ disabled={actionLoading[a.proposal_id]}
355
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition font-semibold flex items-center space-x-1"
356
+ >
357
+ <CheckCircle2 className="w-3.5 h-3.5" />
358
+ <span>Approve Fact</span>
359
+ </button>
360
  </div>
361
+ </div>
362
+ </div>
363
+ ))}
364
 
365
+ {/* Changes / Updates Section */}
366
+ {(filterType === 'all' || filterType === 'changes') && changes.map((ch) => (
367
+ <div
368
+ key={ch.proposal_id}
369
+ className="bg-[#0D121F] border border-slate-800 rounded-xl overflow-hidden shadow-sm transition hover:border-blue-500/40"
370
+ >
371
+ <div className="bg-[#121828] px-4 py-2.5 border-b border-slate-800 flex items-center justify-between">
372
+ <div className="flex items-center space-x-2.5">
373
+ <span className="p-1 rounded bg-blue-500/20 text-blue-400">
374
+ <FileDiff className="w-4 h-4 text-blue-400" />
375
+ </span>
376
+ <span className="text-xs font-semibold text-white">{ch.title || 'Knowledge Update'}</span>
377
+ <span className="text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-300 border border-blue-500/20">
378
+ UPDATE
379
+ </span>
380
+ </div>
381
+ </div>
382
+
383
+ <div className="p-4 space-y-3">
384
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs font-mono">
385
+ <div className="bg-[#141824] border border-slate-800 p-2.5 rounded text-slate-400">
386
+ <div className="text-[10px] text-slate-500 mb-1">- Previous Value</div>
387
+ <div>{ch.existing?.value || 'N/A'}</div>
388
  </div>
389
+ <div className="bg-[#0C1518] border border-emerald-900/30 p-2.5 rounded text-emerald-300">
390
+ <div className="text-[10px] text-emerald-400 mb-1">+ Updated Value</div>
391
+ <div>{ch.proposed?.value || 'N/A'}</div>
392
  </div>
393
  </div>
394
+
395
+ <div className="flex items-center justify-end space-x-2 pt-2 border-t border-slate-800/80 text-xs">
396
+ <button
397
+ onClick={() => handleAction(ch.proposal_id, 'APPROVED')}
398
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-semibold transition"
399
+ >
400
+ Approve Update
401
+ </button>
402
+ </div>
403
  </div>
404
+ </div>
405
+ ))}
406
  </div>
407
  )}
408
  </div>
studio/tailwind.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ darkMode: 'class',
8
+ theme: {
9
+ extend: {
10
+ colors: {
11
+ canvas: {
12
+ default: '#080B11',
13
+ subtle: '#0D131F',
14
+ inset: '#06080D',
15
+ },
16
+ border: {
17
+ subtle: 'rgba(255, 255, 255, 0.08)',
18
+ strong: 'rgba(255, 255, 255, 0.16)',
19
+ },
20
+ },
21
+ fontFamily: {
22
+ sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
23
+ mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
24
+ },
25
+ },
26
+ },
27
+ plugins: [],
28
+ }