ChristopherJKoen commited on
Commit
73b6f92
·
0 Parent(s):
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. .gitignore +20 -0
  3. README.md +86 -0
  4. agents/README.md +13 -0
  5. agents/__init__.py +15 -0
  6. agents/export/__init__.py +3 -0
  7. agents/export/agent.py +112 -0
  8. agents/extraction/__init__.py +3 -0
  9. agents/extraction/agent.py +239 -0
  10. agents/orchestrator/__init__.py +3 -0
  11. agents/orchestrator/agent.py +18 -0
  12. agents/rewrite/__init__.py +3 -0
  13. agents/rewrite/agent.py +315 -0
  14. agents/shared/base.py +49 -0
  15. agents/shared/client.py +97 -0
  16. agents/shared/embeddings.py +19 -0
  17. agents/standards_mapping/__init__.py +3 -0
  18. agents/standards_mapping/agent.py +293 -0
  19. agents/validation/__init__.py +3 -0
  20. agents/validation/agent.py +96 -0
  21. common/README.md +10 -0
  22. common/embedding_store.py +98 -0
  23. docker/Dockerfile +20 -0
  24. docs/README.md +12 -0
  25. docs/agents/orchestrator.md +12 -0
  26. docs/architecture/system-context.md +11 -0
  27. docs/pipelines/pypeflow-overview.md +8 -0
  28. docs/ui/review-workflow.md +17 -0
  29. frontend/.env.example +1 -0
  30. frontend/README.md +17 -0
  31. frontend/index.html +13 -0
  32. frontend/package-lock.json +2030 -0
  33. frontend/package.json +25 -0
  34. frontend/public/favicon.svg +4 -0
  35. frontend/public/index.html +13 -0
  36. frontend/src/App.tsx +34 -0
  37. frontend/src/components/PresetManager.tsx +193 -0
  38. frontend/src/components/SessionDetails.tsx +578 -0
  39. frontend/src/components/SessionList.tsx +59 -0
  40. frontend/src/components/UploadForm.tsx +189 -0
  41. frontend/src/main.tsx +15 -0
  42. frontend/src/pages/ObservatoryPage.tsx +179 -0
  43. frontend/src/pages/PresetEditPage.tsx +202 -0
  44. frontend/src/pages/PresetsPage.tsx +9 -0
  45. frontend/src/pages/SessionsPage.tsx +19 -0
  46. frontend/src/services/api.ts +113 -0
  47. frontend/src/styles.css +643 -0
  48. frontend/src/types/diagnostics.ts +27 -0
  49. frontend/src/types/session.ts +59 -0
  50. frontend/tsconfig.json +21 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+
6
+ # Node
7
+ node_modules/
8
+ *.log
9
+
10
+ # Editors
11
+ .idea/
12
+ .vscode/
13
+
14
+ # Build outputs
15
+ dist/
16
+ build/
17
+
18
+ # OS
19
+ Thumbs.db
20
+ .DS_Store
README.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RightCodes Architecture Scaffold
2
+
3
+ This repository is the starting scaffold for the code and standards migration assistant. The layout mirrors the planned AI-driven workflow (ingestion -> extraction -> mapping -> rewrite -> validation -> export) and keeps agent orchestration, backend services, and UI concerns separated.
4
+
5
+ ## Top-Level Directories
6
+
7
+ - `frontend/` - React application for uploads, diff review, validation checklists, and export actions.
8
+ - `server/` - FastAPI backend that manages sessions, orchestrates agents, and exposes REST/WebSocket APIs.
9
+ - `agents/` - OpenAI agent wrappers plus prompt assets for each processing stage.
10
+ - `workers/` - File ingestion, document parsing, and pipeline jobs executed off the main API thread.
11
+ - `storage/` - Versioned document blobs, manifests, caches, and export artefacts.
12
+ - `docs/` - Architecture notes, pipeline diagrams, agent specs, and UI flows.
13
+ - `scripts/` - Developer utilities, operational scripts, and local tooling hooks.
14
+ - `data/` - Sample inputs, canonical standards metadata, and fixture mappings.
15
+ - `infra/` - DevOps assets (containers, CI pipelines, observability).
16
+ - `common/` - Shared domain models, schemas, and event definitions.
17
+
18
+ ## Getting Started
19
+
20
+ ### Prerequisites
21
+
22
+ - Python 3.8+ (3.11+ recommended)
23
+ - Node.js 18+ and npm (ships with Node.js)
24
+ - Internet connectivity on the first launch so pip/npm can download dependencies
25
+
26
+ ### Launcher Quick Start
27
+
28
+ The repository ships with `start-rightcodes.ps1` (PowerShell) and `start-rightcodes.bat` (Command Prompt) which check for Python and Node.js, create the virtual environment, install dependencies, and open the launcher UI.
29
+
30
+ ```powershell
31
+ .\start-rightcodes.ps1
32
+ ```
33
+
34
+ ```bat
35
+ start-rightcodes.bat
36
+ ```
37
+
38
+ The scripts will guide you to the official Python and Node.js installers if either prerequisite is missing. After a successful first run, the cached `.venv/` and `frontend/node_modules/` folders allow the launcher to work offline.
39
+
40
+ ### Backend (FastAPI)
41
+
42
+ ```bash
43
+ cd server
44
+ python -m venv .venv
45
+ .venv\Scripts\activate # Windows
46
+ pip install -r requirements.txt
47
+ # copy the sample env and add your OpenAI key
48
+ copy .env.example .env # or use New-Item -Path .env -ItemType File
49
+ ```
50
+
51
+ Edit `.env` and set `RIGHTCODES_OPENAI_API_KEY=sk-your-key`.
52
+
53
+ ```bash
54
+ uvicorn app.main:app --reload --port 8000
55
+ ```
56
+
57
+ The API is available at `http://localhost:8000/api` and Swagger UI at `http://localhost:8000/api/docs`.
58
+
59
+ ### Frontend (Vite + React)
60
+
61
+ ```bash
62
+ cd frontend
63
+ npm install
64
+ npm run dev
65
+ ```
66
+
67
+ Navigate to `http://localhost:5173` to access the UI. Configure a custom API base URL by setting `VITE_API_BASE_URL` in `frontend/.env`.
68
+
69
+ ## Usage
70
+
71
+ - Each conversion session requires the original report (`.docx`) plus one or more destination standards packs (`.pdf`). Upload all relevant standards PDFs so the AI pipeline can align existing references with the new context.
72
+ - The web UI now shows live progress bars and an activity log so you can monitor each stage of the pipeline while it runs.
73
+ - Backend agent calls expect an OpenAI API key in `RIGHTCODES_OPENAI_API_KEY`. On Windows PowerShell you can set it temporarily with `setx RIGHTCODES_OPENAI_API_KEY "sk-..."` (reopen your terminal afterward).
74
+ - Optional: override the default OpenAI models by setting `RIGHTCODES_OPENAI_MODEL_EXTRACT`, `RIGHTCODES_OPENAI_MODEL_MAPPING`, `RIGHTCODES_OPENAI_MODEL_REWRITE`, `RIGHTCODES_OPENAI_MODEL_VALIDATE`, and `RIGHTCODES_OPENAI_MODEL_EMBED`. The pipeline stores standards embeddings under `storage/embeddings/`, enabling retrieval-augmented mapping, and the export stage now writes a converted DOCX to `storage/exports/` with a download button once the pipeline completes.
75
+
76
+ ## Offline Distribution Options
77
+
78
+ - **Zip bundle:** Run `python tools/build_offline_package.py` after a successful online launch. The script creates `dist/rightcodes-offline.zip` containing the repository, the prepared `.venv/`, and `frontend/node_modules/`. Pass `--python-runtime` and `--node-runtime` to embed portable runtimes if you have them (for example, a locally extracted Python embeddable zip and Node.js binary folder). Extract the archive on another machine (same OS/architecture) and use the launcher scripts without needing internet access.
79
+ - **Docker image:** Build a pre-baked container with `docker build -t rightcodes-launcher -f docker/Dockerfile .`. Supply `RIGHTCODES_OPENAI_API_KEY` (and optionally `RIGHTCODES_OPENAI_API_KEY_SOURCE`) at runtime: `docker run --rm -p 8765:8765 -p 8000:8000 -p 5173:5173 -e RIGHTCODES_OPENAI_API_KEY=sk-xxx rightcodes-launcher`.
80
+ - **First-run reminder:** Even with these assets, the *initial* bundle build still requires internet so dependencies can be fetched once before packaging.
81
+
82
+ ## Next Steps
83
+
84
+ 1. Flesh out agent logic in `agents/` and integrate with orchestration hooks.
85
+ 2. Replace the in-memory session store with a durable persistence layer.
86
+ 3. Wire the worker queue (`workers/queue/`) to execute long-running stages asynchronously.
agents/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Bundles
2
+
3
+ Encapsulates prompts, tool definitions, and orchestration glue for each OpenAI Agent engaged in the pipeline.
4
+
5
+ ## Structure
6
+
7
+ - `orchestrator/` - Coordinator agent spec, high-level playbooks, and session controller logic.
8
+ - `extraction/` - Document clause extraction prompts, tool configs for parsers, and output schemas.
9
+ - `standards_mapping/` - Normalization/mapping prompts, ontology helpers, and reference datasets.
10
+ - `rewrite/` - Minimal-change rewrite prompts, safety rules, and diff formatting helpers.
11
+ - `validation/` - Post-rewrite review prompts, calculation sanity checks, and reporting templates.
12
+ - `export/` - Finalization prompts, merge instructions, and docx regeneration aids.
13
+ - `shared/` - Common prompt fragments, JSON schema definitions, and evaluation heuristics.
agents/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .orchestrator.agent import OrchestratorAgent
2
+ from .extraction.agent import ExtractionAgent
3
+ from .standards_mapping.agent import StandardsMappingAgent
4
+ from .rewrite.agent import RewriteAgent
5
+ from .validation.agent import ValidationAgent
6
+ from .export.agent import ExportAgent
7
+
8
+ __all__ = [
9
+ "OrchestratorAgent",
10
+ "ExtractionAgent",
11
+ "StandardsMappingAgent",
12
+ "RewriteAgent",
13
+ "ValidationAgent",
14
+ "ExportAgent",
15
+ ]
agents/export/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import ExportAgent
2
+
3
+ __all__ = ["ExportAgent"]
agents/export/agent.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+
7
+ from docx import Document
8
+
9
+ from server.app.services.diagnostics_service import get_diagnostics_service
10
+
11
+ from ..shared.base import AgentContext, BaseAgent
12
+
13
+
14
+ def _apply_table_replacements(document: Document, replacements: List[Dict[str, Any]]) -> int:
15
+ tables = document.tables
16
+ applied = 0
17
+ for item in replacements:
18
+ index = item.get("table_index")
19
+ updated_rows = item.get("updated_rows")
20
+ if not isinstance(index, int) or index < 0 or index >= len(tables):
21
+ continue
22
+ if not isinstance(updated_rows, list):
23
+ continue
24
+ table = tables[index]
25
+ for row_idx, row_values in enumerate(updated_rows):
26
+ if row_idx < len(table.rows):
27
+ row = table.rows[row_idx]
28
+ else:
29
+ row = table.add_row()
30
+ for col_idx, value in enumerate(row_values):
31
+ if col_idx < len(row.cells):
32
+ row.cells[col_idx].text = str(value) if value is not None else ""
33
+ else:
34
+ break
35
+ applied += 1
36
+ return applied
37
+
38
+
39
+ def _apply_replacements(document: Document, replacements: List[Dict[str, Any]]) -> int:
40
+ applied = 0
41
+ paragraphs = document.paragraphs
42
+ for item in replacements:
43
+ try:
44
+ index = int(item.get("paragraph_index"))
45
+ except (TypeError, ValueError):
46
+ continue
47
+ if index < 0 or index >= len(paragraphs):
48
+ continue
49
+ updated_text = item.get("updated_text")
50
+ if not isinstance(updated_text, str):
51
+ continue
52
+ paragraphs[index].text = updated_text
53
+ applied += 1
54
+ return applied
55
+
56
+
57
+ class ExportAgent(BaseAgent):
58
+ name = "export-agent"
59
+
60
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
61
+ await self.emit_debug("Exporting updated document to DOCX.")
62
+
63
+ rewrite_plan = context.payload.get("rewrite_plan") or {}
64
+ replacements = rewrite_plan.get("replacements") or []
65
+ table_replacements = rewrite_plan.get("table_replacements") or []
66
+ source_path = context.payload.get("original_path")
67
+ if not source_path or not Path(source_path).exists():
68
+ raise RuntimeError("Original document path not supplied to export agent.")
69
+
70
+ document = Document(source_path)
71
+ applied_paragraphs = _apply_replacements(document, replacements)
72
+ applied_tables = _apply_table_replacements(document, table_replacements)
73
+
74
+ storage_root = _resolve_storage_root()
75
+ export_dir = Path(storage_root) / "exports"
76
+ export_dir.mkdir(parents=True, exist_ok=True)
77
+ export_path = export_dir / f"{context.session_id}-converted.docx"
78
+ document.save(export_path)
79
+ diagnostics = get_diagnostics_service()
80
+ diagnostics.record_event(
81
+ node_id="exports",
82
+ event_type="export.generated",
83
+ message=f"Generated export for session `{context.session_id}`",
84
+ metadata={
85
+ "session_id": context.session_id,
86
+ "path": str(export_path),
87
+ "paragraph_replacements": applied_paragraphs,
88
+ "table_updates": applied_tables,
89
+ },
90
+ )
91
+
92
+ if applied_paragraphs or applied_tables:
93
+ note = "Converted document generated using rewrite plan."
94
+ else:
95
+ note = "Export completed, but no replacements were applied."
96
+
97
+ return {
98
+ "export_path": str(export_path),
99
+ "notes": note,
100
+ "replacement_count": applied_paragraphs,
101
+ "table_replacement_count": applied_tables,
102
+ "generated_at": datetime.utcnow().isoformat(),
103
+ }
104
+
105
+
106
+ def _resolve_storage_root() -> Path:
107
+ try:
108
+ from server.app.core.config import get_settings # local import avoids circular dependency
109
+
110
+ return get_settings().storage_dir
111
+ except Exception: # noqa: BLE001
112
+ return (Path(__file__).resolve().parents[2] / "storage").resolve()
agents/extraction/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import ExtractionAgent
2
+
3
+ __all__ = ["ExtractionAgent"]
agents/extraction/agent.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, is_dataclass
4
+ import json
5
+ from typing import Any, Dict, Iterable, List
6
+
7
+ from ..shared.base import AgentContext, BaseAgent
8
+
9
+
10
+ class ExtractionAgent(BaseAgent):
11
+ name = "extraction-agent"
12
+ paragraph_chunk_size = 40
13
+ table_chunk_size = 15
14
+ max_text_chars = 1500
15
+
16
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
17
+ raw_paragraphs = context.payload.get("paragraphs", [])
18
+ raw_tables = context.payload.get("tables", [])
19
+ paragraphs = [_prepare_paragraph(item, self.max_text_chars) for item in _normalise_items(raw_paragraphs)]
20
+ tables = [_prepare_table(item, self.max_text_chars) for item in _normalise_items(raw_tables)]
21
+ metadata = context.payload.get("metadata", {})
22
+
23
+ if not paragraphs and not tables:
24
+ await self.emit_debug("No document content supplied to extraction agent.")
25
+ return {
26
+ "document_summary": "",
27
+ "sections": [],
28
+ "tables": [],
29
+ "references": [],
30
+ "notes": "Skipped: no document content provided.",
31
+ }
32
+
33
+ schema = {
34
+ "name": "ExtractionResult",
35
+ "schema": {
36
+ "type": "object",
37
+ "properties": {
38
+ "document_summary": {"type": "string"},
39
+ "sections": {
40
+ "type": "array",
41
+ "items": {
42
+ "type": "object",
43
+ "properties": {
44
+ "paragraph_index": {"type": "integer"},
45
+ "text": {"type": "string"},
46
+ "references": {
47
+ "type": "array",
48
+ "items": {"type": "string"},
49
+ "default": [],
50
+ },
51
+ },
52
+ "required": ["paragraph_index", "text"],
53
+ "additionalProperties": False,
54
+ },
55
+ "default": [],
56
+ },
57
+ "tables": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "properties": {
62
+ "table_index": {"type": "integer"},
63
+ "summary": {"type": "string"},
64
+ "references": {
65
+ "type": "array",
66
+ "items": {"type": "string"},
67
+ "default": [],
68
+ },
69
+ },
70
+ "required": ["table_index", "summary"],
71
+ "additionalProperties": False,
72
+ },
73
+ "default": [],
74
+ },
75
+ "references": {
76
+ "type": "array",
77
+ "items": {"type": "string"},
78
+ "default": [],
79
+ },
80
+ "notes": {"type": "string"},
81
+ },
82
+ "required": ["document_summary", "sections", "tables", "references"],
83
+ "additionalProperties": False,
84
+ },
85
+ }
86
+
87
+ model = self.settings.openai_model_extract
88
+ aggregated_sections: List[Dict[str, Any]] = []
89
+ aggregated_tables: List[Dict[str, Any]] = []
90
+ aggregated_references: set[str] = set()
91
+ summaries: List[str] = []
92
+ notes: List[str] = []
93
+
94
+ for chunk in _chunk_list(paragraphs, self.paragraph_chunk_size):
95
+ batch = await self._process_batch(
96
+ context=context,
97
+ model=model,
98
+ schema=schema,
99
+ paragraphs=chunk,
100
+ tables=[],
101
+ metadata=metadata,
102
+ )
103
+ if batch:
104
+ summaries.append(batch.get("document_summary", ""))
105
+ aggregated_sections.extend(batch.get("sections", []))
106
+ aggregated_tables.extend(batch.get("tables", []))
107
+ aggregated_references.update(batch.get("references", []))
108
+ if batch.get("notes"):
109
+ notes.append(batch["notes"])
110
+
111
+ for chunk in _chunk_list(tables, self.table_chunk_size):
112
+ batch = await self._process_batch(
113
+ context=context,
114
+ model=model,
115
+ schema=schema,
116
+ paragraphs=[],
117
+ tables=chunk,
118
+ metadata=metadata,
119
+ )
120
+ if batch:
121
+ aggregated_tables.extend(batch.get("tables", []))
122
+ aggregated_references.update(batch.get("references", []))
123
+ if batch.get("notes"):
124
+ notes.append(batch["notes"])
125
+
126
+ summary = " ".join(filter(None, summaries)).strip()
127
+
128
+ for item in paragraphs:
129
+ aggregated_references.update(item.get("references", []))
130
+ for item in tables:
131
+ aggregated_references.update(item.get("references", []))
132
+
133
+ return {
134
+ "document_summary": summary,
135
+ "sections": aggregated_sections,
136
+ "tables": aggregated_tables,
137
+ "references": sorted(aggregated_references),
138
+ "notes": " ".join(notes).strip(),
139
+ }
140
+
141
+ async def _process_batch(
142
+ self,
143
+ *,
144
+ context: AgentContext,
145
+ model: str,
146
+ schema: Dict[str, Any],
147
+ paragraphs: List[Dict[str, Any]],
148
+ tables: List[Dict[str, Any]],
149
+ metadata: Dict[str, Any],
150
+ ) -> Dict[str, Any]:
151
+ if not paragraphs and not tables:
152
+ return {}
153
+
154
+ payload = {
155
+ "paragraphs": paragraphs,
156
+ "tables": tables,
157
+ "metadata": metadata,
158
+ }
159
+
160
+ messages = [
161
+ {
162
+ "role": "system",
163
+ "content": (
164
+ "You are an engineering standards analyst. "
165
+ "Analyse the supplied report content, identify normative references, "
166
+ "and return structured data following the JSON schema."
167
+ ),
168
+ },
169
+ {
170
+ "role": "user",
171
+ "content": (
172
+ f"Session ID: {context.session_id}\n"
173
+ f"Payload: {json.dumps(payload, ensure_ascii=False)}"
174
+ ),
175
+ },
176
+ ]
177
+
178
+ try:
179
+ result = await self.call_openai_json(model=model, messages=messages, schema=schema)
180
+ return result
181
+ except Exception as exc: # noqa: BLE001
182
+ await self.emit_debug(f"Extraction chunk failed: {exc}")
183
+ return {
184
+ "document_summary": "",
185
+ "sections": [],
186
+ "tables": [],
187
+ "references": [],
188
+ "notes": f"Chunk failed: {exc}",
189
+ }
190
+
191
+
192
+ def _normalise_items(items: List[Any]) -> List[Dict[str, Any]]:
193
+ normalised: List[Dict[str, Any]] = []
194
+ for item in items:
195
+ if is_dataclass(item):
196
+ normalised.append(asdict(item))
197
+ elif isinstance(item, dict):
198
+ normalised.append(item)
199
+ else:
200
+ normalised.append({"value": str(item)})
201
+ return normalised
202
+
203
+
204
+ def _prepare_paragraph(item: Dict[str, Any], max_chars: int) -> Dict[str, Any]:
205
+ text = item.get("text", "")
206
+ if len(text) > max_chars:
207
+ text = text[:max_chars] + "...(trimmed)"
208
+ return {
209
+ "index": item.get("index"),
210
+ "text": text,
211
+ "style": item.get("style"),
212
+ "heading_level": item.get("heading_level"),
213
+ "references": item.get("references", []),
214
+ }
215
+
216
+
217
+ def _prepare_table(item: Dict[str, Any], max_chars: int) -> Dict[str, Any]:
218
+ rows = item.get("rows", [])
219
+ preview_rows = []
220
+ for row in rows:
221
+ preview_row = []
222
+ for cell in row:
223
+ cell_text = str(cell)
224
+ if len(cell_text) > max_chars:
225
+ cell_text = cell_text[:max_chars] + "...(trimmed)"
226
+ preview_row.append(cell_text)
227
+ preview_rows.append(preview_row)
228
+ return {
229
+ "index": item.get("index"),
230
+ "rows": preview_rows,
231
+ "references": item.get("references", []),
232
+ }
233
+
234
+
235
+ def _chunk_list(items: List[Dict[str, Any]], size: int) -> Iterable[List[Dict[str, Any]]]:
236
+ if size <= 0:
237
+ size = len(items) or 1
238
+ for idx in range(0, len(items), size):
239
+ yield items[idx : idx + size]
agents/orchestrator/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import OrchestratorAgent
2
+
3
+ __all__ = ["OrchestratorAgent"]
agents/orchestrator/agent.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from ..shared.base import AgentContext, BaseAgent
6
+
7
+
8
+ class OrchestratorAgent(BaseAgent):
9
+ name = "orchestrator-agent"
10
+
11
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
12
+ await self.emit_debug(f"Received session {context.session_id}")
13
+ # In a future iteration this agent will orchestrate sub-agent calls.
14
+ return {
15
+ "next_stage": "ingest",
16
+ "notes": "Placeholder orchestrator response.",
17
+ "input_payload": context.payload,
18
+ }
agents/rewrite/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import RewriteAgent
2
+
3
+ __all__ = ["RewriteAgent"]
agents/rewrite/agent.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, Iterable, List, Sequence
5
+
6
+ from ..shared.base import AgentContext, BaseAgent
7
+
8
+
9
+ class RewriteAgent(BaseAgent):
10
+ name = "rewrite-agent"
11
+ paragraph_chunk_size = 20
12
+ max_paragraph_chars = 1600
13
+ max_table_chars = 1200
14
+
15
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
16
+ mapping_result = context.payload.get("mapping_result") or {}
17
+ mappings: List[Dict[str, Any]] = mapping_result.get("mappings", [])
18
+ if not mappings:
19
+ await self.emit_debug("Rewrite skipped: no mappings provided.")
20
+ return {
21
+ "replacements": [],
22
+ "table_replacements": [],
23
+ "change_log": [],
24
+ "notes": "Rewrite skipped: no mappings provided.",
25
+ }
26
+
27
+ mapping_by_reference = _index_mappings(mappings)
28
+ doc_paragraphs = _normalise_paragraphs(
29
+ context.payload.get("document_paragraphs", []), self.max_paragraph_chars
30
+ )
31
+ doc_tables = _normalise_tables(
32
+ context.payload.get("document_tables", []), self.max_table_chars
33
+ )
34
+
35
+ paragraphs_to_rewrite = [
36
+ paragraph
37
+ for paragraph in doc_paragraphs
38
+ if any(ref in mapping_by_reference for ref in paragraph["references"])
39
+ ]
40
+ tables_to_rewrite = [
41
+ table
42
+ for table in doc_tables
43
+ if any(ref in mapping_by_reference for ref in table["references"])
44
+ ]
45
+
46
+ if not paragraphs_to_rewrite and not tables_to_rewrite:
47
+ await self.emit_debug("Rewrite skipped: no paragraphs or tables matched mapped references.")
48
+ return {
49
+ "replacements": [],
50
+ "table_replacements": [],
51
+ "change_log": [],
52
+ "notes": "Rewrite skipped: no references found in document.",
53
+ }
54
+
55
+ aggregated_replacements: List[Dict[str, Any]] = []
56
+ aggregated_table_replacements: List[Dict[str, Any]] = []
57
+ change_log_entries: List[Dict[str, Any]] = []
58
+ change_log_seen: set[tuple] = set()
59
+ notes: List[str] = []
60
+ target_voice = context.payload.get("target_voice", "Professional engineering tone")
61
+ constraints = context.payload.get("constraints", [])
62
+
63
+ pending_tables = {table["index"]: table for table in tables_to_rewrite}
64
+
65
+ for chunk in _chunk_list(paragraphs_to_rewrite, self.paragraph_chunk_size):
66
+ relevant_refs = sorted(
67
+ {
68
+ reference
69
+ for paragraph in chunk
70
+ for reference in paragraph["references"]
71
+ if reference in mapping_by_reference
72
+ }
73
+ )
74
+ if not relevant_refs:
75
+ continue
76
+
77
+ mapping_subset = _collect_mapping_subset(mapping_by_reference, relevant_refs)
78
+ associated_tables = _collect_tables_for_refs(pending_tables, relevant_refs)
79
+
80
+ payload = {
81
+ "instructions": {
82
+ "target_voice": target_voice,
83
+ "constraints": constraints,
84
+ "guidance": [
85
+ "Preserve numbering, bullet markers, and formatting cues.",
86
+ "Do not alter calculations, quantities, or engineering values.",
87
+ "Only update normative references and surrounding wording necessary for clarity.",
88
+ "Maintain section titles and headings.",
89
+ ],
90
+ },
91
+ "paragraphs": chunk,
92
+ "tables": associated_tables,
93
+ "mappings": mapping_subset,
94
+ }
95
+
96
+ schema = _rewrite_schema()
97
+ messages = [
98
+ {
99
+ "role": "system",
100
+ "content": (
101
+ "You are an engineering editor updating a report so its references align with the target standards. "
102
+ "Return JSON matching the schema. Maintain original structure and numbering while replacing each reference with the mapped target references."
103
+ ),
104
+ },
105
+ {
106
+ "role": "user",
107
+ "content": (
108
+ f"Session: {context.session_id}\n"
109
+ f"Payload: {json.dumps(payload, ensure_ascii=False)}"
110
+ ),
111
+ },
112
+ ]
113
+
114
+ try:
115
+ result = await self.call_openai_json(
116
+ model=self.settings.openai_model_rewrite,
117
+ messages=messages,
118
+ schema=schema,
119
+ )
120
+ aggregated_replacements.extend(result.get("replacements", []))
121
+ aggregated_table_replacements.extend(result.get("table_replacements", []))
122
+ for entry in result.get("change_log", []):
123
+ key = (
124
+ entry.get("reference"),
125
+ entry.get("target_reference"),
126
+ tuple(entry.get("affected_paragraphs", [])),
127
+ )
128
+ if key not in change_log_seen:
129
+ change_log_entries.append(entry)
130
+ change_log_seen.add(key)
131
+ if result.get("notes"):
132
+ notes.append(result["notes"])
133
+ except Exception as exc: # noqa: BLE001
134
+ await self.emit_debug(f"Rewrite chunk failed: {exc}")
135
+ notes.append(f"Rewrite chunk failed: {exc}")
136
+
137
+ if not aggregated_replacements and not aggregated_table_replacements:
138
+ return {
139
+ "replacements": [],
140
+ "table_replacements": [],
141
+ "change_log": change_log_entries,
142
+ "notes": "Rewrite completed but no updates were suggested.",
143
+ }
144
+
145
+ return {
146
+ "replacements": aggregated_replacements,
147
+ "table_replacements": aggregated_table_replacements,
148
+ "change_log": change_log_entries,
149
+ "notes": " ".join(notes).strip(),
150
+ }
151
+
152
+
153
+ def _index_mappings(mappings: Sequence[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
154
+ index: Dict[str, List[Dict[str, Any]]] = {}
155
+ for mapping in mappings:
156
+ ref = mapping.get("source_reference")
157
+ if not isinstance(ref, str):
158
+ continue
159
+ index.setdefault(ref, []).append(mapping)
160
+ return index
161
+
162
+
163
+ def _collect_mapping_subset(
164
+ mapping_by_reference: Dict[str, List[Dict[str, Any]]],
165
+ references: Sequence[str],
166
+ ) -> List[Dict[str, Any]]:
167
+ subset: List[Dict[str, Any]] = []
168
+ for reference in references:
169
+ subset.extend(mapping_by_reference.get(reference, []))
170
+ return subset
171
+
172
+
173
+ def _collect_tables_for_refs(
174
+ pending_tables: Dict[int, Dict[str, Any]],
175
+ references: Sequence[str],
176
+ ) -> List[Dict[str, Any]]:
177
+ matched: List[Dict[str, Any]] = []
178
+ for index in list(pending_tables.keys()):
179
+ table = pending_tables[index]
180
+ if any(ref in references for ref in table["references"]):
181
+ matched.append(table)
182
+ pending_tables.pop(index, None)
183
+ return matched
184
+
185
+
186
+ def _normalise_paragraphs(items: Sequence[Dict[str, Any]], max_chars: int) -> List[Dict[str, Any]]:
187
+ paragraphs: List[Dict[str, Any]] = []
188
+ for item in items:
189
+ index = item.get("index")
190
+ if index is None:
191
+ continue
192
+ text = str(item.get("text", ""))
193
+ if len(text) > max_chars:
194
+ text = text[:max_chars] + "...(trimmed)"
195
+ paragraphs.append(
196
+ {
197
+ "index": index,
198
+ "text": text,
199
+ "style": item.get("style"),
200
+ "heading_level": item.get("heading_level"),
201
+ "references": item.get("references", []),
202
+ }
203
+ )
204
+ return paragraphs
205
+
206
+
207
+ def _normalise_tables(items: Sequence[Dict[str, Any]], max_chars: int) -> List[Dict[str, Any]]:
208
+ tables: List[Dict[str, Any]] = []
209
+ for item in items:
210
+ index = item.get("index")
211
+ if index is None:
212
+ continue
213
+ rows = []
214
+ for row in item.get("rows", []):
215
+ preview_row = []
216
+ for cell in row:
217
+ cell_text = str(cell)
218
+ if len(cell_text) > max_chars:
219
+ cell_text = cell_text[:max_chars] + "...(trimmed)"
220
+ preview_row.append(cell_text)
221
+ rows.append(preview_row)
222
+ tables.append(
223
+ {
224
+ "index": index,
225
+ "rows": rows,
226
+ "references": item.get("references", []),
227
+ }
228
+ )
229
+ return tables
230
+
231
+
232
+ def _chunk_list(items: Sequence[Dict[str, Any]], size: int) -> Iterable[List[Dict[str, Any]]]:
233
+ if size <= 0:
234
+ size = len(items) or 1
235
+ for idx in range(0, len(items), size):
236
+ yield list(items[idx : idx + size])
237
+
238
+
239
+ def _rewrite_schema() -> Dict[str, Any]:
240
+ return {
241
+ "name": "RewritePlanChunk",
242
+ "schema": {
243
+ "type": "object",
244
+ "properties": {
245
+ "replacements": {
246
+ "type": "array",
247
+ "items": {
248
+ "type": "object",
249
+ "properties": {
250
+ "paragraph_index": {"type": "integer"},
251
+ "original_text": {"type": "string"},
252
+ "updated_text": {"type": "string"},
253
+ "applied_mappings": {
254
+ "type": "array",
255
+ "items": {"type": "string"},
256
+ "default": [],
257
+ },
258
+ "change_reason": {"type": "string"},
259
+ },
260
+ "required": ["paragraph_index", "updated_text"],
261
+ "additionalProperties": False,
262
+ },
263
+ "default": [],
264
+ },
265
+ "table_replacements": {
266
+ "type": "array",
267
+ "items": {
268
+ "type": "object",
269
+ "properties": {
270
+ "table_index": {"type": "integer"},
271
+ "updated_rows": {
272
+ "type": "array",
273
+ "items": {
274
+ "type": "array",
275
+ "items": {"type": "string"},
276
+ },
277
+ "default": [],
278
+ },
279
+ "applied_mappings": {
280
+ "type": "array",
281
+ "items": {"type": "string"},
282
+ "default": [],
283
+ },
284
+ "change_reason": {"type": "string"},
285
+ },
286
+ "required": ["table_index"],
287
+ "additionalProperties": False,
288
+ },
289
+ "default": [],
290
+ },
291
+ "change_log": {
292
+ "type": "array",
293
+ "items": {
294
+ "type": "object",
295
+ "properties": {
296
+ "reference": {"type": "string"},
297
+ "target_reference": {"type": "string"},
298
+ "affected_paragraphs": {
299
+ "type": "array",
300
+ "items": {"type": "integer"},
301
+ "default": [],
302
+ },
303
+ "note": {"type": "string"},
304
+ },
305
+ "required": ["reference", "target_reference"],
306
+ "additionalProperties": False,
307
+ },
308
+ "default": [],
309
+ },
310
+ "notes": {"type": "string"},
311
+ },
312
+ "required": ["replacements", "table_replacements", "change_log"],
313
+ "additionalProperties": False,
314
+ },
315
+ }
agents/shared/base.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Dict
6
+
7
+ # We import config lazily to avoid circular imports during module initialisation.
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from server.app.core.config import Settings # pragma: no cover
12
+
13
+
14
+ @dataclass
15
+ class AgentContext:
16
+ session_id: str
17
+ payload: Dict[str, Any] = field(default_factory=dict)
18
+
19
+
20
+ class BaseAgent(ABC):
21
+ name: str
22
+
23
+ @abstractmethod
24
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
25
+ """Execute agent logic and return structured output."""
26
+
27
+ async def emit_debug(self, message: str) -> None:
28
+ # Placeholder until logging/event bus is wired in.
29
+ print(f"[{self.name}] {message}")
30
+
31
+ @property
32
+ def settings(self):
33
+ from server.app.core.config import get_settings # import here to avoid circular dependency
34
+
35
+ return get_settings()
36
+
37
+ async def call_openai_json(
38
+ self,
39
+ *,
40
+ model: str,
41
+ messages: list[Dict[str, Any]],
42
+ schema: Dict[str, Any],
43
+ ) -> Dict[str, Any]:
44
+ from .client import create_json_response # import here to avoid circular dependency
45
+
46
+ if not self.settings.openai_api_key:
47
+ await self.emit_debug("OpenAI API key missing; returning empty response.")
48
+ raise RuntimeError("OpenAI API key missing")
49
+ return await create_json_response(model=model, messages=messages, schema=schema)
agents/shared/client.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from functools import lru_cache
6
+ from typing import Any, Iterable
7
+
8
+ from openai import AsyncOpenAI, OpenAIError
9
+
10
+ try:
11
+ from server.app.core.config import get_settings
12
+ from server.app.services.diagnostics_service import get_diagnostics_service
13
+ except ModuleNotFoundError as exc: # pragma: no cover
14
+ raise RuntimeError(
15
+ "Failed to import server configuration. Ensure the project root is on PYTHONPATH."
16
+ ) from exc
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @lru_cache
22
+ def get_openai_client() -> AsyncOpenAI:
23
+ settings = get_settings()
24
+ if not settings.openai_api_key:
25
+ diagnostics = get_diagnostics_service()
26
+ diagnostics.record_event(
27
+ node_id="openai",
28
+ event_type="openai.missing_key",
29
+ message="OpenAI API key missing; requests will fail.",
30
+ metadata={},
31
+ )
32
+ raise RuntimeError(
33
+ "OpenAI API key is not configured. Set RIGHTCODES_OPENAI_API_KEY before invoking agents."
34
+ )
35
+ diagnostics = get_diagnostics_service()
36
+ diagnostics.record_event(
37
+ node_id="openai",
38
+ event_type="openai.client_ready",
39
+ message="OpenAI client initialised.",
40
+ metadata={},
41
+ )
42
+ return AsyncOpenAI(api_key=settings.openai_api_key, base_url=settings.openai_api_base)
43
+
44
+
45
+ async def create_json_response(
46
+ *,
47
+ model: str,
48
+ messages: Iterable[dict[str, Any]],
49
+ schema: dict[str, Any],
50
+ ) -> dict[str, Any]:
51
+ """Invoke OpenAI with a JSON schema response format."""
52
+ client = get_openai_client()
53
+ diagnostics = get_diagnostics_service()
54
+ diagnostics.record_event(
55
+ node_id="openai",
56
+ event_type="openai.request",
57
+ message=f"Requesting model `{model}`",
58
+ metadata={"model": model},
59
+ )
60
+ try:
61
+ response = await client.chat.completions.create(
62
+ model=model,
63
+ messages=list(messages),
64
+ response_format={"type": "json_schema", "json_schema": schema},
65
+ )
66
+ except OpenAIError as exc:
67
+ logger.exception("OpenAI call failed: %s", exc)
68
+ diagnostics.record_event(
69
+ node_id="openai",
70
+ event_type="openai.error",
71
+ message="OpenAI request failed.",
72
+ metadata={"model": model, "error": str(exc)},
73
+ )
74
+ raise
75
+
76
+ try:
77
+ choice = response.choices[0]
78
+ content = choice.message.content if choice and choice.message else None
79
+ if not content:
80
+ raise RuntimeError("OpenAI response did not include message content.")
81
+ payload = json.loads(content)
82
+ diagnostics.record_event(
83
+ node_id="openai",
84
+ event_type="openai.response",
85
+ message="Received OpenAI response.",
86
+ metadata={"model": model},
87
+ )
88
+ return payload
89
+ except (AttributeError, json.JSONDecodeError) as exc:
90
+ logger.exception("Failed to decode OpenAI response: %s", exc)
91
+ diagnostics.record_event(
92
+ node_id="openai",
93
+ event_type="openai.error",
94
+ message="Failed to decode OpenAI response.",
95
+ metadata={"model": model, "error": str(exc)},
96
+ )
97
+ raise RuntimeError("OpenAI response was not valid JSON.") from exc
agents/shared/embeddings.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, List, Sequence
4
+
5
+
6
+
7
+ async def embed_texts(texts: Iterable[str]) -> List[List[float]]:
8
+ texts = [text if text else "" for text in texts]
9
+ if not texts:
10
+ return []
11
+ from ..shared.client import get_openai_client
12
+ client = get_openai_client()
13
+ from server.app.core.config import get_settings
14
+ settings = get_settings()
15
+ response = await client.embeddings.create(
16
+ model=settings.openai_model_embed,
17
+ input=list(texts),
18
+ )
19
+ return [item.embedding for item in response.data]
agents/standards_mapping/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import StandardsMappingAgent
2
+
3
+ __all__ = ["StandardsMappingAgent"]
agents/standards_mapping/agent.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, is_dataclass
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Iterable, List
7
+
8
+ from ..shared.base import AgentContext, BaseAgent
9
+ from ..shared.embeddings import embed_texts
10
+ from common.embedding_store import EmbeddingStore, get_session_embedding_path
11
+
12
+
13
+ class StandardsMappingAgent(BaseAgent):
14
+ name = "standards-mapping-agent"
15
+ reference_chunk_size = 20
16
+ max_excerpt_chars = 800
17
+
18
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
19
+ extraction_result = context.payload.get("extraction_result") or {}
20
+ references: List[str] = extraction_result.get("references") or []
21
+ sections = extraction_result.get("sections") or []
22
+ tables = extraction_result.get("tables") or []
23
+ standards_chunks = _normalise_items(context.payload.get("standards_chunks", []))
24
+ target_metadata = context.payload.get("target_metadata", {})
25
+ store = EmbeddingStore(get_session_embedding_path(context.session_id))
26
+
27
+ if not references or not standards_chunks:
28
+ await self.emit_debug("Insufficient data for standards mapping.")
29
+ return {
30
+ "mappings": [],
31
+ "unmapped_references": references,
32
+ "notes": "Mapping skipped due to missing references or standards content.",
33
+ }
34
+
35
+ schema = {
36
+ "name": "StandardsMapping",
37
+ "schema": {
38
+ "type": "object",
39
+ "properties": {
40
+ "mappings": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "source_reference": {"type": "string"},
46
+ "source_context": {"type": "string"},
47
+ "target_reference": {"type": "string"},
48
+ "target_clause": {"type": "string"},
49
+ "target_summary": {"type": "string"},
50
+ "confidence": {"type": "number"},
51
+ "rationale": {"type": "string"},
52
+ },
53
+ "required": [
54
+ "source_reference",
55
+ "target_reference",
56
+ "confidence",
57
+ ],
58
+ "additionalProperties": False,
59
+ },
60
+ "default": [],
61
+ },
62
+ "unmapped_references": {
63
+ "type": "array",
64
+ "items": {"type": "string"},
65
+ "default": [],
66
+ },
67
+ "notes": {"type": "string"},
68
+ },
69
+ "required": ["mappings", "unmapped_references"],
70
+ "additionalProperties": False,
71
+ },
72
+ }
73
+
74
+ standards_overview = _build_standards_overview(standards_chunks, self.max_excerpt_chars)
75
+
76
+ model = self.settings.openai_model_mapping
77
+ aggregated_mappings: List[Dict[str, Any]] = []
78
+ aggregated_unmapped: set[str] = set()
79
+ notes: List[str] = []
80
+
81
+ for chunk in _chunk_list(references, self.reference_chunk_size):
82
+ reference_context = _build_reference_context(
83
+ chunk, sections, tables, self.max_excerpt_chars
84
+ )
85
+ retrieved_candidates = await _retrieve_candidates(
86
+ chunk, reference_context, store, target_metadata, self.max_excerpt_chars
87
+ )
88
+ payload = {
89
+ "references": chunk,
90
+ "reference_context": reference_context,
91
+ "retrieved_candidates": [
92
+ {"reference": ref, "candidates": retrieved_candidates.get(ref, [])}
93
+ for ref in chunk
94
+ ],
95
+ "standards_overview": standards_overview,
96
+ "target_metadata": target_metadata,
97
+ }
98
+
99
+ messages = [
100
+ {
101
+ "role": "system",
102
+ "content": (
103
+ "You are an engineering standards migration specialist. "
104
+ "Map each legacy reference to the best matching clause in the target standards. "
105
+ "Use the provided context and standards overview to justify your mapping. "
106
+ "Return JSON that conforms to the supplied schema."
107
+ ),
108
+ },
109
+ {
110
+ "role": "user",
111
+ "content": (
112
+ f"Session: {context.session_id}\n"
113
+ f"Payload: {json.dumps(payload, ensure_ascii=False)}"
114
+ ),
115
+ },
116
+ ]
117
+
118
+ try:
119
+ result = await self.call_openai_json(model=model, messages=messages, schema=schema)
120
+ aggregated_mappings.extend(result.get("mappings", []))
121
+ aggregated_unmapped.update(result.get("unmapped_references", []))
122
+ if result.get("notes"):
123
+ notes.append(result["notes"])
124
+ except Exception as exc: # noqa: BLE001
125
+ await self.emit_debug(f"Standards mapping chunk failed: {exc}")
126
+ aggregated_unmapped.update(chunk)
127
+ notes.append(f"Chunk failed: {exc}")
128
+
129
+ await self.emit_debug("Standards mapping completed via OpenAI.")
130
+ return {
131
+ "mappings": aggregated_mappings,
132
+ "unmapped_references": sorted(aggregated_unmapped),
133
+ "notes": " ".join(notes).strip(),
134
+ }
135
+
136
+
137
+ def _normalise_items(items: List[Any]) -> List[Dict[str, Any]]:
138
+ normalised: List[Dict[str, Any]] = []
139
+ for item in items:
140
+ if is_dataclass(item):
141
+ normalised.append(asdict(item))
142
+ elif isinstance(item, dict):
143
+ normalised.append(item)
144
+ else:
145
+ normalised.append({"text": str(item)})
146
+ return normalised
147
+
148
+
149
+ def _chunk_list(items: List[str], size: int) -> Iterable[List[str]]:
150
+ if size <= 0:
151
+ size = len(items) or 1
152
+ for idx in range(0, len(items), size):
153
+ yield items[idx : idx + size]
154
+
155
+
156
+ def _build_reference_context(
157
+ references: List[str],
158
+ sections: List[Dict[str, Any]],
159
+ tables: List[Dict[str, Any]],
160
+ max_chars: int,
161
+ ) -> List[Dict[str, Any]]:
162
+ section_map: Dict[str, List[Dict[str, Any]]] = {}
163
+ for section in sections:
164
+ refs = section.get("references") or []
165
+ for ref in refs:
166
+ section_map.setdefault(ref, [])
167
+ if len(section_map[ref]) < 3:
168
+ text = section.get("text", "")
169
+ if len(text) > max_chars:
170
+ text = text[:max_chars] + "...(trimmed)"
171
+ section_map[ref].append(
172
+ {
173
+ "paragraph_index": section.get("paragraph_index"),
174
+ "text": text,
175
+ }
176
+ )
177
+ table_map: Dict[str, List[Dict[str, Any]]] = {}
178
+ for table in tables:
179
+ refs = table.get("references") or []
180
+ for ref in refs:
181
+ table_map.setdefault(ref, [])
182
+ if len(table_map[ref]) < 2:
183
+ table_map[ref].append({"table_index": table.get("table_index"), "references": refs})
184
+
185
+ context = []
186
+ for ref in references:
187
+ context.append(
188
+ {
189
+ "reference": ref,
190
+ "paragraphs": section_map.get(ref, []),
191
+ "tables": table_map.get(ref, []),
192
+ }
193
+ )
194
+ return context
195
+
196
+
197
+ def _build_standards_overview(
198
+ standards_chunks: List[Dict[str, Any]],
199
+ max_chars: int,
200
+ ) -> List[Dict[str, Any]]:
201
+ grouped: Dict[str, Dict[str, Any]] = {}
202
+ for chunk in standards_chunks:
203
+ path = chunk.get("path", "unknown")
204
+ heading = chunk.get("heading")
205
+ clauses = chunk.get("clause_numbers") or []
206
+ text = chunk.get("text", "")
207
+ if len(text) > max_chars:
208
+ text = text[:max_chars] + "...(trimmed)"
209
+
210
+ group = grouped.setdefault(
211
+ path,
212
+ {
213
+ "document": Path(path).name,
214
+ "headings": [],
215
+ "clauses": [],
216
+ "snippets": [],
217
+ },
218
+ )
219
+ if heading and heading not in group["headings"] and len(group["headings"]) < 120:
220
+ group["headings"].append(heading)
221
+ for clause in clauses:
222
+ if clause not in group["clauses"] and len(group["clauses"]) < 120:
223
+ group["clauses"].append(clause)
224
+ if text and len(group["snippets"]) < 30:
225
+ group["snippets"].append(text)
226
+
227
+ overview: List[Dict[str, Any]] = []
228
+ for data in grouped.values():
229
+ overview.append(
230
+ {
231
+ "document": data["document"],
232
+ "headings": data["headings"][:50],
233
+ "clauses": data["clauses"][:50],
234
+ "snippets": data["snippets"],
235
+ }
236
+ )
237
+ return overview[:30]
238
+
239
+
240
+ async def _retrieve_candidates(
241
+ references: List[str],
242
+ reference_context: List[Dict[str, Any]],
243
+ store: EmbeddingStore,
244
+ target_metadata: Dict[str, Any],
245
+ max_chars: int,
246
+ ) -> Dict[str, List[Dict[str, Any]]]:
247
+ if not references:
248
+ return {}
249
+ if store.is_empty:
250
+ return {ref: [] for ref in references}
251
+
252
+ context_lookup = {entry["reference"]: entry for entry in reference_context}
253
+ embed_inputs = [
254
+ _compose_reference_embedding_input(
255
+ reference,
256
+ context_lookup.get(reference, {}),
257
+ target_metadata,
258
+ max_chars,
259
+ )
260
+ for reference in references
261
+ ]
262
+ vectors = await embed_texts(embed_inputs)
263
+ results: Dict[str, List[Dict[str, Any]]] = {}
264
+ for reference, vector in zip(references, vectors):
265
+ candidates = store.query(vector, top_k=8)
266
+ results[reference] = candidates
267
+ return results
268
+
269
+
270
+ def _compose_reference_embedding_input(
271
+ reference: str,
272
+ context_entry: Dict[str, Any],
273
+ target_metadata: Dict[str, Any],
274
+ max_chars: int,
275
+ ) -> str:
276
+ lines = [reference]
277
+ target_standard = target_metadata.get("target_standard")
278
+ if target_standard:
279
+ lines.append(f"Target standard family: {target_standard}")
280
+ paragraphs = context_entry.get("paragraphs") or []
281
+ for paragraph in paragraphs[:2]:
282
+ text = paragraph.get("text")
283
+ if text:
284
+ lines.append(text)
285
+ tables = context_entry.get("tables") or []
286
+ if tables:
287
+ refs = tables[0].get("references") or []
288
+ if refs:
289
+ lines.append("Table references: " + ", ".join(refs))
290
+ text = "\n".join(filter(None, lines))
291
+ if len(text) > max_chars:
292
+ text = text[:max_chars] + "...(trimmed)"
293
+ return text
agents/validation/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import ValidationAgent
2
+
3
+ __all__ = ["ValidationAgent"]
agents/validation/agent.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, List
5
+
6
+ from ..shared.base import AgentContext, BaseAgent
7
+
8
+
9
+ class ValidationAgent(BaseAgent):
10
+ name = "validation-agent"
11
+
12
+ async def run(self, context: AgentContext) -> Dict[str, Any]:
13
+ await self.emit_debug("Running compliance checks.")
14
+
15
+ extraction = context.payload.get("extraction_result") or {}
16
+ mapping = context.payload.get("mapping_result") or {}
17
+ rewrite_plan = context.payload.get("rewrite_plan") or {}
18
+
19
+ if not mapping or not rewrite_plan:
20
+ return {
21
+ "issues": [],
22
+ "verdict": "pending",
23
+ "notes": "Validation skipped because mapping or rewrite data was unavailable.",
24
+ }
25
+
26
+ schema = {
27
+ "name": "ValidationReport",
28
+ "schema": {
29
+ "type": "object",
30
+ "properties": {
31
+ "verdict": {
32
+ "type": "string",
33
+ "enum": ["approved", "changes_requested", "pending"],
34
+ },
35
+ "issues": {
36
+ "type": "array",
37
+ "items": {
38
+ "type": "object",
39
+ "properties": {
40
+ "description": {"type": "string"},
41
+ "severity": {
42
+ "type": "string",
43
+ "enum": ["info", "low", "medium", "high"],
44
+ },
45
+ "related_reference": {"type": "string"},
46
+ },
47
+ "required": ["description", "severity"],
48
+ "additionalProperties": False,
49
+ },
50
+ "default": [],
51
+ },
52
+ "notes": {"type": "string"},
53
+ },
54
+ "required": ["verdict", "issues"],
55
+ "additionalProperties": False,
56
+ },
57
+ }
58
+
59
+ mapping_snippet = json.dumps(mapping.get("mappings", [])[:20], ensure_ascii=False)
60
+ rewrite_snippet = json.dumps(rewrite_plan.get("replacements", [])[:20], ensure_ascii=False)
61
+ references = extraction.get("references", [])
62
+
63
+ messages = [
64
+ {
65
+ "role": "system",
66
+ "content": (
67
+ "You are a senior structural engineer reviewing a standards migration. "
68
+ "Evaluate whether the proposed replacements maintain compliance and highlight any risks."
69
+ ),
70
+ },
71
+ {
72
+ "role": "user",
73
+ "content": (
74
+ f"Session: {context.session_id}\n"
75
+ f"Detected references: {references}\n"
76
+ f"Mappings sample: {mapping_snippet}\n"
77
+ f"Rewrite sample: {rewrite_snippet}"
78
+ ),
79
+ },
80
+ ]
81
+
82
+ model = self.settings.openai_model_mapping
83
+
84
+ try:
85
+ result = await self.call_openai_json(model=model, messages=messages, schema=schema)
86
+ await self.emit_debug("Validation agent completed via OpenAI.")
87
+ if not result.get("notes"):
88
+ result["notes"] = "Review generated by automated validation agent."
89
+ return result
90
+ except Exception as exc: # noqa: BLE001
91
+ await self.emit_debug(f"Validation agent failed: {exc}")
92
+ return {
93
+ "issues": [],
94
+ "verdict": "pending",
95
+ "notes": f"Validation failed: {exc}",
96
+ }
common/README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Common Domain Assets
2
+
3
+ Cross-cutting models, schemas, and events shared between backend services, agents, and workers.
4
+
5
+ ## Structure
6
+
7
+ - `models/` - Core domain entities and DTOs reused across services.
8
+ - `utils/` - Reusable helper functions (text normalization, ID generation).
9
+ - `schemas/` - JSON schema definitions for agent input/output contracts.
10
+ - `events/` - Event payload definitions for pipeline instrumentation.
common/embedding_store.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, List, Sequence, Tuple
7
+
8
+ import numpy as np
9
+
10
+
11
+ def _resolve_embedding_root() -> Path:
12
+ try:
13
+ from server.app.core.config import get_settings # local import to avoid hard dependency at import time
14
+
15
+ storage_dir = get_settings().storage_dir
16
+ except Exception: # noqa: BLE001
17
+ storage_dir = Path(__file__).resolve().parents[1] / "storage"
18
+ return Path(storage_dir) / "embeddings"
19
+
20
+
21
+ def get_session_embedding_path(session_id: str) -> Path:
22
+ return _resolve_embedding_root() / f"{session_id}.json"
23
+
24
+
25
+ @dataclass
26
+ class EmbeddingRecord:
27
+ vector: List[float]
28
+ metadata: dict[str, Any]
29
+
30
+
31
+ class EmbeddingStore:
32
+ def __init__(self, path: Path) -> None:
33
+ self.path = path
34
+ self._records: list[EmbeddingRecord] = []
35
+ self._matrix: np.ndarray | None = None
36
+ self._load()
37
+
38
+ def _load(self) -> None:
39
+ if not self.path.exists():
40
+ return
41
+ with self.path.open("r", encoding="utf-8") as fh:
42
+ data = json.load(fh)
43
+ self._records = [EmbeddingRecord(**item) for item in data]
44
+
45
+ def save(self) -> None:
46
+ self.path.parent.mkdir(parents=True, exist_ok=True)
47
+ with self.path.open("w", encoding="utf-8") as fh:
48
+ json.dump(
49
+ [{"vector": rec.vector, "metadata": rec.metadata} for rec in self._records],
50
+ fh,
51
+ ensure_ascii=False,
52
+ indent=2,
53
+ )
54
+
55
+ def clear(self) -> None:
56
+ self._records.clear()
57
+ self._matrix = None
58
+
59
+ def extend(self, vectors: Sequence[Sequence[float]], metadatas: Sequence[dict[str, Any]]) -> None:
60
+ for vector, metadata in zip(vectors, metadatas, strict=True):
61
+ self._records.append(EmbeddingRecord(list(vector), dict(metadata)))
62
+ self._matrix = None
63
+
64
+ @property
65
+ def is_empty(self) -> bool:
66
+ return not self._records
67
+
68
+ def _ensure_matrix(self) -> None:
69
+ if self._matrix is None and self._records:
70
+ self._matrix = np.array([rec.vector for rec in self._records], dtype=np.float32)
71
+
72
+ def query(self, vector: Sequence[float], top_k: int = 5) -> List[Tuple[dict[str, Any], float]]:
73
+ if self.is_empty:
74
+ return []
75
+ self._ensure_matrix()
76
+ assert self._matrix is not None
77
+ matrix = self._matrix
78
+ vec = np.array(vector, dtype=np.float32)
79
+ vec_norm = np.linalg.norm(vec)
80
+ if not np.isfinite(vec_norm) or vec_norm == 0:
81
+ return []
82
+ matrix_norms = np.linalg.norm(matrix, axis=1)
83
+ scores = matrix @ vec / (matrix_norms * vec_norm + 1e-12)
84
+ top_k = min(top_k, len(scores))
85
+ indices = np.argsort(scores)[::-1][:top_k]
86
+ results: List[Tuple[dict[str, Any], float]] = []
87
+ for idx in indices:
88
+ score = float(scores[idx])
89
+ metadata = self._records[int(idx)].metadata.copy()
90
+ metadata["score"] = score
91
+ results.append((metadata, score))
92
+ return results
93
+
94
+ def query_many(self, vectors: Sequence[Sequence[float]], top_k: int = 5) -> List[List[dict[str, Any]]]:
95
+ return [
96
+ [meta for meta, _ in self.query(vector, top_k=top_k)]
97
+ for vector in vectors
98
+ ]
docker/Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-bookworm
2
+
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends python3 python3-venv python3-pip \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+
9
+ COPY . /app
10
+
11
+ RUN python3 -m venv /app/.venv \
12
+ && /app/.venv/bin/pip install --upgrade pip \
13
+ && /app/.venv/bin/pip install -r server/requirements.txt \
14
+ && npm install --prefix frontend
15
+
16
+ ENV PATH="/app/.venv/bin:${PATH}"
17
+
18
+ EXPOSE 8000 5173 8765
19
+
20
+ CMD ["python3", "start-rightcodes.py", "--host", "0.0.0.0", "--port", "8765", "--no-browser"]
docs/README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Documentation Hub
2
+
3
+ Authoritative documentation for architecture, pipelines, agents, and UI workflows.
4
+
5
+ ## Structure
6
+
7
+ - `architecture/` - System diagrams, deployment topology, and context views.
8
+ - `agents/` - Detailed specs of each agent, including prompt design and tool APIs.
9
+ - `pipelines/` - Pypeflow diagrams, data contracts, and runbooks for each stage.
10
+ - `standards/` - Reference material and taxonomy notes for supported codes and standards.
11
+ - `ui/` - Wireframes, component inventories, and interaction design specs.
12
+ - `specifications/` - Functional requirements, acceptance criteria, and use cases.
docs/agents/orchestrator.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Orchestrator Agent (Draft)
2
+
3
+ - **Goal:** Coordinate pipeline stages, persist state transitions, and request human intervention when confidence drops below threshold.
4
+ - **Inputs:** `document_manifest.json`, latest stage outputs, user session preferences.
5
+ - **Outputs:** `progress_state.json`, downstream agent invocation plans, notifications/events.
6
+ - **Tool Hooks:** Worker queue enqueuer, storage manifest writer, validation reporter.
7
+
8
+ Action items:
9
+
10
+ 1. Define structured prompt schema and guardrails.
11
+ 2. Enumerate tool signatures for queueing, status updates, and failure escalation.
12
+ 3. Align logging with `common/events/` payload definitions.
docs/architecture/system-context.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # System Context (Draft)
2
+
3
+ - **Primary Actors:** Engineering consultant (user), Orchestrator Agent, Validation Agent, Export Agent.
4
+ - **External Systems:** OpenAI APIs, Object storage (S3-compatible), Auth provider (to be determined).
5
+ - **Key Data Stores:** Session manifest store, document blob storage, telemetry pipeline.
6
+
7
+ Pending tasks:
8
+
9
+ 1. Complete C4 level 1 context diagram.
10
+ 2. Document trust boundaries (uploaded documents vs generated artefacts).
11
+ 3. Define audit/logging requirements tied to standards compliance.
docs/pipelines/pypeflow-overview.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Pypeflow Overview
2
+
3
+ > Placeholder: document the end-to-end job graph once the first pipeline prototype lands. Recommended sections:
4
+ >
5
+ > 1. High-level mermaid diagram showing ingestion -> extraction -> mapping -> rewrite -> validation -> export.
6
+ > 2. Stage-by-stage JSON artefact expectations (input/output schemas).
7
+ > 3. Failure handling and retry strategy per stage.
8
+ > 4. Hooks for agent overrides and manual approvals.
docs/ui/review-workflow.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Review Workflow (Draft)
2
+
3
+ Stages:
4
+
5
+ 1. Upload wizard captures Word report, one or more standards PDFs, and mapping intent.
6
+ 2. Diff workspace highlights replaced clauses with inline confidence tags.
7
+ 3. Validation dashboard lists outstanding checks, comments, and approval history.
8
+
9
+ Open questions:
10
+
11
+ - How should we present clause-level provenance (link back to PDF page)?
12
+ - Do we surface agent rationales verbatim or summarised?
13
+ - What accessibility requirements should inform colour coding and indicators?
14
+
15
+ UI notes:
16
+
17
+ - Show upload progress and pipeline activity log so users know when each processing stage completes.
frontend/.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ VITE_API_BASE_URL=http://localhost:8000/api
frontend/README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frontend Module
2
+
3
+ React/TypeScript single-page app responsible for user interaction and review workflows.
4
+
5
+ ## Structure
6
+
7
+ - `public/` - Static assets and HTML shell.
8
+ - `src/components/` - Shared UI components (uploaders, diff panels, status widgets).
9
+ - `src/pages/` - Route-level containers for upload, review, and export views.
10
+ - `src/hooks/` - Reusable logic for API access, session state, and polling.
11
+ - `src/layouts/` - Shell layouts (wizard, review workspace).
12
+ - `src/state/` - Store configuration (React Query, Zustand, or Redux).
13
+ - `src/services/` - API clients, WebSocket connectors, and agent progress handlers.
14
+ - `src/utils/` - Formatting helpers, doc diff utilities, and schema transformers.
15
+ - `src/types/` - Shared TypeScript declarations.
16
+ - `tests/` - Component and integration tests with fixtures and mocks.
17
+ - `config/` - Build-time configuration (Vite/Webpack, env samples).
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>RightCodes</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2030 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rightcodes-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "rightcodes-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@tanstack/react-query": "^5.51.11",
12
+ "axios": "^1.7.7",
13
+ "react": "^18.3.1",
14
+ "react-dom": "^18.3.1",
15
+ "react-router-dom": "^6.26.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.3.3",
19
+ "@types/react-dom": "^18.3.3",
20
+ "@vitejs/plugin-react": "^4.3.1",
21
+ "typescript": "^5.4.5",
22
+ "vite": "^5.4.8"
23
+ }
24
+ },
25
+ "node_modules/@babel/code-frame": {
26
+ "version": "7.27.1",
27
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
28
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
29
+ "dev": true,
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@babel/helper-validator-identifier": "^7.27.1",
33
+ "js-tokens": "^4.0.0",
34
+ "picocolors": "^1.1.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=6.9.0"
38
+ }
39
+ },
40
+ "node_modules/@babel/compat-data": {
41
+ "version": "7.28.4",
42
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
43
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
44
+ "dev": true,
45
+ "license": "MIT",
46
+ "engines": {
47
+ "node": ">=6.9.0"
48
+ }
49
+ },
50
+ "node_modules/@babel/core": {
51
+ "version": "7.28.4",
52
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
53
+ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
54
+ "dev": true,
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "@babel/code-frame": "^7.27.1",
58
+ "@babel/generator": "^7.28.3",
59
+ "@babel/helper-compilation-targets": "^7.27.2",
60
+ "@babel/helper-module-transforms": "^7.28.3",
61
+ "@babel/helpers": "^7.28.4",
62
+ "@babel/parser": "^7.28.4",
63
+ "@babel/template": "^7.27.2",
64
+ "@babel/traverse": "^7.28.4",
65
+ "@babel/types": "^7.28.4",
66
+ "@jridgewell/remapping": "^2.3.5",
67
+ "convert-source-map": "^2.0.0",
68
+ "debug": "^4.1.0",
69
+ "gensync": "^1.0.0-beta.2",
70
+ "json5": "^2.2.3",
71
+ "semver": "^6.3.1"
72
+ },
73
+ "engines": {
74
+ "node": ">=6.9.0"
75
+ },
76
+ "funding": {
77
+ "type": "opencollective",
78
+ "url": "https://opencollective.com/babel"
79
+ }
80
+ },
81
+ "node_modules/@babel/generator": {
82
+ "version": "7.28.3",
83
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
84
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
85
+ "dev": true,
86
+ "license": "MIT",
87
+ "dependencies": {
88
+ "@babel/parser": "^7.28.3",
89
+ "@babel/types": "^7.28.2",
90
+ "@jridgewell/gen-mapping": "^0.3.12",
91
+ "@jridgewell/trace-mapping": "^0.3.28",
92
+ "jsesc": "^3.0.2"
93
+ },
94
+ "engines": {
95
+ "node": ">=6.9.0"
96
+ }
97
+ },
98
+ "node_modules/@babel/helper-compilation-targets": {
99
+ "version": "7.27.2",
100
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
101
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
102
+ "dev": true,
103
+ "license": "MIT",
104
+ "dependencies": {
105
+ "@babel/compat-data": "^7.27.2",
106
+ "@babel/helper-validator-option": "^7.27.1",
107
+ "browserslist": "^4.24.0",
108
+ "lru-cache": "^5.1.1",
109
+ "semver": "^6.3.1"
110
+ },
111
+ "engines": {
112
+ "node": ">=6.9.0"
113
+ }
114
+ },
115
+ "node_modules/@babel/helper-globals": {
116
+ "version": "7.28.0",
117
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
118
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
119
+ "dev": true,
120
+ "license": "MIT",
121
+ "engines": {
122
+ "node": ">=6.9.0"
123
+ }
124
+ },
125
+ "node_modules/@babel/helper-module-imports": {
126
+ "version": "7.27.1",
127
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
128
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
129
+ "dev": true,
130
+ "license": "MIT",
131
+ "dependencies": {
132
+ "@babel/traverse": "^7.27.1",
133
+ "@babel/types": "^7.27.1"
134
+ },
135
+ "engines": {
136
+ "node": ">=6.9.0"
137
+ }
138
+ },
139
+ "node_modules/@babel/helper-module-transforms": {
140
+ "version": "7.28.3",
141
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
142
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
143
+ "dev": true,
144
+ "license": "MIT",
145
+ "dependencies": {
146
+ "@babel/helper-module-imports": "^7.27.1",
147
+ "@babel/helper-validator-identifier": "^7.27.1",
148
+ "@babel/traverse": "^7.28.3"
149
+ },
150
+ "engines": {
151
+ "node": ">=6.9.0"
152
+ },
153
+ "peerDependencies": {
154
+ "@babel/core": "^7.0.0"
155
+ }
156
+ },
157
+ "node_modules/@babel/helper-plugin-utils": {
158
+ "version": "7.27.1",
159
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
160
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
161
+ "dev": true,
162
+ "license": "MIT",
163
+ "engines": {
164
+ "node": ">=6.9.0"
165
+ }
166
+ },
167
+ "node_modules/@babel/helper-string-parser": {
168
+ "version": "7.27.1",
169
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
170
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
171
+ "dev": true,
172
+ "license": "MIT",
173
+ "engines": {
174
+ "node": ">=6.9.0"
175
+ }
176
+ },
177
+ "node_modules/@babel/helper-validator-identifier": {
178
+ "version": "7.27.1",
179
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
180
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
181
+ "dev": true,
182
+ "license": "MIT",
183
+ "engines": {
184
+ "node": ">=6.9.0"
185
+ }
186
+ },
187
+ "node_modules/@babel/helper-validator-option": {
188
+ "version": "7.27.1",
189
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
190
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
191
+ "dev": true,
192
+ "license": "MIT",
193
+ "engines": {
194
+ "node": ">=6.9.0"
195
+ }
196
+ },
197
+ "node_modules/@babel/helpers": {
198
+ "version": "7.28.4",
199
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
200
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
201
+ "dev": true,
202
+ "license": "MIT",
203
+ "dependencies": {
204
+ "@babel/template": "^7.27.2",
205
+ "@babel/types": "^7.28.4"
206
+ },
207
+ "engines": {
208
+ "node": ">=6.9.0"
209
+ }
210
+ },
211
+ "node_modules/@babel/parser": {
212
+ "version": "7.28.4",
213
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
214
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
215
+ "dev": true,
216
+ "license": "MIT",
217
+ "dependencies": {
218
+ "@babel/types": "^7.28.4"
219
+ },
220
+ "bin": {
221
+ "parser": "bin/babel-parser.js"
222
+ },
223
+ "engines": {
224
+ "node": ">=6.0.0"
225
+ }
226
+ },
227
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
228
+ "version": "7.27.1",
229
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
230
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
231
+ "dev": true,
232
+ "license": "MIT",
233
+ "dependencies": {
234
+ "@babel/helper-plugin-utils": "^7.27.1"
235
+ },
236
+ "engines": {
237
+ "node": ">=6.9.0"
238
+ },
239
+ "peerDependencies": {
240
+ "@babel/core": "^7.0.0-0"
241
+ }
242
+ },
243
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
244
+ "version": "7.27.1",
245
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
246
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
247
+ "dev": true,
248
+ "license": "MIT",
249
+ "dependencies": {
250
+ "@babel/helper-plugin-utils": "^7.27.1"
251
+ },
252
+ "engines": {
253
+ "node": ">=6.9.0"
254
+ },
255
+ "peerDependencies": {
256
+ "@babel/core": "^7.0.0-0"
257
+ }
258
+ },
259
+ "node_modules/@babel/template": {
260
+ "version": "7.27.2",
261
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
262
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
263
+ "dev": true,
264
+ "license": "MIT",
265
+ "dependencies": {
266
+ "@babel/code-frame": "^7.27.1",
267
+ "@babel/parser": "^7.27.2",
268
+ "@babel/types": "^7.27.1"
269
+ },
270
+ "engines": {
271
+ "node": ">=6.9.0"
272
+ }
273
+ },
274
+ "node_modules/@babel/traverse": {
275
+ "version": "7.28.4",
276
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
277
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
278
+ "dev": true,
279
+ "license": "MIT",
280
+ "dependencies": {
281
+ "@babel/code-frame": "^7.27.1",
282
+ "@babel/generator": "^7.28.3",
283
+ "@babel/helper-globals": "^7.28.0",
284
+ "@babel/parser": "^7.28.4",
285
+ "@babel/template": "^7.27.2",
286
+ "@babel/types": "^7.28.4",
287
+ "debug": "^4.3.1"
288
+ },
289
+ "engines": {
290
+ "node": ">=6.9.0"
291
+ }
292
+ },
293
+ "node_modules/@babel/types": {
294
+ "version": "7.28.4",
295
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
296
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
297
+ "dev": true,
298
+ "license": "MIT",
299
+ "dependencies": {
300
+ "@babel/helper-string-parser": "^7.27.1",
301
+ "@babel/helper-validator-identifier": "^7.27.1"
302
+ },
303
+ "engines": {
304
+ "node": ">=6.9.0"
305
+ }
306
+ },
307
+ "node_modules/@esbuild/aix-ppc64": {
308
+ "version": "0.21.5",
309
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
310
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
311
+ "cpu": [
312
+ "ppc64"
313
+ ],
314
+ "dev": true,
315
+ "license": "MIT",
316
+ "optional": true,
317
+ "os": [
318
+ "aix"
319
+ ],
320
+ "engines": {
321
+ "node": ">=12"
322
+ }
323
+ },
324
+ "node_modules/@esbuild/android-arm": {
325
+ "version": "0.21.5",
326
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
327
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
328
+ "cpu": [
329
+ "arm"
330
+ ],
331
+ "dev": true,
332
+ "license": "MIT",
333
+ "optional": true,
334
+ "os": [
335
+ "android"
336
+ ],
337
+ "engines": {
338
+ "node": ">=12"
339
+ }
340
+ },
341
+ "node_modules/@esbuild/android-arm64": {
342
+ "version": "0.21.5",
343
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
344
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
345
+ "cpu": [
346
+ "arm64"
347
+ ],
348
+ "dev": true,
349
+ "license": "MIT",
350
+ "optional": true,
351
+ "os": [
352
+ "android"
353
+ ],
354
+ "engines": {
355
+ "node": ">=12"
356
+ }
357
+ },
358
+ "node_modules/@esbuild/android-x64": {
359
+ "version": "0.21.5",
360
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
361
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
362
+ "cpu": [
363
+ "x64"
364
+ ],
365
+ "dev": true,
366
+ "license": "MIT",
367
+ "optional": true,
368
+ "os": [
369
+ "android"
370
+ ],
371
+ "engines": {
372
+ "node": ">=12"
373
+ }
374
+ },
375
+ "node_modules/@esbuild/darwin-arm64": {
376
+ "version": "0.21.5",
377
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
378
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
379
+ "cpu": [
380
+ "arm64"
381
+ ],
382
+ "dev": true,
383
+ "license": "MIT",
384
+ "optional": true,
385
+ "os": [
386
+ "darwin"
387
+ ],
388
+ "engines": {
389
+ "node": ">=12"
390
+ }
391
+ },
392
+ "node_modules/@esbuild/darwin-x64": {
393
+ "version": "0.21.5",
394
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
395
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
396
+ "cpu": [
397
+ "x64"
398
+ ],
399
+ "dev": true,
400
+ "license": "MIT",
401
+ "optional": true,
402
+ "os": [
403
+ "darwin"
404
+ ],
405
+ "engines": {
406
+ "node": ">=12"
407
+ }
408
+ },
409
+ "node_modules/@esbuild/freebsd-arm64": {
410
+ "version": "0.21.5",
411
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
412
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
413
+ "cpu": [
414
+ "arm64"
415
+ ],
416
+ "dev": true,
417
+ "license": "MIT",
418
+ "optional": true,
419
+ "os": [
420
+ "freebsd"
421
+ ],
422
+ "engines": {
423
+ "node": ">=12"
424
+ }
425
+ },
426
+ "node_modules/@esbuild/freebsd-x64": {
427
+ "version": "0.21.5",
428
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
429
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
430
+ "cpu": [
431
+ "x64"
432
+ ],
433
+ "dev": true,
434
+ "license": "MIT",
435
+ "optional": true,
436
+ "os": [
437
+ "freebsd"
438
+ ],
439
+ "engines": {
440
+ "node": ">=12"
441
+ }
442
+ },
443
+ "node_modules/@esbuild/linux-arm": {
444
+ "version": "0.21.5",
445
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
446
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
447
+ "cpu": [
448
+ "arm"
449
+ ],
450
+ "dev": true,
451
+ "license": "MIT",
452
+ "optional": true,
453
+ "os": [
454
+ "linux"
455
+ ],
456
+ "engines": {
457
+ "node": ">=12"
458
+ }
459
+ },
460
+ "node_modules/@esbuild/linux-arm64": {
461
+ "version": "0.21.5",
462
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
463
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
464
+ "cpu": [
465
+ "arm64"
466
+ ],
467
+ "dev": true,
468
+ "license": "MIT",
469
+ "optional": true,
470
+ "os": [
471
+ "linux"
472
+ ],
473
+ "engines": {
474
+ "node": ">=12"
475
+ }
476
+ },
477
+ "node_modules/@esbuild/linux-ia32": {
478
+ "version": "0.21.5",
479
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
480
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
481
+ "cpu": [
482
+ "ia32"
483
+ ],
484
+ "dev": true,
485
+ "license": "MIT",
486
+ "optional": true,
487
+ "os": [
488
+ "linux"
489
+ ],
490
+ "engines": {
491
+ "node": ">=12"
492
+ }
493
+ },
494
+ "node_modules/@esbuild/linux-loong64": {
495
+ "version": "0.21.5",
496
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
497
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
498
+ "cpu": [
499
+ "loong64"
500
+ ],
501
+ "dev": true,
502
+ "license": "MIT",
503
+ "optional": true,
504
+ "os": [
505
+ "linux"
506
+ ],
507
+ "engines": {
508
+ "node": ">=12"
509
+ }
510
+ },
511
+ "node_modules/@esbuild/linux-mips64el": {
512
+ "version": "0.21.5",
513
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
514
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
515
+ "cpu": [
516
+ "mips64el"
517
+ ],
518
+ "dev": true,
519
+ "license": "MIT",
520
+ "optional": true,
521
+ "os": [
522
+ "linux"
523
+ ],
524
+ "engines": {
525
+ "node": ">=12"
526
+ }
527
+ },
528
+ "node_modules/@esbuild/linux-ppc64": {
529
+ "version": "0.21.5",
530
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
531
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
532
+ "cpu": [
533
+ "ppc64"
534
+ ],
535
+ "dev": true,
536
+ "license": "MIT",
537
+ "optional": true,
538
+ "os": [
539
+ "linux"
540
+ ],
541
+ "engines": {
542
+ "node": ">=12"
543
+ }
544
+ },
545
+ "node_modules/@esbuild/linux-riscv64": {
546
+ "version": "0.21.5",
547
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
548
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
549
+ "cpu": [
550
+ "riscv64"
551
+ ],
552
+ "dev": true,
553
+ "license": "MIT",
554
+ "optional": true,
555
+ "os": [
556
+ "linux"
557
+ ],
558
+ "engines": {
559
+ "node": ">=12"
560
+ }
561
+ },
562
+ "node_modules/@esbuild/linux-s390x": {
563
+ "version": "0.21.5",
564
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
565
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
566
+ "cpu": [
567
+ "s390x"
568
+ ],
569
+ "dev": true,
570
+ "license": "MIT",
571
+ "optional": true,
572
+ "os": [
573
+ "linux"
574
+ ],
575
+ "engines": {
576
+ "node": ">=12"
577
+ }
578
+ },
579
+ "node_modules/@esbuild/linux-x64": {
580
+ "version": "0.21.5",
581
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
582
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
583
+ "cpu": [
584
+ "x64"
585
+ ],
586
+ "dev": true,
587
+ "license": "MIT",
588
+ "optional": true,
589
+ "os": [
590
+ "linux"
591
+ ],
592
+ "engines": {
593
+ "node": ">=12"
594
+ }
595
+ },
596
+ "node_modules/@esbuild/netbsd-x64": {
597
+ "version": "0.21.5",
598
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
599
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
600
+ "cpu": [
601
+ "x64"
602
+ ],
603
+ "dev": true,
604
+ "license": "MIT",
605
+ "optional": true,
606
+ "os": [
607
+ "netbsd"
608
+ ],
609
+ "engines": {
610
+ "node": ">=12"
611
+ }
612
+ },
613
+ "node_modules/@esbuild/openbsd-x64": {
614
+ "version": "0.21.5",
615
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
616
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
617
+ "cpu": [
618
+ "x64"
619
+ ],
620
+ "dev": true,
621
+ "license": "MIT",
622
+ "optional": true,
623
+ "os": [
624
+ "openbsd"
625
+ ],
626
+ "engines": {
627
+ "node": ">=12"
628
+ }
629
+ },
630
+ "node_modules/@esbuild/sunos-x64": {
631
+ "version": "0.21.5",
632
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
633
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
634
+ "cpu": [
635
+ "x64"
636
+ ],
637
+ "dev": true,
638
+ "license": "MIT",
639
+ "optional": true,
640
+ "os": [
641
+ "sunos"
642
+ ],
643
+ "engines": {
644
+ "node": ">=12"
645
+ }
646
+ },
647
+ "node_modules/@esbuild/win32-arm64": {
648
+ "version": "0.21.5",
649
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
650
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
651
+ "cpu": [
652
+ "arm64"
653
+ ],
654
+ "dev": true,
655
+ "license": "MIT",
656
+ "optional": true,
657
+ "os": [
658
+ "win32"
659
+ ],
660
+ "engines": {
661
+ "node": ">=12"
662
+ }
663
+ },
664
+ "node_modules/@esbuild/win32-ia32": {
665
+ "version": "0.21.5",
666
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
667
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
668
+ "cpu": [
669
+ "ia32"
670
+ ],
671
+ "dev": true,
672
+ "license": "MIT",
673
+ "optional": true,
674
+ "os": [
675
+ "win32"
676
+ ],
677
+ "engines": {
678
+ "node": ">=12"
679
+ }
680
+ },
681
+ "node_modules/@esbuild/win32-x64": {
682
+ "version": "0.21.5",
683
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
684
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
685
+ "cpu": [
686
+ "x64"
687
+ ],
688
+ "dev": true,
689
+ "license": "MIT",
690
+ "optional": true,
691
+ "os": [
692
+ "win32"
693
+ ],
694
+ "engines": {
695
+ "node": ">=12"
696
+ }
697
+ },
698
+ "node_modules/@jridgewell/gen-mapping": {
699
+ "version": "0.3.13",
700
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
701
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
702
+ "dev": true,
703
+ "license": "MIT",
704
+ "dependencies": {
705
+ "@jridgewell/sourcemap-codec": "^1.5.0",
706
+ "@jridgewell/trace-mapping": "^0.3.24"
707
+ }
708
+ },
709
+ "node_modules/@jridgewell/remapping": {
710
+ "version": "2.3.5",
711
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
712
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
713
+ "dev": true,
714
+ "license": "MIT",
715
+ "dependencies": {
716
+ "@jridgewell/gen-mapping": "^0.3.5",
717
+ "@jridgewell/trace-mapping": "^0.3.24"
718
+ }
719
+ },
720
+ "node_modules/@jridgewell/resolve-uri": {
721
+ "version": "3.1.2",
722
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
723
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
724
+ "dev": true,
725
+ "license": "MIT",
726
+ "engines": {
727
+ "node": ">=6.0.0"
728
+ }
729
+ },
730
+ "node_modules/@jridgewell/sourcemap-codec": {
731
+ "version": "1.5.5",
732
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
733
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
734
+ "dev": true,
735
+ "license": "MIT"
736
+ },
737
+ "node_modules/@jridgewell/trace-mapping": {
738
+ "version": "0.3.31",
739
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
740
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
741
+ "dev": true,
742
+ "license": "MIT",
743
+ "dependencies": {
744
+ "@jridgewell/resolve-uri": "^3.1.0",
745
+ "@jridgewell/sourcemap-codec": "^1.4.14"
746
+ }
747
+ },
748
+ "node_modules/@remix-run/router": {
749
+ "version": "1.23.0",
750
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
751
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
752
+ "license": "MIT",
753
+ "engines": {
754
+ "node": ">=14.0.0"
755
+ }
756
+ },
757
+ "node_modules/@rolldown/pluginutils": {
758
+ "version": "1.0.0-beta.27",
759
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
760
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
761
+ "dev": true,
762
+ "license": "MIT"
763
+ },
764
+ "node_modules/@rollup/rollup-android-arm-eabi": {
765
+ "version": "4.52.4",
766
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
767
+ "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
768
+ "cpu": [
769
+ "arm"
770
+ ],
771
+ "dev": true,
772
+ "license": "MIT",
773
+ "optional": true,
774
+ "os": [
775
+ "android"
776
+ ]
777
+ },
778
+ "node_modules/@rollup/rollup-android-arm64": {
779
+ "version": "4.52.4",
780
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
781
+ "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
782
+ "cpu": [
783
+ "arm64"
784
+ ],
785
+ "dev": true,
786
+ "license": "MIT",
787
+ "optional": true,
788
+ "os": [
789
+ "android"
790
+ ]
791
+ },
792
+ "node_modules/@rollup/rollup-darwin-arm64": {
793
+ "version": "4.52.4",
794
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
795
+ "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
796
+ "cpu": [
797
+ "arm64"
798
+ ],
799
+ "dev": true,
800
+ "license": "MIT",
801
+ "optional": true,
802
+ "os": [
803
+ "darwin"
804
+ ]
805
+ },
806
+ "node_modules/@rollup/rollup-darwin-x64": {
807
+ "version": "4.52.4",
808
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
809
+ "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
810
+ "cpu": [
811
+ "x64"
812
+ ],
813
+ "dev": true,
814
+ "license": "MIT",
815
+ "optional": true,
816
+ "os": [
817
+ "darwin"
818
+ ]
819
+ },
820
+ "node_modules/@rollup/rollup-freebsd-arm64": {
821
+ "version": "4.52.4",
822
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
823
+ "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
824
+ "cpu": [
825
+ "arm64"
826
+ ],
827
+ "dev": true,
828
+ "license": "MIT",
829
+ "optional": true,
830
+ "os": [
831
+ "freebsd"
832
+ ]
833
+ },
834
+ "node_modules/@rollup/rollup-freebsd-x64": {
835
+ "version": "4.52.4",
836
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
837
+ "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
838
+ "cpu": [
839
+ "x64"
840
+ ],
841
+ "dev": true,
842
+ "license": "MIT",
843
+ "optional": true,
844
+ "os": [
845
+ "freebsd"
846
+ ]
847
+ },
848
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
849
+ "version": "4.52.4",
850
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
851
+ "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
852
+ "cpu": [
853
+ "arm"
854
+ ],
855
+ "dev": true,
856
+ "license": "MIT",
857
+ "optional": true,
858
+ "os": [
859
+ "linux"
860
+ ]
861
+ },
862
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
863
+ "version": "4.52.4",
864
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
865
+ "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
866
+ "cpu": [
867
+ "arm"
868
+ ],
869
+ "dev": true,
870
+ "license": "MIT",
871
+ "optional": true,
872
+ "os": [
873
+ "linux"
874
+ ]
875
+ },
876
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
877
+ "version": "4.52.4",
878
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
879
+ "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
880
+ "cpu": [
881
+ "arm64"
882
+ ],
883
+ "dev": true,
884
+ "license": "MIT",
885
+ "optional": true,
886
+ "os": [
887
+ "linux"
888
+ ]
889
+ },
890
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
891
+ "version": "4.52.4",
892
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
893
+ "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
894
+ "cpu": [
895
+ "arm64"
896
+ ],
897
+ "dev": true,
898
+ "license": "MIT",
899
+ "optional": true,
900
+ "os": [
901
+ "linux"
902
+ ]
903
+ },
904
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
905
+ "version": "4.52.4",
906
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
907
+ "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
908
+ "cpu": [
909
+ "loong64"
910
+ ],
911
+ "dev": true,
912
+ "license": "MIT",
913
+ "optional": true,
914
+ "os": [
915
+ "linux"
916
+ ]
917
+ },
918
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
919
+ "version": "4.52.4",
920
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
921
+ "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
922
+ "cpu": [
923
+ "ppc64"
924
+ ],
925
+ "dev": true,
926
+ "license": "MIT",
927
+ "optional": true,
928
+ "os": [
929
+ "linux"
930
+ ]
931
+ },
932
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
933
+ "version": "4.52.4",
934
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
935
+ "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
936
+ "cpu": [
937
+ "riscv64"
938
+ ],
939
+ "dev": true,
940
+ "license": "MIT",
941
+ "optional": true,
942
+ "os": [
943
+ "linux"
944
+ ]
945
+ },
946
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
947
+ "version": "4.52.4",
948
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
949
+ "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
950
+ "cpu": [
951
+ "riscv64"
952
+ ],
953
+ "dev": true,
954
+ "license": "MIT",
955
+ "optional": true,
956
+ "os": [
957
+ "linux"
958
+ ]
959
+ },
960
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
961
+ "version": "4.52.4",
962
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
963
+ "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
964
+ "cpu": [
965
+ "s390x"
966
+ ],
967
+ "dev": true,
968
+ "license": "MIT",
969
+ "optional": true,
970
+ "os": [
971
+ "linux"
972
+ ]
973
+ },
974
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
975
+ "version": "4.52.4",
976
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
977
+ "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
978
+ "cpu": [
979
+ "x64"
980
+ ],
981
+ "dev": true,
982
+ "license": "MIT",
983
+ "optional": true,
984
+ "os": [
985
+ "linux"
986
+ ]
987
+ },
988
+ "node_modules/@rollup/rollup-linux-x64-musl": {
989
+ "version": "4.52.4",
990
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
991
+ "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
992
+ "cpu": [
993
+ "x64"
994
+ ],
995
+ "dev": true,
996
+ "license": "MIT",
997
+ "optional": true,
998
+ "os": [
999
+ "linux"
1000
+ ]
1001
+ },
1002
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1003
+ "version": "4.52.4",
1004
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
1005
+ "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
1006
+ "cpu": [
1007
+ "arm64"
1008
+ ],
1009
+ "dev": true,
1010
+ "license": "MIT",
1011
+ "optional": true,
1012
+ "os": [
1013
+ "openharmony"
1014
+ ]
1015
+ },
1016
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1017
+ "version": "4.52.4",
1018
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
1019
+ "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
1020
+ "cpu": [
1021
+ "arm64"
1022
+ ],
1023
+ "dev": true,
1024
+ "license": "MIT",
1025
+ "optional": true,
1026
+ "os": [
1027
+ "win32"
1028
+ ]
1029
+ },
1030
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1031
+ "version": "4.52.4",
1032
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
1033
+ "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
1034
+ "cpu": [
1035
+ "ia32"
1036
+ ],
1037
+ "dev": true,
1038
+ "license": "MIT",
1039
+ "optional": true,
1040
+ "os": [
1041
+ "win32"
1042
+ ]
1043
+ },
1044
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1045
+ "version": "4.52.4",
1046
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
1047
+ "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
1048
+ "cpu": [
1049
+ "x64"
1050
+ ],
1051
+ "dev": true,
1052
+ "license": "MIT",
1053
+ "optional": true,
1054
+ "os": [
1055
+ "win32"
1056
+ ]
1057
+ },
1058
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1059
+ "version": "4.52.4",
1060
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
1061
+ "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
1062
+ "cpu": [
1063
+ "x64"
1064
+ ],
1065
+ "dev": true,
1066
+ "license": "MIT",
1067
+ "optional": true,
1068
+ "os": [
1069
+ "win32"
1070
+ ]
1071
+ },
1072
+ "node_modules/@tanstack/query-core": {
1073
+ "version": "5.90.3",
1074
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.3.tgz",
1075
+ "integrity": "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==",
1076
+ "license": "MIT",
1077
+ "funding": {
1078
+ "type": "github",
1079
+ "url": "https://github.com/sponsors/tannerlinsley"
1080
+ }
1081
+ },
1082
+ "node_modules/@tanstack/react-query": {
1083
+ "version": "5.90.3",
1084
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.3.tgz",
1085
+ "integrity": "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==",
1086
+ "license": "MIT",
1087
+ "dependencies": {
1088
+ "@tanstack/query-core": "5.90.3"
1089
+ },
1090
+ "funding": {
1091
+ "type": "github",
1092
+ "url": "https://github.com/sponsors/tannerlinsley"
1093
+ },
1094
+ "peerDependencies": {
1095
+ "react": "^18 || ^19"
1096
+ }
1097
+ },
1098
+ "node_modules/@types/babel__core": {
1099
+ "version": "7.20.5",
1100
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1101
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1102
+ "dev": true,
1103
+ "license": "MIT",
1104
+ "dependencies": {
1105
+ "@babel/parser": "^7.20.7",
1106
+ "@babel/types": "^7.20.7",
1107
+ "@types/babel__generator": "*",
1108
+ "@types/babel__template": "*",
1109
+ "@types/babel__traverse": "*"
1110
+ }
1111
+ },
1112
+ "node_modules/@types/babel__generator": {
1113
+ "version": "7.27.0",
1114
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1115
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1116
+ "dev": true,
1117
+ "license": "MIT",
1118
+ "dependencies": {
1119
+ "@babel/types": "^7.0.0"
1120
+ }
1121
+ },
1122
+ "node_modules/@types/babel__template": {
1123
+ "version": "7.4.4",
1124
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1125
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1126
+ "dev": true,
1127
+ "license": "MIT",
1128
+ "dependencies": {
1129
+ "@babel/parser": "^7.1.0",
1130
+ "@babel/types": "^7.0.0"
1131
+ }
1132
+ },
1133
+ "node_modules/@types/babel__traverse": {
1134
+ "version": "7.28.0",
1135
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1136
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1137
+ "dev": true,
1138
+ "license": "MIT",
1139
+ "dependencies": {
1140
+ "@babel/types": "^7.28.2"
1141
+ }
1142
+ },
1143
+ "node_modules/@types/estree": {
1144
+ "version": "1.0.8",
1145
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1146
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1147
+ "dev": true,
1148
+ "license": "MIT"
1149
+ },
1150
+ "node_modules/@types/prop-types": {
1151
+ "version": "15.7.15",
1152
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1153
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1154
+ "dev": true,
1155
+ "license": "MIT"
1156
+ },
1157
+ "node_modules/@types/react": {
1158
+ "version": "18.3.26",
1159
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
1160
+ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
1161
+ "dev": true,
1162
+ "license": "MIT",
1163
+ "dependencies": {
1164
+ "@types/prop-types": "*",
1165
+ "csstype": "^3.0.2"
1166
+ }
1167
+ },
1168
+ "node_modules/@types/react-dom": {
1169
+ "version": "18.3.7",
1170
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1171
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1172
+ "dev": true,
1173
+ "license": "MIT",
1174
+ "peerDependencies": {
1175
+ "@types/react": "^18.0.0"
1176
+ }
1177
+ },
1178
+ "node_modules/@vitejs/plugin-react": {
1179
+ "version": "4.7.0",
1180
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1181
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1182
+ "dev": true,
1183
+ "license": "MIT",
1184
+ "dependencies": {
1185
+ "@babel/core": "^7.28.0",
1186
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1187
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1188
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1189
+ "@types/babel__core": "^7.20.5",
1190
+ "react-refresh": "^0.17.0"
1191
+ },
1192
+ "engines": {
1193
+ "node": "^14.18.0 || >=16.0.0"
1194
+ },
1195
+ "peerDependencies": {
1196
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1197
+ }
1198
+ },
1199
+ "node_modules/asynckit": {
1200
+ "version": "0.4.0",
1201
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
1202
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
1203
+ "license": "MIT"
1204
+ },
1205
+ "node_modules/axios": {
1206
+ "version": "1.12.2",
1207
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
1208
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
1209
+ "license": "MIT",
1210
+ "dependencies": {
1211
+ "follow-redirects": "^1.15.6",
1212
+ "form-data": "^4.0.4",
1213
+ "proxy-from-env": "^1.1.0"
1214
+ }
1215
+ },
1216
+ "node_modules/baseline-browser-mapping": {
1217
+ "version": "2.8.16",
1218
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
1219
+ "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
1220
+ "dev": true,
1221
+ "license": "Apache-2.0",
1222
+ "bin": {
1223
+ "baseline-browser-mapping": "dist/cli.js"
1224
+ }
1225
+ },
1226
+ "node_modules/browserslist": {
1227
+ "version": "4.26.3",
1228
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
1229
+ "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
1230
+ "dev": true,
1231
+ "funding": [
1232
+ {
1233
+ "type": "opencollective",
1234
+ "url": "https://opencollective.com/browserslist"
1235
+ },
1236
+ {
1237
+ "type": "tidelift",
1238
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1239
+ },
1240
+ {
1241
+ "type": "github",
1242
+ "url": "https://github.com/sponsors/ai"
1243
+ }
1244
+ ],
1245
+ "license": "MIT",
1246
+ "dependencies": {
1247
+ "baseline-browser-mapping": "^2.8.9",
1248
+ "caniuse-lite": "^1.0.30001746",
1249
+ "electron-to-chromium": "^1.5.227",
1250
+ "node-releases": "^2.0.21",
1251
+ "update-browserslist-db": "^1.1.3"
1252
+ },
1253
+ "bin": {
1254
+ "browserslist": "cli.js"
1255
+ },
1256
+ "engines": {
1257
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1258
+ }
1259
+ },
1260
+ "node_modules/call-bind-apply-helpers": {
1261
+ "version": "1.0.2",
1262
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1263
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1264
+ "license": "MIT",
1265
+ "dependencies": {
1266
+ "es-errors": "^1.3.0",
1267
+ "function-bind": "^1.1.2"
1268
+ },
1269
+ "engines": {
1270
+ "node": ">= 0.4"
1271
+ }
1272
+ },
1273
+ "node_modules/caniuse-lite": {
1274
+ "version": "1.0.30001750",
1275
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz",
1276
+ "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==",
1277
+ "dev": true,
1278
+ "funding": [
1279
+ {
1280
+ "type": "opencollective",
1281
+ "url": "https://opencollective.com/browserslist"
1282
+ },
1283
+ {
1284
+ "type": "tidelift",
1285
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1286
+ },
1287
+ {
1288
+ "type": "github",
1289
+ "url": "https://github.com/sponsors/ai"
1290
+ }
1291
+ ],
1292
+ "license": "CC-BY-4.0"
1293
+ },
1294
+ "node_modules/combined-stream": {
1295
+ "version": "1.0.8",
1296
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1297
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1298
+ "license": "MIT",
1299
+ "dependencies": {
1300
+ "delayed-stream": "~1.0.0"
1301
+ },
1302
+ "engines": {
1303
+ "node": ">= 0.8"
1304
+ }
1305
+ },
1306
+ "node_modules/convert-source-map": {
1307
+ "version": "2.0.0",
1308
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1309
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1310
+ "dev": true,
1311
+ "license": "MIT"
1312
+ },
1313
+ "node_modules/csstype": {
1314
+ "version": "3.1.3",
1315
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1316
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1317
+ "dev": true,
1318
+ "license": "MIT"
1319
+ },
1320
+ "node_modules/debug": {
1321
+ "version": "4.4.3",
1322
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1323
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1324
+ "dev": true,
1325
+ "license": "MIT",
1326
+ "dependencies": {
1327
+ "ms": "^2.1.3"
1328
+ },
1329
+ "engines": {
1330
+ "node": ">=6.0"
1331
+ },
1332
+ "peerDependenciesMeta": {
1333
+ "supports-color": {
1334
+ "optional": true
1335
+ }
1336
+ }
1337
+ },
1338
+ "node_modules/delayed-stream": {
1339
+ "version": "1.0.0",
1340
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1341
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1342
+ "license": "MIT",
1343
+ "engines": {
1344
+ "node": ">=0.4.0"
1345
+ }
1346
+ },
1347
+ "node_modules/dunder-proto": {
1348
+ "version": "1.0.1",
1349
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1350
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1351
+ "license": "MIT",
1352
+ "dependencies": {
1353
+ "call-bind-apply-helpers": "^1.0.1",
1354
+ "es-errors": "^1.3.0",
1355
+ "gopd": "^1.2.0"
1356
+ },
1357
+ "engines": {
1358
+ "node": ">= 0.4"
1359
+ }
1360
+ },
1361
+ "node_modules/electron-to-chromium": {
1362
+ "version": "1.5.237",
1363
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
1364
+ "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
1365
+ "dev": true,
1366
+ "license": "ISC"
1367
+ },
1368
+ "node_modules/es-define-property": {
1369
+ "version": "1.0.1",
1370
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1371
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1372
+ "license": "MIT",
1373
+ "engines": {
1374
+ "node": ">= 0.4"
1375
+ }
1376
+ },
1377
+ "node_modules/es-errors": {
1378
+ "version": "1.3.0",
1379
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1380
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1381
+ "license": "MIT",
1382
+ "engines": {
1383
+ "node": ">= 0.4"
1384
+ }
1385
+ },
1386
+ "node_modules/es-object-atoms": {
1387
+ "version": "1.1.1",
1388
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1389
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1390
+ "license": "MIT",
1391
+ "dependencies": {
1392
+ "es-errors": "^1.3.0"
1393
+ },
1394
+ "engines": {
1395
+ "node": ">= 0.4"
1396
+ }
1397
+ },
1398
+ "node_modules/es-set-tostringtag": {
1399
+ "version": "2.1.0",
1400
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
1401
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
1402
+ "license": "MIT",
1403
+ "dependencies": {
1404
+ "es-errors": "^1.3.0",
1405
+ "get-intrinsic": "^1.2.6",
1406
+ "has-tostringtag": "^1.0.2",
1407
+ "hasown": "^2.0.2"
1408
+ },
1409
+ "engines": {
1410
+ "node": ">= 0.4"
1411
+ }
1412
+ },
1413
+ "node_modules/esbuild": {
1414
+ "version": "0.21.5",
1415
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1416
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1417
+ "dev": true,
1418
+ "hasInstallScript": true,
1419
+ "license": "MIT",
1420
+ "bin": {
1421
+ "esbuild": "bin/esbuild"
1422
+ },
1423
+ "engines": {
1424
+ "node": ">=12"
1425
+ },
1426
+ "optionalDependencies": {
1427
+ "@esbuild/aix-ppc64": "0.21.5",
1428
+ "@esbuild/android-arm": "0.21.5",
1429
+ "@esbuild/android-arm64": "0.21.5",
1430
+ "@esbuild/android-x64": "0.21.5",
1431
+ "@esbuild/darwin-arm64": "0.21.5",
1432
+ "@esbuild/darwin-x64": "0.21.5",
1433
+ "@esbuild/freebsd-arm64": "0.21.5",
1434
+ "@esbuild/freebsd-x64": "0.21.5",
1435
+ "@esbuild/linux-arm": "0.21.5",
1436
+ "@esbuild/linux-arm64": "0.21.5",
1437
+ "@esbuild/linux-ia32": "0.21.5",
1438
+ "@esbuild/linux-loong64": "0.21.5",
1439
+ "@esbuild/linux-mips64el": "0.21.5",
1440
+ "@esbuild/linux-ppc64": "0.21.5",
1441
+ "@esbuild/linux-riscv64": "0.21.5",
1442
+ "@esbuild/linux-s390x": "0.21.5",
1443
+ "@esbuild/linux-x64": "0.21.5",
1444
+ "@esbuild/netbsd-x64": "0.21.5",
1445
+ "@esbuild/openbsd-x64": "0.21.5",
1446
+ "@esbuild/sunos-x64": "0.21.5",
1447
+ "@esbuild/win32-arm64": "0.21.5",
1448
+ "@esbuild/win32-ia32": "0.21.5",
1449
+ "@esbuild/win32-x64": "0.21.5"
1450
+ }
1451
+ },
1452
+ "node_modules/escalade": {
1453
+ "version": "3.2.0",
1454
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1455
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1456
+ "dev": true,
1457
+ "license": "MIT",
1458
+ "engines": {
1459
+ "node": ">=6"
1460
+ }
1461
+ },
1462
+ "node_modules/follow-redirects": {
1463
+ "version": "1.15.11",
1464
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
1465
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
1466
+ "funding": [
1467
+ {
1468
+ "type": "individual",
1469
+ "url": "https://github.com/sponsors/RubenVerborgh"
1470
+ }
1471
+ ],
1472
+ "license": "MIT",
1473
+ "engines": {
1474
+ "node": ">=4.0"
1475
+ },
1476
+ "peerDependenciesMeta": {
1477
+ "debug": {
1478
+ "optional": true
1479
+ }
1480
+ }
1481
+ },
1482
+ "node_modules/form-data": {
1483
+ "version": "4.0.4",
1484
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
1485
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
1486
+ "license": "MIT",
1487
+ "dependencies": {
1488
+ "asynckit": "^0.4.0",
1489
+ "combined-stream": "^1.0.8",
1490
+ "es-set-tostringtag": "^2.1.0",
1491
+ "hasown": "^2.0.2",
1492
+ "mime-types": "^2.1.12"
1493
+ },
1494
+ "engines": {
1495
+ "node": ">= 6"
1496
+ }
1497
+ },
1498
+ "node_modules/fsevents": {
1499
+ "version": "2.3.3",
1500
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1501
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1502
+ "dev": true,
1503
+ "hasInstallScript": true,
1504
+ "license": "MIT",
1505
+ "optional": true,
1506
+ "os": [
1507
+ "darwin"
1508
+ ],
1509
+ "engines": {
1510
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1511
+ }
1512
+ },
1513
+ "node_modules/function-bind": {
1514
+ "version": "1.1.2",
1515
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1516
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1517
+ "license": "MIT",
1518
+ "funding": {
1519
+ "url": "https://github.com/sponsors/ljharb"
1520
+ }
1521
+ },
1522
+ "node_modules/gensync": {
1523
+ "version": "1.0.0-beta.2",
1524
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1525
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1526
+ "dev": true,
1527
+ "license": "MIT",
1528
+ "engines": {
1529
+ "node": ">=6.9.0"
1530
+ }
1531
+ },
1532
+ "node_modules/get-intrinsic": {
1533
+ "version": "1.3.0",
1534
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1535
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1536
+ "license": "MIT",
1537
+ "dependencies": {
1538
+ "call-bind-apply-helpers": "^1.0.2",
1539
+ "es-define-property": "^1.0.1",
1540
+ "es-errors": "^1.3.0",
1541
+ "es-object-atoms": "^1.1.1",
1542
+ "function-bind": "^1.1.2",
1543
+ "get-proto": "^1.0.1",
1544
+ "gopd": "^1.2.0",
1545
+ "has-symbols": "^1.1.0",
1546
+ "hasown": "^2.0.2",
1547
+ "math-intrinsics": "^1.1.0"
1548
+ },
1549
+ "engines": {
1550
+ "node": ">= 0.4"
1551
+ },
1552
+ "funding": {
1553
+ "url": "https://github.com/sponsors/ljharb"
1554
+ }
1555
+ },
1556
+ "node_modules/get-proto": {
1557
+ "version": "1.0.1",
1558
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1559
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1560
+ "license": "MIT",
1561
+ "dependencies": {
1562
+ "dunder-proto": "^1.0.1",
1563
+ "es-object-atoms": "^1.0.0"
1564
+ },
1565
+ "engines": {
1566
+ "node": ">= 0.4"
1567
+ }
1568
+ },
1569
+ "node_modules/gopd": {
1570
+ "version": "1.2.0",
1571
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1572
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1573
+ "license": "MIT",
1574
+ "engines": {
1575
+ "node": ">= 0.4"
1576
+ },
1577
+ "funding": {
1578
+ "url": "https://github.com/sponsors/ljharb"
1579
+ }
1580
+ },
1581
+ "node_modules/has-symbols": {
1582
+ "version": "1.1.0",
1583
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1584
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1585
+ "license": "MIT",
1586
+ "engines": {
1587
+ "node": ">= 0.4"
1588
+ },
1589
+ "funding": {
1590
+ "url": "https://github.com/sponsors/ljharb"
1591
+ }
1592
+ },
1593
+ "node_modules/has-tostringtag": {
1594
+ "version": "1.0.2",
1595
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1596
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1597
+ "license": "MIT",
1598
+ "dependencies": {
1599
+ "has-symbols": "^1.0.3"
1600
+ },
1601
+ "engines": {
1602
+ "node": ">= 0.4"
1603
+ },
1604
+ "funding": {
1605
+ "url": "https://github.com/sponsors/ljharb"
1606
+ }
1607
+ },
1608
+ "node_modules/hasown": {
1609
+ "version": "2.0.2",
1610
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1611
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1612
+ "license": "MIT",
1613
+ "dependencies": {
1614
+ "function-bind": "^1.1.2"
1615
+ },
1616
+ "engines": {
1617
+ "node": ">= 0.4"
1618
+ }
1619
+ },
1620
+ "node_modules/js-tokens": {
1621
+ "version": "4.0.0",
1622
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1623
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1624
+ "license": "MIT"
1625
+ },
1626
+ "node_modules/jsesc": {
1627
+ "version": "3.1.0",
1628
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1629
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1630
+ "dev": true,
1631
+ "license": "MIT",
1632
+ "bin": {
1633
+ "jsesc": "bin/jsesc"
1634
+ },
1635
+ "engines": {
1636
+ "node": ">=6"
1637
+ }
1638
+ },
1639
+ "node_modules/json5": {
1640
+ "version": "2.2.3",
1641
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1642
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1643
+ "dev": true,
1644
+ "license": "MIT",
1645
+ "bin": {
1646
+ "json5": "lib/cli.js"
1647
+ },
1648
+ "engines": {
1649
+ "node": ">=6"
1650
+ }
1651
+ },
1652
+ "node_modules/loose-envify": {
1653
+ "version": "1.4.0",
1654
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1655
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1656
+ "license": "MIT",
1657
+ "dependencies": {
1658
+ "js-tokens": "^3.0.0 || ^4.0.0"
1659
+ },
1660
+ "bin": {
1661
+ "loose-envify": "cli.js"
1662
+ }
1663
+ },
1664
+ "node_modules/lru-cache": {
1665
+ "version": "5.1.1",
1666
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1667
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1668
+ "dev": true,
1669
+ "license": "ISC",
1670
+ "dependencies": {
1671
+ "yallist": "^3.0.2"
1672
+ }
1673
+ },
1674
+ "node_modules/math-intrinsics": {
1675
+ "version": "1.1.0",
1676
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1677
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1678
+ "license": "MIT",
1679
+ "engines": {
1680
+ "node": ">= 0.4"
1681
+ }
1682
+ },
1683
+ "node_modules/mime-db": {
1684
+ "version": "1.52.0",
1685
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1686
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1687
+ "license": "MIT",
1688
+ "engines": {
1689
+ "node": ">= 0.6"
1690
+ }
1691
+ },
1692
+ "node_modules/mime-types": {
1693
+ "version": "2.1.35",
1694
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1695
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1696
+ "license": "MIT",
1697
+ "dependencies": {
1698
+ "mime-db": "1.52.0"
1699
+ },
1700
+ "engines": {
1701
+ "node": ">= 0.6"
1702
+ }
1703
+ },
1704
+ "node_modules/ms": {
1705
+ "version": "2.1.3",
1706
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1707
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1708
+ "dev": true,
1709
+ "license": "MIT"
1710
+ },
1711
+ "node_modules/nanoid": {
1712
+ "version": "3.3.11",
1713
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1714
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1715
+ "dev": true,
1716
+ "funding": [
1717
+ {
1718
+ "type": "github",
1719
+ "url": "https://github.com/sponsors/ai"
1720
+ }
1721
+ ],
1722
+ "license": "MIT",
1723
+ "bin": {
1724
+ "nanoid": "bin/nanoid.cjs"
1725
+ },
1726
+ "engines": {
1727
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1728
+ }
1729
+ },
1730
+ "node_modules/node-releases": {
1731
+ "version": "2.0.23",
1732
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
1733
+ "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
1734
+ "dev": true,
1735
+ "license": "MIT"
1736
+ },
1737
+ "node_modules/picocolors": {
1738
+ "version": "1.1.1",
1739
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1740
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1741
+ "dev": true,
1742
+ "license": "ISC"
1743
+ },
1744
+ "node_modules/postcss": {
1745
+ "version": "8.5.6",
1746
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1747
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1748
+ "dev": true,
1749
+ "funding": [
1750
+ {
1751
+ "type": "opencollective",
1752
+ "url": "https://opencollective.com/postcss/"
1753
+ },
1754
+ {
1755
+ "type": "tidelift",
1756
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1757
+ },
1758
+ {
1759
+ "type": "github",
1760
+ "url": "https://github.com/sponsors/ai"
1761
+ }
1762
+ ],
1763
+ "license": "MIT",
1764
+ "dependencies": {
1765
+ "nanoid": "^3.3.11",
1766
+ "picocolors": "^1.1.1",
1767
+ "source-map-js": "^1.2.1"
1768
+ },
1769
+ "engines": {
1770
+ "node": "^10 || ^12 || >=14"
1771
+ }
1772
+ },
1773
+ "node_modules/proxy-from-env": {
1774
+ "version": "1.1.0",
1775
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1776
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1777
+ "license": "MIT"
1778
+ },
1779
+ "node_modules/react": {
1780
+ "version": "18.3.1",
1781
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1782
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1783
+ "license": "MIT",
1784
+ "dependencies": {
1785
+ "loose-envify": "^1.1.0"
1786
+ },
1787
+ "engines": {
1788
+ "node": ">=0.10.0"
1789
+ }
1790
+ },
1791
+ "node_modules/react-dom": {
1792
+ "version": "18.3.1",
1793
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1794
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1795
+ "license": "MIT",
1796
+ "dependencies": {
1797
+ "loose-envify": "^1.1.0",
1798
+ "scheduler": "^0.23.2"
1799
+ },
1800
+ "peerDependencies": {
1801
+ "react": "^18.3.1"
1802
+ }
1803
+ },
1804
+ "node_modules/react-refresh": {
1805
+ "version": "0.17.0",
1806
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1807
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1808
+ "dev": true,
1809
+ "license": "MIT",
1810
+ "engines": {
1811
+ "node": ">=0.10.0"
1812
+ }
1813
+ },
1814
+ "node_modules/react-router": {
1815
+ "version": "6.30.1",
1816
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
1817
+ "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
1818
+ "license": "MIT",
1819
+ "dependencies": {
1820
+ "@remix-run/router": "1.23.0"
1821
+ },
1822
+ "engines": {
1823
+ "node": ">=14.0.0"
1824
+ },
1825
+ "peerDependencies": {
1826
+ "react": ">=16.8"
1827
+ }
1828
+ },
1829
+ "node_modules/react-router-dom": {
1830
+ "version": "6.30.1",
1831
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
1832
+ "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
1833
+ "license": "MIT",
1834
+ "dependencies": {
1835
+ "@remix-run/router": "1.23.0",
1836
+ "react-router": "6.30.1"
1837
+ },
1838
+ "engines": {
1839
+ "node": ">=14.0.0"
1840
+ },
1841
+ "peerDependencies": {
1842
+ "react": ">=16.8",
1843
+ "react-dom": ">=16.8"
1844
+ }
1845
+ },
1846
+ "node_modules/rollup": {
1847
+ "version": "4.52.4",
1848
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
1849
+ "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
1850
+ "dev": true,
1851
+ "license": "MIT",
1852
+ "dependencies": {
1853
+ "@types/estree": "1.0.8"
1854
+ },
1855
+ "bin": {
1856
+ "rollup": "dist/bin/rollup"
1857
+ },
1858
+ "engines": {
1859
+ "node": ">=18.0.0",
1860
+ "npm": ">=8.0.0"
1861
+ },
1862
+ "optionalDependencies": {
1863
+ "@rollup/rollup-android-arm-eabi": "4.52.4",
1864
+ "@rollup/rollup-android-arm64": "4.52.4",
1865
+ "@rollup/rollup-darwin-arm64": "4.52.4",
1866
+ "@rollup/rollup-darwin-x64": "4.52.4",
1867
+ "@rollup/rollup-freebsd-arm64": "4.52.4",
1868
+ "@rollup/rollup-freebsd-x64": "4.52.4",
1869
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
1870
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.4",
1871
+ "@rollup/rollup-linux-arm64-gnu": "4.52.4",
1872
+ "@rollup/rollup-linux-arm64-musl": "4.52.4",
1873
+ "@rollup/rollup-linux-loong64-gnu": "4.52.4",
1874
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.4",
1875
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.4",
1876
+ "@rollup/rollup-linux-riscv64-musl": "4.52.4",
1877
+ "@rollup/rollup-linux-s390x-gnu": "4.52.4",
1878
+ "@rollup/rollup-linux-x64-gnu": "4.52.4",
1879
+ "@rollup/rollup-linux-x64-musl": "4.52.4",
1880
+ "@rollup/rollup-openharmony-arm64": "4.52.4",
1881
+ "@rollup/rollup-win32-arm64-msvc": "4.52.4",
1882
+ "@rollup/rollup-win32-ia32-msvc": "4.52.4",
1883
+ "@rollup/rollup-win32-x64-gnu": "4.52.4",
1884
+ "@rollup/rollup-win32-x64-msvc": "4.52.4",
1885
+ "fsevents": "~2.3.2"
1886
+ }
1887
+ },
1888
+ "node_modules/scheduler": {
1889
+ "version": "0.23.2",
1890
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1891
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1892
+ "license": "MIT",
1893
+ "dependencies": {
1894
+ "loose-envify": "^1.1.0"
1895
+ }
1896
+ },
1897
+ "node_modules/semver": {
1898
+ "version": "6.3.1",
1899
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1900
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1901
+ "dev": true,
1902
+ "license": "ISC",
1903
+ "bin": {
1904
+ "semver": "bin/semver.js"
1905
+ }
1906
+ },
1907
+ "node_modules/source-map-js": {
1908
+ "version": "1.2.1",
1909
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1910
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1911
+ "dev": true,
1912
+ "license": "BSD-3-Clause",
1913
+ "engines": {
1914
+ "node": ">=0.10.0"
1915
+ }
1916
+ },
1917
+ "node_modules/typescript": {
1918
+ "version": "5.9.3",
1919
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1920
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1921
+ "dev": true,
1922
+ "license": "Apache-2.0",
1923
+ "bin": {
1924
+ "tsc": "bin/tsc",
1925
+ "tsserver": "bin/tsserver"
1926
+ },
1927
+ "engines": {
1928
+ "node": ">=14.17"
1929
+ }
1930
+ },
1931
+ "node_modules/update-browserslist-db": {
1932
+ "version": "1.1.3",
1933
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
1934
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
1935
+ "dev": true,
1936
+ "funding": [
1937
+ {
1938
+ "type": "opencollective",
1939
+ "url": "https://opencollective.com/browserslist"
1940
+ },
1941
+ {
1942
+ "type": "tidelift",
1943
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1944
+ },
1945
+ {
1946
+ "type": "github",
1947
+ "url": "https://github.com/sponsors/ai"
1948
+ }
1949
+ ],
1950
+ "license": "MIT",
1951
+ "dependencies": {
1952
+ "escalade": "^3.2.0",
1953
+ "picocolors": "^1.1.1"
1954
+ },
1955
+ "bin": {
1956
+ "update-browserslist-db": "cli.js"
1957
+ },
1958
+ "peerDependencies": {
1959
+ "browserslist": ">= 4.21.0"
1960
+ }
1961
+ },
1962
+ "node_modules/vite": {
1963
+ "version": "5.4.20",
1964
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
1965
+ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
1966
+ "dev": true,
1967
+ "license": "MIT",
1968
+ "dependencies": {
1969
+ "esbuild": "^0.21.3",
1970
+ "postcss": "^8.4.43",
1971
+ "rollup": "^4.20.0"
1972
+ },
1973
+ "bin": {
1974
+ "vite": "bin/vite.js"
1975
+ },
1976
+ "engines": {
1977
+ "node": "^18.0.0 || >=20.0.0"
1978
+ },
1979
+ "funding": {
1980
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1981
+ },
1982
+ "optionalDependencies": {
1983
+ "fsevents": "~2.3.3"
1984
+ },
1985
+ "peerDependencies": {
1986
+ "@types/node": "^18.0.0 || >=20.0.0",
1987
+ "less": "*",
1988
+ "lightningcss": "^1.21.0",
1989
+ "sass": "*",
1990
+ "sass-embedded": "*",
1991
+ "stylus": "*",
1992
+ "sugarss": "*",
1993
+ "terser": "^5.4.0"
1994
+ },
1995
+ "peerDependenciesMeta": {
1996
+ "@types/node": {
1997
+ "optional": true
1998
+ },
1999
+ "less": {
2000
+ "optional": true
2001
+ },
2002
+ "lightningcss": {
2003
+ "optional": true
2004
+ },
2005
+ "sass": {
2006
+ "optional": true
2007
+ },
2008
+ "sass-embedded": {
2009
+ "optional": true
2010
+ },
2011
+ "stylus": {
2012
+ "optional": true
2013
+ },
2014
+ "sugarss": {
2015
+ "optional": true
2016
+ },
2017
+ "terser": {
2018
+ "optional": true
2019
+ }
2020
+ }
2021
+ },
2022
+ "node_modules/yallist": {
2023
+ "version": "3.1.1",
2024
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2025
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2026
+ "dev": true,
2027
+ "license": "ISC"
2028
+ }
2029
+ }
2030
+ }
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rightcodes-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@tanstack/react-query": "^5.51.11",
13
+ "axios": "^1.7.7",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1",
16
+ "react-router-dom": "^6.26.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.3.3",
20
+ "@types/react-dom": "^18.3.3",
21
+ "@vitejs/plugin-react": "^4.3.1",
22
+ "typescript": "^5.4.5",
23
+ "vite": "^5.4.8"
24
+ }
25
+ }
frontend/public/favicon.svg ADDED
frontend/public/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>RightCodes</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/src/App.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter, NavLink, Route, Routes } from "react-router-dom";
2
+ import SessionsPage from "./pages/SessionsPage";
3
+ import PresetsPage from "./pages/PresetsPage";
4
+ import PresetEditPage from "./pages/PresetEditPage";
5
+ import ObservatoryPage from "./pages/ObservatoryPage";
6
+
7
+ export default function App() {
8
+ return (
9
+ <BrowserRouter>
10
+ <div className="app-shell">
11
+ <header>
12
+ <h1>RightCodes Converter</h1>
13
+ <p>Upload reports, review AI-assisted updates, and export revised documents.</p>
14
+ <nav className="primary-nav">
15
+ <NavLink to="/" end>
16
+ Sessions
17
+ </NavLink>
18
+ <NavLink to="/presets">Presets</NavLink>
19
+ <NavLink to="/observatory">Observatory</NavLink>
20
+ </nav>
21
+ </header>
22
+ <main>
23
+ <Routes>
24
+ <Route path="/" element={<SessionsPage />} />
25
+ <Route path="/presets" element={<PresetsPage />} />
26
+ <Route path="/presets/:presetId/edit" element={<PresetEditPage />} />
27
+ <Route path="/observatory" element={<ObservatoryPage />} />
28
+ </Routes>
29
+ </main>
30
+ <footer>RightCodes {new Date().getFullYear()}</footer>
31
+ </div>
32
+ </BrowserRouter>
33
+ );
34
+ }
frontend/src/components/PresetManager.tsx ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { createPreset, deletePreset, fetchPresets } from "../services/api";
5
+ import type { StandardsPreset } from "../types/session";
6
+
7
+ export default function PresetManager() {
8
+ const queryClient = useQueryClient();
9
+ const { data: presets = [], isLoading, isError, error } = useQuery({
10
+ queryKey: ["presets"],
11
+ queryFn: fetchPresets,
12
+ refetchInterval: (currentPresets) =>
13
+ Array.isArray(currentPresets) &&
14
+ currentPresets.some((preset) => preset.status === "processing")
15
+ ? 2000
16
+ : false,
17
+ });
18
+
19
+ const [name, setName] = useState("");
20
+ const [description, setDescription] = useState("");
21
+ const [files, setFiles] = useState<File[]>([]);
22
+
23
+ const createPresetMutation = useMutation({
24
+ mutationFn: createPreset,
25
+ onSuccess: () => {
26
+ queryClient.invalidateQueries({ queryKey: ["presets"] });
27
+ resetForm();
28
+ },
29
+ onError: (err: unknown) => {
30
+ alert(`Preset creation failed: ${(err as Error).message}`);
31
+ },
32
+ });
33
+
34
+ const deletePresetMutation = useMutation({
35
+ mutationFn: (id: string) => deletePreset(id),
36
+ onSuccess: () => {
37
+ queryClient.invalidateQueries({ queryKey: ["presets"] });
38
+ },
39
+ onError: (err: unknown) => {
40
+ alert(`Preset deletion failed: ${(err as Error).message}`);
41
+ },
42
+ });
43
+
44
+ const resetForm = () => {
45
+ setName("");
46
+ setDescription("");
47
+ setFiles([]);
48
+ const fileInput = document.getElementById("preset-standards-input") as HTMLInputElement | null;
49
+ if (fileInput) {
50
+ fileInput.value = "";
51
+ }
52
+ };
53
+
54
+ const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
55
+ event.preventDefault();
56
+ if (!files.length) {
57
+ alert("Select at least one PDF to build a preset.");
58
+ return;
59
+ }
60
+ createPresetMutation.mutate({
61
+ name,
62
+ description: description || undefined,
63
+ files,
64
+ });
65
+ };
66
+
67
+ const handleDelete = (preset: StandardsPreset) => {
68
+ if (!window.confirm(`Delete preset "${preset.name}"? This cannot be undone.`)) {
69
+ return;
70
+ }
71
+ deletePresetMutation.mutate(preset.id);
72
+ };
73
+
74
+ return (
75
+ <section className="card">
76
+ <h2>Manage Standards Presets</h2>
77
+ <p className="muted">
78
+ Reuse commonly referenced standards without re-uploading them for every session.
79
+ </p>
80
+ <form className="stacked" onSubmit={handleSubmit}>
81
+ <label className="field">
82
+ <span>Preset Name</span>
83
+ <input value={name} onChange={(event) => setName(event.target.value)} required />
84
+ </label>
85
+ <label className="field">
86
+ <span>Description (optional)</span>
87
+ <input value={description} onChange={(event) => setDescription(event.target.value)} />
88
+ </label>
89
+ <label className="field">
90
+ <span>Preset Standards PDFs</span>
91
+ <input
92
+ id="preset-standards-input"
93
+ type="file"
94
+ accept=".pdf"
95
+ multiple
96
+ onChange={(event) => setFiles(Array.from(event.target.files ?? []))}
97
+ />
98
+ <small>
99
+ {files.length ? `${files.length} file(s) selected` : "Select one or more PDF references."}
100
+ </small>
101
+ </label>
102
+ <button type="submit" disabled={createPresetMutation.isPending}>
103
+ {createPresetMutation.isPending ? "Creating preset..." : "Save Preset"}
104
+ </button>
105
+ {createPresetMutation.isError && (
106
+ <p className="error">Preset creation failed: {(createPresetMutation.error as Error).message}</p>
107
+ )}
108
+ {createPresetMutation.isSuccess && (
109
+ <p className="success">
110
+ Preset queued for processing. Progress will appear below shortly.
111
+ </p>
112
+ )}
113
+ </form>
114
+
115
+ <div className="preset-list">
116
+ <h3>Saved Presets</h3>
117
+ {isLoading && <p className="muted">Loading presets...</p>}
118
+ {isError && <p className="error">Failed to load presets: {(error as Error).message}</p>}
119
+ {!isLoading && !presets.length && <p className="muted">No presets created yet.</p>}
120
+ {presets.length > 0 && (
121
+ <ul>
122
+ {presets.map((preset: StandardsPreset) => {
123
+ const totalDocs = preset.total_count || preset.document_count || 0;
124
+ const processed = Math.min(preset.processed_count || 0, totalDocs);
125
+ const progressPercent = totalDocs
126
+ ? Math.min(100, Math.round((processed / totalDocs) * 100))
127
+ : preset.status === "ready"
128
+ ? 100
129
+ : 0;
130
+ const nextDoc = Math.min(processed + 1, totalDocs);
131
+ return (
132
+ <li key={preset.id}>
133
+ <div className="preset-header">
134
+ <div>
135
+ <strong>{preset.name}</strong>
136
+ <p className="muted">
137
+ {preset.document_count} file{preset.document_count === 1 ? "" : "s"} · Updated{" "}
138
+ {new Date(preset.updated_at).toLocaleString()}
139
+ </p>
140
+ {preset.description && <p className="muted">{preset.description}</p>}
141
+ </div>
142
+ <div className="preset-status">
143
+ {preset.status === "ready" && <span className="status-ready">Ready</span>}
144
+ {preset.status === "processing" && (
145
+ <span className="status-processing">
146
+ Processing {processed}/{totalDocs}
147
+ </span>
148
+ )}
149
+ {preset.status === "failed" && (
150
+ <span className="status-failed">
151
+ Failed{preset.last_error ? `: ${preset.last_error}` : ""}
152
+ </span>
153
+ )}
154
+ </div>
155
+ </div>
156
+ {preset.status === "processing" && (
157
+ <div className="progress-block">
158
+ <div className="linear-progress">
159
+ <div
160
+ className="linear-progress-fill"
161
+ style={{ width: `${progressPercent}%` }}
162
+ />
163
+ </div>
164
+ <p className="muted">
165
+ Currently processing document {nextDoc} of {totalDocs}.
166
+ </p>
167
+ </div>
168
+ )}
169
+ <div className="muted preset-docs">
170
+ {preset.documents.map((doc) => doc.split(/[/\\]/).pop() ?? doc).join(", ")}
171
+ </div>
172
+ <div className="preset-actions">
173
+ <Link className="ghost-button" to={`/presets/${preset.id}/edit`}>
174
+ Edit
175
+ </Link>
176
+ <button
177
+ type="button"
178
+ className="ghost-button danger"
179
+ onClick={() => handleDelete(preset)}
180
+ disabled={deletePresetMutation.isPending || preset.status === "processing"}
181
+ >
182
+ Delete
183
+ </button>
184
+ </div>
185
+ </li>
186
+ );
187
+ })}
188
+ </ul>
189
+ )}
190
+ </div>
191
+ </section>
192
+ );
193
+ }
frontend/src/components/SessionDetails.tsx ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { fetchSessionById } from "../services/api";
4
+ import type { Session, SessionSummary } from "../types/session";
5
+
6
+ interface DocParse {
7
+ path: string;
8
+ paragraphs: Array<{ index: number; text: string; style?: string | null; heading_level?: number | null; references?: string[] }>;
9
+ tables: Array<{ index: number; rows: string[][]; references?: string[] }>;
10
+ summary?: Record<string, unknown>;
11
+ }
12
+
13
+ interface StandardsParseEntry {
14
+ path: string;
15
+ summary?: Record<string, unknown>;
16
+ chunks?: Array<{
17
+ page_number: number;
18
+ chunk_index: number;
19
+ text: string;
20
+ heading?: string | null;
21
+ clause_numbers?: string[];
22
+ references?: string[];
23
+ is_ocr?: boolean;
24
+ }>;
25
+ }
26
+
27
+ interface ExtractionResult {
28
+ document_summary?: string;
29
+ sections?: Array<{ paragraph_index: number; text: string; references?: string[] }>;
30
+ tables?: Array<{ table_index: number; summary: string; references?: string[] }>;
31
+ references?: string[];
32
+ notes?: string;
33
+ }
34
+
35
+ interface MappingResult {
36
+ mappings?: Array<{
37
+ source_reference: string;
38
+ source_context?: string;
39
+ target_reference: string;
40
+ target_clause?: string;
41
+ target_summary?: string;
42
+ confidence?: number;
43
+ rationale?: string;
44
+ }>;
45
+ unmapped_references?: string[];
46
+ notes?: string;
47
+ }
48
+
49
+ interface RewritePlan {
50
+ replacements?: Array<{
51
+ paragraph_index: number;
52
+ original_text: string;
53
+ updated_text: string;
54
+ applied_mapping: string;
55
+ change_reason?: string;
56
+ }>;
57
+ notes?: string;
58
+ }
59
+
60
+ interface ValidationReport {
61
+ issues?: Array<{ description?: string; severity?: string }>;
62
+ verdict?: string;
63
+ notes?: string;
64
+ }
65
+
66
+ interface ExportManifest {
67
+ export_path?: string | null;
68
+ notes?: string;
69
+ replacement_count?: number;
70
+ }
71
+
72
+ interface SessionDetailsProps {
73
+ session: SessionSummary | null;
74
+ }
75
+
76
+ export default function SessionDetails({ session }: SessionDetailsProps) {
77
+ if (!session) {
78
+ return <div className="card">Select a session to inspect its progress.</div>;
79
+ }
80
+
81
+ const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000/api";
82
+ const partialSession = session as Partial<Session>;
83
+ const summaryStandards = Array.isArray(partialSession.standards_docs)
84
+ ? [...partialSession.standards_docs]
85
+ : [];
86
+ const summaryLogs = Array.isArray(partialSession.logs) ? [...partialSession.logs] : [];
87
+ const summaryMetadata =
88
+ partialSession.metadata && typeof partialSession.metadata === "object"
89
+ ? { ...(partialSession.metadata as Record<string, unknown>) }
90
+ : {};
91
+ const normalizedSummary: Session = {
92
+ ...session,
93
+ standards_docs: summaryStandards,
94
+ logs: summaryLogs,
95
+ metadata: summaryMetadata,
96
+ };
97
+ const sessionQuery = useQuery({
98
+ queryKey: ["session", session.id],
99
+ queryFn: () => fetchSessionById(session.id),
100
+ enabled: Boolean(session?.id),
101
+ refetchInterval: (data) =>
102
+ data && ["review", "completed", "failed"].includes(data.status) ? false : 2000,
103
+ placeholderData: () => normalizedSummary,
104
+ });
105
+
106
+ const latest = sessionQuery.data ?? normalizedSummary;
107
+ const standardsDocs = Array.isArray(latest.standards_docs) ? latest.standards_docs : [];
108
+ const standardNames = standardsDocs.map((path) => path.split(/[/\\]/).pop() ?? path);
109
+ const activityLogs = Array.isArray(latest.logs) ? latest.logs : [];
110
+ const inFlight = !["review", "completed", "failed"].includes(latest.status);
111
+
112
+ useEffect(() => {
113
+ if (sessionQuery.data) {
114
+ console.log(`[Session ${sessionQuery.data.id}] status: ${sessionQuery.data.status}`);
115
+ }
116
+ }, [sessionQuery.data?.id, sessionQuery.data?.status]);
117
+
118
+ const metadata = (latest.metadata ?? {}) as Record<string, unknown>;
119
+ const docParse = metadata.doc_parse as DocParse | undefined;
120
+ const standardsParse = metadata.standards_parse as StandardsParseEntry[] | undefined;
121
+ const extractionResult = metadata.extraction_result as ExtractionResult | undefined;
122
+ const mappingResult = metadata.mapping_result as MappingResult | undefined;
123
+ const rewritePlan = metadata.rewrite_plan as RewritePlan | undefined;
124
+ const validationReport = metadata.validation_report as ValidationReport | undefined;
125
+ const exportManifest = metadata.export_manifest as ExportManifest | undefined;
126
+ const exportDownloadUrl =
127
+ exportManifest?.export_path !== undefined
128
+ ? `${apiBaseUrl}/sessions/${latest.id}/export`
129
+ : null;
130
+
131
+ const docSummary = docParse?.summary as
132
+ | { paragraph_count?: number; table_count?: number; reference_count?: number }
133
+ | undefined;
134
+
135
+ const standardsProgress = metadata.standards_ingest_progress as
136
+ | {
137
+ total?: number;
138
+ processed?: number;
139
+ current_file?: string;
140
+ cached_count?: number;
141
+ parsed_count?: number;
142
+ completed?: boolean;
143
+ }
144
+ | undefined;
145
+ const standardsProgressFile =
146
+ standardsProgress?.current_file?.split(/[/\\]/).pop() ?? undefined;
147
+ const standardsProgressTotal =
148
+ typeof standardsProgress?.total === "number" ? standardsProgress.total : undefined;
149
+ const standardsProgressProcessed =
150
+ standardsProgressTotal !== undefined
151
+ ? Math.min(
152
+ typeof standardsProgress?.processed === "number" ? standardsProgress.processed : 0,
153
+ standardsProgressTotal
154
+ )
155
+ : undefined;
156
+ const standardsCachedCount =
157
+ typeof standardsProgress?.cached_count === "number" ? standardsProgress.cached_count : undefined;
158
+ const standardsParsedCount =
159
+ typeof standardsProgress?.parsed_count === "number" ? standardsProgress.parsed_count : undefined;
160
+ const pipelineProgress = metadata.pipeline_progress as
161
+ | {
162
+ total?: number;
163
+ current_index?: number;
164
+ stage?: string | null;
165
+ status?: string;
166
+ }
167
+ | undefined;
168
+ const pipelineStageLabel = pipelineProgress?.stage
169
+ ? pipelineProgress.stage
170
+ .split("_")
171
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
172
+ .join(" ")
173
+ : undefined;
174
+ const pipelineStageStatus = pipelineProgress?.status
175
+ ? pipelineProgress.status.charAt(0).toUpperCase() + pipelineProgress.status.slice(1)
176
+ : undefined;
177
+ const presetMetadata = Array.isArray(metadata.presets)
178
+ ? (metadata.presets as Array<{ id?: string; name?: string | null; documents?: string[] }>)
179
+ : [];
180
+ const presetNames = presetMetadata
181
+ .map((item) => item?.name || item?.id || "")
182
+ .filter((value) => Boolean(value)) as string[];
183
+ const presetDocTotal = presetMetadata.reduce(
184
+ (acc, item) => acc + (Array.isArray(item?.documents) ? item.documents.length : 0),
185
+ 0
186
+ );
187
+
188
+ const [showAllMappings, setShowAllMappings] = useState(false);
189
+ const [showAllReplacements, setShowAllReplacements] = useState(false);
190
+
191
+ const sections = extractionResult?.sections ?? [];
192
+ const sectionsPreview = sections.slice(0, 6);
193
+
194
+ const mappingLimit = 6;
195
+ const mappings = mappingResult?.mappings ?? [];
196
+ const mappingsToDisplay = showAllMappings ? mappings : mappings.slice(0, mappingLimit);
197
+
198
+ const replacementLimit = 6;
199
+ const replacements = rewritePlan?.replacements ?? [];
200
+ const replacementsToDisplay = showAllReplacements ? replacements : replacements.slice(0, replacementLimit);
201
+
202
+ const tableReplacements = rewritePlan?.table_replacements ?? [];
203
+ const changeLog = rewritePlan?.change_log ?? [];
204
+
205
+ return (
206
+ <div className="card">
207
+ <div className="card-header">
208
+ <h2>Session Overview</h2>
209
+ <div className="header-actions">
210
+ {sessionQuery.isFetching && <span className="muted text-small">Updating...</span>}
211
+ <button
212
+ type="button"
213
+ className="ghost-button"
214
+ onClick={() => sessionQuery.refetch()}
215
+ disabled={sessionQuery.isFetching}
216
+ >
217
+ {sessionQuery.isFetching ? "Refreshing..." : "Refresh now"}
218
+ </button>
219
+ </div>
220
+ </div>
221
+ {inFlight && (
222
+ <div className="progress-block">
223
+ <div className="progress-bar">
224
+ <div className="progress-bar-fill" />
225
+ </div>
226
+ <p className="muted">Processing pipeline... refreshing every 2s.</p>
227
+ {pipelineProgress?.total ? (
228
+ <p className="muted">
229
+ Pipeline stages:{" "}
230
+ {Math.min(pipelineProgress.current_index ?? 0, pipelineProgress.total)} /{" "}
231
+ {pipelineProgress.total}
232
+ {pipelineStageLabel ? ` — ${pipelineStageLabel}` : ""}
233
+ {pipelineStageStatus ? ` (${pipelineStageStatus})` : ""}
234
+ </p>
235
+ ) : null}
236
+ {standardsProgressTotal !== undefined ? (
237
+ <p className="muted">
238
+ Standards PDFs: {standardsProgressProcessed ?? 0} / {standardsProgressTotal}
239
+ {standardsProgressFile ? ` — ${standardsProgressFile}` : ""}
240
+ {standardsCachedCount !== undefined || standardsParsedCount !== undefined ? (
241
+ <>
242
+ {" "}(cached {standardsCachedCount ?? 0}, parsed {standardsParsedCount ?? 0})
243
+ </>
244
+ ) : null}
245
+ </p>
246
+ ) : null}
247
+ </div>
248
+ )}
249
+ <dl className="details-grid">
250
+ <div>
251
+ <dt>Status</dt>
252
+ <dd>{latest.status}</dd>
253
+ </div>
254
+ <div>
255
+ <dt>Created</dt>
256
+ <dd>{new Date(latest.created_at).toLocaleString()}</dd>
257
+ </div>
258
+ <div>
259
+ <dt>Updated</dt>
260
+ <dd>{new Date(latest.updated_at).toLocaleString()}</dd>
261
+ </div>
262
+ <div>
263
+ <dt>Origin</dt>
264
+ <dd>{latest.target_standard}</dd>
265
+ </div>
266
+ <div>
267
+ <dt>Destination</dt>
268
+ <dd>{latest.destination_standard}</dd>
269
+ </div>
270
+ {standardNames.length > 0 && (
271
+ <div>
272
+ <dt>Standards PDFs</dt>
273
+ <dd>{standardNames.join(", ")}</dd>
274
+ </div>
275
+ )}
276
+ {presetNames.length ? (
277
+ <div>
278
+ <dt>Preset</dt>
279
+ <dd>
280
+ {presetNames.join(", ")}
281
+ {presetDocTotal ? ` (${presetDocTotal} file${presetDocTotal === 1 ? "" : "s"})` : ""}
282
+ </dd>
283
+ </div>
284
+ ) : null}
285
+ </dl>
286
+ {latest.status === "review" && (
287
+ <div className="notice success">
288
+ Pipeline completed. Review the AI change set and validation notes next.
289
+ </div>
290
+ )}
291
+ {latest.last_error && (
292
+ <p className="error">
293
+ <strong>Last error:</strong> {latest.last_error}
294
+ </p>
295
+ )}
296
+ <div className="log-panel">
297
+ <h3>Activity Log</h3>
298
+ {activityLogs.length ? (
299
+ <ul>
300
+ {activityLogs.map((entry, index) => (
301
+ <li key={`${entry}-${index}`}>{entry}</li>
302
+ ))}
303
+ </ul>
304
+ ) : (
305
+ <p className="muted">No activity recorded yet.</p>
306
+ )}
307
+ </div>
308
+
309
+ {docParse && (
310
+ <div className="section-card">
311
+ <h3>Document Parsing</h3>
312
+ <p className="muted">
313
+ Analysed {docSummary?.paragraph_count ?? docParse.paragraphs.length} paragraphs and{" "}
314
+ {docSummary?.table_count ?? docParse.tables.length} tables.
315
+ </p>
316
+ <ul className="section-list">
317
+ {docParse.paragraphs.slice(0, 5).map((paragraph) => (
318
+ <li key={paragraph.index}>
319
+ <strong>Paragraph {paragraph.index}:</strong> {paragraph.text}
320
+ {paragraph.references?.length ? (
321
+ <span className="muted"> — refs: {paragraph.references.join(", ")}</span>
322
+ ) : null}
323
+ </li>
324
+ ))}
325
+ </ul>
326
+ </div>
327
+ )}
328
+
329
+ {standardsParse && standardsParse.length > 0 && (
330
+ <div className="section-card">
331
+ <h3>Standards Corpus</h3>
332
+ <ul className="section-list">
333
+ {standardsParse.slice(0, 4).map((entry) => {
334
+ const name = entry.path.split(/[/\\]/).pop() ?? entry.path;
335
+ const summary = entry.summary as
336
+ | { chunk_count?: number; reference_count?: number; ocr_chunk_count?: number }
337
+ | undefined;
338
+ const chunkCount = summary?.chunk_count ?? entry.chunks?.length ?? 0;
339
+ const ocrChunkCount =
340
+ summary?.ocr_chunk_count ?? entry.chunks?.filter((chunk) => chunk.is_ocr)?.length ?? 0;
341
+ return (
342
+ <li key={entry.path}>
343
+ <strong>{name}</strong> - {chunkCount} chunks analysed
344
+ {ocrChunkCount ? (
345
+ <span className="muted">
346
+ {" "}
347
+ ({ocrChunkCount} OCR supplement{ocrChunkCount === 1 ? "" : "s"})
348
+ </span>
349
+ ) : null}
350
+ </li>
351
+ );
352
+ })}
353
+ </ul>
354
+ </div>
355
+ )}
356
+
357
+ {extractionResult && (
358
+ <div className="section-card">
359
+ <h3>Extraction Summary</h3>
360
+ {extractionResult.document_summary && (
361
+ <p>{extractionResult.document_summary}</p>
362
+ )}
363
+ {extractionResult.references && extractionResult.references.length ? (
364
+ <p className="muted">
365
+ References detected: {extractionResult.references.slice(0, 20).join(", ")}
366
+ {extractionResult.references.length > 20 ? "..." : ""}
367
+ </p>
368
+ ) : null}
369
+ {sectionsPreview.length ? (
370
+ <ul className="section-list">
371
+ {sectionsPreview.map((section) => (
372
+ <li key={section.paragraph_index}>
373
+ <strong>Paragraph {section.paragraph_index}:</strong> {section.text}
374
+ </li>
375
+ ))}
376
+ </ul>
377
+ ) : null}
378
+ {extractionResult.notes && <p className="muted">{extractionResult.notes}</p>}
379
+ </div>
380
+ )}
381
+
382
+ {mappingResult && (
383
+ <div className="section-card">
384
+ <h3>Reference Mapping</h3>
385
+ {mappings.length ? (
386
+ <>
387
+ <p className="muted">
388
+ Showing {mappingsToDisplay.length} of {mappings.length} mappings.
389
+ </p>
390
+ <ul className="section-list">
391
+ {mappingsToDisplay.map((mapping, idx) => {
392
+ const cappedConfidence =
393
+ typeof mapping.confidence === "number"
394
+ ? Math.round(Math.min(Math.max(mapping.confidence, 0), 1) * 100)
395
+ : null;
396
+ return (
397
+ <li key={`${mapping.source_reference}-${idx}`}>
398
+ <strong>{mapping.source_reference}</strong>{" -> "}{mapping.target_reference}
399
+ {cappedConfidence !== null && (
400
+ <span className="muted"> (confidence {cappedConfidence}%)</span>
401
+ )}
402
+ {mapping.target_clause && (
403
+ <div className="muted">Clause: {mapping.target_clause}</div>
404
+ )}
405
+ {mapping.rationale && (
406
+ <div className="muted">Reason: {mapping.rationale}</div>
407
+ )}
408
+ </li>
409
+ );
410
+ })}
411
+ </ul>
412
+ {mappings.length > mappingLimit && (
413
+ <button
414
+ type="button"
415
+ className="ghost-button"
416
+ onClick={() => setShowAllMappings((prev) => !prev)}
417
+ >
418
+ {showAllMappings ? "Show fewer" : `Show all ${mappings.length}`}
419
+ </button>
420
+ )}
421
+ </>
422
+ ) : (
423
+ <p className="muted">
424
+ {mappingResult.notes ?? "No mapping actions recorded."}
425
+ </p>
426
+ )}
427
+ </div>
428
+ )}
429
+
430
+ {rewritePlan && (
431
+ <div className="section-card">
432
+ <h3>Rewrite Plan</h3>
433
+ {replacements.length ? (
434
+ <>
435
+ <p className="muted">
436
+ Showing {replacementsToDisplay.length} of {replacements.length} replacements.
437
+ </p>
438
+ <ul className="section-list">
439
+ {replacementsToDisplay.map((replacement, idx) => {
440
+ const appliedMappings =
441
+ Array.isArray(replacement.applied_mappings) && replacement.applied_mappings.length
442
+ ? replacement.applied_mappings
443
+ : replacement.applied_mapping
444
+ ? [replacement.applied_mapping]
445
+ : [];
446
+ return (
447
+ <li key={`${replacement.paragraph_index}-${idx}`}>
448
+ <strong>Paragraph {replacement.paragraph_index}</strong>
449
+ <div className="diff-block">
450
+ <div>
451
+ <span className="muted">Original:</span> {replacement.original_text}
452
+ </div>
453
+ <div>
454
+ <span className="muted">Updated:</span> {replacement.updated_text}
455
+ </div>
456
+ {appliedMappings.length ? (
457
+ <div className="muted">Mapping: {appliedMappings.join(", ")}</div>
458
+ ) : null}
459
+ {replacement.change_reason && (
460
+ <div className="muted">Reason: {replacement.change_reason}</div>
461
+ )}
462
+ </div>
463
+ </li>
464
+ );
465
+ })}
466
+ </ul>
467
+ {replacements.length > replacementLimit && (
468
+ <button
469
+ type="button"
470
+ className="ghost-button"
471
+ onClick={() => setShowAllReplacements((prev) => !prev)}
472
+ >
473
+ {showAllReplacements ? "Show fewer" : `Show all ${replacements.length}`}
474
+ </button>
475
+ )}
476
+ </>
477
+ ) : (
478
+ <p className="muted">{rewritePlan.notes ?? "No rewrite actions required."}</p>
479
+ )}
480
+ {tableReplacements.length ? (
481
+ <div className="subsection">
482
+ <h4>Table Updates</h4>
483
+ <ul className="section-list">
484
+ {tableReplacements.map((table, idx) => {
485
+ const appliedMappings =
486
+ Array.isArray(table.applied_mappings) && table.applied_mappings.length
487
+ ? table.applied_mappings
488
+ : table.applied_mapping
489
+ ? [table.applied_mapping]
490
+ : [];
491
+ return (
492
+ <li key={`${table.table_index}-${idx}`}>
493
+ <strong>Table {table.table_index}</strong>
494
+ {appliedMappings.length ? (
495
+ <div className="muted">Mapping: {appliedMappings.join(", ")}</div>
496
+ ) : null}
497
+ {table.change_reason && <div className="muted">{table.change_reason}</div>}
498
+ {Array.isArray(table.updated_rows) && table.updated_rows.length ? (
499
+ <div className="muted">Updated rows: {table.updated_rows.length}</div>
500
+ ) : null}
501
+ </li>
502
+ );
503
+ })}
504
+ </ul>
505
+ </div>
506
+ ) : null}
507
+ {changeLog.length ? (
508
+ <div className="subsection">
509
+ <h4>Change Log</h4>
510
+ <ul className="section-list">
511
+ {changeLog.map((entry, idx) => (
512
+ <li key={`${entry.reference}-${idx}`}>
513
+ <strong>{entry.reference}</strong>{" -> "}{entry.target_reference}
514
+ {entry.affected_paragraphs && entry.affected_paragraphs.length ? (
515
+ <span className="muted">{" "}(paragraphs {entry.affected_paragraphs.join(", ")})</span>
516
+ ) : null}
517
+ {entry.note && <div className="muted">{entry.note}</div>}
518
+ </li>
519
+ ))}
520
+ </ul>
521
+ </div>
522
+ ) : null}
523
+ {rewritePlan.notes && replacements.length ? (
524
+ <p className="muted">{rewritePlan.notes}</p>
525
+ ) : null}
526
+ </div>
527
+ )}
528
+
529
+ {validationReport && (
530
+ <div className="section-card">
531
+ <h3>Validation</h3>
532
+ {validationReport.verdict && (
533
+ <p>
534
+ Verdict: <strong>{validationReport.verdict}</strong>
535
+ </p>
536
+ )}
537
+ {validationReport.issues?.length ? (
538
+ <ul className="section-list">
539
+ {validationReport.issues.map((issue, idx) => (
540
+ <li key={idx}>
541
+ {issue.description ?? "Issue reported"}
542
+ {issue.severity && <span className="muted"> ({issue.severity})</span>}
543
+ </li>
544
+ ))}
545
+ </ul>
546
+ ) : (
547
+ <p className="muted">No validation issues reported.</p>
548
+ )}
549
+ {validationReport.notes && <p className="muted">{validationReport.notes}</p>}
550
+ </div>
551
+ )}
552
+
553
+ {exportManifest && (
554
+ <div className="section-card">
555
+ <h3>Export</h3>
556
+ {exportDownloadUrl ? (
557
+ <>
558
+ <p className="muted">
559
+ {exportManifest.notes ?? "Converted document generated using rewrite plan."}
560
+ </p>
561
+ {typeof exportManifest.replacement_count === "number" && (
562
+ <p className="muted">
563
+ {exportManifest.replacement_count} paragraph replacements applied.
564
+ </p>
565
+ )}
566
+ <a className="ghost-button" href={exportDownloadUrl} download>
567
+ Download DOCX
568
+ </a>
569
+ </>
570
+ ) : (
571
+ <p className="muted">{exportManifest.notes ?? "Export not yet generated."}</p>
572
+ )}
573
+ </div>
574
+ )}
575
+ </div>
576
+ );
577
+ }
578
+
frontend/src/components/SessionList.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { fetchSessions } from "../services/api";
3
+ import type { SessionSummary } from "../types/session";
4
+
5
+ interface SessionListProps {
6
+ selectedId?: string | null;
7
+ onSelect?: (session: SessionSummary) => void;
8
+ }
9
+
10
+ export default function SessionList({ selectedId, onSelect }: SessionListProps) {
11
+ const { data, isLoading, isError, error } = useQuery({
12
+ queryKey: ["sessions"],
13
+ queryFn: fetchSessions,
14
+ refetchInterval: 5000,
15
+ });
16
+ const dateFormatter = new Intl.DateTimeFormat(undefined, {
17
+ dateStyle: "medium",
18
+ timeStyle: "short",
19
+ });
20
+
21
+ if (isLoading) {
22
+ return <div className="card">Loading sessions...</div>;
23
+ }
24
+ if (isError) {
25
+ return <div className="card error">Failed to load sessions: {(error as Error).message}</div>;
26
+ }
27
+
28
+ if (!data?.length) {
29
+ return <div className="card">No sessions yet. Upload a report to get started.</div>;
30
+ }
31
+
32
+ return (
33
+ <div className="card">
34
+ <h2>Recent Sessions</h2>
35
+ <p className="muted">Auto-refreshing every 5s.</p>
36
+ <ul className="session-list">
37
+ {data.map((session) => (
38
+ <li
39
+ key={session.id}
40
+ className={session.id === selectedId ? "selected" : ""}
41
+ onClick={() => onSelect?.(session)}
42
+ >
43
+ <div>
44
+ <strong>{session.name}</strong>
45
+ <p>
46
+ {session.target_standard} {"->"} {session.destination_standard}
47
+ </p>
48
+ <p className="muted" title={session.source_doc}>
49
+ {dateFormatter.format(new Date(session.created_at))} {" · "}
50
+ {session.source_doc.split(/[/\\]/).pop() ?? session.source_doc}
51
+ </p>
52
+ </div>
53
+ <span className={`status status-${session.status}`}>{session.status}</span>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ </div>
58
+ );
59
+ }
frontend/src/components/UploadForm.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { fetchPresets, uploadSession } from "../services/api";
4
+ import type { Session, StandardsPreset } from "../types/session";
5
+
6
+ interface UploadFormProps {
7
+ onSuccess?: (session: Session) => void;
8
+ }
9
+
10
+ export default function UploadForm({ onSuccess }: UploadFormProps) {
11
+ const queryClient = useQueryClient();
12
+ const [name, setName] = useState("");
13
+ const [targetStandard, setTargetStandard] = useState("");
14
+ const [destinationStandard, setDestinationStandard] = useState("");
15
+ const [file, setFile] = useState<File | null>(null);
16
+ const [standardsFiles, setStandardsFiles] = useState<File[]>([]);
17
+ const [selectedPresetId, setSelectedPresetId] = useState<string>("");
18
+
19
+ const { data: presets = [], isLoading: presetsLoading, isError: presetsError } = useQuery({
20
+ queryKey: ["presets"],
21
+ queryFn: fetchPresets,
22
+ refetchInterval: (currentPresets) =>
23
+ Array.isArray(currentPresets) &&
24
+ currentPresets.some((preset) => preset.status === "processing")
25
+ ? 2000
26
+ : false,
27
+ });
28
+ const selectedPreset = presets.find((preset) => preset.id === selectedPresetId);
29
+
30
+ const mutation = useMutation({
31
+ mutationFn: uploadSession,
32
+ onSuccess: (session) => {
33
+ console.log("Session created:", session);
34
+ queryClient.invalidateQueries({ queryKey: ["sessions"] });
35
+ onSuccess?.(session);
36
+ resetForm();
37
+ },
38
+ onError: (error: unknown) => {
39
+ console.error("Session creation failed:", error);
40
+ },
41
+ });
42
+
43
+ function resetForm() {
44
+ setName("");
45
+ setTargetStandard("");
46
+ setDestinationStandard("");
47
+ setFile(null);
48
+ setStandardsFiles([]);
49
+ setSelectedPresetId("");
50
+ const reportInput = document.getElementById("source-doc-input") as HTMLInputElement | null;
51
+ if (reportInput) {
52
+ reportInput.value = "";
53
+ }
54
+ const standardsInput = document.getElementById("standards-doc-input") as HTMLInputElement | null;
55
+ if (standardsInput) {
56
+ standardsInput.value = "";
57
+ }
58
+ }
59
+
60
+ const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
61
+ event.preventDefault();
62
+ if (!file) {
63
+ alert("Please select a report to upload.");
64
+ return;
65
+ }
66
+ if (!standardsFiles.length && !selectedPresetId) {
67
+ alert("Upload at least one standards PDF or choose a preset bundle.");
68
+ return;
69
+ }
70
+ if (selectedPresetId && selectedPreset && selectedPreset.status !== "ready") {
71
+ alert("The selected preset is still processing. Please wait until it is ready.");
72
+ return;
73
+ }
74
+ mutation.mutate({
75
+ name,
76
+ target_standard: targetStandard,
77
+ destination_standard: destinationStandard,
78
+ metadata: {},
79
+ sourceFile: file,
80
+ standardsFiles,
81
+ standardsPresetId: selectedPresetId || undefined,
82
+ });
83
+ };
84
+
85
+ return (
86
+ <form className="card" onSubmit={handleSubmit}>
87
+ <h2>Create Conversion Session</h2>
88
+ <label className="field">
89
+ <span>Session Name</span>
90
+ <input value={name} onChange={(event) => setName(event.target.value)} required />
91
+ </label>
92
+ <label className="field">
93
+ <span>Current Standard</span>
94
+ <input
95
+ value={targetStandard}
96
+ onChange={(event) => setTargetStandard(event.target.value)}
97
+ required
98
+ />
99
+ </label>
100
+ <label className="field">
101
+ <span>Target Standard</span>
102
+ <input
103
+ value={destinationStandard}
104
+ onChange={(event) => setDestinationStandard(event.target.value)}
105
+ required
106
+ />
107
+ </label>
108
+ <label className="field">
109
+ <span>Word Report</span>
110
+ <input
111
+ id="source-doc-input"
112
+ type="file"
113
+ accept=".docx"
114
+ onChange={(event) => setFile(event.target.files?.[0] ?? null)}
115
+ required
116
+ />
117
+ </label>
118
+ <label className="field">
119
+ <span>New Standards PDFs</span>
120
+ <input
121
+ id="standards-doc-input"
122
+ type="file"
123
+ accept=".pdf"
124
+ multiple
125
+ onChange={(event) => setStandardsFiles(Array.from(event.target.files ?? []))}
126
+ />
127
+ <small>
128
+ {standardsFiles.length
129
+ ? `${standardsFiles.length} file(s) selected`
130
+ : "Select one or more PDF references."}
131
+ </small>
132
+ </label>
133
+ <label className="field">
134
+ <span>Saved Standards Preset (optional)</span>
135
+ <select
136
+ value={selectedPresetId}
137
+ onChange={(event) => setSelectedPresetId(event.target.value)}
138
+ disabled={presetsLoading || !presets.length}
139
+ >
140
+ <option value="">Select a preset...</option>
141
+ {presets.map((preset: StandardsPreset) => (
142
+ <option
143
+ key={preset.id}
144
+ value={preset.id}
145
+ disabled={preset.status !== "ready"}
146
+ >
147
+ {preset.name} ({preset.document_count} file{preset.document_count === 1 ? "" : "s"})
148
+ {preset.status === "processing"
149
+ ? ` — processing ${preset.processed_count}/${preset.total_count || preset.document_count}`
150
+ : preset.status === "failed"
151
+ ? " — failed"
152
+ : ""}
153
+ </option>
154
+ ))}
155
+ </select>
156
+ <small>
157
+ {presetsLoading
158
+ ? "Loading saved presets..."
159
+ : presetsError
160
+ ? "Failed to load presets."
161
+ : presets.length
162
+ ? selectedPreset
163
+ ? selectedPreset.status === "processing"
164
+ ? `Processing preset ${selectedPreset.processed_count}/${selectedPreset.total_count || selectedPreset.document_count}.`
165
+ : selectedPreset.status === "failed"
166
+ ? `Preset failed${selectedPreset.last_error ? `: ${selectedPreset.last_error}` : ""}`
167
+ : "Optionally reuse a previously parsed standards bundle."
168
+ : "Optionally reuse a previously parsed standards bundle."
169
+ : "No presets yet. Create one below."}
170
+ </small>
171
+ </label>
172
+ <button type="submit" disabled={mutation.isPending}>
173
+ {mutation.isPending ? "Uploading..." : "Start Conversion"}
174
+ </button>
175
+ {mutation.isPending && (
176
+ <div className="progress-block">
177
+ <div className="progress-bar">
178
+ <div className="progress-bar-fill" />
179
+ </div>
180
+ <p className="muted">Uploading files and starting pipeline...</p>
181
+ </div>
182
+ )}
183
+ {mutation.isError && (
184
+ <p className="error">Upload failed: {(mutation.error as Error).message}</p>
185
+ )}
186
+ {mutation.isSuccess && <p className="success">Session created successfully.</p>}
187
+ </form>
188
+ );
189
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import App from "./App";
5
+ import "./styles.css";
6
+
7
+ const queryClient = new QueryClient();
8
+
9
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
10
+ <React.StrictMode>
11
+ <QueryClientProvider client={queryClient}>
12
+ <App />
13
+ </QueryClientProvider>
14
+ </React.StrictMode>,
15
+ );
frontend/src/pages/ObservatoryPage.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { fetchDiagnosticsEvents, fetchDiagnosticsTopology } from "../services/api";
4
+ import type { DiagnosticsEvent, DiagnosticsNode, DiagnosticsTopology } from "../types/diagnostics";
5
+
6
+ const EVENT_PULSE_WINDOW_MS = 5000;
7
+ const EVENTS_LIMIT = 60;
8
+
9
+ function buildNodeMap(topology?: DiagnosticsTopology): Map<string, DiagnosticsNode> {
10
+ if (!topology) {
11
+ return new Map();
12
+ }
13
+ return new Map(topology.nodes.map((node) => [node.id, node]));
14
+ }
15
+
16
+ function useActiveNodes(events: DiagnosticsEvent[] | undefined, tick: number): Set<string> {
17
+ return useMemo(() => {
18
+ const active = new Set<string>();
19
+ if (!events) {
20
+ return active;
21
+ }
22
+ const now = Date.now();
23
+ for (const event of events) {
24
+ if (!event.node_id) {
25
+ continue;
26
+ }
27
+ const timestamp = Date.parse(event.timestamp);
28
+ if (Number.isNaN(timestamp)) {
29
+ continue;
30
+ }
31
+ if (now - timestamp <= EVENT_PULSE_WINDOW_MS) {
32
+ active.add(event.node_id);
33
+ }
34
+ }
35
+ return active;
36
+ }, [events, tick]);
37
+ }
38
+
39
+ const timeFormatter = new Intl.DateTimeFormat(undefined, {
40
+ hour: "2-digit",
41
+ minute: "2-digit",
42
+ second: "2-digit",
43
+ });
44
+
45
+ export default function ObservatoryPage() {
46
+ const [timeTick, setTimeTick] = useState(Date.now());
47
+ useEffect(() => {
48
+ const id = window.setInterval(() => setTimeTick(Date.now()), 1000);
49
+ return () => window.clearInterval(id);
50
+ }, []);
51
+
52
+ const {
53
+ data: topology,
54
+ isLoading: topologyLoading,
55
+ isError: topologyError,
56
+ error: topologyErrorDetails,
57
+ } = useQuery({
58
+ queryKey: ["diagnostics", "topology"],
59
+ queryFn: fetchDiagnosticsTopology,
60
+ refetchInterval: 15000,
61
+ });
62
+
63
+ const {
64
+ data: eventsData,
65
+ isLoading: eventsLoading,
66
+ isError: eventsError,
67
+ error: eventsErrorDetails,
68
+ } = useQuery({
69
+ queryKey: ["diagnostics", "events"],
70
+ queryFn: () => fetchDiagnosticsEvents(EVENTS_LIMIT),
71
+ refetchInterval: 2000,
72
+ });
73
+
74
+ const nodeMap = buildNodeMap(topology);
75
+ const activeNodes = useActiveNodes(eventsData, timeTick);
76
+
77
+ const edges = useMemo(() => {
78
+ if (!topology) {
79
+ return [];
80
+ }
81
+ return topology.edges
82
+ .map((edge) => {
83
+ const source = nodeMap.get(edge.source);
84
+ const target = nodeMap.get(edge.target);
85
+ if (!source || !target) {
86
+ return null;
87
+ }
88
+ return (
89
+ <line
90
+ key={edge.id}
91
+ x1={source.position.x}
92
+ y1={source.position.y}
93
+ x2={target.position.x}
94
+ y2={target.position.y}
95
+ className="observatory-edge"
96
+ />
97
+ );
98
+ })
99
+ .filter(Boolean) as JSX.Element[];
100
+ }, [nodeMap, topology]);
101
+
102
+ const nodes = useMemo(() => {
103
+ if (!topology) {
104
+ return [];
105
+ }
106
+ return topology.nodes.map((node) => {
107
+ const isActive = activeNodes.has(node.id);
108
+ return (
109
+ <g key={node.id} className={`observatory-node group-${node.group}`}>
110
+ <circle
111
+ cx={node.position.x}
112
+ cy={node.position.y}
113
+ r={isActive ? 6 : 4}
114
+ className={isActive ? "pulse" : undefined}
115
+ />
116
+ <text x={node.position.x} y={node.position.y - 9} textAnchor="middle">
117
+ {node.label}
118
+ </text>
119
+ </g>
120
+ );
121
+ });
122
+ }, [activeNodes, topology]);
123
+
124
+ return (
125
+ <div className="observatory-page">
126
+ <div className="observatory-header">
127
+ <h2>System Observatory</h2>
128
+ <p>
129
+ Visual map of key storage locations, services, and external dependencies. Nodes pulse when recent activity
130
+ is detected.
131
+ </p>
132
+ </div>
133
+ <div className="observatory-content">
134
+ <section className="observatory-graph-card">
135
+ <h3>Topology</h3>
136
+ {topologyLoading ? (
137
+ <p className="muted">Loading topology...</p>
138
+ ) : topologyError ? (
139
+ <p className="error">Failed to load topology: {(topologyErrorDetails as Error).message}</p>
140
+ ) : (
141
+ <svg className="observatory-canvas" viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet">
142
+ {edges}
143
+ {nodes}
144
+ </svg>
145
+ )}
146
+ </section>
147
+ <aside className="observatory-feed-card">
148
+ <h3>Activity Feed</h3>
149
+ {eventsLoading ? (
150
+ <p className="muted">Monitoring events...</p>
151
+ ) : eventsError ? (
152
+ <p className="error">Failed to load events: {(eventsErrorDetails as Error).message}</p>
153
+ ) : eventsData && eventsData.length ? (
154
+ <ul className="observatory-feed">
155
+ {eventsData.map((event) => {
156
+ const timestamp = Date.parse(event.timestamp);
157
+ const timeString = Number.isNaN(timestamp)
158
+ ? event.timestamp
159
+ : timeFormatter.format(new Date(timestamp));
160
+ const node = event.node_id ? nodeMap.get(event.node_id) : undefined;
161
+ return (
162
+ <li key={event.id}>
163
+ <div className="feed-row">
164
+ <strong>{node?.label ?? event.node_id ?? "System"}</strong>
165
+ <span className="muted">{timeString}</span>
166
+ </div>
167
+ <div>{event.message}</div>
168
+ </li>
169
+ );
170
+ })}
171
+ </ul>
172
+ ) : (
173
+ <p className="muted">No activity detected yet.</p>
174
+ )}
175
+ </aside>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
frontend/src/pages/PresetEditPage.tsx ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { Link, Navigate, useParams } from "react-router-dom";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import {
5
+ addPresetDocuments,
6
+ fetchPresetById,
7
+ removePresetDocument,
8
+ updatePreset,
9
+ } from "../services/api";
10
+ import type { StandardsPreset } from "../types/session";
11
+
12
+ export default function PresetEditPage() {
13
+ const { presetId } = useParams<{ presetId: string }>();
14
+ const queryClient = useQueryClient();
15
+
16
+ const {
17
+ data: preset,
18
+ isLoading,
19
+ isError,
20
+ error,
21
+ } = useQuery({
22
+ queryKey: ["preset", presetId],
23
+ queryFn: () => fetchPresetById(presetId ?? ""),
24
+ enabled: Boolean(presetId),
25
+ refetchInterval: (currentPreset) =>
26
+ currentPreset?.status === "processing" ? 2000 : false,
27
+ });
28
+
29
+ const [name, setName] = useState("");
30
+ const [description, setDescription] = useState("");
31
+
32
+ useEffect(() => {
33
+ if (preset) {
34
+ setName(preset.name);
35
+ setDescription(preset.description ?? "");
36
+ }
37
+ }, [preset?.id, preset?.name, preset?.description]);
38
+
39
+ const updateMutation = useMutation({
40
+ mutationFn: (payload: { name?: string; description?: string | null }) =>
41
+ updatePreset(presetId ?? "", payload),
42
+ onSuccess: (updated) => {
43
+ queryClient.setQueryData(["preset", presetId], updated);
44
+ queryClient.invalidateQueries({ queryKey: ["presets"] });
45
+ },
46
+ onError: (err: unknown) => {
47
+ alert(`Failed to update preset: ${(err as Error).message}`);
48
+ },
49
+ });
50
+
51
+ const addDocumentsMutation = useMutation({
52
+ mutationFn: (files: File[]) => addPresetDocuments(presetId ?? "", files),
53
+ onSuccess: (updated) => {
54
+ queryClient.setQueryData(["preset", presetId], updated);
55
+ queryClient.invalidateQueries({ queryKey: ["presets"] });
56
+ },
57
+ onError: (err: unknown) => {
58
+ alert(`Failed to add documents: ${(err as Error).message}`);
59
+ },
60
+ });
61
+
62
+ const removeDocumentMutation = useMutation({
63
+ mutationFn: (documentPath: string) => removePresetDocument(presetId ?? "", documentPath),
64
+ onSuccess: (updated) => {
65
+ queryClient.setQueryData(["preset", presetId], updated);
66
+ queryClient.invalidateQueries({ queryKey: ["presets"] });
67
+ },
68
+ onError: (err: unknown) => {
69
+ alert(`Failed to remove document: ${(err as Error).message}`);
70
+ },
71
+ });
72
+
73
+ if (!presetId) {
74
+ return <Navigate to="/presets" replace />;
75
+ }
76
+
77
+ const totalDocs = preset?.total_count ?? preset?.document_count ?? 0;
78
+ const processedDocs = Math.min(preset?.processed_count ?? 0, totalDocs);
79
+ const progressPercent = totalDocs
80
+ ? Math.min(100, Math.round((processedDocs / totalDocs) * 100))
81
+ : preset?.status === "ready"
82
+ ? 100
83
+ : 0;
84
+ const nextDoc = Math.min(processedDocs + 1, totalDocs);
85
+
86
+ const handleSave = (event: React.FormEvent<HTMLFormElement>) => {
87
+ event.preventDefault();
88
+ const trimmedName = name.trim();
89
+ if (!trimmedName) {
90
+ alert("Preset name cannot be empty.");
91
+ return;
92
+ }
93
+ updateMutation.mutate({
94
+ name: trimmedName,
95
+ description: description.trim() || "",
96
+ });
97
+ };
98
+
99
+ const handleAddFiles = (event: React.ChangeEvent<HTMLInputElement>) => {
100
+ const files = Array.from(event.target.files ?? []);
101
+ if (!files.length) {
102
+ return;
103
+ }
104
+ addDocumentsMutation.mutate(files);
105
+ event.target.value = "";
106
+ };
107
+
108
+ const handleRemoveDocument = (documentPath: string) => {
109
+ if (!window.confirm("Remove this document from the preset?")) {
110
+ return;
111
+ }
112
+ removeDocumentMutation.mutate(documentPath);
113
+ };
114
+
115
+ return (
116
+ <div className="page-stack">
117
+ <div className="card">
118
+ <div className="edit-header">
119
+ <div>
120
+ <h2>Edit Preset</h2>
121
+ <p className="muted">Update preset metadata and manage the associated PDF files.</p>
122
+ </div>
123
+ <Link className="ghost-button" to="/presets">
124
+ ← Back to Presets
125
+ </Link>
126
+ </div>
127
+
128
+ {isLoading && <p className="muted">Loading preset details...</p>}
129
+ {isError && <p className="error">Failed to load preset: {(error as Error).message}</p>}
130
+ {preset && (
131
+ <>
132
+ <form className="stacked" onSubmit={handleSave}>
133
+ <label className="field">
134
+ <span>Preset Name</span>
135
+ <input value={name} onChange={(event) => setName(event.target.value)} required />
136
+ </label>
137
+ <label className="field">
138
+ <span>Description</span>
139
+ <input value={description} onChange={(event) => setDescription(event.target.value)} />
140
+ </label>
141
+ <button type="submit" disabled={updateMutation.isPending}>
142
+ {updateMutation.isPending ? "Saving..." : "Save Changes"}
143
+ </button>
144
+ </form>
145
+
146
+ <div className="preset-status-card">
147
+ <p className="muted">
148
+ Status: <strong>{preset.status}</strong>
149
+ {preset.status === "processing" && (
150
+ <>
151
+ {" "}
152
+ · {processedDocs}/{totalDocs} processed
153
+ </>
154
+ )}
155
+ </p>
156
+ {preset.last_error && <p className="error">Last error: {preset.last_error}</p>}
157
+ </div>
158
+ {preset.status === "processing" && totalDocs > 0 && (
159
+ <div className="progress-block">
160
+ <div className="linear-progress">
161
+ <div className="linear-progress-fill" style={{ width: `${progressPercent}%` }} />
162
+ </div>
163
+ <p className="muted">
164
+ Currently parsing document {nextDoc} of {totalDocs}.
165
+ </p>
166
+ </div>
167
+ )}
168
+
169
+ <div className="card subsection">
170
+ <h3>Documents</h3>
171
+ {preset.documents.length ? (
172
+ <ul className="document-list">
173
+ {preset.documents.map((doc) => (
174
+ <li key={doc} className="document-item">
175
+ <span>{doc.split(/[/\\]/).pop() ?? doc}</span>
176
+ <button
177
+ type="button"
178
+ className="ghost-button danger"
179
+ onClick={() => handleRemoveDocument(doc)}
180
+ disabled={removeDocumentMutation.isPending || preset.status === "processing"}
181
+ >
182
+ Remove
183
+ </button>
184
+ </li>
185
+ ))}
186
+ </ul>
187
+ ) : (
188
+ <p className="muted">No documents attached yet.</p>
189
+ )}
190
+ <label className="field">
191
+ <span>Add Additional PDFs</span>
192
+ <input type="file" accept=".pdf" multiple onChange={handleAddFiles} />
193
+ {addDocumentsMutation.isPending && <p className="muted">Uploading and parsing...</p>}
194
+ </label>
195
+ </div>
196
+ </>
197
+ )}
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
frontend/src/pages/PresetsPage.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import PresetManager from "../components/PresetManager";
2
+
3
+ export default function PresetsPage() {
4
+ return (
5
+ <div className="page-stack">
6
+ <PresetManager />
7
+ </div>
8
+ );
9
+ }
frontend/src/pages/SessionsPage.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import UploadForm from "../components/UploadForm";
3
+ import SessionDetails from "../components/SessionDetails";
4
+ import SessionList from "../components/SessionList";
5
+ import type { Session, SessionSummary } from "../types/session";
6
+
7
+ export default function SessionsPage() {
8
+ const [selectedSession, setSelectedSession] = useState<Session | SessionSummary | null>(null);
9
+
10
+ return (
11
+ <div className="grid">
12
+ <div>
13
+ <UploadForm onSuccess={setSelectedSession} />
14
+ <SessionDetails session={selectedSession} />
15
+ </div>
16
+ <SessionList selectedId={selectedSession?.id ?? null} onSelect={setSelectedSession} />
17
+ </div>
18
+ );
19
+ }
frontend/src/services/api.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios";
2
+ import type {
3
+ CreatePresetPayload,
4
+ CreateSessionPayload,
5
+ Session,
6
+ SessionStatusResponse,
7
+ SessionSummary,
8
+ StandardsPreset,
9
+ UpdatePresetPayload,
10
+ } from "../types/session";
11
+ import type { DiagnosticsEvent, DiagnosticsTopology } from "../types/diagnostics";
12
+
13
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000/api";
14
+
15
+ export async function uploadSession(payload: CreateSessionPayload): Promise<Session> {
16
+ const formData = new FormData();
17
+ formData.append("name", payload.name);
18
+ formData.append("target_standard", payload.target_standard);
19
+ formData.append("destination_standard", payload.destination_standard);
20
+ formData.append("source_doc", payload.sourceFile);
21
+ (payload.standardsFiles ?? []).forEach((file) => {
22
+ formData.append("standards_pdfs", file);
23
+ });
24
+ if (payload.standardsPresetId) {
25
+ formData.append("standards_preset_id", payload.standardsPresetId);
26
+ }
27
+ if (payload.metadata) {
28
+ formData.append("metadata", JSON.stringify(payload.metadata));
29
+ }
30
+
31
+ const response = await axios.post<Session>(`${API_BASE_URL}/sessions`, formData, {
32
+ headers: { "Content-Type": "multipart/form-data" },
33
+ });
34
+ return response.data;
35
+ }
36
+
37
+ export async function fetchSessions(): Promise<SessionSummary[]> {
38
+ const response = await axios.get<SessionSummary[]>(`${API_BASE_URL}/sessions`);
39
+ return response.data;
40
+ }
41
+
42
+ export async function fetchSessionById(id: string): Promise<Session> {
43
+ const response = await axios.get<Session>(`${API_BASE_URL}/sessions/${id}`);
44
+ return response.data;
45
+ }
46
+
47
+ export async function fetchSessionStatus(id: string): Promise<SessionStatusResponse> {
48
+ const response = await axios.get<SessionStatusResponse>(`${API_BASE_URL}/sessions/${id}/status`);
49
+ return response.data;
50
+ }
51
+
52
+ export async function fetchPresets(): Promise<StandardsPreset[]> {
53
+ const response = await axios.get<StandardsPreset[]>(`${API_BASE_URL}/presets`);
54
+ return response.data;
55
+ }
56
+
57
+ export async function fetchPresetById(id: string): Promise<StandardsPreset> {
58
+ const response = await axios.get<StandardsPreset>(`${API_BASE_URL}/presets/${id}`);
59
+ return response.data;
60
+ }
61
+
62
+ export async function createPreset(payload: CreatePresetPayload): Promise<StandardsPreset> {
63
+ const formData = new FormData();
64
+ formData.append("name", payload.name);
65
+ if (payload.description) {
66
+ formData.append("description", payload.description);
67
+ }
68
+ payload.files.forEach((file) => {
69
+ formData.append("standards_pdfs", file);
70
+ });
71
+
72
+ const response = await axios.post<StandardsPreset>(`${API_BASE_URL}/presets`, formData, {
73
+ headers: { "Content-Type": "multipart/form-data" },
74
+ });
75
+ return response.data;
76
+ }
77
+
78
+ export async function addPresetDocuments(id: string, files: File[]): Promise<StandardsPreset> {
79
+ const formData = new FormData();
80
+ files.forEach((file) => formData.append("standards_pdfs", file));
81
+ const response = await axios.post<StandardsPreset>(`${API_BASE_URL}/presets/${id}/documents`, formData, {
82
+ headers: { "Content-Type": "multipart/form-data" },
83
+ });
84
+ return response.data;
85
+ }
86
+
87
+ export async function updatePreset(id: string, payload: UpdatePresetPayload): Promise<StandardsPreset> {
88
+ const response = await axios.patch<StandardsPreset>(`${API_BASE_URL}/presets/${id}`, payload);
89
+ return response.data;
90
+ }
91
+
92
+ export async function deletePreset(id: string): Promise<void> {
93
+ await axios.delete(`${API_BASE_URL}/presets/${id}`);
94
+ }
95
+
96
+ export async function removePresetDocument(id: string, documentPath: string): Promise<StandardsPreset> {
97
+ const response = await axios.delete<StandardsPreset>(`${API_BASE_URL}/presets/${id}/documents`, {
98
+ params: { document: documentPath },
99
+ });
100
+ return response.data;
101
+ }
102
+
103
+ export async function fetchDiagnosticsTopology(): Promise<DiagnosticsTopology> {
104
+ const response = await axios.get<DiagnosticsTopology>(`${API_BASE_URL}/diagnostics/topology`);
105
+ return response.data;
106
+ }
107
+
108
+ export async function fetchDiagnosticsEvents(limit = 50): Promise<DiagnosticsEvent[]> {
109
+ const response = await axios.get<{ events: DiagnosticsEvent[] }>(`${API_BASE_URL}/diagnostics/events`, {
110
+ params: { limit },
111
+ });
112
+ return response.data.events;
113
+ }
frontend/src/styles.css ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: light;
3
+ font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ line-height: 1.5;
5
+ font-weight: 400;
6
+ color: #0f172a;
7
+ background-color: #f8fafc;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ margin: 0;
16
+ }
17
+
18
+ a {
19
+ color: inherit;
20
+ }
21
+
22
+ button {
23
+ font: inherit;
24
+ }
25
+
26
+ .app-shell {
27
+ min-height: 100vh;
28
+ display: flex;
29
+ flex-direction: column;
30
+ padding: 1.5rem;
31
+ gap: 1.5rem;
32
+ }
33
+
34
+ header {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 0.5rem;
38
+ }
39
+
40
+ .primary-nav {
41
+ display: flex;
42
+ gap: 0.75rem;
43
+ margin-top: 0.5rem;
44
+ }
45
+
46
+ .primary-nav a {
47
+ color: #1d4ed8;
48
+ padding: 0.4rem 0.75rem;
49
+ border-radius: 999px;
50
+ border: 1px solid transparent;
51
+ text-decoration: none;
52
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
53
+ font-weight: 600;
54
+ font-size: 0.9rem;
55
+ }
56
+
57
+ .primary-nav a:hover {
58
+ border-color: #bfdbfe;
59
+ background: #e0f2fe;
60
+ }
61
+
62
+ .primary-nav a.active {
63
+ background: #1d4ed8;
64
+ color: #ffffff;
65
+ }
66
+
67
+ main {
68
+ flex: 1;
69
+ }
70
+
71
+ .grid {
72
+ display: grid;
73
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
74
+ gap: 1.5rem;
75
+ }
76
+
77
+ .card {
78
+ background: #ffffff;
79
+ padding: 1.25rem;
80
+ border-radius: 12px;
81
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 0.75rem;
85
+ }
86
+
87
+ .field {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 0.25rem;
91
+ font-weight: 600;
92
+ color: #0f172a;
93
+ }
94
+
95
+ .field input {
96
+ padding: 0.5rem 0.75rem;
97
+ border-radius: 8px;
98
+ border: 1px solid #cbd5f5;
99
+ font: inherit;
100
+ }
101
+
102
+ .field select {
103
+ padding: 0.5rem 0.75rem;
104
+ border-radius: 8px;
105
+ border: 1px solid #cbd5f5;
106
+ font: inherit;
107
+ background: #ffffff;
108
+ }
109
+
110
+ button[type="submit"] {
111
+ background: #1d4ed8;
112
+ color: #ffffff;
113
+ border: none;
114
+ padding: 0.6rem 1rem;
115
+ border-radius: 8px;
116
+ cursor: pointer;
117
+ transition: background 0.2s;
118
+ }
119
+
120
+ button[type="submit"]:disabled {
121
+ background: #93c5fd;
122
+ cursor: not-allowed;
123
+ }
124
+
125
+ .error {
126
+ color: #b91c1c;
127
+ }
128
+
129
+ .success {
130
+ color: #047857;
131
+ }
132
+
133
+ .muted {
134
+ color: #64748b;
135
+ font-size: 0.875rem;
136
+ }
137
+
138
+ .progress-block {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 0.5rem;
142
+ }
143
+
144
+ .progress-bar {
145
+ position: relative;
146
+ width: 100%;
147
+ height: 6px;
148
+ background: #dbeafe;
149
+ border-radius: 999px;
150
+ overflow: hidden;
151
+ }
152
+
153
+ .progress-bar-fill {
154
+ position: absolute;
155
+ inset: 0;
156
+ background: linear-gradient(90deg, #1d4ed8, #38bdf8);
157
+ animation: progress-pulse 1.2s linear infinite;
158
+ }
159
+
160
+ @keyframes progress-pulse {
161
+ 0% {
162
+ transform: translateX(-50%);
163
+ }
164
+ 50% {
165
+ transform: translateX(0%);
166
+ }
167
+ 100% {
168
+ transform: translateX(100%);
169
+ }
170
+ }
171
+
172
+ .notice {
173
+ padding: 0.75rem;
174
+ border-radius: 8px;
175
+ border: 1px solid transparent;
176
+ }
177
+
178
+ .notice.success {
179
+ background: #ecfdf5;
180
+ border-color: #bbf7d0;
181
+ color: #036949;
182
+ }
183
+
184
+ .log-panel {
185
+ display: flex;
186
+ flex-direction: column;
187
+ gap: 0.5rem;
188
+ }
189
+
190
+ .log-panel ul {
191
+ margin: 0;
192
+ padding-left: 1rem;
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 0.35rem;
196
+ }
197
+
198
+ .log-panel li {
199
+ font-size: 0.9rem;
200
+ color: #0f172a;
201
+ }
202
+
203
+ .section-card {
204
+ margin-top: 1.25rem;
205
+ padding-top: 1rem;
206
+ border-top: 1px solid #e2e8f0;
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: 0.6rem;
210
+ }
211
+
212
+ .section-card h3 {
213
+ margin: 0;
214
+ font-size: 1rem;
215
+ color: #1e293b;
216
+ }
217
+
218
+ .section-list {
219
+ list-style: disc;
220
+ margin: 0;
221
+ padding-left: 1.25rem;
222
+ display: flex;
223
+ flex-direction: column;
224
+ gap: 0.5rem;
225
+ }
226
+
227
+ .section-list li {
228
+ color: #0f172a;
229
+ }
230
+
231
+ .diff-block {
232
+ display: flex;
233
+ flex-direction: column;
234
+ gap: 0.35rem;
235
+ margin-top: 0.35rem;
236
+ padding-left: 0.5rem;
237
+ border-left: 2px solid #cbd5f5;
238
+ }
239
+
240
+ .field small {
241
+ font-weight: 400;
242
+ color: #64748b;
243
+ }
244
+
245
+ .session-list {
246
+ list-style: none;
247
+ margin: 0;
248
+ padding: 0;
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 0.75rem;
252
+ }
253
+
254
+ .session-list li {
255
+ display: flex;
256
+ justify-content: space-between;
257
+ align-items: center;
258
+ padding: 0.75rem;
259
+ border-radius: 10px;
260
+ border: 1px solid transparent;
261
+ cursor: pointer;
262
+ transition: border-color 0.2s, background 0.2s;
263
+ }
264
+
265
+ .session-list li:hover {
266
+ border-color: #bfdbfe;
267
+ }
268
+
269
+ .session-list li.selected {
270
+ border-color: #1d4ed8;
271
+ background: #eff6ff;
272
+ }
273
+
274
+ .status {
275
+ text-transform: uppercase;
276
+ font-weight: 600;
277
+ font-size: 0.75rem;
278
+ }
279
+
280
+ .status-created,
281
+ .status-processing {
282
+ color: #1d4ed8;
283
+ }
284
+
285
+ .status-review {
286
+ color: #0f766e;
287
+ }
288
+
289
+ .status-completed {
290
+ color: #16a34a;
291
+ }
292
+
293
+ .status-failed {
294
+ color: #b91c1c;
295
+ }
296
+
297
+ .details-grid {
298
+ display: grid;
299
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
300
+ gap: 0.75rem;
301
+ }
302
+
303
+ .details-grid dt {
304
+ font-size: 0.75rem;
305
+ text-transform: uppercase;
306
+ color: #475569;
307
+ }
308
+
309
+ .details-grid dd {
310
+ margin: 0;
311
+ font-weight: 600;
312
+ }
313
+
314
+ footer {
315
+ text-align: center;
316
+ color: #64748b;
317
+ font-size: 0.875rem;
318
+ }
319
+
320
+ @media (max-width: 960px) {
321
+ .grid {
322
+ grid-template-columns: 1fr;
323
+ }
324
+ }
325
+ .actions-row {
326
+ display: flex;
327
+ justify-content: flex-end;
328
+ }
329
+
330
+ .card-header {
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: space-between;
334
+ gap: 1rem;
335
+ margin-bottom: 1rem;
336
+ }
337
+
338
+ .card-header h2 {
339
+ margin: 0;
340
+ }
341
+
342
+ .header-actions {
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 0.75rem;
346
+ }
347
+
348
+ .text-small {
349
+ font-size: 0.875rem;
350
+ }
351
+
352
+ .ghost-button {
353
+ background: transparent;
354
+ border: 1px solid #cbd5f5;
355
+ color: #1d4ed8;
356
+ padding: 0.4rem 0.8rem;
357
+ border-radius: 8px;
358
+ cursor: pointer;
359
+ transition: background 0.2s, color 0.2s;
360
+ }
361
+
362
+ .ghost-button:hover:not(:disabled) {
363
+ background: #e0f2fe;
364
+ }
365
+
366
+ .ghost-button:disabled {
367
+ color: #94a3b8;
368
+ cursor: not-allowed;
369
+ }
370
+
371
+ .subsection {
372
+ margin-top: 0.75rem;
373
+ display: flex;
374
+ flex-direction: column;
375
+ gap: 0.5rem;
376
+ }
377
+
378
+ .stacked {
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 0.75rem;
382
+ }
383
+
384
+ .preset-list ul {
385
+ list-style: none;
386
+ padding: 0;
387
+ margin: 0;
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 0.75rem;
391
+ }
392
+
393
+ .preset-list li {
394
+ border: 1px solid #e2e8f0;
395
+ border-radius: 10px;
396
+ padding: 0.75rem;
397
+ display: flex;
398
+ flex-direction: column;
399
+ gap: 0.35rem;
400
+ }
401
+
402
+ .preset-docs {
403
+ font-size: 0.8rem;
404
+ }
405
+
406
+ .preset-actions {
407
+ display: flex;
408
+ gap: 0.5rem;
409
+ margin-top: 0.5rem;
410
+ }
411
+
412
+ .ghost-button.danger {
413
+ border-color: #fca5a5;
414
+ color: #b91c1c;
415
+ }
416
+
417
+ .ghost-button.danger:hover:not(:disabled) {
418
+ background: #fee2e2;
419
+ }
420
+
421
+ .ghost-button.danger:disabled {
422
+ color: #fca5a5;
423
+ border-color: #fca5a5;
424
+ }
425
+
426
+ .edit-header {
427
+ display: flex;
428
+ align-items: center;
429
+ justify-content: space-between;
430
+ gap: 1rem;
431
+ flex-wrap: wrap;
432
+ }
433
+
434
+ .document-list {
435
+ list-style: none;
436
+ padding: 0;
437
+ margin: 0;
438
+ display: flex;
439
+ flex-direction: column;
440
+ gap: 0.5rem;
441
+ }
442
+
443
+ .document-item {
444
+ display: flex;
445
+ align-items: center;
446
+ justify-content: space-between;
447
+ gap: 0.75rem;
448
+ padding: 0.6rem 0.75rem;
449
+ border: 1px solid #e2e8f0;
450
+ border-radius: 8px;
451
+ }
452
+
453
+ .preset-status-card {
454
+ margin-top: 1rem;
455
+ padding: 0.75rem;
456
+ border: 1px solid #e2e8f0;
457
+ border-radius: 10px;
458
+ background: #f8fafc;
459
+ }
460
+
461
+
462
+ .page-stack {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 1.5rem;
466
+ }
467
+
468
+ .preset-header {
469
+ display: flex;
470
+ justify-content: space-between;
471
+ gap: 1rem;
472
+ }
473
+
474
+ .preset-status {
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 0.5rem;
478
+ font-size: 0.85rem;
479
+ }
480
+
481
+ .status-ready {
482
+ color: #16a34a;
483
+ font-weight: 600;
484
+ }
485
+
486
+ .status-processing {
487
+ color: #1d4ed8;
488
+ font-weight: 600;
489
+ }
490
+
491
+ .status-failed {
492
+ color: #b91c1c;
493
+ font-weight: 600;
494
+ }
495
+
496
+ .linear-progress {
497
+ width: 100%;
498
+ height: 6px;
499
+ border-radius: 999px;
500
+ background: #dbeafe;
501
+ overflow: hidden;
502
+ }
503
+
504
+ .linear-progress-fill {
505
+ height: 100%;
506
+ background: linear-gradient(90deg, #1d4ed8, #38bdf8);
507
+ transition: width 0.3s ease;
508
+ }
509
+
510
+
511
+ /* Observatory */
512
+ .observatory-page {
513
+ display: flex;
514
+ flex-direction: column;
515
+ gap: 1.5rem;
516
+ }
517
+
518
+ .observatory-header h2 {
519
+ margin: 0;
520
+ }
521
+
522
+ .observatory-header p {
523
+ margin: 0.25rem 0 0;
524
+ color: #475569;
525
+ }
526
+
527
+ .observatory-content {
528
+ display: flex;
529
+ flex-wrap: wrap;
530
+ gap: 1.5rem;
531
+ }
532
+
533
+ .observatory-graph-card,
534
+ .observatory-feed-card {
535
+ background: #ffffff;
536
+ border-radius: 12px;
537
+ padding: 1.25rem;
538
+ box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
539
+ flex: 1 1 320px;
540
+ min-height: 320px;
541
+ }
542
+
543
+ .observatory-graph-card {
544
+ flex: 2 1 520px;
545
+ display: flex;
546
+ flex-direction: column;
547
+ }
548
+
549
+ .observatory-canvas {
550
+ margin-top: 1rem;
551
+ width: 100%;
552
+ height: 100%;
553
+ min-height: 320px;
554
+ border-radius: 12px;
555
+ background: radial-gradient(circle at center, #dbeafe, #e2e8f0 70%);
556
+ padding: 1rem;
557
+ }
558
+
559
+ .observatory-edge {
560
+ stroke: rgba(30, 64, 175, 0.35);
561
+ stroke-width: 0.8;
562
+ stroke-linecap: round;
563
+ }
564
+
565
+ .observatory-node circle {
566
+ transform-origin: center;
567
+ transform-box: fill-box;
568
+ fill: #1d4ed8;
569
+ opacity: 0.7;
570
+ transition: r 0.2s ease, opacity 0.2s ease;
571
+ }
572
+
573
+ .observatory-node text {
574
+ font-size: 3px;
575
+ fill: #0f172a;
576
+ pointer-events: none;
577
+ }
578
+
579
+ .observatory-node.group-storage circle {
580
+ fill: #0ea5e9;
581
+ }
582
+
583
+ .observatory-node.group-service circle {
584
+ fill: #16a34a;
585
+ }
586
+
587
+ .observatory-node.group-external circle {
588
+ fill: #f97316;
589
+ }
590
+
591
+ .observatory-node circle.pulse {
592
+ animation: observatory-pulse 1.2s ease-in-out infinite;
593
+ opacity: 0.95;
594
+ }
595
+
596
+ @keyframes observatory-pulse {
597
+ 0%, 100% {
598
+ transform: scale(1);
599
+ opacity: 0.95;
600
+ }
601
+ 50% {
602
+ transform: scale(1.3);
603
+ opacity: 0.6;
604
+ }
605
+ }
606
+
607
+ .observatory-feed-card {
608
+ display: flex;
609
+ flex-direction: column;
610
+ }
611
+
612
+ .observatory-feed {
613
+ list-style: none;
614
+ padding: 0;
615
+ margin: 1rem 0 0;
616
+ display: flex;
617
+ flex-direction: column;
618
+ gap: 0.75rem;
619
+ max-height: 360px;
620
+ overflow-y: auto;
621
+ }
622
+
623
+ .observatory-feed li {
624
+ padding: 0.75rem;
625
+ border-radius: 10px;
626
+ background: #f8fafc;
627
+ border: 1px solid #e2e8f0;
628
+ }
629
+
630
+ .observatory-feed .feed-row {
631
+ display: flex;
632
+ align-items: center;
633
+ justify-content: space-between;
634
+ margin-bottom: 0.35rem;
635
+ }
636
+
637
+ .observatory-feed .feed-row strong {
638
+ color: #0f172a;
639
+ }
640
+
641
+ .observatory-feed .feed-row .muted {
642
+ font-size: 0.85rem;
643
+ }
frontend/src/types/diagnostics.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface DiagnosticsNode {
2
+ id: string;
3
+ label: string;
4
+ group: "storage" | "service" | "external" | string;
5
+ position: { x: number; y: number };
6
+ last_event_at?: string | null;
7
+ }
8
+
9
+ export interface DiagnosticsEdge {
10
+ id: string;
11
+ source: string;
12
+ target: string;
13
+ }
14
+
15
+ export interface DiagnosticsTopology {
16
+ nodes: DiagnosticsNode[];
17
+ edges: DiagnosticsEdge[];
18
+ }
19
+
20
+ export interface DiagnosticsEvent {
21
+ id: string;
22
+ timestamp: string;
23
+ event_type: string;
24
+ message: string;
25
+ node_id?: string | null;
26
+ metadata?: Record<string, unknown>;
27
+ }
frontend/src/types/session.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface SessionSummary {
2
+ id: string;
3
+ name: string;
4
+ status: string;
5
+ created_at: string;
6
+ updated_at: string;
7
+ source_doc: string;
8
+ target_standard: string;
9
+ destination_standard: string;
10
+ standards_count: number;
11
+ last_error?: string | null;
12
+ }
13
+
14
+ export interface Session extends SessionSummary {
15
+ standards_docs: string[];
16
+ logs: string[];
17
+ metadata: Record<string, unknown>;
18
+ }
19
+
20
+ export interface CreateSessionPayload {
21
+ name: string;
22
+ target_standard: string;
23
+ destination_standard: string;
24
+ metadata?: Record<string, unknown>;
25
+ sourceFile: File;
26
+ standardsFiles?: File[];
27
+ standardsPresetId?: string | null;
28
+ }
29
+
30
+ export interface SessionStatusResponse {
31
+ id: string;
32
+ status: string;
33
+ updated_at: string;
34
+ }
35
+
36
+ export interface StandardsPreset {
37
+ id: string;
38
+ name: string;
39
+ description?: string | null;
40
+ documents: string[];
41
+ document_count: number;
42
+ status: string;
43
+ processed_count: number;
44
+ total_count: number;
45
+ last_error?: string | null;
46
+ created_at: string;
47
+ updated_at: string;
48
+ }
49
+
50
+ export interface CreatePresetPayload {
51
+ name: string;
52
+ description?: string;
53
+ files: File[];
54
+ }
55
+
56
+ export interface UpdatePresetPayload {
57
+ name?: string;
58
+ description?: string | null;
59
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": false,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }