diff --git a/README.MD b/README.MD index b0b59406..2f0d1a41 100644 --- a/README.MD +++ b/README.MD @@ -22,6 +22,7 @@ - [Local Model Support](#-local-model-support) - [Universal File Import](#-universal-file-import) - [Academic Citations](#-academic-citations) +- [Architecture](#-architecture) - [Technical Documentation](#-technical-documentation) - [Contributing](#-contributing) @@ -132,6 +133,57 @@ Markdown Reader preserves citation syntax during editing and export, including H --- +## 🏗️ Architecture + +Markdown Reader consists of a Next.js / React interface, a Tauri desktop host, and a local FastAPI service. The diagrams below summarize the five high-level views from the [interactive Architecture Explorer](./docs/architecture/index.html). They are simplified architectural maps rather than exhaustive function-call traces. + +### 01 · System Overview + +The desktop interface communicates with native Tauri capabilities and the Python backend. Document storage stays local; model requests can reach a configured cloud or local provider. + +![system overview architecture diagram](./docs/architecture/system-overview.svg) + +### 02 · Frontend Modules + +The page composes the editor, feature panels, and stateful hooks. Panels and hooks use the typed API client for backend requests. + +![frontend modules architecture diagram](./docs/architecture/frontend-modules.svg) + +### 03 · Backend Modules + +FastAPI registers routers for the main feature areas. Each router delegates work to the relevant domain implementation. + +![backend modules architecture diagram](./docs/architecture/backend-modules.svg) + +### 04 · AI Chat Request Flow + +The chat panel passes a message through the React hook and typed client to the FastAPI AI endpoint, which delegates to the configured model provider. + +![ai chat request flow architecture diagram](./docs/architecture/ai-chat-request-flow.svg) + +In the packaged desktop app, API requests traverse the Tauri request proxy; the arrow from the client to the router abstracts that transport step. + +### 05 · Desktop Runtime + +In packaged builds, Tauri launches and supervises the Python sidecar. The Rust host retains the backend connection details and forwards authenticated requests from the WebView. + +![desktop runtime architecture diagram](./docs/architecture/desktop-runtime.svg) + +### Generate and Open the Interactive Architecture Explorer + +These README diagrams provide an at-a-glance overview. To regenerate the source-scanned interactive explorer from your current checkout, run the following commands **from the repository root**: + +```bash +python3 tools/generate_architecture.py +open docs/architecture/index.html +``` + +The generator writes `docs/architecture/index.html`; the `open` command opens that file on macOS. On Linux, use `xdg-open docs/architecture/index.html`; on Windows, use `start docs/architecture/index.html`. + +The explorer supports zooming, panning, node inspection, and exporting a selected view as SVG. Its source-derived function views may vary as the code changes. The five static SVG diagrams embedded above are maintained in `docs/architecture/` and do **not** update automatically when the generator is run. + +--- + ## 📖 Technical Documentation For advanced and implementation details, see [Technical Documentation](./docs/TECHNICAL.md). It covers: diff --git a/docs/architecture/ai-chat-request-flow.svg b/docs/architecture/ai-chat-request-flow.svg new file mode 100644 index 00000000..bc5b73ad --- /dev/null +++ b/docs/architecture/ai-chat-request-flow.svg @@ -0,0 +1,10 @@ +ai chat request flow + + + +AIPanel +useAIChat / sendMessage() +AI.chat / Typed API +AI router / POST /chat +AI logic +Cloud or local model diff --git a/docs/architecture/backend-modules.svg b/docs/architecture/backend-modules.svg new file mode 100644 index 00000000..0efd6857 --- /dev/null +++ b/docs/architecture/backend-modules.svg @@ -0,0 +1,21 @@ +backend modules + + + + + + + + + +FastAPI entry +AI router +Files router +Markdown router +Export router +Citations and knowledge +AI logic +Converters +Markdown renderer +DOCX and PDF exporters +Citation and knowledge logic diff --git a/docs/architecture/desktop-runtime.svg b/docs/architecture/desktop-runtime.svg new file mode 100644 index 00000000..c309c828 --- /dev/null +++ b/docs/architecture/desktop-runtime.svg @@ -0,0 +1,10 @@ +desktop runtime + + + +Tauri startup +Python sidecar +Rust host state +WebView client +Rust request proxy +FastAPI token middleware diff --git a/docs/architecture/frontend-modules.svg b/docs/architecture/frontend-modules.svg new file mode 100644 index 00000000..d58ab686 --- /dev/null +++ b/docs/architecture/frontend-modules.svg @@ -0,0 +1,11 @@ +frontend modules + + + + +HomePage +EditorWorkspace +Feature panels +Stateful hooks +Typed API client +FastAPI endpoints diff --git a/docs/architecture/index.html b/docs/architecture/index.html new file mode 100644 index 00000000..3319805f --- /dev/null +++ b/docs/architecture/index.html @@ -0,0 +1,83 @@ + +Markdown Reader | Architecture Explorer + +

Markdown Reader / Architecture Explorer

Interactive source-grounded architecture prototype

Read-only prototype · main
+
+

System overview

Application processes and integration boundaries

Click nodes to inspect · Drag to pan · Scroll to zoom
Application moduleLocal resourceExternal serviceRuntime process
+
+ + diff --git a/docs/architecture/system-overview.svg b/docs/architecture/system-overview.svg new file mode 100644 index 00000000..95131609 --- /dev/null +++ b/docs/architecture/system-overview.svg @@ -0,0 +1,10 @@ +system overview + + + +Desktop user +Next.js / React UI +Tauri Rust host +FastAPI sidecar +Local files and settings +Cloud or local AI providers diff --git a/docs/architecture/template.html b/docs/architecture/template.html new file mode 100644 index 00000000..a68dbb8d --- /dev/null +++ b/docs/architecture/template.html @@ -0,0 +1,68 @@ + +Markdown Reader | Architecture Explorer + +

Markdown Reader / Architecture Explorer

Interactive source-grounded architecture prototype

Read-only prototype · main
+
+

System overview

Application processes and integration boundaries

Click nodes to inspect · Drag to pan · Scroll to zoom
Application moduleLocal resourceExternal serviceRuntime process
+
+ diff --git a/tools/generate_architecture.py b/tools/generate_architecture.py new file mode 100644 index 00000000..97a72d84 --- /dev/null +++ b/tools/generate_architecture.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Generate a self-contained architecture explorer from repository source files.""" + +import argparse +import ast +import json +import os +import re +from bisect import bisect_left +from collections import defaultdict +from pathlib import Path + +EXCLUDED = { + ".git", + "node_modules", + ".next", + "dist", + "build", + ".venv", + "__pycache__", + "coverage", +} +HIDDEN_FUNCTION_VIEWS = {"backend/ai_logic.py", "backend/citation_logic.py"} +CALL_RE = re.compile(r"\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*\(") +TS_DEF = re.compile( + r"\b(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(|\b(?:export\s+)?(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>", + re.M, +) +TS_IMPORT = re.compile( + r"\b(?:import|export)\s+(?:[^;]*?\s+from\s+)?[\'\"]([^\'\"]+)[\'\"]", re.S +) + + +def files(root): + extensions = {".py", ".ts", ".tsx", ".js", ".mjs"} + for directory, dirs, names in os.walk(root): + dirs[:] = sorted(d for d in dirs if d not in EXCLUDED) + for name in sorted(names): + path = Path(directory) / name + if path.suffix in extensions and path.is_file(): + yield path + + +def analyze(root): + entries = [] + for path in files(root): + rel = path.relative_to(root).as_posix() + source = path.read_text(encoding="utf-8", errors="replace") + defs = [] + edges = [] + if path.suffix == ".py": + try: + tree = ast.parse(source, filename=rel) + except SyntaxError: + continue + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + defs.append( + (node.name, node.lineno, node.end_lineno or node.lineno) + ) + for call in ast.walk(node): + if isinstance(call, ast.Call): + if isinstance(call.func, ast.Name): + edges.append((node.name, call.func.id, call.lineno)) + elif isinstance(call.func, ast.Attribute): + edges.append((node.name, call.func.attr, call.lineno)) + else: + newline_positions = [i for i, char in enumerate(source) if char == "\n"] + + def line_number(offset): + return bisect_left(newline_positions, offset) + 1 + + matches = list(TS_DEF.finditer(source)) + for i, match in enumerate(matches): + name = match.group(1) or match.group(2) + start = line_number(match.start()) + end_pos = ( + matches[i + 1].start() if i + 1 < len(matches) else len(source) + ) + end = line_number(end_pos) + defs.append((name, start, end)) + body = source[match.end() : end_pos] + for call in CALL_RE.finditer(body): + callee = call.group(1).split(".")[-1] + edges.append( + (name, callee, line_number(match.end() + call.start())) + ) + entries.append({"file": rel, "defs": defs, "edges": edges}) + return entries + + +def make_views(entries, root): + grouped = defaultdict(list) + for entry in entries: + path = entry["file"] + if path.startswith("frontend/src/"): + group = "Frontend functions" + elif path.startswith("backend/"): + group = "Backend functions" + elif path.startswith("frontend/src-tauri/"): + group = "Desktop functions" + else: + continue + grouped[group].append(entry) + views = [] + for index, (group, subset) in enumerate(grouped.items()): + by_name = defaultdict(list) + for entry in subset: + for name, start, end in entry["defs"]: + by_name[name].append((entry["file"], start, end)) + nodes = [] + edges = set() + ids = {} + for entry in subset: + for name, start, end in entry["defs"]: + identifier = f"{entry['file']}:{name}:{start}" + ids[(entry["file"], name)] = identifier + nodes.append( + { + "id": identifier, + "title": name[:29], + "subtitle": entry["file"].split("/")[-1][:35], + "files": [entry["file"]], + "line": start, + "description": f"Defined at {entry['file']}:{start}-{end}.", + "kind": "module", + "x": 0, + "y": 0, + } + ) + for entry in subset: + for caller, callee, line in entry["edges"]: + src = ids.get((entry["file"], caller)) + if not src: + continue + candidates = by_name.get(callee, []) + same_file = [x for x in candidates if x[0] == entry["file"]] + if same_file: + target = ids.get((same_file[0][0], callee)) + if target and target != src: + edges.add((src, target)) + # Split large graphs into per-file views to retain legibility. + for entry in subset: + if entry["file"] in HIDDEN_FUNCTION_VIEWS: + continue + local = [n.copy() for n in nodes if n["files"][0] == entry["file"]] + if len(local) < 2: + continue + local_ids = {n["id"] for n in local} + local_edges = [ + list(e) + for e in sorted(edges) + if e[0] in local_ids and e[1] in local_ids + ] + if len(local) > 120: + local = local[:120] + local_ids = {n["id"] for n in local} + local_edges = [ + e for e in local_edges if e[0] in local_ids and e[1] in local_ids + ] + views.append( + { + "id": f"functions-{len(views)}", + "title": entry["file"].split("/")[-1] + " calls", + "subtitle": entry["file"] + " | local static calls", + "nodes": local, + "edges": local_edges, + } + ) + return views + + +def inject(template, views, sha): + # Existing explorer uses a constant view array and a DOM-based SVG renderer. + script = ( + '" + ) + template = template.replace( + "