mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
Merge branch 'main' into feat/onboarding-overlay
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "2.6.3",
|
||||
"version": "2.7.3",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "2.6.3",
|
||||
"version": "2.7.3",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "understand-anything",
|
||||
"displayName": "Understand Anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "2.6.3",
|
||||
"version": "2.7.3",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -7,17 +7,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="READMEs/README.zh-CN.md">简体中文</a> | <a href="READMEs/README.zh-TW.md">繁體中文</a> | <a href="READMEs/README.ja-JP.md">日本語</a> | <a href="READMEs/README.ko-KR.md">한국어</a> | <a href="READMEs/README.es-ES.md">Español</a> | <a href="READMEs/README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="README.md">English</a> | <a href="READMEs/README.zh-CN.md">简体中文</a> | <a href="READMEs/README.zh-TW.md">繁體中文</a> | <a href="READMEs/README.ja-JP.md">日本語</a> | <a href="READMEs/README.ko-KR.md">한국어</a> | <a href="READMEs/README.es-ES.md">Español</a> | <a href="READMEs/README.tr-TR.md">Türkçe</a> | <a href="READMEs/README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -46,14 +36,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **A huge thank you to the community!** The support for Understand-Anything has been incredible. If this tool saves you a few minutes of digging through complexity, that's all I wanted. 🚀
|
||||
|
||||
**You just joined a new team. The codebase is 200,000 lines of code. Where do you even start?**
|
||||
|
||||
Understand Anything is a [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference) that analyzes your project with a multi-agent pipeline, builds a knowledge graph of every file, function, class, and dependency, then gives you an interactive dashboard to explore it all visually. Stop reading code blind. Start seeing the big picture.
|
||||
|
||||
> **Graphs that teach > graphs that impress.**
|
||||
> **The goal isn't a graph that wows you with how complex your codebase is — it's a graph that quietly teaches you how every piece fits together.**
|
||||
|
||||
---
|
||||
|
||||
@@ -66,18 +53,10 @@ Understand Anything is a [Claude Code Plugin](https://code.claude.com/docs/en/pl
|
||||
|
||||
Navigate your codebase as an interactive knowledge graph — every file, function, and class is a node you can click, search, and explore. Select any node to see plain-English summaries, relationships, and guided tours.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/overview-structural.gif" alt="Structural graph — explore files, functions, classes and their relationships" width="750" />
|
||||
</p>
|
||||
|
||||
### Understand business logic
|
||||
|
||||
Switch to the domain view and see how your code maps to real business processes — domains, flows, and steps laid out as a horizontal graph.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/overview-domain.gif" alt="Domain graph — business domains, flows, and process steps" width="750" />
|
||||
</p>
|
||||
|
||||
### Analyze knowledge bases
|
||||
|
||||
Point `/understand-knowledge` at a [Karpathy-pattern LLM wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) and get a force-directed knowledge graph with community clustering. The deterministic parser extracts wikilinks and categories from `index.md`, then LLM agents discover implicit relationships, extract entities, and surface claims — turning your wiki into a navigable graph of interconnected ideas.
|
||||
@@ -134,6 +113,20 @@ Point `/understand-knowledge` at a [Karpathy-pattern LLM wiki](https://gist.gith
|
||||
|
||||
A multi-agent pipeline scans your project, extracts every file, function, class, and dependency, then builds a knowledge graph saved to `.understand-anything/knowledge-graph.json`.
|
||||
|
||||
**Localized output:** Use `--language` to generate content in your preferred language:
|
||||
|
||||
```bash
|
||||
# Generate Chinese content (知识图节点描述和 Dashboard UI)
|
||||
/understand --language zh
|
||||
|
||||
# Supported languages: en (default), zh, zh-TW, ja, ko, ru
|
||||
```
|
||||
|
||||
The `--language` parameter affects:
|
||||
- Node summaries and descriptions in the knowledge graph
|
||||
- Dashboard UI labels, buttons, and tooltips
|
||||
- Guided tour explanations
|
||||
|
||||
### 3. Explore the dashboard
|
||||
|
||||
```bash
|
||||
@@ -177,7 +170,7 @@ Understand-Anything works across multiple AI coding platforms.
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### One-line install (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot)
|
||||
### One-line install (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -193,7 +186,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
The installer clones the repo to `~/.understand-anything/repo` and creates the right symlinks for the chosen platform. Restart your CLI/IDE afterwards.
|
||||
|
||||
- Supported `<platform>` values: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`
|
||||
- Supported `<platform>` values: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
|
||||
- Update later: `./install.sh --update`
|
||||
- Uninstall: `./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -228,6 +221,9 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Gemini CLI | ✅ Supported | `install.sh gemini` |
|
||||
| Pi Agent | ✅ Supported | `install.sh pi` |
|
||||
| Vibe CLI | ✅ Supported | `install.sh vibe` |
|
||||
| Hermes | ✅ Supported | `install.sh hermes` |
|
||||
| Cline | ✅ Supported | `install.sh cline` |
|
||||
| KIMI CLI | ✅ Supported | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -303,6 +299,10 @@ Please open an issue first for major changes so we can discuss the approach.
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>Thanks to everyone who's used and contributed — knowing this saves people time is what made it worth building.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MIT License © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
+26
-25
@@ -6,17 +6,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -44,14 +34,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **¡Un enorme agradecimiento a la comunidad!** El apoyo a Understand-Anything ha sido increíble. Si esta herramienta te ahorra unos minutos de buscar entre la complejidad, eso es todo lo que quería. 🚀
|
||||
|
||||
**Acabas de unirte a un nuevo equipo. El código tiene 200,000 líneas. ¿Por dónde empiezas?**
|
||||
|
||||
Understand Anything es un [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference) que analiza tu proyecto con un pipeline multi-agente, construye un grafo de conocimiento de cada archivo, función, clase y dependencia, y luego te ofrece un panel interactivo para explorarlo visualmente. Deja de leer código a ciegas. Empieza a ver el panorama completo.
|
||||
|
||||
> **Grafos que enseñan > grafos que solo presumen.**
|
||||
> **El objetivo no es un grafo que te impresione mostrándote lo complejo que es tu código — es un grafo que, sin alardes, te enseña cómo encaja cada pieza.**
|
||||
|
||||
---
|
||||
|
||||
@@ -64,18 +51,10 @@ Understand Anything es un [Claude Code Plugin](https://code.claude.com/docs/en/p
|
||||
|
||||
Navega tu código como un grafo de conocimiento interactivo: cada archivo, función y clase es un nodo que puedes hacer clic, buscar y explorar. Selecciona cualquier nodo para ver resúmenes en lenguaje natural, relaciones y recorridos guiados.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-structural.gif" alt="Grafo estructural — explora archivos, funciones, clases y sus relaciones" width="750" />
|
||||
</p>
|
||||
|
||||
### Comprende la lógica de negocio
|
||||
|
||||
Cambia a la vista de dominio y observa cómo tu código se mapea a procesos de negocio reales: dominios, flujos y pasos representados como un grafo horizontal.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-domain.gif" alt="Grafo de dominio — dominios de negocio, flujos y pasos de proceso" width="750" />
|
||||
</p>
|
||||
|
||||
### Analiza bases de conocimiento
|
||||
|
||||
Apunta `/understand-knowledge` a un [wiki LLM con patrón Karpathy](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) y obtén un grafo de conocimiento dirigido por fuerzas con agrupación por comunidad. El parser determinístico extrae wikilinks y categorías de `index.md`, luego los agentes LLM descubren relaciones implícitas, extraen entidades y revelan afirmaciones, convirtiendo tu wiki en un grafo navegable de ideas interconectadas.
|
||||
@@ -132,6 +111,20 @@ Apunta `/understand-knowledge` a un [wiki LLM con patrón Karpathy](https://gist
|
||||
|
||||
Un pipeline multi-agente escanea tu proyecto, extrae cada archivo, función, clase y dependencia, y construye un grafo de conocimiento guardado en `.understand-anything/knowledge-graph.json`.
|
||||
|
||||
**Salida localizada:** Usa `--language` para generar contenido en tu idioma preferido:
|
||||
|
||||
```bash
|
||||
# Genera contenido en el idioma preferido (descripciones de nodos y UI del dashboard)
|
||||
/understand --language en
|
||||
|
||||
# Idiomas soportados: en (default), zh, zh-TW, ja, ko, ru
|
||||
```
|
||||
|
||||
El parámetro `--language` afecta:
|
||||
- Resúmenes y descripciones de nodos en el grafo de conocimiento
|
||||
- Etiquetas, botones y tooltips de la UI del dashboard
|
||||
- Explicaciones de los tours guiados
|
||||
|
||||
### 3. Explora el panel
|
||||
|
||||
```bash
|
||||
@@ -175,7 +168,7 @@ Understand-Anything funciona en múltiples plataformas de codificación con IA.
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### Instalación de una línea (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / VS Code Copilot)
|
||||
### Instalación de una línea (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -191,7 +184,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
El instalador clona el repositorio en `~/.understand-anything/repo` y crea los enlaces simbólicos correspondientes para la plataforma elegida. Reinicia tu CLI/IDE al terminar.
|
||||
|
||||
- Valores soportados de `<platform>`: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vscode`
|
||||
- Valores soportados de `<platform>`: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
|
||||
- Actualizar más adelante: `./install.sh --update`
|
||||
- Desinstalar: `./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -225,6 +218,10 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Antigravity | ✅ Soportado | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ Soportado | `install.sh gemini` |
|
||||
| Pi Agent | ✅ Soportado | `install.sh pi` |
|
||||
| Vibe CLI | ✅ Soportado | `install.sh vibe` |
|
||||
| Hermes | ✅ Soportado | `install.sh hermes` |
|
||||
| Cline | ✅ Soportado | `install.sh cline` |
|
||||
| KIMI CLI | ✅ Soportado | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -300,6 +297,10 @@ Para cambios importantes, abre primero un issue para que podamos discutir el enf
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>Gracias a todas las personas que lo han usado y han contribuido — saber que les ahorra tiempo es lo que hizo que valiera la pena construirlo.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Licencia MIT © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
+26
-25
@@ -7,17 +7,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -45,14 +35,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **コミュニティの皆さんに感謝!** Understand-Anythingへのサポートは本当に素晴らしいものです。このツールが複雑なコードを理解する時間を少しでも短縮できたなら、それが私の望みです。🚀
|
||||
|
||||
**新しいチームに参加したばかり。コードベースは20万行。どこから手をつければいいのか?**
|
||||
|
||||
Understand Anything は [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference) です。マルチエージェントパイプラインでプロジェクトを分析し、すべてのファイル・関数・クラス・依存関係のナレッジグラフを構築して、インタラクティブなダッシュボードで視覚的に探索できるようにします。コードを闇雲に読むのはやめて、全体像を把握しましょう。
|
||||
|
||||
> **寓教於楽のグラフ > 見栄えだけのグラフ。**
|
||||
> **目指すのは、コードベースの複雑さで圧倒するグラフではなく、すべてのパーツがどう噛み合っているかを静かに教えてくれるグラフ。**
|
||||
|
||||
---
|
||||
|
||||
@@ -65,18 +52,10 @@ Understand Anything は [Claude Code Plugin](https://code.claude.com/docs/en/plu
|
||||
|
||||
コードベースをインタラクティブなナレッジグラフとして表示——すべてのファイル、関数、クラスがクリック・検索・探索可能なノードです。ノードを選択すると、わかりやすい要約、依存関係、ガイド付きツアーが表示されます。
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-structural.gif" alt="構造グラフ——ファイル、関数、クラスとその関係を探索" width="750" />
|
||||
</p>
|
||||
|
||||
### ビジネスロジックを理解
|
||||
|
||||
ドメインビューに切り替えると、コードが実際のビジネスプロセスにどう対応するかが一目でわかります——ドメイン、フロー、ステップが横方向のグラフとして表示されます。
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-domain.gif" alt="ドメイングラフ——ビジネスドメイン、フロー、プロセスステップ" width="750" />
|
||||
</p>
|
||||
|
||||
### ナレッジベースを分析
|
||||
|
||||
`/understand-knowledge` を [Karpathy パターンの LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) に向けると、コミュニティクラスタリング付きのフォースディレクテッドナレッジグラフが生成されます。決定論的パーサーが `index.md` から wikilinks とカテゴリを抽出し、LLM エージェントが暗黙の関係を発見、エンティティを抽出、主張を浮き彫りにして、wiki をナビゲート可能な相互接続されたアイデアのグラフに変換します。
|
||||
@@ -133,6 +112,20 @@ Understand Anything は [Claude Code Plugin](https://code.claude.com/docs/en/plu
|
||||
|
||||
マルチエージェントパイプラインがプロジェクトをスキャンし、すべてのファイル・関数・クラス・依存関係を抽出して、`.understand-anything/knowledge-graph.json` にナレッジグラフを保存します。
|
||||
|
||||
**ローカライズされた出力:** `--language` を使用して、希望の言語でコンテンツを生成:
|
||||
|
||||
```bash
|
||||
# 日本語でコンテンツを生成(ナレッジグラフのノード説明とダッシュボードUI)
|
||||
/understand --language ja
|
||||
|
||||
# サポート言語:en(デフォルト)、zh、zh-TW、ja、ko、ru
|
||||
```
|
||||
|
||||
`--language` パラメータは以下に影響します:
|
||||
- ナレッジグラフのノードサマリーと説明
|
||||
- ダッシュボードUIのラベル、ボタン、ツールチップ
|
||||
- ガイド付きツアーの説明
|
||||
|
||||
### 3. ダッシュボードで探索
|
||||
|
||||
```bash
|
||||
@@ -176,7 +169,7 @@ Understand-Anythingは複数のAIコーディングプラットフォームで
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### ワンラインインストール(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / VS Code Copilot)
|
||||
### ワンラインインストール(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -192,7 +185,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
インストーラーはリポジトリを `~/.understand-anything/repo` にクローンし、選択したプラットフォーム用のシンボリックリンクを作成します。完了後はCLI/IDEを再起動してください。
|
||||
|
||||
- サポートされる `<platform>` 値:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vscode`
|
||||
- サポートされる `<platform>` 値:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`
|
||||
- 後で更新:`./install.sh --update`
|
||||
- アンインストール:`./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -226,6 +219,10 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Antigravity | ✅ サポート | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ サポート | `install.sh gemini` |
|
||||
| Pi Agent | ✅ サポート | `install.sh pi` |
|
||||
| Vibe CLI | ✅ サポート | `install.sh vibe` |
|
||||
| Hermes | ✅ サポート | `install.sh hermes` |
|
||||
| Cline | ✅ サポート | `install.sh cline` |
|
||||
| KIMI CLI | ✅ サポート | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -301,6 +298,10 @@ git add .gitattributes .understand-anything/
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>使ってくれた、貢献してくれたすべての方へ ── 少しでも時間を節約できていると知ること、それがこれを作って良かったと思える理由です。</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MIT License © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
+26
-25
@@ -6,17 +6,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -44,14 +34,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **커뮤니티의 엄청난 응원에 감사드립니다!** Understand-Anything에 대한 관심이 정말 놀라웠습니다. 이 도구가 복잡한 코드를 파악하는 시간을 단 몇 분이라도 줄여드린다면, 그것만으로도 충분합니다. 🚀
|
||||
|
||||
**새 팀에 합류했습니다. 코드베이스가 20만 줄입니다. 어디서부터 시작하시겠습니까?**
|
||||
|
||||
Understand Anything은 [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference)으로, 멀티 에이전트 파이프라인을 통해 프로젝트를 분석하고, 모든 파일, 함수, 클래스, 의존성에 대한 지식 그래프를 구축한 뒤, 이를 시각적으로 탐색할 수 있는 인터랙티브 대시보드를 제공합니다. 더 이상 코드를 맹목적으로 읽지 마세요. 전체 그림을 파악하세요.
|
||||
|
||||
> **배움을 주는 그래프 > 과시만 하는 그래프.**
|
||||
> **목표는 코드베이스의 복잡함으로 감탄을 자아내는 그래프가 아니라, 각 조각이 어떻게 맞물리는지 조용히 가르쳐주는 그래프입니다.**
|
||||
|
||||
---
|
||||
|
||||
@@ -64,18 +51,10 @@ Understand Anything은 [Claude Code Plugin](https://code.claude.com/docs/en/plug
|
||||
|
||||
코드베이스를 인터랙티브 지식 그래프로 탐색하세요. 모든 파일, 함수, 클래스가 클릭, 검색, 탐색 가능한 노드입니다. 노드를 선택하면 이해하기 쉬운 요약, 관계, 가이드 투어를 확인할 수 있습니다.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-structural.gif" alt="구조 그래프 — 파일, 함수, 클래스 및 관계 탐색" width="750" />
|
||||
</p>
|
||||
|
||||
### 비즈니스 로직 이해
|
||||
|
||||
도메인 뷰로 전환하면 코드가 실제 비즈니스 프로세스에 어떻게 매핑되는지 확인할 수 있습니다. 도메인, 흐름, 단계가 수평 그래프로 표시됩니다.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-domain.gif" alt="도메인 그래프 — 비즈니스 도메인, 흐름 및 프로세스 단계" width="750" />
|
||||
</p>
|
||||
|
||||
### 지식 베이스 분석
|
||||
|
||||
`/understand-knowledge`를 [Karpathy 패턴 LLM 위키](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)에 연결하면 커뮤니티 클러스터링이 적용된 힘 기반 지식 그래프를 생성합니다. 결정론적 파서가 `index.md`에서 위키링크와 카테고리를 추출한 후, LLM 에이전트가 암묵적 관계를 발견하고, 엔티티를 추출하며, 주장을 도출하여 위키를 탐색 가능한 상호 연결된 아이디어 그래프로 변환합니다.
|
||||
@@ -132,6 +111,20 @@ Understand Anything은 [Claude Code Plugin](https://code.claude.com/docs/en/plug
|
||||
|
||||
멀티 에이전트 파이프라인이 프로젝트를 스캔하고, 모든 파일, 함수, 클래스, 의존성을 추출한 뒤, `.understand-anything/knowledge-graph.json`에 지식 그래프를 저장합니다.
|
||||
|
||||
**로컬라이즈된 출력:** `--language`를 사용하여 원하는 언어로 내용을 생성:
|
||||
|
||||
```bash
|
||||
# 한국어로 내용 생성 (지식 그래프 노드 설명과 대시보드 UI)
|
||||
/understand --language ko
|
||||
|
||||
# 지원 언어: en(기본값), zh, zh-TW, ja, ko, ru
|
||||
```
|
||||
|
||||
`--language` 매개변수는 다음에 영향합니다:
|
||||
- 지식 그래프의 노드 요약과 설명
|
||||
- 대시보드 UI의 레이블, 버튼, 툴팁
|
||||
- 가이드 투어의 설명
|
||||
|
||||
### 3. 대시보드 탐색
|
||||
|
||||
```bash
|
||||
@@ -175,7 +168,7 @@ Understand-Anything은 다양한 AI 코딩 플랫폼에서 사용할 수 있습
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### 한 줄 설치 (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / VS Code Copilot)
|
||||
### 한 줄 설치 (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -191,7 +184,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
설치 스크립트는 저장소를 `~/.understand-anything/repo`에 클론하고 선택한 플랫폼에 맞는 심볼릭 링크를 생성합니다. 설치 후 CLI 또는 IDE를 재시작하세요.
|
||||
|
||||
- 지원되는 `<platform>` 값: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vscode`
|
||||
- 지원되는 `<platform>` 값: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
|
||||
- 이후 업데이트: `./install.sh --update`
|
||||
- 제거: `./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -225,6 +218,10 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Antigravity | ✅ 지원 | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ 지원 | `install.sh gemini` |
|
||||
| Pi Agent | ✅ 지원 | `install.sh pi` |
|
||||
| Vibe CLI | ✅ 지원 | `install.sh vibe` |
|
||||
| Hermes | ✅ 지원 | `install.sh hermes` |
|
||||
| Cline | ✅ 지원 | `install.sh cline` |
|
||||
| KIMI CLI | ✅ 지원 | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -300,6 +297,10 @@ git add .gitattributes .understand-anything/
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>이 도구를 사용해 주시고 기여해 주신 모든 분들께 감사드립니다 — 누군가의 시간을 아껴드리고 있다는 사실이, 이걸 만들 가치가 있게 만든 이유였습니다.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MIT 라이선스 © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<h1 align="center">Understand Anything</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Превращай любую кодовую базу, базу знаний или документацию в интерактивный граф знаний, который можно исследовать, искать в нём и задавать вопросы.</strong>
|
||||
<br />
|
||||
<em>Работает с Claude Code, Codex, Cursor, Copilot, Gemini CLI и другими.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#-быстрый-старт"><img src="https://img.shields.io/badge/Быстрый_старт-blue" alt="Quick Start" /></a>
|
||||
<a href="https://github.com/Lum1104/Understand-Anything/blob/main/LICENSE"><img src="https://img.shields.io/badge/Лицензия-MIT-yellow" alt="License: MIT" /></a>
|
||||
<a href="https://docs.anthropic.com/en/docs/claude-code"><img src="https://img.shields.io/badge/Claude_Code-8A2BE2" alt="Claude Code" /></a>
|
||||
<a href="#codex"><img src="https://img.shields.io/badge/Codex-000000" alt="Codex" /></a>
|
||||
<a href="#vs-code--github-copilot"><img src="https://img.shields.io/badge/Copilot-24292e" alt="Copilot" /></a>
|
||||
<a href="#copilot-cli"><img src="https://img.shields.io/badge/Copilot_CLI-24292e" alt="Copilot CLI" /></a>
|
||||
<a href="#gemini-cli"><img src="https://img.shields.io/badge/Gemini_CLI-4285F4" alt="Gemini CLI" /></a>
|
||||
<a href="#opencode"><img src="https://img.shields.io/badge/OpenCode-38bdf8" alt="OpenCode" /></a>
|
||||
<a href="https://understand-anything.com"><img src="https://img.shields.io/badge/Сайт-d4a574" alt="Homepage" /></a>
|
||||
<a href="https://understand-anything.com/demo/"><img src="https://img.shields.io/badge/Демо-00c853" alt="Live Demo" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/hero.png" alt="Understand Anything — Превратите любую кодовую базу в интерактивный граф знаний" width="800" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>💬 <a href="https://discord.gg/pydat66RY">Присоединяйтесь к сообществу в Discord →</a></strong>
|
||||
<br />
|
||||
<em>Задавайте вопросы, делитесь тем, что вы построили, получайте помощь от сообщества.</em>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
**Вы только что присоединились к новой команде. Кодовая база — 200 000 строк. С чего вообще начинать?**
|
||||
|
||||
Understand Anything — это [плагин для Claude Code](https://code.claude.com/docs/en/plugins-reference#plugins-reference), который анализирует ваш проект с помощью мультиагентного пайплайна, строит граф знаний из всех файлов, функций, классов и зависимостей, а затем предоставляет интерактивную панель, чтобы исследовать всё это визуально. Хватит читать код вслепую. Пора увидеть общую картину.
|
||||
|
||||
> **Цель — не граф, который поражает сложностью вашей кодовой базы, а граф, который ненавязчиво объясняет, как все части складываются вместе.**
|
||||
|
||||
---
|
||||
|
||||
## ✨ Возможности
|
||||
|
||||
> [!NOTE]
|
||||
> **Хотите пропустить чтение?** Попробуйте [живое демо](https://understand-anything.com/demo/) на нашем [сайте](https://understand-anything.com/) — полностью интерактивная панель, по которой можно перемещаться, масштабировать, искать и исследовать прямо в браузере.
|
||||
|
||||
### Исследуйте структурный граф
|
||||
|
||||
Перемещайтесь по своему коду как по интерактивному графу знаний — каждый файл, функция и класс является узлом, который можно кликнуть, найти и изучить. Выберите любой узел, чтобы увидеть понятные описания, связи и пошаговые обзоры.
|
||||
|
||||
### Понимайте бизнес-логику
|
||||
|
||||
Переключитесь на доменное представление и увидите, как ваш код отображается на реальные бизнес-процессы — домены, потоки и шаги, выстроенные в виде горизонтального графа.
|
||||
|
||||
### Анализируйте базы знаний
|
||||
|
||||
Направьте `/understand-knowledge` на [LLM-вики в стиле Карпати](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) и получите force-directed граф знаний с кластеризацией по сообществам. Детерминированный парсер извлекает wikilinks и категории из `index.md`, а LLM-агенты находят неявные связи, извлекают сущности и выявляют утверждения — превращая вашу вики в навигируемый граф взаимосвязанных идей.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🧭 Пошаговые обзоры</h3>
|
||||
<p>Автоматически создаваемые экскурсии по архитектуре, упорядоченные по зависимостям. Изучайте кодовую базу в правильном порядке.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🔍 Нечёткий и семантический поиск</h3>
|
||||
<p>Находите что угодно по имени или по смыслу. Поищите «какие части отвечают за авторизацию?» и получите релевантные результаты по всему графу.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>📊 Анализ влияния изменений</h3>
|
||||
<p>Смотрите, какие части системы затрагивают ваши изменения, ещё до коммита. Понимайте каскадные эффекты по всей кодовой базе.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🎭 UI, адаптирующийся к роли</h3>
|
||||
<p>Панель подстраивает уровень детализации под пользователя — junior-разработчика, PM или продвинутого пользователя.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🏗️ Визуализация слоёв</h3>
|
||||
<p>Автоматическая группировка по архитектурным слоям — API, Service, Data, UI, Utility — с цветовой легендой.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>📚 Концепции языка</h3>
|
||||
<p>12 шаблонов программирования (дженерики, замыкания, декораторы и т.д.) объясняются в контексте там, где они встречаются.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 1. Установите плагин
|
||||
|
||||
```bash
|
||||
/plugin marketplace add Lum1104/Understand-Anything
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### 2. Проанализируйте кодовую базу
|
||||
|
||||
```bash
|
||||
/understand
|
||||
```
|
||||
|
||||
Мультиагентный пайплайн сканирует ваш проект, извлекает каждый файл, функцию, класс и зависимость, а затем строит граф знаний и сохраняет его в `.understand-anything/knowledge-graph.json`.
|
||||
|
||||
**Локализованный вывод:** используйте `--language`, чтобы генерировать контент на нужном языке:
|
||||
|
||||
```bash
|
||||
# Генерация контента на русском (описания узлов графа знаний и UI панели)
|
||||
/understand --language ru
|
||||
|
||||
# Поддерживаемые языки: en (по умолчанию), zh, zh-TW, ja, ko, ru
|
||||
```
|
||||
|
||||
Параметр `--language` влияет на:
|
||||
- Резюме и описания узлов в графе знаний
|
||||
- Подписи, кнопки и подсказки UI панели
|
||||
- Объяснения в пошаговых обзорах
|
||||
|
||||
### 3. Откройте панель
|
||||
|
||||
```bash
|
||||
/understand-dashboard
|
||||
```
|
||||
|
||||
Открывается интерактивная веб-панель с визуализацией вашей кодовой базы в виде графа — с цветовой кодировкой по архитектурным слоям, поиском и кликабельными узлами. Выберите любой узел, чтобы увидеть его код, связи и описание простым языком.
|
||||
|
||||
### 4. Продолжайте учиться
|
||||
|
||||
```bash
|
||||
# Задайте любой вопрос о кодовой базе
|
||||
/understand-chat How does the payment flow work?
|
||||
|
||||
# Проанализируйте влияние ваших текущих изменений
|
||||
/understand-diff
|
||||
|
||||
# Подробно разберитесь с конкретным файлом или функцией
|
||||
/understand-explain src/auth/login.ts
|
||||
|
||||
# Сгенерируйте онбординг-гайд для новых членов команды
|
||||
/understand-onboard
|
||||
|
||||
# Извлеките знания о бизнес-доменах (домены, потоки, шаги)
|
||||
/understand-domain
|
||||
|
||||
# Проанализируйте LLM-вики в стиле Карпати
|
||||
/understand-knowledge ~/path/to/wiki
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Установка на разных платформах
|
||||
|
||||
Understand-Anything работает с несколькими платформами AI-разработки.
|
||||
|
||||
### Claude Code (нативно)
|
||||
|
||||
```bash
|
||||
/plugin marketplace add Lum1104/Understand-Anything
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### Установка одной командой (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash
|
||||
# или передайте платформу, чтобы пропустить интерактивный выбор:
|
||||
curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash -s codex
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
Установщик клонирует репозиторий в `~/.understand-anything/repo` и создаёт нужные симлинки для выбранной платформы. После установки перезапустите свой CLI/IDE.
|
||||
|
||||
- Поддерживаемые значения `<platform>`: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
|
||||
- Обновление: `./install.sh --update`
|
||||
- Удаление: `./install.sh --uninstall <platform>`
|
||||
|
||||
### Cursor
|
||||
|
||||
Cursor автоматически обнаруживает плагин через `.cursor-plugin/plugin.json` при клонировании этого репозитория. Ручная установка не требуется — просто склонируйте и откройте в Cursor.
|
||||
|
||||
### VS Code + GitHub Copilot
|
||||
|
||||
VS Code с GitHub Copilot (v1.108+) автоматически обнаруживает плагин через `.copilot-plugin/plugin.json` при клонировании этого репозитория. Ручная установка не требуется — просто склонируйте и откройте в VS Code.
|
||||
|
||||
Для персональных skills (доступных во всех проектах) запустите `install.sh` выше с платформой `vscode`.
|
||||
|
||||
### Copilot CLI
|
||||
|
||||
```bash
|
||||
copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
```
|
||||
|
||||
### Совместимость с платформами
|
||||
|
||||
| Платформа | Статус | Способ установки |
|
||||
|----------|--------|----------------|
|
||||
| Claude Code | ✅ Нативно | Marketplace плагинов |
|
||||
| Cursor | ✅ Поддерживается | Автообнаружение |
|
||||
| VS Code + GitHub Copilot | ✅ Поддерживается | Автообнаружение |
|
||||
| Copilot CLI | ✅ Поддерживается | Установка плагина |
|
||||
| Codex | ✅ Поддерживается | `install.sh codex` |
|
||||
| OpenCode | ✅ Поддерживается | `install.sh opencode` |
|
||||
| OpenClaw | ✅ Поддерживается | `install.sh openclaw` |
|
||||
| Antigravity | ✅ Поддерживается | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ Поддерживается | `install.sh gemini` |
|
||||
| Pi Agent | ✅ Поддерживается | `install.sh pi` |
|
||||
| Vibe CLI | ✅ Поддерживается | `install.sh vibe` |
|
||||
| Hermes | ✅ Поддерживается | `install.sh hermes` |
|
||||
| Cline | ✅ Поддерживается | `install.sh cline` |
|
||||
| KIMI CLI | ✅ Поддерживается | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Поделитесь графом с командой
|
||||
|
||||
Граф — это просто JSON. **Зафиксируйте его один раз, и коллеги смогут пропустить весь пайплайн.** Полезно для онбординга, ревью PR и подхода docs-as-code.
|
||||
|
||||
> **Пример:** [GoogleCloudPlatform/microservices-demo (форк)](https://github.com/Lum1104/microservices-demo) — мультиязыковой проект (Go / Java / Python / Node) с уже зафиксированным графом.
|
||||
|
||||
**Что коммитить:** всё содержимое `.understand-anything/`, *кроме* `intermediate/` и `diff-overlay.json` (это локальные временные файлы).
|
||||
|
||||
```gitignore
|
||||
.understand-anything/intermediate/
|
||||
.understand-anything/diff-overlay.json
|
||||
```
|
||||
|
||||
**Держите граф в актуальном состоянии:** включите `/understand --auto-update` — post-commit хук будет инкрементально обновлять граф, так что каждый коммит сопровождается соответствующим графом. Либо запускайте `/understand` вручную перед релизами.
|
||||
|
||||
**Большие графы (10 МБ+):** храните через **git-lfs**.
|
||||
|
||||
```bash
|
||||
git lfs install
|
||||
git lfs track ".understand-anything/*.json"
|
||||
git add .gitattributes .understand-anything/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Под капотом
|
||||
|
||||
### Мультиагентный пайплайн
|
||||
|
||||
Команда `/understand` оркестрирует 5 специализированных агентов, а `/understand-domain` добавляет шестого:
|
||||
|
||||
| Агент | Роль |
|
||||
|-------|------|
|
||||
| `project-scanner` | Обнаружение файлов, определение языков и фреймворков |
|
||||
| `file-analyzer` | Извлечение функций, классов, импортов; создание узлов и рёбер графа |
|
||||
| `architecture-analyzer` | Определение архитектурных слоёв |
|
||||
| `tour-builder` | Генерация пошаговых обучающих обзоров |
|
||||
| `graph-reviewer` | Проверка полноты и целостности ссылок графа (по умолчанию выполняется inline; используйте `--review` для полного ревью с участием LLM) |
|
||||
| `domain-analyzer` | Извлечение бизнес-доменов, потоков и шагов процессов (используется командой `/understand-domain`) |
|
||||
| `article-analyzer` | Извлечение сущностей, утверждений и неявных связей из статей вики (используется командой `/understand-knowledge`) |
|
||||
|
||||
Анализаторы файлов работают параллельно (до 5 одновременно, 20–30 файлов на батч). Поддерживаются инкрементальные обновления — повторно анализируются только файлы, изменившиеся с прошлого запуска.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Вклад в проект
|
||||
|
||||
Будем рады вашим контрибьюшенам! Как начать:
|
||||
|
||||
1. Сделайте форк репозитория
|
||||
2. Создайте ветку для фичи (`git checkout -b feature/my-feature`)
|
||||
3. Запустите тесты (`pnpm --filter @understand-anything/core test`)
|
||||
4. Закоммитьте изменения и откройте pull request
|
||||
|
||||
Для крупных изменений сначала откройте issue, чтобы можно было обсудить подход.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Хватит читать код вслепую. Начните понимать всё.</strong>
|
||||
</p>
|
||||
|
||||
## История звёзд
|
||||
|
||||
<a href="https://www.star-history.com/?repos=Lum1104%2FUnderstand-Anything&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>Спасибо всем, кто пользовался проектом и вкладывался в него — знание того, что это экономит людям время, и было главной причиной, ради которой стоило его делать.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Лицензия MIT © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
+26
-25
@@ -7,17 +7,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -45,14 +35,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **Topluluğa çok teşekkürler!** Understand-Anything'e gösterilen destek inanılmaz oldu. Bu araç sana karmaşıklığı anlamak için birkaç dakika kazandırıyorsa, istediğim tek şey buydu. 🚀
|
||||
|
||||
**Yeni bir ekibe katıldın. Kod tabanı 200.000 satır kod. Nereden başlayacaksın bile bilemiyorsun?**
|
||||
|
||||
Understand Anything, projenizi çok-ajan hattıyla analiz eden, her dosya, fonksiyon, sınıf ve bağımlılığın bilgi grafiğini oluşturan ve hepsini görsel olarak keşfetmen için interaktif bir kontrol paneli sunan bir [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference)'dir. Kodu körü körüne okumayı bırak. Büyük resmi görmeye başla.
|
||||
|
||||
> **Öğreten grafikler > sadece gösteriş yapan grafikler.**
|
||||
> **Amaç, kod tabanının ne kadar karmaşık olduğunu görkemle gösteren bir grafik değil — her parçanın nasıl birbirine geçtiğini sessizce öğreten bir grafik.**
|
||||
|
||||
---
|
||||
|
||||
@@ -65,18 +52,10 @@ Understand Anything, projenizi çok-ajan hattıyla analiz eden, her dosya, fonks
|
||||
|
||||
Kod tabanınızı interaktif bir bilgi grafiği olarak görüntüleyin — her dosya, fonksiyon ve sınıf tıklanabilir, aranabilir ve keşfedilebilir bir düğümdür. Herhangi bir düğümü seçerek anlaşılır özetleri, bağımlılıkları ve rehberli turları görün.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-structural.gif" alt="Yapısal grafik — dosyaları, fonksiyonları, sınıfları ve ilişkilerini keşfedin" width="750" />
|
||||
</p>
|
||||
|
||||
### İş mantığını anlayın
|
||||
|
||||
Alan görünümüne geçin ve kodunuzun gerçek iş süreçleriyle nasıl eşleştiğini görün — alanlar, akışlar ve adımlar yatay bir grafik olarak sunulur.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-domain.gif" alt="Alan grafiği — iş alanları, akışlar ve süreç adımları" width="750" />
|
||||
</p>
|
||||
|
||||
### Bilgi tabanlarını analiz et
|
||||
|
||||
`/understand-knowledge` komutunu bir [Karpathy deseni LLM Wiki'sine](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) yönlendirin ve topluluk kümeleme ile kuvvet yönelimli bir bilgi grafiği elde edin. Deterministik ayrıştırıcı `index.md`'den wikilinkleri ve kategorileri çıkarır, ardından LLM ajanları örtük ilişkileri keşfeder, varlıkları çıkarır ve iddiaları ortaya çıkarır — wiki'nizi gezinilebilir, birbirine bağlı fikirler grafiğine dönüştürür.
|
||||
@@ -133,6 +112,20 @@ Alan görünümüne geçin ve kodunuzun gerçek iş süreçleriyle nasıl eşle
|
||||
|
||||
Çok-ajan hattı projenizi tarar, her dosya, fonksiyon, sınıf ve bağımlılığı çıkarır, ardından `.understand-anything/knowledge-graph.json` dosyasına kaydedilen bir bilgi grafiği oluşturur.
|
||||
|
||||
**Yerelleştirilmiş çıktı:** İstediğiniz dilde içerik oluşturmak için `--language` kullanın:
|
||||
|
||||
```bash
|
||||
# İstediğiniz dilde içerik oluştur (düğüm açıklamaları ve dashboard UI)
|
||||
/understand --language en
|
||||
|
||||
# Desteklenen diller: en (varsayılan), zh, zh-TW, ja, ko, ru
|
||||
```
|
||||
|
||||
`--language` parametresi şunları etkiler:
|
||||
- Bilgi grafiğindeki düğüm özetleri ve açıklamalar
|
||||
- Dashboard UI etiketleri, butonlar ve araç ipuçları
|
||||
- Rehberli tur açıklamaları
|
||||
|
||||
### 3. Kontrol panelini keşfet
|
||||
|
||||
```bash
|
||||
@@ -176,7 +169,7 @@ Understand-Anything birden fazla AI kodlama platformunda çalışır.
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### Tek satırlık kurulum (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / VS Code Copilot)
|
||||
### Tek satırlık kurulum (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -192,7 +185,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
Kurulum betiği depoyu `~/.understand-anything/repo` dizinine klonlar ve seçilen platform için uygun sembolik bağlantıları oluşturur. Sonrasında CLI/IDE'ni yeniden başlat.
|
||||
|
||||
- Desteklenen `<platform>` değerleri: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vscode`
|
||||
- Desteklenen `<platform>` değerleri: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
|
||||
- Daha sonra güncelle: `./install.sh --update`
|
||||
- Kaldır: `./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -226,6 +219,10 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Antigravity | ✅ Destekleniyor | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ Destekleniyor | `install.sh gemini` |
|
||||
| Pi Agent | ✅ Destekleniyor | `install.sh pi` |
|
||||
| Vibe CLI | ✅ Destekleniyor | `install.sh vibe` |
|
||||
| Hermes | ✅ Destekleniyor | `install.sh hermes` |
|
||||
| Cline | ✅ Destekleniyor | `install.sh cline` |
|
||||
| KIMI CLI | ✅ Destekleniyor | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -301,6 +298,10 @@ Büyük değişiklikler için lütfen önce bir issue aç ki yaklaşımı tartı
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>Kullanan ve katkıda bulunan herkese teşekkürler — bunun insanlara zaman kazandırdığını bilmek, yapmaya değer kılan tek şeydi.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MIT Lisansı © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
+26
-25
@@ -6,17 +6,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -44,14 +34,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **衷心感谢社区的支持!** Understand-Anything 收到的关注超出了我的预期。如果这个工具能帮你从复杂的资料中理出一点头绪,少走几分钟弯路,那我开发它的初衷就达到了。🚀
|
||||
|
||||
**当你刚加入一个新团队,面对 20 万行代码,你从哪里开始?**
|
||||
|
||||
Understand Anything 是一个 [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference),通过多智能体(multi-agent)架构分析你的项目,构建包含文件、函数、类以及依赖关系的知识图谱,并提供一个可视化交互界面,帮助你理解整个系统。不再”盲读代码”,而是从全局视角理解系统结构。
|
||||
|
||||
> **寓教于乐的图表 > 仅为炫技的图表。**
|
||||
> **目标不是用代码库的复杂程度来惊艳你 —— 而是默默告诉你每一块是怎么拼在一起的。**
|
||||
|
||||
---
|
||||
|
||||
@@ -64,18 +51,10 @@ Understand Anything 是一个 [Claude Code Plugin](https://code.claude.com/docs/
|
||||
|
||||
将你的代码库以交互式知识图谱的形式呈现——每个文件、函数和类都是可点击、可搜索、可探索的节点。选择任意节点即可查看通俗易懂的摘要、依赖关系和引导式学习路径。
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-structural.gif" alt="结构图——探索文件、函数、类及其关系" width="750" />
|
||||
</p>
|
||||
|
||||
### 理解业务逻辑
|
||||
|
||||
切换到领域视图,查看代码如何映射到真实的业务流程——以水平图的形式展示领域、流程和步骤。
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-domain.gif" alt="领域图——业务领域、流程和处理步骤" width="750" />
|
||||
</p>
|
||||
|
||||
### 分析知识库
|
||||
|
||||
将 `/understand-knowledge` 指向一个 [Karpathy 模式的 LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f),即可获得带有社区聚类的力导向知识图谱。确定性解析器从 `index.md` 中提取 wikilinks 和分类,然后 LLM 代理发现隐式关系、提取实体并挖掘论断——将你的 wiki 转化为可导航的互联思想图谱。
|
||||
@@ -132,6 +111,20 @@ Understand Anything 是一个 [Claude Code Plugin](https://code.claude.com/docs/
|
||||
|
||||
多智能体(multi-agent)架构会:扫描你的项目,提取函数 / 类 / 依赖,构建知识图谱保存至`.understand-anything/knowledge-graph.json`.
|
||||
|
||||
**本地化输出:** 使用 `--language` 参数生成中文内容:
|
||||
|
||||
```bash
|
||||
# 生成中文内容(知识图节点描述和 Dashboard UI)
|
||||
/understand --language zh
|
||||
|
||||
# 支持的语言:en(默认)、zh、zh-TW、ja、ko、ru
|
||||
```
|
||||
|
||||
`--language` 参数会影响:
|
||||
- 知识图谱中的节点摘要和描述
|
||||
- Dashboard UI 的标签、按钮和提示
|
||||
- 导览路线的解释说明
|
||||
|
||||
### 3. 打开数据看板
|
||||
|
||||
```bash
|
||||
@@ -175,7 +168,7 @@ Understand-Anything 可在多个 AI 编码平台上运行。
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### 一行命令安装(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / VS Code Copilot)
|
||||
### 一行命令安装(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -191,7 +184,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
安装脚本会将仓库克隆到 `~/.understand-anything/repo`,并为所选平台创建相应的符号链接。安装完成后请重启 CLI 或 IDE。
|
||||
|
||||
- 支持的 `<platform>` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vscode`
|
||||
- 支持的 `<platform>` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`
|
||||
- 后续更新:`./install.sh --update`
|
||||
- 卸载:`./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -225,6 +218,10 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Antigravity | ✅ 支持 | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ 支持 | `install.sh gemini` |
|
||||
| Pi Agent | ✅ 支持 | `install.sh pi` |
|
||||
| Vibe CLI | ✅ 支持 | `install.sh vibe` |
|
||||
| Hermes | ✅ 支持 | `install.sh hermes` |
|
||||
| Cline | ✅ 支持 | `install.sh cline` |
|
||||
| KIMI CLI | ✅ 支持 | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -300,6 +297,10 @@ git add .gitattributes .understand-anything/
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>感谢每一位使用过、贡献过的朋友 —— 知道它替你们省下了一些时间,就是当初做它最值得的理由。</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MIT 许可证 © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
+26
-25
@@ -6,17 +6,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/lum1104/understand-anything">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="../README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a> | <a href="README.ru-RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -44,14 +34,11 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **衷心感謝社群的支持!** Understand-Anything 收到的關注超出了我的預期。如果這個工具能幫你從複雜的資料中理出一點頭緒,少走幾分鐘彎路,那我開發它的初衷就達到了。🚀
|
||||
|
||||
**當你剛加入一個新團隊,面對 20 萬行程式碼,你從哪裡開始?**
|
||||
|
||||
Understand Anything 是一個 [Claude Code Plugin](https://code.claude.com/docs/en/plugins-reference#plugins-reference),透過多智能體(multi-agent)架構分析你的專案,建構包含檔案、函式、類別以及相依關係的知識圖譜,並提供一個視覺化互動介面,幫助你理解整個系統。不再「盲讀程式碼」,而是從全局視角理解系統結構。
|
||||
|
||||
> **寓教於樂的圖表 > 僅為炫技的圖表。**
|
||||
> **目標不是用程式碼庫的複雜程度驚豔你 —— 而是默默告訴你每一塊是怎麼拼在一起的。**
|
||||
|
||||
---
|
||||
|
||||
@@ -64,18 +51,10 @@ Understand Anything 是一個 [Claude Code Plugin](https://code.claude.com/docs/
|
||||
|
||||
將你的程式碼庫以互動式知識圖譜呈現——每個檔案、函式和類別都是可點擊、可搜尋、可探索的節點。選取任意節點即可檢視淺顯易懂的摘要、依賴關係和引導式學習路徑。
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-structural.gif" alt="結構圖——探索檔案、函式、類別及其關係" width="750" />
|
||||
</p>
|
||||
|
||||
### 理解業務邏輯
|
||||
|
||||
切換到領域視圖,查看程式碼如何對應到真實的業務流程——以水平圖的形式展示領域、流程和步驟。
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/overview-domain.gif" alt="領域圖——業務領域、流程和處理步驟" width="750" />
|
||||
</p>
|
||||
|
||||
### 分析知識庫
|
||||
|
||||
將 `/understand-knowledge` 指向一個 [Karpathy 模式的 LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f),即可獲得帶有社群聚類的力導向知識圖譜。確定性解析器從 `index.md` 中提取 wikilinks 和分類,然後 LLM 代理發現隱式關係、提取實體並挖掘論斷——將你的 wiki 轉化為可導航的互聯思想圖譜。
|
||||
@@ -132,6 +111,20 @@ Understand Anything 是一個 [Claude Code Plugin](https://code.claude.com/docs/
|
||||
|
||||
多智能體(multi-agent)架構會:掃描你的專案,提取函式 / 類別 / 相依關係,建構知識圖譜並儲存至 `.understand-anything/knowledge-graph.json`。
|
||||
|
||||
**在地化輸出:** 使用 `--language` 參數產生中文內容:
|
||||
|
||||
```bash
|
||||
# 產生繁體中文內容(知識圖節點描述和 Dashboard UI)
|
||||
/understand --language zh-TW
|
||||
|
||||
# 支援的語言:en(預設)、zh、zh-TW、ja、ko、ru
|
||||
```
|
||||
|
||||
`--language` 參數會影響:
|
||||
- 知識圖譜中的節點摘要和描述
|
||||
- Dashboard UI 的標籤、按鈕和提示
|
||||
-導覽路線的解釋說明
|
||||
|
||||
### 3. 開啟資料看板
|
||||
|
||||
```bash
|
||||
@@ -175,7 +168,7 @@ Understand-Anything 可在多個 AI 編碼平台上執行。
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### 一行指令安裝(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / VS Code Copilot)
|
||||
### 一行指令安裝(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
@@ -191,7 +184,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
|
||||
|
||||
安裝指令稿會將儲存庫複製到 `~/.understand-anything/repo`,並為所選平台建立相應的符號連結。安裝完成後請重新啟動 CLI 或 IDE。
|
||||
|
||||
- 支援的 `<platform>` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vscode`
|
||||
- 支援的 `<platform>` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`
|
||||
- 後續更新:`./install.sh --update`
|
||||
- 解除安裝:`./install.sh --uninstall <platform>`
|
||||
|
||||
@@ -225,6 +218,10 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
|
||||
| Antigravity | ✅ 支援 | `install.sh antigravity` |
|
||||
| Gemini CLI | ✅ 支援 | `install.sh gemini` |
|
||||
| Pi Agent | ✅ 支援 | `install.sh pi` |
|
||||
| Vibe CLI | ✅ 支援 | `install.sh vibe` |
|
||||
| Hermes | ✅ 支援 | `install.sh hermes` |
|
||||
| Cline | ✅ 支援 | `install.sh cline` |
|
||||
| KIMI CLI | ✅ 支援 | `install.sh kimi` |
|
||||
|
||||
---
|
||||
|
||||
@@ -300,6 +297,10 @@ git add .gitattributes .understand-anything/
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<em>感謝每一位使用過、貢獻過的朋友 —— 知道它替你們省下了一些時間,就是當初做它最值得的理由。</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MIT 授權條款 © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 MiB |
@@ -51,6 +51,13 @@ const discordUrl = 'https://discord.gg/pydat66RY';
|
||||
<span class="hero-enterprise-arrow">→</span>
|
||||
</a>
|
||||
|
||||
<a href="https://lum.is-a.dev/" target="_blank" rel="noopener noreferrer" class="hero-author anim anim-5">
|
||||
<span class="hero-author-label">Author</span>
|
||||
<span class="hero-author-divider">·</span>
|
||||
<span class="hero-author-handle">Lum1104</span>
|
||||
<span class="hero-author-arrow">→</span>
|
||||
</a>
|
||||
|
||||
<details class="hero-subscribe anim anim-6">
|
||||
<summary class="hero-subscribe-toggle">
|
||||
<span>Subscribe to release updates</span>
|
||||
@@ -333,6 +340,60 @@ const discordUrl = 'https://discord.gg/pydat66RY';
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Author homepage — pill mirroring Enterprise, sits directly under it */
|
||||
.hero-author {
|
||||
margin-top: 0.6rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.45rem 1.1rem;
|
||||
border: 1px solid rgba(212, 165, 116, 0.3);
|
||||
border-radius: 100px;
|
||||
background: rgba(212, 165, 116, 0.04);
|
||||
text-decoration: none;
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.2s ease,
|
||||
background-color 0.25s ease;
|
||||
}
|
||||
|
||||
.hero-author:hover {
|
||||
border-color: rgba(212, 165, 116, 0.55);
|
||||
background: rgba(212, 165, 116, 0.08);
|
||||
box-shadow: 0 0 24px rgba(212, 165, 116, 0.15);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hero-author-label {
|
||||
font-family: var(--font-code);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
.hero-author-divider {
|
||||
color: rgba(212, 165, 116, 0.45);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hero-author-handle {
|
||||
font-family: var(--font-code);
|
||||
font-size: 0.82rem;
|
||||
color: #e8e2d8;
|
||||
}
|
||||
|
||||
.hero-author-arrow {
|
||||
color: #d4a574;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.hero-author:hover .hero-author-arrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Subscribe */
|
||||
.hero-subscribe {
|
||||
margin-top: 2rem;
|
||||
@@ -465,6 +526,14 @@ const discordUrl = 'https://discord.gg/pydat66RY';
|
||||
}
|
||||
.hero-enterprise-divider { display: none; }
|
||||
.hero-enterprise-email { font-size: 0.78rem; }
|
||||
.hero-author {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.6rem 1.25rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.hero-author-divider { display: none; }
|
||||
.hero-author-handle { font-size: 0.78rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
|
||||
@@ -35,6 +35,9 @@ $Platforms = [ordered]@{
|
||||
openclaw = @{ Target = (Join-Path $HOME '.openclaw\skills'); Style = 'folder' }
|
||||
antigravity = @{ Target = (Join-Path $HOME '.gemini\antigravity\skills'); Style = 'folder' }
|
||||
vscode = @{ Target = (Join-Path $HOME '.copilot\skills'); Style = 'per-skill' }
|
||||
hermes = @{ Target = (Join-Path $HOME '.hermes\skills'); Style = 'folder' }
|
||||
cline = @{ Target = (Join-Path $HOME '.cline\skills'); Style = 'folder' }
|
||||
kimi = @{ Target = (Join-Path $HOME '.kimi\skills'); Style = 'folder' }
|
||||
}
|
||||
|
||||
function Show-Usage {
|
||||
|
||||
@@ -36,6 +36,9 @@ openclaw|$HOME/.openclaw/skills|folder
|
||||
antigravity|$HOME/.gemini/antigravity/skills|folder
|
||||
vibe|$HOME/.vibe/skills|per-skill
|
||||
vscode|$HOME/.copilot/skills|per-skill
|
||||
hermes|$HOME/.hermes/skills|folder
|
||||
cline|$HOME/.cline/skills|folder
|
||||
kimi|$HOME/.kimi/skills|folder
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
@@ -14,5 +14,22 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.1.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-c-sharp",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-java",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-php",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "2.6.3",
|
||||
"version": "2.7.3",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,11 @@ You are an expert software architect. Your job is to analyze a codebase's file s
|
||||
|
||||
Given a list of file nodes (with paths, summaries, tags, and node types) and import edges, identify 3-10 logical architecture layers and assign every file node to exactly one layer. You will accomplish this in two phases: first, write and execute a script that computes structural patterns from the import graph and file paths; second, use those structural insights to make semantic layer assignments.
|
||||
|
||||
**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to:
|
||||
- Layer `name` — Translate to the specified language (e.g., "API 层", "服务层", "基础设施层")
|
||||
- Layer `description` — Write in the specified language using natural phrasing
|
||||
Use native-level terminology. Keep established English terms when appropriate (e.g., "CI/CD", "ORM", "REST API" may remain untranslated in some languages).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 -- Structural Analysis Script
|
||||
|
||||
@@ -17,6 +17,12 @@ For each file in the batch provided to you, extract structural data via a script
|
||||
|
||||
**File categories in this batch:** Each file has a `fileCategory` field indicating its type: `code`, `config`, `docs`, `infra`, `data`, `script`, or `markup`. Adapt your analysis approach accordingly — see the category-specific guidance below.
|
||||
|
||||
**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to ALL textual output:
|
||||
- `summary` — Write in the specified language
|
||||
- `tags` — Use localized tags when natural (e.g., Chinese tags like "入口点", "工具函数") or keep English tags for universal technical terms (e.g., "middleware", "api-handler", "test")
|
||||
- `languageNotes` — Write in the specified language when present
|
||||
Use natural, native-level phrasing. Keep technical terms in English when no standard translation exists.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 -- Structural Extraction (Bundled Script)
|
||||
|
||||
@@ -14,6 +14,8 @@ You are a meticulous project inventory specialist. Your job is to scan a codebas
|
||||
|
||||
Scan the project directory provided in the prompt and produce a JSON inventory. You will accomplish this in two phases: first, write and execute a discovery script that performs all deterministic file scanning; second, review the script's results and add a human-readable project description.
|
||||
|
||||
**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to the `description` field you synthesize in Phase 2. Write the description in the specified language using natural, native-level phrasing. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel").
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 -- Discovery Script
|
||||
|
||||
@@ -14,6 +14,12 @@ You are an expert technical educator who designs learning paths through codebase
|
||||
|
||||
Given a codebase's nodes, edges, and layers, design a guided tour that teaches the project's architecture and key concepts. The tour must reference only real node IDs from the provided graph data. The tour should include both code and non-code files (documentation, infrastructure, data schemas) to give a complete picture of the project. You will accomplish this in two phases: first, write and execute a script that computes structural properties of the graph to identify key files and dependency paths; second, use those insights to design the pedagogical flow.
|
||||
|
||||
**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to:
|
||||
- Tour `title` — Write in the specified language (e.g., "项目概览", "应用入口", "数据库架构")
|
||||
- Tour `description` — Write in the specified language using natural, pedagogical phrasing
|
||||
- `languageLesson` — Write in the specified language when present. Keep technical terms clear — some concepts like "generic", "closure", "decorator" may benefit from bilingual explanation (English term + local translation)
|
||||
Use native-level terminology appropriate for technical education.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 -- Graph Topology Script
|
||||
|
||||
@@ -37,6 +37,58 @@ Incrementally update the knowledge graph using deterministic structural fingerpr
|
||||
mkdir -p $PROJECT_ROOT/.understand-anything/intermediate
|
||||
```
|
||||
|
||||
9. **Apply `.understandignore` exclusions** (same semantics as `/understand` Step 2.5 in `agents/project-scanner.md`).
|
||||
|
||||
Without this step, files in user-excluded paths (migrations, vendored code, tests) are counted as structural changes and can spuriously escalate the action to `FULL_UPDATE` even when the real change set is tiny.
|
||||
|
||||
1. If neither `$PROJECT_ROOT/.understand-anything/.understandignore` nor `$PROJECT_ROOT/.understandignore` exists, the step 7 extension filter is sufficient — skip to Phase 1.
|
||||
|
||||
2. Write the step 7 file list to `$PROJECT_ROOT/.understand-anything/intermediate/changed-files-pre.json` as a JSON array of relative paths.
|
||||
|
||||
3. Resolve `$PLUGIN_ROOT`:
|
||||
- Use `$CLAUDE_PLUGIN_ROOT` if set (Claude Code's hook context sets this).
|
||||
- Otherwise try `$HOME/.understand-anything-plugin`.
|
||||
- Validate the chosen candidate by checking `$candidate/packages/core/dist/ignore-filter.js` exists.
|
||||
- If neither resolves: report "Cannot locate plugin install at `$CLAUDE_PLUGIN_ROOT` or `$HOME/.understand-anything-plugin`; auto-update aborted. Run `/understand` to re-baseline." and **STOP**. Do **not** silently skip — silent skip reproduces issue #153.
|
||||
|
||||
4. Write `$PROJECT_ROOT/.understand-anything/intermediate/ignore-filter.mjs`:
|
||||
```javascript
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const PROJECT_ROOT = process.cwd();
|
||||
const PLUGIN_ROOT = process.argv[2];
|
||||
const inputPath = process.argv[3];
|
||||
|
||||
const modUrl = pathToFileURL(
|
||||
path.join(PLUGIN_ROOT, 'packages/core/dist/ignore-filter.js'),
|
||||
).href;
|
||||
const { createIgnoreFilter } = await import(modUrl);
|
||||
const filter = createIgnoreFilter(PROJECT_ROOT);
|
||||
|
||||
const input = JSON.parse(readFileSync(inputPath, 'utf-8'));
|
||||
const kept = input.filter((p) => !filter.isIgnored(p));
|
||||
const removed = input.length - kept.length;
|
||||
|
||||
writeFileSync(
|
||||
path.join(PROJECT_ROOT, '.understand-anything/intermediate/changed-files.json'),
|
||||
JSON.stringify({ kept, removed, total: input.length }, null, 2),
|
||||
);
|
||||
console.log(`.understandignore: kept ${kept.length}/${input.length} (removed ${removed})`);
|
||||
```
|
||||
|
||||
5. Run it:
|
||||
```bash
|
||||
node $PROJECT_ROOT/.understand-anything/intermediate/ignore-filter.mjs \
|
||||
"$PLUGIN_ROOT" \
|
||||
$PROJECT_ROOT/.understand-anything/intermediate/changed-files-pre.json
|
||||
```
|
||||
|
||||
6. Read `$PROJECT_ROOT/.understand-anything/intermediate/changed-files.json`. Pass the `kept` array as the input file list for Phase 1's fingerprint-check script.
|
||||
|
||||
7. If `kept.length === 0`: update `meta.json` with the new commit hash, report "All changed source files are in ignored paths. Metadata updated." and **STOP**.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Structural Fingerprint Check (Zero LLM Tokens)
|
||||
@@ -188,12 +240,54 @@ Perform lightweight validation (no graph-reviewer agent):
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update fingerprints:** Write and execute a Node.js script that:
|
||||
- Reads the existing `fingerprints.json`
|
||||
- For each re-analyzed file: computes new content hash and extracts structural elements via regex
|
||||
- For deleted files: removes their entries
|
||||
- Merges with existing fingerprints (keep unchanged files as-is)
|
||||
- Writes updated `fingerprints.json`
|
||||
3. **Update fingerprints (LOAD-PATCH-SAVE, not OVERWRITE).**
|
||||
|
||||
The most common failure mode here: writing only the freshly-computed batch entries to `fingerprints.json`, discarding every other file's fingerprint. The next auto-update then sees all those files as new (no stored fingerprint), classifies them as STRUCTURAL, and escalates to FULL_UPDATE permanently (issue #152). The script must LOAD ALL existing entries, PATCH only the re-analyzed ones, and SAVE the full dict back.
|
||||
|
||||
Write and execute a Node.js script in this exact ordering:
|
||||
|
||||
```javascript
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
|
||||
const fpPath = path.join(PROJECT_ROOT, '.understand-anything', 'fingerprints.json');
|
||||
const existedAndNonEmpty = existsSync(fpPath) && readFileSync(fpPath, 'utf-8').trim().length > 0;
|
||||
|
||||
// 1. LOAD ALL existing entries (NEVER skip — preserves un-analyzed files)
|
||||
const all = existedAndNonEmpty
|
||||
? JSON.parse(readFileSync(fpPath, 'utf-8'))
|
||||
: {};
|
||||
const before = Object.keys(all).length;
|
||||
|
||||
// 2. PATCH (file still exists) or REMOVE (file deleted) for each re-analyzed path.
|
||||
// `filesToReanalyze` may include paths that were deleted in this commit —
|
||||
// handle both branches inline rather than expecting a separate deleted list.
|
||||
for (const filePath of filesToReanalyze) {
|
||||
const fullPath = path.join(PROJECT_ROOT, filePath);
|
||||
if (!existsSync(fullPath)) {
|
||||
delete all[filePath];
|
||||
continue;
|
||||
}
|
||||
const content = readFileSync(fullPath, 'utf-8');
|
||||
const contentHash = createHash('sha256').update(content).digest('hex');
|
||||
// Extract functions, classes, imports, exports via the same regex as Phase 1.
|
||||
all[filePath] = { contentHash, functions, classes, imports, exports };
|
||||
}
|
||||
|
||||
// 3. GUARD against silent load failure: if fingerprints.json existed and was
|
||||
// non-empty but `before` came out as 0, refuse to overwrite — something
|
||||
// went wrong reading the file and writing now would clobber every entry.
|
||||
if (existedAndNonEmpty && before === 0) {
|
||||
throw new Error('fingerprints.json existed and was non-empty but loaded as {} — refusing to overwrite');
|
||||
}
|
||||
|
||||
// 4. SAVE ALL entries back (full dict — not just the patched subset)
|
||||
writeFileSync(fpPath, JSON.stringify(all, null, 2));
|
||||
console.log(`Fingerprints: ${before} → ${Object.keys(all).length}`);
|
||||
```
|
||||
|
||||
The `existedAndNonEmpty && before === 0` guard catches the silent-load-failure case before it corrupts the store. If the count shrinks from N to a small number that matches the batch size, the LOAD step was skipped — abort the write rather than persist the wrong dict.
|
||||
|
||||
4. Clean up intermediate files:
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@understand-anything/skill",
|
||||
"version": "2.6.3",
|
||||
"version": "2.7.3",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -130,7 +130,7 @@ export function loadFingerprints(projectRoot: string): FingerprintStore | null {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: ProjectConfig = { autoUpdate: false };
|
||||
const DEFAULT_CONFIG: ProjectConfig = { autoUpdate: false, outputLanguage: "en" };
|
||||
|
||||
export function saveConfig(projectRoot: string, config: ProjectConfig): void {
|
||||
const dir = ensureDir(projectRoot);
|
||||
|
||||
@@ -189,7 +189,7 @@ describe("persistence", () => {
|
||||
it("should return default config when no file exists", () => {
|
||||
const loaded = loadConfig(tempDir);
|
||||
|
||||
expect(loaded).toEqual({ autoUpdate: false });
|
||||
expect(loaded).toEqual({ autoUpdate: false, outputLanguage: "en" });
|
||||
});
|
||||
|
||||
it("should return default config when config.json is corrupted", () => {
|
||||
@@ -198,7 +198,7 @@ describe("persistence", () => {
|
||||
writeFileSync(join(dir, "config.json"), "not json!!", "utf-8");
|
||||
|
||||
const loaded = loadConfig(tempDir);
|
||||
expect(loaded).toEqual({ autoUpdate: false });
|
||||
expect(loaded).toEqual({ autoUpdate: false, outputLanguage: "en" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,9 +113,10 @@ export interface AnalysisMeta {
|
||||
theme?: ThemeConfig;
|
||||
}
|
||||
|
||||
// Project config (for auto-update opt-in)
|
||||
// Project config (for auto-update opt-in and language preference)
|
||||
export interface ProjectConfig {
|
||||
autoUpdate: boolean;
|
||||
outputLanguage?: string;
|
||||
}
|
||||
|
||||
// Non-code structural sub-interfaces
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts";
|
||||
import { ThemeProvider } from "./themes/index.ts";
|
||||
import { ThemePicker } from "./components/ThemePicker.tsx";
|
||||
import type { ThemeConfig } from "./themes/index.ts";
|
||||
import { I18nProvider, useI18n } from "./contexts/I18nContext.tsx";
|
||||
|
||||
// Lazy-load heavy / optional components so they ship in separate chunks.
|
||||
const CodeViewer = lazy(() => import("./components/CodeViewer"));
|
||||
@@ -45,6 +46,7 @@ function dataUrl(fileName: string, token: string | null): string {
|
||||
"domain-graph.json": import.meta.env.VITE_DOMAIN_GRAPH_URL,
|
||||
"meta.json": import.meta.env.VITE_META_URL,
|
||||
"diff-overlay.json": import.meta.env.VITE_DIFF_OVERLAY_URL,
|
||||
"config.json": import.meta.env.VITE_CONFIG_URL,
|
||||
};
|
||||
const url = envMap[fileName];
|
||||
if (url) return url;
|
||||
@@ -96,38 +98,13 @@ function App() {
|
||||
}
|
||||
|
||||
function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const setGraph = useDashboardStore((s) => s.setGraph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const tourActive = useDashboardStore((s) => s.tourActive);
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
|
||||
const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded);
|
||||
const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer);
|
||||
const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer);
|
||||
const setDomainGraph = useDashboardStore((s) => s.setDomainGraph);
|
||||
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
|
||||
const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen);
|
||||
const togglePathFinder = useDashboardStore((s) => s.togglePathFinder);
|
||||
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
|
||||
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [graphIssues, setGraphIssues] = useState<GraphIssue[]>([]);
|
||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||
const [metaTheme, setMetaTheme] = useState<ThemeConfig | null>(null);
|
||||
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("info");
|
||||
const viewMode = useDashboardStore((s) => s.viewMode);
|
||||
const setViewMode = useDashboardStore((s) => s.setViewMode);
|
||||
const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph);
|
||||
const domainGraph = useDashboardStore((s) => s.domainGraph);
|
||||
const setDomainGraph = useDashboardStore((s) => s.setDomainGraph);
|
||||
const layoutIssues = useDashboardStore((s) => s.layoutIssues);
|
||||
const isMobile = useIsMobile();
|
||||
// Schema issues + ELK layout issues share the WarningBanner — graph-load
|
||||
// problems and dashboard rendering problems are equally surfaced.
|
||||
const allIssues = useMemo(
|
||||
() => [...graphIssues, ...layoutIssues],
|
||||
[graphIssues, layoutIssues],
|
||||
);
|
||||
const [outputLanguage, setOutputLanguage] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(dataUrl("meta.json", accessToken))
|
||||
@@ -136,130 +113,14 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
if (meta?.theme) setMetaTheme(meta.theme);
|
||||
})
|
||||
.catch(() => {});
|
||||
fetch(dataUrl("config.json", accessToken))
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((config) => {
|
||||
if (config?.outputLanguage) setOutputLanguage(config.outputLanguage);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNodeId) setSidebarTab("info");
|
||||
}, [selectedNodeId]);
|
||||
|
||||
// Define keyboard shortcuts
|
||||
const shortcuts = useMemo<KeyboardShortcut[]>(
|
||||
() => [
|
||||
// Help
|
||||
{
|
||||
key: "?",
|
||||
shiftKey: true,
|
||||
description: "Show keyboard shortcuts",
|
||||
action: () => setShowKeyboardHelp((prev) => !prev),
|
||||
category: "General",
|
||||
},
|
||||
// Navigation
|
||||
{
|
||||
key: "Escape",
|
||||
description: "Close panels and modals / go back to overview",
|
||||
action: () => {
|
||||
// Read from store at invocation time to avoid stale closures
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.pathFinderOpen) {
|
||||
state.togglePathFinder();
|
||||
} else if (state.filterPanelOpen) {
|
||||
state.toggleFilterPanel();
|
||||
} else if (state.exportMenuOpen) {
|
||||
state.toggleExportMenu();
|
||||
} else if (state.codeViewerExpanded) {
|
||||
state.collapseCodeViewer();
|
||||
} else if (state.codeViewerOpen) {
|
||||
state.closeCodeViewer();
|
||||
} else if (state.selectedNodeId) {
|
||||
state.selectNode(null);
|
||||
} else if (state.navigationLevel === "layer-detail") {
|
||||
state.navigateToOverview();
|
||||
} else if (state.tourActive) {
|
||||
state.stopTour();
|
||||
} else {
|
||||
setShowKeyboardHelp(false);
|
||||
}
|
||||
},
|
||||
category: "Navigation",
|
||||
},
|
||||
{
|
||||
key: "/",
|
||||
description: "Focus search bar",
|
||||
action: () => {
|
||||
const searchInput = document.querySelector<HTMLInputElement>(
|
||||
'input[placeholder*="Search"]'
|
||||
);
|
||||
searchInput?.focus();
|
||||
},
|
||||
category: "Navigation",
|
||||
},
|
||||
// Tour controls
|
||||
{
|
||||
key: "ArrowRight",
|
||||
description: "Next tour step",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.tourActive) {
|
||||
state.nextTourStep();
|
||||
}
|
||||
},
|
||||
category: "Tour",
|
||||
},
|
||||
{
|
||||
key: "ArrowLeft",
|
||||
description: "Previous tour step",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.tourActive) {
|
||||
state.prevTourStep();
|
||||
}
|
||||
},
|
||||
category: "Tour",
|
||||
},
|
||||
// View toggles
|
||||
{
|
||||
key: "d",
|
||||
description: "Toggle diff mode",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleDiffMode();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "f",
|
||||
description: "Toggle filter panel",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleFilterPanel();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
description: "Toggle export menu",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleExportMenu();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "p",
|
||||
description: "Open path finder",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.togglePathFinder();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Register keyboard shortcuts
|
||||
useKeyboardShortcuts(shortcuts);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(dataUrl("knowledge-graph.json", accessToken))
|
||||
.then((res) => res.json())
|
||||
@@ -268,9 +129,8 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
if (result.success && result.data) {
|
||||
setGraph(result.data);
|
||||
setGraphIssues(result.issues);
|
||||
// Auto-detect knowledge graph kind
|
||||
if ((data as Record<string, unknown>).kind === "knowledge") {
|
||||
setViewMode("knowledge");
|
||||
useDashboardStore.getState().setViewMode("knowledge");
|
||||
useDashboardStore.getState().setIsKnowledgeGraph(true);
|
||||
}
|
||||
for (const issue of result.issues) {
|
||||
@@ -315,9 +175,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore - diff overlay is optional
|
||||
});
|
||||
.catch(() => {});
|
||||
}, [setDiffOverlay]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -335,11 +193,183 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
console.warn(`[domain-graph] validation failed: ${result.fatal}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore — domain graph is optional
|
||||
});
|
||||
.catch(() => {});
|
||||
}, [setDomainGraph]);
|
||||
|
||||
return (
|
||||
<I18nProvider language={outputLanguage ?? "en"}>
|
||||
<ThemeProvider metaTheme={metaTheme}>
|
||||
<DashboardContent
|
||||
accessToken={accessToken}
|
||||
loadError={loadError}
|
||||
graphIssues={graphIssues}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardContent({
|
||||
accessToken,
|
||||
loadError,
|
||||
graphIssues,
|
||||
}: {
|
||||
accessToken: string;
|
||||
loadError: string | null;
|
||||
graphIssues: GraphIssue[];
|
||||
}) {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const tourActive = useDashboardStore((s) => s.tourActive);
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
|
||||
const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded);
|
||||
const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer);
|
||||
const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer);
|
||||
const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen);
|
||||
const togglePathFinder = useDashboardStore((s) => s.togglePathFinder);
|
||||
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
|
||||
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
|
||||
const detailLevel = useDashboardStore((s) => s.detailLevel);
|
||||
const setDetailLevel = useDashboardStore((s) => s.setDetailLevel);
|
||||
const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView);
|
||||
const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView);
|
||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("info");
|
||||
const viewMode = useDashboardStore((s) => s.viewMode);
|
||||
const setViewMode = useDashboardStore((s) => s.setViewMode);
|
||||
const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph);
|
||||
const domainGraph = useDashboardStore((s) => s.domainGraph);
|
||||
const layoutIssues = useDashboardStore((s) => s.layoutIssues);
|
||||
const isMobile = useIsMobile();
|
||||
const { t } = useI18n();
|
||||
const allIssues = useMemo(
|
||||
() => [...graphIssues, ...layoutIssues],
|
||||
[graphIssues, layoutIssues],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNodeId) setSidebarTab("info");
|
||||
}, [selectedNodeId]);
|
||||
|
||||
// Define keyboard shortcuts
|
||||
const shortcuts = useMemo<KeyboardShortcut[]>(
|
||||
() => [
|
||||
// Help
|
||||
{
|
||||
key: "?",
|
||||
shiftKey: true,
|
||||
description: t.keyboardShortcuts.showHelp,
|
||||
action: () => setShowKeyboardHelp((prev) => !prev),
|
||||
category: "General",
|
||||
},
|
||||
// Navigation
|
||||
{
|
||||
key: "Escape",
|
||||
description: t.keyboardShortcuts.escapeDesc,
|
||||
action: () => {
|
||||
// Read from store at invocation time to avoid stale closures
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.pathFinderOpen) {
|
||||
state.togglePathFinder();
|
||||
} else if (state.filterPanelOpen) {
|
||||
state.toggleFilterPanel();
|
||||
} else if (state.exportMenuOpen) {
|
||||
state.toggleExportMenu();
|
||||
} else if (state.codeViewerExpanded) {
|
||||
state.collapseCodeViewer();
|
||||
} else if (state.codeViewerOpen) {
|
||||
state.closeCodeViewer();
|
||||
} else if (state.selectedNodeId) {
|
||||
state.selectNode(null);
|
||||
} else if (state.navigationLevel === "layer-detail") {
|
||||
state.navigateToOverview();
|
||||
} else if (state.tourActive) {
|
||||
state.stopTour();
|
||||
} else {
|
||||
setShowKeyboardHelp(false);
|
||||
}
|
||||
},
|
||||
category: "Navigation",
|
||||
},
|
||||
{
|
||||
key: "/",
|
||||
description: t.keyboardShortcuts.focusSearch,
|
||||
action: () => {
|
||||
const searchInput = document.querySelector<HTMLInputElement>(
|
||||
'[data-testid="search-input"]'
|
||||
);
|
||||
searchInput?.focus();
|
||||
},
|
||||
category: "Navigation",
|
||||
},
|
||||
// Tour controls
|
||||
{
|
||||
key: "ArrowRight",
|
||||
description: t.keyboardShortcuts.nextStep,
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.tourActive) {
|
||||
state.nextTourStep();
|
||||
}
|
||||
},
|
||||
category: "Tour",
|
||||
},
|
||||
{
|
||||
key: "ArrowLeft",
|
||||
description: t.keyboardShortcuts.prevStep,
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.tourActive) {
|
||||
state.prevTourStep();
|
||||
}
|
||||
},
|
||||
category: "Tour",
|
||||
},
|
||||
// View toggles
|
||||
{
|
||||
key: "d",
|
||||
description: t.keyboardShortcuts.toggleDiff,
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleDiffMode();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "f",
|
||||
description: t.keyboardShortcuts.toggleFilter,
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleFilterPanel();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
description: t.keyboardShortcuts.toggleExport,
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleExportMenu();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "p",
|
||||
description: t.keyboardShortcuts.openPathFinder,
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.togglePathFinder();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
// Register keyboard shortcuts
|
||||
useKeyboardShortcuts(shortcuts);
|
||||
|
||||
// Determine sidebar content
|
||||
// NodeInfo always takes priority when a node is selected.
|
||||
// Learn mode adds LearnPanel below it; otherwise ProjectOverview shows when idle.
|
||||
@@ -370,7 +400,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
: "text-text-muted hover:text-text-primary hover:bg-elevated"
|
||||
}`}
|
||||
>
|
||||
{tab === "info" ? "Info" : "Files"}
|
||||
{tab === "info" ? t.sidebar.info : t.sidebar.files}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -382,28 +412,25 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<ThemeProvider metaTheme={metaTheme}>
|
||||
<MobileLayout
|
||||
accessToken={accessToken}
|
||||
showKeyboardHelp={showKeyboardHelp}
|
||||
setShowKeyboardHelp={setShowKeyboardHelp}
|
||||
loadError={loadError}
|
||||
allIssues={allIssues}
|
||||
shortcuts={shortcuts}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
<MobileLayout
|
||||
accessToken={accessToken}
|
||||
showKeyboardHelp={showKeyboardHelp}
|
||||
setShowKeyboardHelp={setShowKeyboardHelp}
|
||||
loadError={loadError}
|
||||
allIssues={allIssues}
|
||||
shortcuts={shortcuts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider metaTheme={metaTheme}>
|
||||
<div className="h-screen w-screen flex flex-col bg-root text-text-primary noise-overlay">
|
||||
{/* Header */}
|
||||
<header className="flex items-center px-3 sm:px-5 py-3 bg-surface border-b border-border-subtle shrink-0 gap-2 sm:gap-4">
|
||||
{/* Left — fixed */}
|
||||
<div className="flex items-center gap-3 sm:gap-5 shrink-0 min-w-0">
|
||||
<h1 className="font-heading text-base sm:text-lg text-text-primary tracking-wide truncate max-w-[160px] sm:max-w-[220px] lg:max-w-none">
|
||||
{graph?.project.name ?? "Understand Anything"}
|
||||
{graph?.project.name ?? t.common.appName}
|
||||
</h1>
|
||||
<div className="w-px h-5 bg-border-subtle hidden sm:block" />
|
||||
<PersonaSelector />
|
||||
@@ -414,26 +441,26 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode("domain")}
|
||||
title="Switch to domain view"
|
||||
title={t.drawer.domain}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
viewMode === "domain"
|
||||
? "bg-accent/20 text-accent"
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Domain
|
||||
{t.drawer.domain}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode("structural")}
|
||||
title="Switch to structural view"
|
||||
title={t.drawer.structural}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
viewMode === "structural"
|
||||
? "bg-accent/20 text-accent"
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Structural
|
||||
{t.drawer.structural}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -444,17 +471,63 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
<div className="flex-1 min-w-0 overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center gap-4 w-max">
|
||||
<DiffToggle />
|
||||
{/* Detail level: file view (architecture) / class view (code structure) */}
|
||||
{!isKnowledgeGraph && viewMode !== "domain" && (
|
||||
<>
|
||||
<div className="w-px h-5 bg-border-subtle" />
|
||||
<div className="flex items-center bg-elevated rounded-lg p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDetailLevel("file")}
|
||||
title={t.detailLevel.filesTitle}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
detailLevel === "file"
|
||||
? "bg-accent/20 text-accent"
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{t.detailLevel.files}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDetailLevel("class")}
|
||||
title={t.detailLevel.classesTitle}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
detailLevel === "class"
|
||||
? "bg-accent/20 text-accent"
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{t.detailLevel.classes}
|
||||
</button>
|
||||
</div>
|
||||
{detailLevel === "class" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleShowFunctionsInClassView}
|
||||
title={t.detailLevel.fnTitle}
|
||||
className={`text-[10px] font-semibold uppercase tracking-wider px-2 py-1 rounded border transition-colors ${
|
||||
showFunctionsInClassView
|
||||
? "border-amber-500/50 bg-amber-500/10 text-amber-400"
|
||||
: "border-border-medium bg-elevated text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{t.detailLevel.fn}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{(isKnowledgeGraph ? [
|
||||
{ key: "knowledge" as const, label: "All", color: "var(--color-node-article)" },
|
||||
{ key: "knowledge" as const, label: t.nodeTypeLabels.all, color: "var(--color-node-article)" },
|
||||
] : [
|
||||
{ key: "code" as const, label: "Code", color: "var(--color-node-file)" },
|
||||
{ key: "config" as const, label: "Config", color: "var(--color-node-config)" },
|
||||
{ key: "docs" as const, label: "Docs", color: "var(--color-node-document)" },
|
||||
{ key: "infra" as const, label: "Infra", color: "var(--color-node-service)" },
|
||||
{ key: "data" as const, label: "Data", color: "var(--color-node-table)" },
|
||||
{ key: "domain" as const, label: "Domain", color: "var(--color-node-concept)" },
|
||||
{ key: "knowledge" as const, label: "Knowledge", color: "var(--color-node-article)" },
|
||||
{ key: "code" as const, label: t.nodeTypeLabels.code, color: "var(--color-node-file)" },
|
||||
{ key: "config" as const, label: t.nodeTypeLabels.config, color: "var(--color-node-config)" },
|
||||
{ key: "docs" as const, label: t.nodeTypeLabels.docs, color: "var(--color-node-document)" },
|
||||
{ key: "infra" as const, label: t.nodeTypeLabels.infra, color: "var(--color-node-service)" },
|
||||
{ key: "data" as const, label: t.nodeTypeLabels.data, color: "var(--color-node-table)" },
|
||||
{ key: "domain" as const, label: t.nodeTypeLabels.domain, color: "var(--color-node-concept)" },
|
||||
{ key: "knowledge" as const, label: t.nodeTypeLabels.knowledge, color: "var(--color-node-article)" },
|
||||
]).map((cat) => (
|
||||
<button
|
||||
key={cat.key}
|
||||
@@ -488,7 +561,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
<button
|
||||
onClick={togglePathFinder}
|
||||
className="flex items-center gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-sm bg-elevated text-text-secondary hover:text-text-primary transition-colors"
|
||||
title="Find path between nodes (P)"
|
||||
title={t.pathFinder.title}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -503,13 +576,13 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden md:inline">Path</span>
|
||||
<span className="hidden md:inline">{t.common.path}</span>
|
||||
</button>
|
||||
<ThemePicker />
|
||||
<button
|
||||
onClick={() => setShowKeyboardHelp(true)}
|
||||
className="text-text-muted hover:text-accent transition-colors"
|
||||
title="Keyboard shortcuts (Shift + ?)"
|
||||
title={t.keyboardShortcuts.showHelp}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
@@ -555,7 +628,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
<GraphView />
|
||||
)}
|
||||
<div className="absolute top-3 right-3 text-sm text-text-muted/60 pointer-events-none select-none">
|
||||
Press <kbd className="kbd">?</kbd> for keyboard shortcuts
|
||||
{t.common.pressKeyboard}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -617,7 +690,6 @@ function Dashboard({ accessToken }: { accessToken: string }) {
|
||||
<OnboardingOverlay />
|
||||
</Suspense>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export default function Breadcrumb() {
|
||||
const navigationLevel = useDashboardStore((s) => s.navigationLevel);
|
||||
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const navigateToOverview = useDashboardStore((s) => s.navigateToOverview);
|
||||
const { t } = useI18n();
|
||||
|
||||
const activeLayer = graph?.layers.find((l) => l.id === activeLayerId);
|
||||
|
||||
@@ -12,7 +14,7 @@ export default function Breadcrumb() {
|
||||
<div className="absolute top-4 left-4 z-10 flex items-center gap-2">
|
||||
{navigationLevel === "overview" && (
|
||||
<div className="px-4 py-2 rounded-full bg-elevated border border-border-subtle text-xs font-semibold tracking-wider uppercase text-text-secondary shadow-lg">
|
||||
Project Overview
|
||||
{t.breadcrumb.projectOverview}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,14 +24,14 @@ export default function Breadcrumb() {
|
||||
onClick={navigateToOverview}
|
||||
className="text-gold hover:text-gold-bright transition-colors"
|
||||
>
|
||||
Project
|
||||
{t.breadcrumb.project}
|
||||
</button>
|
||||
<span className="text-text-muted">›</span>
|
||||
<span className="text-text-primary">
|
||||
{activeLayer?.name ?? "Layer"}
|
||||
{activeLayer?.name ?? t.layer.defaultName}
|
||||
</span>
|
||||
<span className="text-text-muted ml-1 text-[10px] normal-case tracking-normal">
|
||||
(Esc to go back)
|
||||
({t.breadcrumb.escBack})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Highlight, themes } from "prism-react-renderer";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
interface CodeViewerProps {
|
||||
accessToken: string;
|
||||
@@ -78,6 +79,7 @@ export default function CodeViewer({
|
||||
source: null,
|
||||
error: null,
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
if (!node?.filePath) {
|
||||
@@ -125,7 +127,7 @@ export default function CodeViewer({
|
||||
if (!node) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-surface">
|
||||
<p className="text-text-muted text-sm">No file selected</p>
|
||||
<p className="text-text-muted text-sm">{t.codeViewer.noFile}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -133,8 +135,8 @@ export default function CodeViewer({
|
||||
const source = state.source;
|
||||
const language = source?.language ?? fallbackLanguage(node.filePath);
|
||||
const lineInfo = highlightedRange
|
||||
? `Lines ${highlightedRange.start}-${highlightedRange.end}`
|
||||
: "Full file";
|
||||
? `${t.codeViewer.lines} ${highlightedRange.start}-${highlightedRange.end}`
|
||||
: t.codeViewer.fullFile;
|
||||
const isModal = presentation === "modal";
|
||||
const handleClose = onClose ?? closeCodeViewer;
|
||||
|
||||
@@ -170,8 +172,8 @@ export default function CodeViewer({
|
||||
type="button"
|
||||
onClick={onExpand}
|
||||
className="text-text-muted hover:text-text-primary transition-colors"
|
||||
title="Open larger code viewer"
|
||||
aria-label="Open larger code viewer"
|
||||
title={t.codeViewer.openLarger}
|
||||
aria-label={t.codeViewer.openLarger}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 9V4h5M20 15v5h-5M4 4l6 6M20 20l-6-6" />
|
||||
@@ -182,8 +184,8 @@ export default function CodeViewer({
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="text-text-muted hover:text-text-primary transition-colors"
|
||||
title={isModal ? "Close expanded code viewer" : "Close code viewer"}
|
||||
aria-label={isModal ? "Close expanded code viewer" : "Close code viewer"}
|
||||
title={isModal ? t.codeViewer.closeExpanded : t.codeViewer.closeViewer}
|
||||
aria-label={isModal ? t.codeViewer.closeExpanded : t.codeViewer.closeViewer}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -194,13 +196,13 @@ export default function CodeViewer({
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-auto bg-root">
|
||||
{state.status === "loading" && (
|
||||
<div className="p-5 text-sm text-text-muted">Loading source...</div>
|
||||
<div className="p-5 text-sm text-text-muted">{t.codeViewer.loading}</div>
|
||||
)}
|
||||
|
||||
{state.status === "error" && (
|
||||
<div className="p-5">
|
||||
<div className="rounded-lg border border-border-subtle bg-elevated p-4">
|
||||
<div className="text-sm font-medium text-text-primary mb-2">Source unavailable</div>
|
||||
<div className="text-sm font-medium text-text-primary mb-2">{t.codeViewer.sourceUnavailable}</div>
|
||||
<p className="text-sm text-text-secondary leading-relaxed">{state.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,7 +211,7 @@ export default function CodeViewer({
|
||||
{source && (
|
||||
<>
|
||||
<div className="px-4 py-2 border-b border-border-subtle bg-surface text-[11px] text-text-muted flex items-center justify-between">
|
||||
<span>{source.lineCount} lines</span>
|
||||
<span>{source.lineCount} {t.codeViewer.linesLabel}</span>
|
||||
<span>{formatBytes(source.sizeBytes)}</span>
|
||||
</div>
|
||||
<Highlight code={source.content} language={language} theme={themes.vsDark}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { memo } from "react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import type { NodeProps, Node } from "@xyflow/react";
|
||||
import type { NodeType } from "@understand-anything/core/types";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
// Color maps keyed by NodeType — must be kept in sync with core NodeType union.
|
||||
const typeColors: Record<NodeType, string> = {
|
||||
@@ -88,6 +89,7 @@ function CustomNodeComponent({
|
||||
const barColor = typeColors[knownType] ?? typeColors.file;
|
||||
const textColor = typeTextColors[knownType] ?? typeTextColors.file;
|
||||
const complexityColor = complexityColors[data.complexity] ?? complexityColors.simple;
|
||||
const { t } = useI18n();
|
||||
|
||||
if (import.meta.env.DEV && !(knownType in typeColors)) {
|
||||
console.warn(`[CustomNode] Unknown node type "${data.nodeType}" — using "file" colors`);
|
||||
@@ -159,8 +161,8 @@ function CustomNodeComponent({
|
||||
<span
|
||||
className="inline-block w-1.5 h-1.5 rounded-full bg-node-function shadow-[0_0_4px_rgba(90,158,111,0.6)]"
|
||||
role="img"
|
||||
aria-label="Tested"
|
||||
title="Has tests"
|
||||
aria-label={t.customNode.tested}
|
||||
title={t.customNode.hasTests}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export default function DiffToggle() {
|
||||
const diffMode = useDashboardStore((s) => s.diffMode);
|
||||
const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode);
|
||||
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
|
||||
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
|
||||
const { t } = useI18n();
|
||||
|
||||
const hasDiff = changedNodeIds.size > 0;
|
||||
|
||||
@@ -23,9 +25,9 @@ export default function DiffToggle() {
|
||||
title={
|
||||
hasDiff
|
||||
? diffMode
|
||||
? "Hide diff overlay"
|
||||
: "Show diff overlay"
|
||||
: "No diff data loaded"
|
||||
? t.diffToggle.hideOverlay
|
||||
: t.diffToggle.showOverlay
|
||||
: t.diffToggle.noData
|
||||
}
|
||||
>
|
||||
Diff {diffMode && hasDiff ? "ON" : "OFF"}
|
||||
@@ -39,7 +41,7 @@ export default function DiffToggle() {
|
||||
style={{ backgroundColor: "var(--color-diff-changed)" }}
|
||||
/>
|
||||
<span className="text-text-secondary text-[11px]">
|
||||
Changed
|
||||
{t.diffToggle.changed}
|
||||
<span className="text-text-muted ml-0.5">
|
||||
({changedNodeIds.size})
|
||||
</span>
|
||||
@@ -51,7 +53,7 @@ export default function DiffToggle() {
|
||||
style={{ backgroundColor: "var(--color-diff-affected)" }}
|
||||
/>
|
||||
<span className="text-text-secondary text-[11px]">
|
||||
Affected
|
||||
{t.diffToggle.affected}
|
||||
<span className="text-text-muted ml-0.5">
|
||||
({affectedNodeIds.size})
|
||||
</span>
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { FlowFlowNode } from "./FlowNode";
|
||||
import StepNode from "./StepNode";
|
||||
import type { StepFlowNode } from "./StepNode";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
import { mergeElkPositions, nodesToElkInput } from "../utils/layout";
|
||||
import { applyElkLayout } from "../utils/elk-layout";
|
||||
import type { KnowledgeGraph, GraphNode } from "@understand-anything/core/types";
|
||||
@@ -167,6 +168,7 @@ function DomainGraphViewInner() {
|
||||
const domainGraph = useDashboardStore((s) => s.domainGraph);
|
||||
const activeDomainId = useDashboardStore((s) => s.activeDomainId);
|
||||
const clearActiveDomain = useDashboardStore((s) => s.clearActiveDomain);
|
||||
const { t } = useI18n();
|
||||
|
||||
// Build structural nodes/edges/dims synchronously; only the layout call
|
||||
// itself is async, so we memo the structural pieces and run ELK in an
|
||||
@@ -237,7 +239,7 @@ function DomainGraphViewInner() {
|
||||
onClick={() => clearActiveDomain()}
|
||||
className="px-3 py-1.5 text-xs rounded-lg bg-elevated border border-border-subtle text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
Back to domains
|
||||
{t.domainView.backToDomains}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
import type { KnowledgeGraph } from "@understand-anything/core/types";
|
||||
import { filterNodes, filterEdges } from "../utils/filters";
|
||||
|
||||
@@ -26,6 +27,7 @@ export default function ExportMenu() {
|
||||
const toggleExportMenu = useDashboardStore((s) => s.toggleExportMenu);
|
||||
const reactFlowInstance = useDashboardStore((s) => s.reactFlowInstance);
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
const { t } = useI18n();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -218,7 +220,7 @@ export default function ExportMenu() {
|
||||
<button
|
||||
onClick={toggleExportMenu}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-elevated text-text-secondary hover:text-text-primary transition-colors"
|
||||
title="Export graph (E)"
|
||||
title={t.export.title}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -233,7 +235,7 @@ export default function ExportMenu() {
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
Export
|
||||
{t.export.label}
|
||||
</button>
|
||||
|
||||
{exportMenuOpen && (
|
||||
@@ -247,7 +249,7 @@ export default function ExportMenu() {
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>Export as PNG</span>
|
||||
<span>{t.export.asPNG}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={exportSVG}
|
||||
@@ -257,7 +259,7 @@ export default function ExportMenu() {
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
</svg>
|
||||
<span>Export as SVG</span>
|
||||
<span>{t.export.asSVG}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={exportJSON}
|
||||
@@ -267,7 +269,7 @@ export default function ExportMenu() {
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<span>Export as JSON</span>
|
||||
<span>{t.export.asJSON}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import type { GraphNode } from "@understand-anything/core/types";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
interface FileEntry {
|
||||
name: string;
|
||||
@@ -142,6 +143,7 @@ export default function FileExplorer() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const openCodeViewer = useDashboardStore((s) => s.openCodeViewer);
|
||||
const navigateToNode = useDashboardStore((s) => s.navigateToNode);
|
||||
const { t } = useI18n();
|
||||
const entries = useMemo(() => buildFileTree(graph?.nodes ?? []), [graph]);
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
||||
|
||||
@@ -176,7 +178,7 @@ export default function FileExplorer() {
|
||||
if (!graph) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-5 text-sm text-text-muted">
|
||||
No graph loaded
|
||||
{t.common.noGraphLoaded}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -185,15 +187,15 @@ export default function FileExplorer() {
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<div className="px-4 py-3 border-b border-border-subtle shrink-0">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-accent">
|
||||
Analyzed Files
|
||||
{t.fileExplorer.analyzedFiles}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-1">
|
||||
{totalFiles} files from the current knowledge graph
|
||||
{totalFiles} {t.fileExplorer.filesFromGraph}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto py-2">
|
||||
{entries.length === 0 ? (
|
||||
<div className="px-4 py-6 text-sm text-text-muted">No file paths found.</div>
|
||||
<div className="px-4 py-6 text-sm text-text-muted">{t.fileExplorer.noFilePathsFound}</div>
|
||||
) : (
|
||||
entries.map((entry) => (
|
||||
<FileTreeRow
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useDashboardStore, ALL_NODE_TYPES, ALL_COMPLEXITIES, ALL_EDGE_CATEGORIES } from "../store";
|
||||
import type { NodeType, Complexity, EdgeCategory } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export default function FilterPanel() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
@@ -10,6 +11,7 @@ export default function FilterPanel() {
|
||||
const hasActiveFilters = useDashboardStore((s) => s.hasActiveFilters);
|
||||
const filterPanelOpen = useDashboardStore((s) => s.filterPanelOpen);
|
||||
const toggleFilterPanel = useDashboardStore((s) => s.toggleFilterPanel);
|
||||
const { t } = useI18n();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -97,7 +99,7 @@ export default function FilterPanel() {
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
Filter
|
||||
{t.common.filter}
|
||||
</button>
|
||||
|
||||
{filterPanelOpen && (
|
||||
@@ -106,7 +108,7 @@ export default function FilterPanel() {
|
||||
{/* Node Types */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
||||
Node Types
|
||||
{t.filterPanel.nodeTypes}
|
||||
</h3>
|
||||
<div className="space-y-1.5">
|
||||
{allNodeTypes.map((type) => (
|
||||
@@ -129,7 +131,7 @@ export default function FilterPanel() {
|
||||
{/* Complexity */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
||||
Complexity
|
||||
{t.filterPanel.complexity}
|
||||
</h3>
|
||||
<div className="space-y-1.5">
|
||||
{allComplexities.map((complexity) => (
|
||||
@@ -153,7 +155,7 @@ export default function FilterPanel() {
|
||||
{layers.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
||||
Layers
|
||||
{t.filterPanel.layers}
|
||||
</h3>
|
||||
<div className="space-y-1.5">
|
||||
{layers.map((layer) => (
|
||||
@@ -178,7 +180,7 @@ export default function FilterPanel() {
|
||||
{/* Edge Categories */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
||||
Edge Categories
|
||||
{t.filterPanel.edgeCategories}
|
||||
</h3>
|
||||
<div className="space-y-1.5">
|
||||
{allEdgeCategories.map((category) => (
|
||||
@@ -206,7 +208,7 @@ export default function FilterPanel() {
|
||||
onClick={resetFilters}
|
||||
className="w-full px-3 py-1.5 text-sm bg-elevated hover:bg-gold/20 text-text-secondary hover:text-gold rounded-lg transition-colors"
|
||||
>
|
||||
Reset All
|
||||
{t.common.resetAll}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -366,6 +366,7 @@ function useLayerDetailTopology(): LayerDetailTopology & {
|
||||
layoutStatus: "computing" | "ready";
|
||||
} {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const nodesById = useDashboardStore((s) => s.nodesById);
|
||||
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
@@ -375,6 +376,8 @@ function useLayerDetailTopology(): LayerDetailTopology & {
|
||||
const focusNodeId = useDashboardStore((s) => s.focusNodeId);
|
||||
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
|
||||
const drillIntoLayer = useDashboardStore((s) => s.drillIntoLayer);
|
||||
const detailLevel = useDashboardStore((s) => s.detailLevel);
|
||||
const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView);
|
||||
|
||||
const handleNodeSelect = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -404,10 +407,20 @@ function useLayerDetailTopology(): LayerDetailTopology & {
|
||||
|
||||
// Expand layer membership to include sub-file nodes (function/class)
|
||||
// whose parent file is in this layer. Joined via "contains" edges.
|
||||
// File view: file nodes only (architecture-level dependencies).
|
||||
// Class view: file nodes + class nodes, functions only when toggle is on.
|
||||
const expandedLayerNodeIds = new Set(layerNodeIds);
|
||||
for (const edge of graph.edges) {
|
||||
if (edge.type === "contains" && layerNodeIds.has(edge.source)) {
|
||||
expandedLayerNodeIds.add(edge.target);
|
||||
if (detailLevel !== "file") {
|
||||
for (const edge of graph.edges) {
|
||||
if (edge.type === "contains" && layerNodeIds.has(edge.source)) {
|
||||
const child = nodesById.get(edge.target);
|
||||
if (!child) continue;
|
||||
if (child.type === "class") {
|
||||
expandedLayerNodeIds.add(edge.target);
|
||||
} else if (child.type === "function" && showFunctionsInClassView) {
|
||||
expandedLayerNodeIds.add(edge.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,6 +639,7 @@ function useLayerDetailTopology(): LayerDetailTopology & {
|
||||
};
|
||||
}, [
|
||||
graph,
|
||||
nodesById,
|
||||
activeLayerId,
|
||||
persona,
|
||||
diffMode,
|
||||
@@ -634,6 +648,8 @@ function useLayerDetailTopology(): LayerDetailTopology & {
|
||||
focusNodeId,
|
||||
nodeTypeFilters,
|
||||
drillIntoLayer,
|
||||
detailLevel,
|
||||
showFunctionsInClassView,
|
||||
handleNodeSelect,
|
||||
handleContainerToggle,
|
||||
]);
|
||||
|
||||
+15
-4
@@ -1,5 +1,6 @@
|
||||
import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts";
|
||||
import { formatShortcutKey } from "../hooks/useKeyboardShortcuts";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
interface KeyboardShortcutsHelpProps {
|
||||
shortcuts: KeyboardShortcut[];
|
||||
@@ -10,6 +11,8 @@ export default function KeyboardShortcutsHelp({
|
||||
shortcuts,
|
||||
onClose,
|
||||
}: KeyboardShortcutsHelpProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
// Group shortcuts by category
|
||||
const groupedShortcuts = shortcuts.reduce((acc, shortcut) => {
|
||||
if (!acc[shortcut.category]) {
|
||||
@@ -19,6 +22,14 @@ export default function KeyboardShortcutsHelp({
|
||||
return acc;
|
||||
}, {} as Record<string, KeyboardShortcut[]>);
|
||||
|
||||
// Translate category names
|
||||
const categoryTranslations: Record<string, string> = {
|
||||
"General": t.keyboardShortcuts.general,
|
||||
"Navigation": t.keyboardShortcuts.navigation,
|
||||
"Tour": t.keyboardShortcuts.tour,
|
||||
"View": t.keyboardShortcuts.view,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
@@ -32,10 +43,10 @@ export default function KeyboardShortcutsHelp({
|
||||
<div className="sticky top-0 glass-heavy border-b border-border-subtle px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-heading text-text-primary">
|
||||
Keyboard Shortcuts
|
||||
{t.keyboardShortcuts.title}
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Press <kbd className="kbd">?</kbd> anytime to toggle this help
|
||||
{t.keyboardShortcuts.toggleHint}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -63,7 +74,7 @@ export default function KeyboardShortcutsHelp({
|
||||
{Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
|
||||
<div key={category}>
|
||||
<h3 className="text-sm font-semibold text-accent uppercase tracking-wider mb-3">
|
||||
{category}
|
||||
{categoryTranslations[category] ?? category}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{categoryShortcuts.map((shortcut, index) => (
|
||||
@@ -85,7 +96,7 @@ export default function KeyboardShortcutsHelp({
|
||||
{/* Footer */}
|
||||
<div className="sticky bottom-0 glass-heavy border-t border-border-subtle px-6 py-3 text-center">
|
||||
<p className="text-xs text-text-muted">
|
||||
Press <kbd className="kbd">ESC</kbd> to close
|
||||
{t.keyboardShortcuts.closeHint}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
// Shared layer color palette — used by LayerLegend, LayerClusterNode, PortalNode, and GraphView
|
||||
export const LAYER_PALETTE = [
|
||||
@@ -19,6 +20,7 @@ export default function LayerLegend() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const navigationLevel = useDashboardStore((s) => s.navigationLevel);
|
||||
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
|
||||
const { t } = useI18n();
|
||||
|
||||
const layers = graph?.layers ?? [];
|
||||
const hasLayers = layers.length > 0;
|
||||
@@ -31,8 +33,8 @@ export default function LayerLegend() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-medium text-text-secondary whitespace-nowrap">
|
||||
{navigationLevel === "overview"
|
||||
? `${layers.length} layers`
|
||||
: activeLayer?.name ?? "Layer"}
|
||||
? `${layers.length} ${t.layer.label}`
|
||||
: activeLayer?.name ?? t.layer.defaultName}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export default function LearnPanel() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
@@ -12,6 +13,7 @@ export default function LearnPanel() {
|
||||
const nextTourStep = useDashboardStore((s) => s.nextTourStep);
|
||||
const prevTourStep = useDashboardStore((s) => s.prevTourStep);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
const { t } = useI18n();
|
||||
|
||||
const tourSteps = useMemo(
|
||||
() => graph?.tour ? [...graph.tour].sort((a, b) => a.order - b.order) : [],
|
||||
@@ -25,9 +27,9 @@ export default function LearnPanel() {
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<div className="text-center px-4">
|
||||
<div className="text-2xl mb-2 text-text-muted">🧭</div>
|
||||
<p className="text-text-muted text-sm">No tour available</p>
|
||||
<p className="text-text-muted text-sm">{t.learnPanel.noTour}</p>
|
||||
<p className="text-text-muted text-xs mt-1">
|
||||
Generate a tour from your knowledge graph to get a guided walkthrough
|
||||
{t.learnPanel.noTourHint}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,9 +41,9 @@ export default function LearnPanel() {
|
||||
return (
|
||||
<div className="h-full w-full overflow-auto p-5">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-heading text-text-primary mb-1">Project Tour</h2>
|
||||
<h2 className="text-lg font-heading text-text-primary mb-1">{t.learnPanel.projectTour}</h2>
|
||||
<p className="text-xs text-text-muted">
|
||||
{tourSteps.length} steps · Guided walkthrough of the codebase
|
||||
{tourSteps.length} {t.learnPanel.steps} · {t.learnPanel.guidedWalkthrough}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -49,12 +51,12 @@ export default function LearnPanel() {
|
||||
onClick={startTour}
|
||||
className="w-full mb-4 bg-accent/10 border border-accent/30 text-accent text-sm font-medium py-2.5 px-4 rounded-lg hover:bg-accent/20 transition-colors"
|
||||
>
|
||||
Start Tour
|
||||
{t.learnPanel.startTour}
|
||||
</button>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">
|
||||
Steps
|
||||
{t.learnPanel.steps}
|
||||
</h3>
|
||||
{tourSteps.map((step, i) => (
|
||||
<div
|
||||
@@ -87,7 +89,7 @@ export default function LearnPanel() {
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-border-subtle shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider">
|
||||
Tour
|
||||
{t.learnPanel.tour}
|
||||
</h3>
|
||||
<span className="text-xs text-text-muted">
|
||||
{currentTourStep + 1} / {totalSteps}
|
||||
@@ -97,7 +99,7 @@ export default function LearnPanel() {
|
||||
onClick={stopTour}
|
||||
className="text-[10px] text-text-muted hover:text-text-secondary transition-colors"
|
||||
>
|
||||
Exit Tour
|
||||
{t.learnPanel.exitTour}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -213,13 +215,13 @@ export default function LearnPanel() {
|
||||
disabled={isFirst}
|
||||
className="flex-1 text-xs bg-elevated text-text-secondary py-1.5 rounded-lg hover:bg-surface disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Prev
|
||||
{t.learnPanel.prev}
|
||||
</button>
|
||||
<button
|
||||
onClick={isLast ? stopTour : nextTourStep}
|
||||
className="flex-1 text-xs bg-accent/10 border border-accent/30 text-accent py-1.5 rounded-lg hover:bg-accent/20 transition-colors"
|
||||
>
|
||||
{isLast ? "Finish" : "Next"}
|
||||
{isLast ? t.learnPanel.finish : t.learnPanel.next}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export type MobileTab = "graph" | "info" | "files";
|
||||
|
||||
@@ -7,61 +8,58 @@ interface Props {
|
||||
onTabChange: (tab: MobileTab) => void;
|
||||
}
|
||||
|
||||
const tabs: { id: MobileTab; label: string; icon: ReactNode }[] = [
|
||||
{
|
||||
id: "graph",
|
||||
label: "Graph",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
|
||||
<circle cx="6" cy="7" r="2" />
|
||||
<circle cx="18" cy="7" r="2" />
|
||||
<circle cx="12" cy="17" r="2" />
|
||||
<path strokeLinecap="round" d="M7.6 8.5L11 15.5M16.4 8.5L13 15.5M8 7h8" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "info",
|
||||
label: "Info",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 11v5M12 8h.01" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "files",
|
||||
label: "Files",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 6.5A1.5 1.5 0 0 1 5.5 5h3.382a1.5 1.5 0 0 1 1.342.83l.671 1.34A1.5 1.5 0 0 0 12.236 8H18.5A1.5 1.5 0 0 1 20 9.5v8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
const tabIcons: Record<MobileTab, ReactNode> = {
|
||||
graph: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
|
||||
<circle cx="6" cy="7" r="2" />
|
||||
<circle cx="18" cy="7" r="2" />
|
||||
<circle cx="12" cy="17" r="2" />
|
||||
<path strokeLinecap="round" d="M7.6 8.5L11 15.5M16.4 8.5L13 15.5M8 7h8" />
|
||||
</svg>
|
||||
),
|
||||
info: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 11v5M12 8h.01" />
|
||||
</svg>
|
||||
),
|
||||
files: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 6.5A1.5 1.5 0 0 1 5.5 5h3.382a1.5 1.5 0 0 1 1.342.83l.671 1.34A1.5 1.5 0 0 0 12.236 8H18.5A1.5 1.5 0 0 1 20 9.5v8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
const tabOrder: MobileTab[] = ["graph", "info", "files"];
|
||||
|
||||
export default function MobileBottomNav({ activeTab, onTabChange }: Props) {
|
||||
const { t } = useI18n();
|
||||
const labels: Record<MobileTab, string> = {
|
||||
graph: t.mobile.graph,
|
||||
info: t.mobile.info,
|
||||
files: t.mobile.files,
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="flex shrink-0 bg-surface border-t border-border-subtle">
|
||||
{tabs.map((tab) => {
|
||||
const active = activeTab === tab.id;
|
||||
{tabOrder.map((id) => {
|
||||
const active = activeTab === id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
onClick={() => onTabChange(id)}
|
||||
className={`relative flex-1 flex flex-col items-center justify-center gap-1 py-2.5 text-[10px] font-semibold uppercase tracking-[0.14em] transition-colors ${
|
||||
active ? "text-accent" : "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
aria-current={active ? "page" : undefined}
|
||||
>
|
||||
<span className="w-5 h-5">{tab.icon}</span>
|
||||
{tab.label}
|
||||
<span className="w-5 h-5">{tabIcons[id]}</span>
|
||||
{labels[id]}
|
||||
{active && (
|
||||
<span className="absolute top-0 left-1/2 -translate-x-1/2 w-8 h-px bg-accent" />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
import PersonaSelector from "./PersonaSelector";
|
||||
import DiffToggle from "./DiffToggle";
|
||||
import LayerLegend from "./LayerLegend";
|
||||
@@ -20,19 +21,7 @@ interface NodeTypeFilterDef {
|
||||
color: string;
|
||||
}
|
||||
|
||||
const STRUCTURAL_FILTERS: NodeTypeFilterDef[] = [
|
||||
{ key: "code", label: "Code", color: "var(--color-node-file)" },
|
||||
{ key: "config", label: "Config", color: "var(--color-node-config)" },
|
||||
{ key: "docs", label: "Docs", color: "var(--color-node-document)" },
|
||||
{ key: "infra", label: "Infra", color: "var(--color-node-service)" },
|
||||
{ key: "data", label: "Data", color: "var(--color-node-table)" },
|
||||
{ key: "domain", label: "Domain", color: "var(--color-node-concept)" },
|
||||
{ key: "knowledge", label: "Knowledge", color: "var(--color-node-article)" },
|
||||
];
|
||||
|
||||
const KNOWLEDGE_FILTERS: NodeTypeFilterDef[] = [
|
||||
{ key: "knowledge", label: "All", color: "var(--color-node-article)" },
|
||||
];
|
||||
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -55,6 +44,21 @@ export default function MobileDrawer({
|
||||
const setViewMode = useDashboardStore((s) => s.setViewMode);
|
||||
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
|
||||
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
|
||||
const { t } = useI18n();
|
||||
|
||||
const structuralFilters: NodeTypeFilterDef[] = [
|
||||
{ key: "code", label: t.nodeTypeLabels.code, color: "var(--color-node-file)" },
|
||||
{ key: "config", label: t.nodeTypeLabels.config, color: "var(--color-node-config)" },
|
||||
{ key: "docs", label: t.nodeTypeLabels.docs, color: "var(--color-node-document)" },
|
||||
{ key: "infra", label: t.nodeTypeLabels.infra, color: "var(--color-node-service)" },
|
||||
{ key: "data", label: t.nodeTypeLabels.data, color: "var(--color-node-table)" },
|
||||
{ key: "domain", label: t.nodeTypeLabels.domain, color: "var(--color-node-concept)" },
|
||||
{ key: "knowledge", label: t.nodeTypeLabels.knowledge, color: "var(--color-node-article)" },
|
||||
];
|
||||
|
||||
const knowledgeFilters: NodeTypeFilterDef[] = [
|
||||
{ key: "knowledge", label: t.nodeTypeLabels.all, color: "var(--color-node-article)" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -75,7 +79,7 @@ export default function MobileDrawer({
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const filterDefs = isKnowledgeGraph ? KNOWLEDGE_FILTERS : STRUCTURAL_FILTERS;
|
||||
const filterDefs = isKnowledgeGraph ? knowledgeFilters : structuralFilters;
|
||||
const showViewToggle = Boolean(graph && !isKnowledgeGraph && domainGraph);
|
||||
|
||||
return (
|
||||
@@ -105,10 +109,10 @@ export default function MobileDrawer({
|
||||
<header className="flex items-center justify-between px-5 py-4 border-b border-border-subtle shrink-0">
|
||||
<div>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.2em] text-accent">
|
||||
Controls
|
||||
{t.drawer.controls}
|
||||
</span>
|
||||
<h2 className="font-heading text-lg text-text-primary mt-0.5 leading-none">
|
||||
{graph?.project.name ?? "Dashboard"}
|
||||
{graph?.project.name ?? t.drawer.dashboard}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
@@ -132,13 +136,13 @@ export default function MobileDrawer({
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-auto px-5 py-5 space-y-7">
|
||||
<section>
|
||||
<SectionLabel>Role</SectionLabel>
|
||||
<SectionLabel>{t.drawer.role}</SectionLabel>
|
||||
<PersonaSelector />
|
||||
</section>
|
||||
|
||||
{showViewToggle && (
|
||||
<section>
|
||||
<SectionLabel>View</SectionLabel>
|
||||
<SectionLabel>{t.drawer.view}</SectionLabel>
|
||||
<div className="inline-flex items-center bg-elevated rounded-lg p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
@@ -149,7 +153,7 @@ export default function MobileDrawer({
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Domain
|
||||
{t.drawer.domain}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -160,19 +164,19 @@ export default function MobileDrawer({
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Structural
|
||||
{t.drawer.structural}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<SectionLabel>Diff overlay</SectionLabel>
|
||||
<SectionLabel>{t.drawer.diffOverlay}</SectionLabel>
|
||||
<DiffToggle />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionLabel>Node types</SectionLabel>
|
||||
<SectionLabel>{t.drawer.nodeTypes}</SectionLabel>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{filterDefs.map((cat) => {
|
||||
const active = nodeTypeFilters[cat.key] !== false;
|
||||
@@ -203,7 +207,7 @@ export default function MobileDrawer({
|
||||
|
||||
{graph && (graph.layers?.length ?? 0) > 0 && (
|
||||
<section>
|
||||
<SectionLabel>Layers</SectionLabel>
|
||||
<SectionLabel>{t.drawer.layers}</SectionLabel>
|
||||
<div className="-mx-1">
|
||||
<LayerLegend />
|
||||
</div>
|
||||
@@ -211,7 +215,7 @@ export default function MobileDrawer({
|
||||
)}
|
||||
|
||||
<section>
|
||||
<SectionLabel>Tools</SectionLabel>
|
||||
<SectionLabel>{t.drawer.tools}</SectionLabel>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<FilterPanel />
|
||||
<ExportMenu />
|
||||
@@ -231,7 +235,7 @@ export default function MobileDrawer({
|
||||
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||
/>
|
||||
</svg>
|
||||
Path
|
||||
{t.drawer.path}
|
||||
</button>
|
||||
<ThemePicker />
|
||||
<button
|
||||
@@ -241,7 +245,7 @@ export default function MobileDrawer({
|
||||
onClose();
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-elevated text-text-secondary hover:text-text-primary transition-colors"
|
||||
aria-label="Keyboard shortcuts"
|
||||
aria-label={t.drawer.help}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -251,7 +255,7 @@ export default function MobileDrawer({
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Help
|
||||
{t.drawer.help}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import type { GraphIssue } from "@understand-anything/core/schema";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
import GraphView from "./GraphView";
|
||||
import DomainGraphView from "./DomainGraphView";
|
||||
import KnowledgeGraphView from "./KnowledgeGraphView";
|
||||
@@ -45,6 +46,7 @@ export default function MobileLayout({
|
||||
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
|
||||
const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen);
|
||||
const togglePathFinder = useDashboardStore((s) => s.togglePathFinder);
|
||||
const { t } = useI18n();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<MobileTab>("graph");
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
@@ -96,7 +98,7 @@ export default function MobileLayout({
|
||||
</button>
|
||||
|
||||
<h1 className="font-heading text-base flex-1 min-w-0 truncate text-center text-text-primary tracking-wide">
|
||||
{graph?.project.name ?? "Understand Anything"}
|
||||
{graph?.project.name ?? t.common.appName}
|
||||
</h1>
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
import type { NodeType, EdgeType, KnowledgeGraph, GraphNode } from "@understand-anything/core/types";
|
||||
|
||||
// Badge color classes keyed by NodeType — must be kept in sync with core NodeType union.
|
||||
@@ -33,56 +34,9 @@ const complexityBadgeColors: Record<string, string> = {
|
||||
complex: "text-[#c97070] border border-[#c97070]/30 bg-[#c97070]/10",
|
||||
};
|
||||
|
||||
/**
|
||||
* Human-readable directional labels for all 29 edge types.
|
||||
* Must be kept in sync with core EdgeType.
|
||||
*/
|
||||
const EDGE_LABELS: Record<EdgeType, { forward: string; backward: string }> = {
|
||||
imports: { forward: "imports", backward: "imported by" },
|
||||
exports: { forward: "exports to", backward: "exported by" },
|
||||
contains: { forward: "contains", backward: "contained in" },
|
||||
inherits: { forward: "inherits from", backward: "inherited by" },
|
||||
implements: { forward: "implements", backward: "implemented by" },
|
||||
calls: { forward: "calls", backward: "called by" },
|
||||
subscribes: { forward: "subscribes to", backward: "subscribed by" },
|
||||
publishes: { forward: "publishes to", backward: "consumed by" },
|
||||
middleware: { forward: "middleware for", backward: "uses middleware" },
|
||||
reads_from: { forward: "reads from", backward: "read by" },
|
||||
writes_to: { forward: "writes to", backward: "written by" },
|
||||
transforms: { forward: "transforms", backward: "transformed by" },
|
||||
validates: { forward: "validates", backward: "validated by" },
|
||||
depends_on: { forward: "depends on", backward: "depended on by" },
|
||||
tested_by: { forward: "tested by", backward: "tests" },
|
||||
configures: { forward: "configures", backward: "configured by" },
|
||||
related: { forward: "related to", backward: "related to" },
|
||||
similar_to: { forward: "similar to", backward: "similar to" },
|
||||
deploys: { forward: "deploys", backward: "deployed by" },
|
||||
serves: { forward: "serves", backward: "served by" },
|
||||
migrates: { forward: "migrates", backward: "migrated by" },
|
||||
documents: { forward: "documents", backward: "documented by" },
|
||||
provisions: { forward: "provisions", backward: "provisioned by" },
|
||||
routes: { forward: "routes to", backward: "routed from" },
|
||||
defines_schema: { forward: "defines schema for", backward: "schema defined by" },
|
||||
triggers: { forward: "triggers", backward: "triggered by" },
|
||||
contains_flow: { forward: "contains flow", backward: "flow in" },
|
||||
flow_step: { forward: "flow step", backward: "step of" },
|
||||
cross_domain: { forward: "cross-domain to", backward: "cross-domain from" },
|
||||
cites: { forward: "cites", backward: "cited by" },
|
||||
contradicts: { forward: "contradicts", backward: "contradicted by" },
|
||||
builds_on: { forward: "builds on", backward: "built upon by" },
|
||||
exemplifies: { forward: "exemplifies", backward: "exemplified by" },
|
||||
categorized_under: { forward: "categorized under", backward: "categorizes" },
|
||||
authored_by: { forward: "authored by", backward: "authored" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a human-readable directional label for an edge type.
|
||||
* Falls back to formatted type name for unknown edge types.
|
||||
*/
|
||||
function getDirectionalLabel(edgeType: string, isSource: boolean): string {
|
||||
const labels = (EDGE_LABELS as Record<string, { forward: string; backward: string }>)[edgeType];
|
||||
function getDirectionalLabel(edgeType: string, isSource: boolean, t: ReturnType<typeof useI18n>["t"]): string {
|
||||
const labels = t.edgeLabels[edgeType as EdgeType];
|
||||
if (!labels) {
|
||||
// Fallback for unknown edge types
|
||||
const formatted = edgeType.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
return isSource ? formatted : `${formatted} (reverse)`;
|
||||
}
|
||||
@@ -91,6 +45,7 @@ function getDirectionalLabel(edgeType: string, isSource: boolean): string {
|
||||
|
||||
function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) {
|
||||
const navigateToNode = useDashboardStore((s) => s.navigateToNode);
|
||||
const { t } = useI18n();
|
||||
const meta = node.knowledgeMeta;
|
||||
|
||||
// Wikilinks (outgoing related edges)
|
||||
@@ -117,7 +72,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
|
||||
<div className="space-y-3">
|
||||
{categoryNode && (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Category</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.category}</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigateToNode(categoryNode.id)}
|
||||
@@ -130,7 +85,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
|
||||
{meta?.wikilinks && meta.wikilinks.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">
|
||||
Wikilinks ({wikilinks.length})
|
||||
{t.nodeInfo.wikilinks} ({wikilinks.length})
|
||||
</h4>
|
||||
<div className="space-y-1 max-h-[200px] overflow-auto">
|
||||
{wikilinks.map((n) => (
|
||||
@@ -149,7 +104,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
|
||||
{backlinks.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">
|
||||
Backlinks ({backlinks.length})
|
||||
{t.nodeInfo.backlinks} ({backlinks.length})
|
||||
</h4>
|
||||
<div className="space-y-1 max-h-[200px] overflow-auto">
|
||||
{backlinks.map((n) => (
|
||||
@@ -167,11 +122,11 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
|
||||
)}
|
||||
{meta?.content && (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Preview</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.common.preview}</h4>
|
||||
<div className="text-[11px] text-text-secondary leading-relaxed bg-elevated rounded-lg p-3 max-h-[300px] overflow-auto whitespace-pre-wrap font-mono">
|
||||
{meta.content.slice(0, 1500)}
|
||||
{meta.content.length > 1500 && (
|
||||
<span className="text-text-muted">... (truncated)</span>
|
||||
<span className="text-text-muted">... {t.common.truncated}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,6 +138,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
|
||||
function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) {
|
||||
const navigateToDomain = useDashboardStore((s) => s.navigateToDomain);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
const { t } = useI18n();
|
||||
const meta = node.domainMeta;
|
||||
|
||||
if (node.type === "domain") {
|
||||
@@ -195,7 +151,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
|
||||
<div className="space-y-3">
|
||||
{Array.isArray(meta?.entities) && meta.entities.length > 0 ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entities</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.entities}</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{meta.entities.map((e) => (
|
||||
<span key={e} className="text-[11px] px-2 py-0.5 rounded bg-elevated text-text-secondary">{e}</span>
|
||||
@@ -205,7 +161,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
|
||||
) : null}
|
||||
{Array.isArray(meta?.businessRules) && meta.businessRules.length > 0 ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Business Rules</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.businessRules}</h4>
|
||||
<ul className="text-[11px] text-text-secondary space-y-1">
|
||||
{meta.businessRules.map((r, i) => (
|
||||
<li key={i} className="flex gap-1.5"><span className="text-accent shrink-0">-</span>{r}</li>
|
||||
@@ -215,7 +171,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
|
||||
) : null}
|
||||
{Array.isArray(meta?.crossDomainInteractions) && meta.crossDomainInteractions.length > 0 ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Cross-Domain</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.crossDomain}</h4>
|
||||
<ul className="text-[11px] text-text-secondary space-y-1">
|
||||
{meta.crossDomainInteractions.map((c, i) => (
|
||||
<li key={i}>{c}</li>
|
||||
@@ -225,7 +181,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
|
||||
) : null}
|
||||
{flows.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Flows</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.flows}</h4>
|
||||
<div className="space-y-1">
|
||||
{flows.map((f) => (
|
||||
<button
|
||||
@@ -255,13 +211,13 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
|
||||
<div className="space-y-3">
|
||||
{meta?.entryPoint ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entry Point</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.entryPoint}</h4>
|
||||
<div className="text-[11px] font-mono text-accent">{meta.entryPoint}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{steps.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Steps</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.steps}</h4>
|
||||
<ol className="space-y-1">
|
||||
{steps.map((s, i) => (
|
||||
<li key={s.id}>
|
||||
@@ -287,7 +243,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Implementation</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.implementation}</h4>
|
||||
<div className="text-[11px] font-mono text-text-secondary">
|
||||
{node.filePath}
|
||||
{node.lineRange && <span className="text-text-muted">:{node.lineRange[0]}-{node.lineRange[1]}</span>}
|
||||
@@ -306,6 +262,7 @@ export default function NodeInfo() {
|
||||
const nodeHistory = useDashboardStore((s) => s.nodeHistory);
|
||||
const goBackNode = useDashboardStore((s) => s.goBackNode);
|
||||
const [languageExpanded, setLanguageExpanded] = useState(true);
|
||||
const { t } = useI18n();
|
||||
|
||||
const navigateToNode = useDashboardStore((s) => s.navigateToNode);
|
||||
const navigateToHistoryIndex = useDashboardStore((s) => s.navigateToHistoryIndex);
|
||||
@@ -327,7 +284,7 @@ export default function NodeInfo() {
|
||||
if (!node) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-surface">
|
||||
<p className="text-text-muted text-sm">Select a node to see details</p>
|
||||
<p className="text-text-muted text-sm">{t.common.selectNode}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -369,7 +326,7 @@ export default function NodeInfo() {
|
||||
className="text-[10px] font-semibold text-gold hover:text-gold-bright transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>←</span>
|
||||
<span>Back</span>
|
||||
<span>{t.common.back}</span>
|
||||
</button>
|
||||
<span className="text-text-muted text-[10px]">│</span>
|
||||
{historyNodes.slice(-3).map((h, i, arr) => (
|
||||
@@ -419,7 +376,7 @@ export default function NodeInfo() {
|
||||
: "text-text-muted border border-border-subtle hover:text-gold hover:border-gold/30"
|
||||
}`}
|
||||
>
|
||||
{focusNodeId === node.id ? "Unfocus" : "Focus"}
|
||||
{focusNodeId === node.id ? t.common.unfocus : t.common.focus}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -431,7 +388,7 @@ export default function NodeInfo() {
|
||||
<div className="text-xs text-text-secondary mb-4 rounded-lg border border-border-subtle bg-elevated/60 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-text-muted mb-1">File</div>
|
||||
<div className="font-medium text-text-muted mb-1">{t.common.file}</div>
|
||||
<div className="font-mono truncate" title={node.filePath}>
|
||||
{node.filePath}
|
||||
{node.lineRange && (
|
||||
@@ -446,7 +403,7 @@ export default function NodeInfo() {
|
||||
onClick={() => openCodeViewer(node.id)}
|
||||
className="shrink-0 text-[10px] font-semibold uppercase tracking-wider px-2.5 py-1 rounded border border-accent/30 text-accent hover:text-accent-bright hover:border-accent/60 transition-colors"
|
||||
>
|
||||
Open code
|
||||
{t.common.openCode}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -466,7 +423,7 @@ export default function NodeInfo() {
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
Language Concepts
|
||||
{t.nodeInfo.languageConcepts}
|
||||
</button>
|
||||
{languageExpanded && (
|
||||
<div className="bg-accent/5 border border-accent/20 rounded-lg p-3">
|
||||
@@ -481,7 +438,7 @@ export default function NodeInfo() {
|
||||
{node.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">
|
||||
Tags
|
||||
{t.common.tags}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{node.tags.map((tag) => (
|
||||
@@ -510,7 +467,7 @@ export default function NodeInfo() {
|
||||
{childNodes.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-[11px] font-semibold text-gold uppercase tracking-wider mb-2">
|
||||
Defined in this file ({childNodes.length})
|
||||
{t.nodeInfo.definedInThisFile} ({childNodes.length})
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{childNodes.map((child) => {
|
||||
@@ -548,14 +505,14 @@ export default function NodeInfo() {
|
||||
{otherConnections.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-[11px] font-semibold text-gold uppercase tracking-wider mb-2">
|
||||
Connections ({otherConnections.length})
|
||||
{t.common.connections} ({otherConnections.length})
|
||||
</h3>
|
||||
<div className="space-y-1.5">
|
||||
{otherConnections.map((edge, i) => {
|
||||
const isSource = edge.source === node.id;
|
||||
const otherId = isSource ? edge.target : edge.source;
|
||||
const otherNode = activeGraph?.nodes.find((n) => n.id === otherId);
|
||||
const dirLabel = getDirectionalLabel(edge.type, isSource);
|
||||
const dirLabel = getDirectionalLabel(edge.type, isSource, t);
|
||||
const arrow = isSource ? "\u2192" : "\u2190";
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
import type { Persona } from "../store";
|
||||
|
||||
const personas: { id: Persona; label: string; description: string }[] = [
|
||||
{
|
||||
id: "non-technical",
|
||||
label: "Overview",
|
||||
description: "High-level architecture view",
|
||||
},
|
||||
{
|
||||
id: "junior",
|
||||
label: "Learn",
|
||||
description: "Full dashboard with guided learning",
|
||||
},
|
||||
{
|
||||
id: "experienced",
|
||||
label: "Deep Dive",
|
||||
description: "Code-focused with chat",
|
||||
},
|
||||
];
|
||||
|
||||
export default function PersonaSelector() {
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
const setPersona = useDashboardStore((s) => s.setPersona);
|
||||
const { t } = useI18n();
|
||||
|
||||
const personas: { id: Persona; label: string; description: string }[] = [
|
||||
{
|
||||
id: "non-technical",
|
||||
label: t.personaSelector.overview,
|
||||
description: t.personaSelector.overviewDesc,
|
||||
},
|
||||
{
|
||||
id: "junior",
|
||||
label: t.personaSelector.learn,
|
||||
description: t.personaSelector.learnDesc,
|
||||
},
|
||||
{
|
||||
id: "experienced",
|
||||
label: t.personaSelector.deepDive,
|
||||
description: t.personaSelector.deepDiveDesc,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 bg-elevated rounded-lg p-0.5">
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export default function ProjectOverview() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const startTour = useDashboardStore((s) => s.startTour);
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!graph) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<p className="text-text-muted text-sm">Loading project...</p>
|
||||
<p className="text-text-muted text-sm">{t.common.loading}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,13 +17,11 @@ export default function ProjectOverview() {
|
||||
const { project, nodes, edges, layers } = graph;
|
||||
const hasTour = graph.tour.length > 0;
|
||||
|
||||
// Count node types
|
||||
const typeCounts: Record<string, number> = {};
|
||||
for (const node of nodes) {
|
||||
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Count complexity
|
||||
const complexityCounts: Record<string, number> = { simple: 0, moderate: 0, complex: 0 };
|
||||
for (const node of nodes) {
|
||||
if (node.complexity) {
|
||||
@@ -29,7 +29,6 @@ export default function ProjectOverview() {
|
||||
}
|
||||
}
|
||||
|
||||
// Find top connected nodes
|
||||
const nodeConnections = new Map<string, number>();
|
||||
for (const edge of edges) {
|
||||
nodeConnections.set(edge.source, (nodeConnections.get(edge.source) ?? 0) + 1);
|
||||
@@ -45,16 +44,15 @@ export default function ProjectOverview() {
|
||||
|
||||
const avgConnections = nodes.length > 0 ? (edges.length * 2 / nodes.length).toFixed(1) : "0";
|
||||
|
||||
// Category breakdowns
|
||||
const categoryBreakdown = [
|
||||
{ label: "Code", color: "var(--color-node-file)", count: (typeCounts["file"] ?? 0) + (typeCounts["function"] ?? 0) + (typeCounts["class"] ?? 0) + (typeCounts["module"] ?? 0) + (typeCounts["concept"] ?? 0) },
|
||||
{ label: "Config", color: "var(--color-node-config)", count: typeCounts["config"] ?? 0 },
|
||||
{ label: "Docs", color: "var(--color-node-document)", count: typeCounts["document"] ?? 0 },
|
||||
{ label: "Infra", color: "var(--color-node-service)", count: (typeCounts["service"] ?? 0) + (typeCounts["resource"] ?? 0) + (typeCounts["pipeline"] ?? 0) },
|
||||
{ label: "Data", color: "var(--color-node-table)", count: (typeCounts["table"] ?? 0) + (typeCounts["endpoint"] ?? 0) + (typeCounts["schema"] ?? 0) },
|
||||
{ label: "Domain", color: "var(--color-node-concept)", count: (typeCounts["domain"] ?? 0) + (typeCounts["flow"] ?? 0) + (typeCounts["step"] ?? 0) },
|
||||
{ label: t.projectOverview.code, color: "var(--color-node-file)", count: (typeCounts["file"] ?? 0) + (typeCounts["function"] ?? 0) + (typeCounts["class"] ?? 0) + (typeCounts["module"] ?? 0) + (typeCounts["concept"] ?? 0) },
|
||||
{ label: t.projectOverview.config, color: "var(--color-node-config)", count: typeCounts["config"] ?? 0 },
|
||||
{ label: t.projectOverview.docs, color: "var(--color-node-document)", count: typeCounts["document"] ?? 0 },
|
||||
{ label: t.projectOverview.infra, color: "var(--color-node-service)", count: (typeCounts["service"] ?? 0) + (typeCounts["resource"] ?? 0) + (typeCounts["pipeline"] ?? 0) },
|
||||
{ label: t.projectOverview.data, color: "var(--color-node-table)", count: (typeCounts["table"] ?? 0) + (typeCounts["endpoint"] ?? 0) + (typeCounts["schema"] ?? 0) },
|
||||
{ label: t.projectOverview.domain, color: "var(--color-node-concept)", count: (typeCounts["domain"] ?? 0) + (typeCounts["flow"] ?? 0) + (typeCounts["step"] ?? 0) },
|
||||
];
|
||||
const hasNonCodeNodes = categoryBreakdown.some((c) => c.label !== "Code" && c.count > 0);
|
||||
const hasNonCodeNodes = categoryBreakdown.some((c) => c.label !== t.projectOverview.code && c.count > 0);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-auto p-5 animate-fade-slide-in">
|
||||
@@ -66,26 +64,26 @@ export default function ProjectOverview() {
|
||||
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
|
||||
<div className="text-2xl font-mono font-medium text-accent">{nodes.length}</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Nodes</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.nodes}</div>
|
||||
</div>
|
||||
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
|
||||
<div className="text-2xl font-mono font-medium text-accent">{edges.length}</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Edges</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.edges}</div>
|
||||
</div>
|
||||
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
|
||||
<div className="text-2xl font-mono font-medium text-accent">{layers.length}</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Layers</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.layers}</div>
|
||||
</div>
|
||||
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
|
||||
<div className="text-2xl font-mono font-medium text-accent">{Object.keys(typeCounts).length}</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Types</div>
|
||||
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.types}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Types breakdown */}
|
||||
{hasNonCodeNodes && (
|
||||
<div className="mb-5">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">File Types</h3>
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">{t.projectOverview.fileTypes}</h3>
|
||||
<div className="space-y-1.5">
|
||||
{categoryBreakdown.filter((c) => c.count > 0).map((cat) => (
|
||||
<div key={cat.label} className="flex items-center gap-2">
|
||||
@@ -104,7 +102,7 @@ export default function ProjectOverview() {
|
||||
{/* Languages */}
|
||||
{project.languages.length > 0 && (
|
||||
<div className="mb-5">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">Languages</h3>
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">{t.projectOverview.languages}</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.languages.map((lang) => (
|
||||
<span key={lang} className="text-[11px] glass text-text-secondary px-2.5 py-1 rounded-full">
|
||||
@@ -118,7 +116,7 @@ export default function ProjectOverview() {
|
||||
{/* Frameworks */}
|
||||
{project.frameworks.length > 0 && (
|
||||
<div className="mb-5">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">Frameworks</h3>
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">{t.projectOverview.frameworks}</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.frameworks.map((fw) => (
|
||||
<span key={fw} className="text-[11px] glass text-text-secondary px-2.5 py-1 rounded-full">
|
||||
@@ -131,7 +129,7 @@ export default function ProjectOverview() {
|
||||
|
||||
{/* Node Type Breakdown */}
|
||||
<div className="mb-5">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">Node Type Distribution</h3>
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">{t.projectOverview.nodeTypeDistribution}</h3>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(typeCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
@@ -158,19 +156,19 @@ export default function ProjectOverview() {
|
||||
{/* Complexity Breakdown */}
|
||||
{Object.values(complexityCounts).some((c) => c > 0) && (
|
||||
<div className="mb-5">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">Complexity Distribution</h3>
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">{t.projectOverview.complexityDistribution}</h3>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="bg-elevated rounded-lg p-2 border border-border-subtle text-center">
|
||||
<div className="text-lg font-mono font-medium text-green-400">{complexityCounts.simple}</div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">Simple</div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">{t.projectOverview.simple}</div>
|
||||
</div>
|
||||
<div className="bg-elevated rounded-lg p-2 border border-border-subtle text-center">
|
||||
<div className="text-lg font-mono font-medium text-yellow-400">{complexityCounts.moderate}</div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">Moderate</div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">{t.projectOverview.moderate}</div>
|
||||
</div>
|
||||
<div className="bg-elevated rounded-lg p-2 border border-border-subtle text-center">
|
||||
<div className="text-lg font-mono font-medium text-red-400">{complexityCounts.complex}</div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">Complex</div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">{t.projectOverview.complex}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +177,7 @@ export default function ProjectOverview() {
|
||||
{/* Top Connected Nodes */}
|
||||
{topNodes.length > 0 && (
|
||||
<div className="mb-5">
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">Most Connected Nodes</h3>
|
||||
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">{t.projectOverview.mostConnectedNodes}</h3>
|
||||
<div className="space-y-2">
|
||||
{topNodes.map((node, idx) => (
|
||||
<div
|
||||
@@ -200,14 +198,14 @@ export default function ProjectOverview() {
|
||||
{/* Average Connections */}
|
||||
<div className="mb-5 bg-elevated rounded-lg p-3 border border-border-subtle">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-secondary">Avg Connections per Node</span>
|
||||
<span className="text-xs text-text-secondary">{t.projectOverview.avgConnectionsPerNode}</span>
|
||||
<span className="text-lg font-mono font-medium text-accent">{avgConnections}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analyzed at */}
|
||||
<div className="text-[11px] text-text-muted mb-6">
|
||||
Analyzed: {new Date(project.analyzedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
{t.common.analyzed}: {new Date(project.analyzedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
|
||||
{/* Start Tour button */}
|
||||
@@ -216,7 +214,7 @@ export default function ProjectOverview() {
|
||||
onClick={startTour}
|
||||
className="w-full bg-accent/10 border border-accent/30 text-accent text-sm font-medium py-2.5 px-4 rounded-lg hover:bg-accent/20 transition-all duration-200"
|
||||
>
|
||||
Start Guided Tour
|
||||
{t.common.startGuidedTour}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
const typeBadgeColors: Record<string, string> = {
|
||||
file: "text-node-file border border-node-file/30 bg-node-file/10",
|
||||
@@ -28,6 +29,7 @@ export default function SearchBar() {
|
||||
const navigateToNodeInLayer = useDashboardStore((s) => s.navigateToNodeInLayer);
|
||||
const searchMode = useDashboardStore((s) => s.searchMode);
|
||||
const setSearchMode = useDashboardStore((s) => s.setSearchMode);
|
||||
const { t } = useI18n();
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -104,7 +106,8 @@ export default function SearchBar() {
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => setDropdownOpen(true)}
|
||||
placeholder="Search nodes by name, summary, or tags..."
|
||||
placeholder={t.search.placeholder}
|
||||
data-testid="search-input"
|
||||
className="flex-1 min-w-0 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-accent/50 placeholder-text-muted"
|
||||
/>
|
||||
<div className="flex items-center gap-1 bg-elevated rounded-lg p-0.5 shrink-0">
|
||||
@@ -116,7 +119,7 @@ export default function SearchBar() {
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Fuzzy
|
||||
{t.search.fuzzy}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSearchMode("semantic")}
|
||||
@@ -126,12 +129,12 @@ export default function SearchBar() {
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Semantic
|
||||
{t.search.semantic}
|
||||
</button>
|
||||
</div>
|
||||
{searchQuery.trim() && (
|
||||
<span className="hidden sm:inline text-xs text-text-muted shrink-0">
|
||||
{searchResults.length} result{searchResults.length !== 1 ? "s" : ""}{" "}
|
||||
{searchResults.length} {t.search.result}{searchResults.length !== 1 ? "s" : ""}{" "}
|
||||
<span className="text-text-muted">({searchMode})</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTheme, PRESETS } from "../themes/index.ts";
|
||||
import type { HeadingFont } from "../themes/index.ts";
|
||||
import { useI18n } from "../contexts/I18nContext";
|
||||
|
||||
export function ThemePicker() {
|
||||
const { config, preset, setPreset, setAccent, setHeadingFont } = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useI18n();
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
@@ -41,7 +43,7 @@ export function ThemePicker() {
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-text-secondary hover:text-text-primary transition-colors"
|
||||
title="Change theme"
|
||||
title={t.themePicker.changeTheme}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@@ -59,7 +61,7 @@ export function ThemePicker() {
|
||||
<circle cx="12" cy="7" r="1.5" fill="currentColor" />
|
||||
<circle cx="16" cy="10" r="1.5" fill="currentColor" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Theme</span>
|
||||
<span className="hidden sm:inline">{t.common.theme}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
@@ -67,7 +69,7 @@ export function ThemePicker() {
|
||||
{/* Presets */}
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold text-text-muted uppercase tracking-wider mb-2">
|
||||
Theme
|
||||
{t.themePicker.theme}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{PRESETS.map((p) => (
|
||||
@@ -119,7 +121,7 @@ export function ThemePicker() {
|
||||
{/* Accent swatches */}
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold text-text-muted uppercase tracking-wider mb-2">
|
||||
Accent Color
|
||||
{t.themePicker.accentColor}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{preset.accentSwatches.map((swatch) => (
|
||||
@@ -141,13 +143,13 @@ export function ThemePicker() {
|
||||
{/* Heading font */}
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold text-text-muted uppercase tracking-wider mb-2">
|
||||
Heading Font
|
||||
{t.themePicker.headingFont}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{([
|
||||
{ id: "serif" as HeadingFont, label: "Serif", sample: "Aa" },
|
||||
{ id: "sans" as HeadingFont, label: "Sans", sample: "Aa" },
|
||||
{ id: "mono" as HeadingFont, label: "Mono", sample: "Aa" },
|
||||
{ id: "serif" as HeadingFont, label: t.themePicker.serif, sample: "Aa" },
|
||||
{ id: "sans" as HeadingFont, label: t.themePicker.sans, sample: "Aa" },
|
||||
{ id: "mono" as HeadingFont, label: t.themePicker.mono, sample: "Aa" },
|
||||
]).map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
||||
import { getLocale, resolveLocaleKey, type Locale, type LocaleKey } from "../locales";
|
||||
|
||||
interface I18nContextValue {
|
||||
locale: Locale;
|
||||
localeKey: LocaleKey;
|
||||
t: Locale;
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
export function useI18n(): I18nContextValue {
|
||||
const ctx = useContext(I18nContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useI18n must be used within an I18nProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function I18nProvider({
|
||||
language,
|
||||
children,
|
||||
}: {
|
||||
language?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const localeKey = useMemo(() => resolveLocaleKey(language), [language]);
|
||||
const locale = useMemo(() => getLocale(localeKey), [localeKey]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
locale,
|
||||
localeKey,
|
||||
t: locale,
|
||||
}),
|
||||
[locale, localeKey]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={value}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
export const en = {
|
||||
common: {
|
||||
loading: "Loading project...",
|
||||
noGraphLoaded: "No graph loaded",
|
||||
selectNode: "Select a node to see details",
|
||||
back: "Back",
|
||||
focus: "Focus",
|
||||
unfocus: "Unfocus",
|
||||
openCode: "Open code",
|
||||
file: "File",
|
||||
tags: "Tags",
|
||||
connections: "Connections",
|
||||
filter: "Filter",
|
||||
resetAll: "Reset All",
|
||||
analyzed: "Analyzed",
|
||||
startGuidedTour: "Start Guided Tour",
|
||||
truncated: "(truncated)",
|
||||
preview: "Preview",
|
||||
doubleClickToOpen: "double-click to open",
|
||||
appName: "Understand Anything",
|
||||
pressKeyboard: "Press ? for keyboard shortcuts",
|
||||
path: "Path",
|
||||
theme: "Theme",
|
||||
},
|
||||
projectOverview: {
|
||||
nodes: "Nodes",
|
||||
edges: "Edges",
|
||||
layers: "Layers",
|
||||
types: "Types",
|
||||
fileTypes: "File Types",
|
||||
code: "Code",
|
||||
config: "Config",
|
||||
docs: "Docs",
|
||||
infra: "Infra",
|
||||
data: "Data",
|
||||
domain: "Domain",
|
||||
knowledge: "Knowledge",
|
||||
languages: "Languages",
|
||||
frameworks: "Frameworks",
|
||||
nodeTypeDistribution: "Node Type Distribution",
|
||||
complexityDistribution: "Complexity Distribution",
|
||||
simple: "Simple",
|
||||
moderate: "Moderate",
|
||||
complex: "Complex",
|
||||
mostConnectedNodes: "Most Connected Nodes",
|
||||
avgConnectionsPerNode: "Avg Connections per Node",
|
||||
},
|
||||
nodeInfo: {
|
||||
definedInThisFile: "Defined in this file",
|
||||
languageConcepts: "Language Concepts",
|
||||
category: "Category",
|
||||
wikilinks: "Wikilinks",
|
||||
backlinks: "Backlinks",
|
||||
entities: "Entities",
|
||||
businessRules: "Business Rules",
|
||||
crossDomain: "Cross-Domain",
|
||||
flows: "Flows",
|
||||
entryPoint: "Entry Point",
|
||||
steps: "Steps",
|
||||
implementation: "Implementation",
|
||||
},
|
||||
fileExplorer: {
|
||||
analyzedFiles: "Analyzed Files",
|
||||
filesFromGraph: "files from the current knowledge graph",
|
||||
noFilePathsFound: "No file paths found.",
|
||||
},
|
||||
filterPanel: {
|
||||
nodeTypes: "Node Types",
|
||||
complexity: "Complexity",
|
||||
layers: "Layers",
|
||||
edgeCategories: "Edge Categories",
|
||||
},
|
||||
personaSelector: {
|
||||
overview: "Overview",
|
||||
overviewDesc: "High-level architecture view",
|
||||
learn: "Learn",
|
||||
learnDesc: "Full dashboard with guided learning",
|
||||
deepDive: "Deep Dive",
|
||||
deepDiveDesc: "Code-focused with chat",
|
||||
},
|
||||
sidebar: {
|
||||
info: "Info",
|
||||
files: "Files",
|
||||
},
|
||||
mobile: {
|
||||
graph: "Graph",
|
||||
info: "Info",
|
||||
files: "Files",
|
||||
},
|
||||
drawer: {
|
||||
controls: "Controls",
|
||||
dashboard: "Dashboard",
|
||||
role: "Role",
|
||||
view: "View",
|
||||
diffOverlay: "Diff overlay",
|
||||
nodeTypes: "Node types",
|
||||
layers: "Layers",
|
||||
tools: "Tools",
|
||||
path: "Path",
|
||||
help: "Help",
|
||||
structural: "Structural",
|
||||
domain: "Domain",
|
||||
},
|
||||
domainView: {
|
||||
backToDomains: "Back to domains",
|
||||
},
|
||||
detailLevel: {
|
||||
filesTitle: "Files only — architecture-level dependencies (fast)",
|
||||
classesTitle: "Files + Classes — code structure with inheritance",
|
||||
files: "Files",
|
||||
classes: "+Classes",
|
||||
fnTitle: "Toggle function nodes (may slow down rendering)",
|
||||
fn: "fn",
|
||||
},
|
||||
nodeTypeLabels: {
|
||||
all: "All",
|
||||
code: "Code",
|
||||
config: "Config",
|
||||
docs: "Docs",
|
||||
infra: "Infra",
|
||||
data: "Data",
|
||||
domain: "Domain",
|
||||
knowledge: "Knowledge",
|
||||
},
|
||||
tokenGate: {
|
||||
validating: "Validating...",
|
||||
continue: "Continue",
|
||||
},
|
||||
diffToggle: {
|
||||
hideOverlay: "Hide diff overlay",
|
||||
showOverlay: "Show diff overlay",
|
||||
noData: "No diff data loaded",
|
||||
changed: "Changed",
|
||||
affected: "Affected",
|
||||
},
|
||||
learnPanel: {
|
||||
finish: "Finish",
|
||||
next: "Next",
|
||||
prev: "Prev",
|
||||
noTour: "No tour available",
|
||||
noTourHint: "Generate a tour from your knowledge graph to get a guided walkthrough",
|
||||
projectTour: "Project Tour",
|
||||
steps: "steps",
|
||||
stepsTitle: "Steps",
|
||||
guidedWalkthrough: "Guided walkthrough of the codebase",
|
||||
startTour: "Start Tour",
|
||||
tour: "Tour",
|
||||
exitTour: "Exit Tour",
|
||||
},
|
||||
layer: {
|
||||
defaultName: "Layer",
|
||||
label: "layers",
|
||||
},
|
||||
breadcrumb: {
|
||||
projectOverview: "Project Overview",
|
||||
project: "Project",
|
||||
escBack: "Esc to go back",
|
||||
},
|
||||
warningBanner: {
|
||||
dropped: "Dropped",
|
||||
fatal: "Fatal",
|
||||
},
|
||||
themePicker: {
|
||||
changeTheme: "Change theme",
|
||||
theme: "Theme",
|
||||
accentColor: "Accent Color",
|
||||
headingFont: "Heading Font",
|
||||
serif: "Serif",
|
||||
sans: "Sans",
|
||||
mono: "Mono",
|
||||
},
|
||||
codeViewer: {
|
||||
fullFile: "Full file",
|
||||
lines: "Lines",
|
||||
linesLabel: "lines",
|
||||
noFile: "No file selected",
|
||||
loading: "Loading source...",
|
||||
openLarger: "Open larger code viewer",
|
||||
closeExpanded: "Close expanded code viewer",
|
||||
closeViewer: "Close code viewer",
|
||||
sourceUnavailable: "Source unavailable",
|
||||
},
|
||||
customNode: {
|
||||
tested: "Tested",
|
||||
hasTests: "Has tests",
|
||||
},
|
||||
ariaLabels: {
|
||||
openMenu: "Open menu",
|
||||
closeMenu: "Close menu",
|
||||
settings: "Settings",
|
||||
hideSearch: "Hide search",
|
||||
showSearch: "Show search",
|
||||
},
|
||||
nodeTypeFilter: {
|
||||
hide: "Hide",
|
||||
show: "Show",
|
||||
nodesLabel: "nodes",
|
||||
},
|
||||
keyboardShortcuts: {
|
||||
showHelp: "Show keyboard shortcuts",
|
||||
general: "General",
|
||||
navigation: "Navigation",
|
||||
tour: "Tour",
|
||||
view: "View",
|
||||
focusSearch: "Focus search bar",
|
||||
nextStep: "Next tour step",
|
||||
prevStep: "Previous tour step",
|
||||
toggleDiff: "Toggle diff mode",
|
||||
toggleFilter: "Toggle filter panel",
|
||||
toggleExport: "Toggle export menu",
|
||||
openPathFinder: "Open path finder",
|
||||
title: "Keyboard Shortcuts",
|
||||
toggleHint: "Press ? anytime to toggle this help",
|
||||
closeHint: "Press ESC to close",
|
||||
escapeDesc: "Close panels and modals / go back to overview",
|
||||
},
|
||||
search: {
|
||||
placeholder: "Search nodes by name, summary, or tags...",
|
||||
fuzzy: "Fuzzy",
|
||||
semantic: "Semantic",
|
||||
result: "result",
|
||||
},
|
||||
export: {
|
||||
label: "Export",
|
||||
title: "Export graph (E)",
|
||||
asPNG: "Export as PNG",
|
||||
asSVG: "Export as SVG",
|
||||
asJSON: "Export as JSON",
|
||||
},
|
||||
edgeLabels: {
|
||||
imports: { forward: "imports", backward: "imported by" },
|
||||
exports: { forward: "exports to", backward: "exported by" },
|
||||
contains: { forward: "contains", backward: "contained in" },
|
||||
inherits: { forward: "inherits from", backward: "inherited by" },
|
||||
implements: { forward: "implements", backward: "implemented by" },
|
||||
calls: { forward: "calls", backward: "called by" },
|
||||
subscribes: { forward: "subscribes to", backward: "subscribed by" },
|
||||
publishes: { forward: "publishes to", backward: "consumed by" },
|
||||
middleware: { forward: "middleware for", backward: "uses middleware" },
|
||||
reads_from: { forward: "reads from", backward: "read by" },
|
||||
writes_to: { forward: "writes to", backward: "written by" },
|
||||
transforms: { forward: "transforms", backward: "transformed by" },
|
||||
validates: { forward: "validates", backward: "validated by" },
|
||||
depends_on: { forward: "depends on", backward: "depended on by" },
|
||||
tested_by: { forward: "tested by", backward: "tests" },
|
||||
configures: { forward: "configures", backward: "configured by" },
|
||||
related: { forward: "related to", backward: "related to" },
|
||||
similar_to: { forward: "similar to", backward: "similar to" },
|
||||
deploys: { forward: "deploys", backward: "deployed by" },
|
||||
serves: { forward: "serves", backward: "served by" },
|
||||
migrates: { forward: "migrates", backward: "migrated by" },
|
||||
documents: { forward: "documents", backward: "documented by" },
|
||||
provisions: { forward: "provisions", backward: "provisioned by" },
|
||||
routes: { forward: "routes to", backward: "routed from" },
|
||||
defines_schema: { forward: "defines schema for", backward: "schema defined by" },
|
||||
triggers: { forward: "triggers", backward: "triggered by" },
|
||||
contains_flow: { forward: "contains flow", backward: "flow in" },
|
||||
flow_step: { forward: "flow step", backward: "step of" },
|
||||
cross_domain: { forward: "cross-domain to", backward: "cross-domain from" },
|
||||
cites: { forward: "cites", backward: "cited by" },
|
||||
contradicts: { forward: "contradicts", backward: "contradicted by" },
|
||||
builds_on: { forward: "builds on", backward: "built upon by" },
|
||||
exemplifies: { forward: "exemplifies", backward: "exemplified by" },
|
||||
categorized_under: { forward: "categorized under", backward: "categorizes" },
|
||||
authored_by: { forward: "authored by", backward: "authored" },
|
||||
},
|
||||
pathFinder: {
|
||||
title: "Find path between nodes (P)",
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -0,0 +1,35 @@
|
||||
import en from "./en";
|
||||
import zh from "./zh";
|
||||
import zhTW from "./zh-TW";
|
||||
import ja from "./ja";
|
||||
import ko from "./ko";
|
||||
import ru from "./ru";
|
||||
|
||||
export type LocaleKey = "en" | "zh" | "zh-TW" | "ja" | "ko" | "ru";
|
||||
export type Locale = typeof en;
|
||||
|
||||
export const locales: Record<LocaleKey, Locale> = {
|
||||
en,
|
||||
zh,
|
||||
"zh-TW": zhTW,
|
||||
ja,
|
||||
ko,
|
||||
ru,
|
||||
};
|
||||
|
||||
export function getLocale(key: LocaleKey): Locale {
|
||||
return locales[key] ?? locales.en;
|
||||
}
|
||||
|
||||
export function resolveLocaleKey(lang: string | undefined): LocaleKey {
|
||||
if (!lang) return "en";
|
||||
const normalized = lang.toLowerCase().replace(/[_\s]/g, "-");
|
||||
if (normalized === "zh" || normalized === "chinese" || normalized === "zh-cn") return "zh";
|
||||
if (normalized === "zh-tw" || normalized === "traditional-chinese") return "zh-TW";
|
||||
if (normalized === "ja" || normalized === "japanese") return "ja";
|
||||
if (normalized === "ko" || normalized === "korean") return "ko";
|
||||
if (normalized === "ru" || normalized === "russian" || normalized === "ru-ru") return "ru";
|
||||
return "en";
|
||||
}
|
||||
|
||||
export { en, zh, zhTW as "zh-TW", ja, ko, ru };
|
||||
@@ -0,0 +1,272 @@
|
||||
export const ja = {
|
||||
common: {
|
||||
loading: "プロジェクトを読み込み中...",
|
||||
noGraphLoaded: "知識グラフが読み込まれていません",
|
||||
selectNode: "ノードを選択して詳細を表示",
|
||||
back: "戻る",
|
||||
focus: "フォーカス",
|
||||
unfocus: "フォーカス解除",
|
||||
openCode: "コードを開く",
|
||||
file: "ファイル",
|
||||
tags: "タグ",
|
||||
connections: "接続",
|
||||
filter: "フィルター",
|
||||
resetAll: "すべてリセット",
|
||||
analyzed: "分析日時",
|
||||
startGuidedTour: "ガイド付きツアーを開始",
|
||||
truncated: "(省略)",
|
||||
preview: "プレビュー",
|
||||
doubleClickToOpen: "ダブルクリックで開く",
|
||||
appName: "Understand Anything",
|
||||
pressKeyboard: "? を押してキーボードショートカットを表示",
|
||||
path: "パス",
|
||||
theme: "テーマ",
|
||||
},
|
||||
projectOverview: {
|
||||
nodes: "ノード",
|
||||
edges: "エッジ",
|
||||
layers: "レイヤー",
|
||||
types: "タイプ",
|
||||
fileTypes: "ファイルタイプ",
|
||||
code: "コード",
|
||||
config: "設定",
|
||||
docs: "ドキュメント",
|
||||
infra: "インフラ",
|
||||
data: "データ",
|
||||
domain: "ドメイン",
|
||||
knowledge: "ナレッジ",
|
||||
languages: "プログラミング言語",
|
||||
frameworks: "フレームワーク",
|
||||
nodeTypeDistribution: "ノードタイプ分布",
|
||||
complexityDistribution: "複雑度分布",
|
||||
simple: "単純",
|
||||
moderate: "中程度",
|
||||
complex: "複雑",
|
||||
mostConnectedNodes: "最も接続されているノード",
|
||||
avgConnectionsPerNode: "ノード平均接続数",
|
||||
},
|
||||
nodeInfo: {
|
||||
definedInThisFile: "このファイルで定義",
|
||||
languageConcepts: "言語概念",
|
||||
category: "カテゴリ",
|
||||
wikilinks: "Wikilinks",
|
||||
backlinks: "Backlinks",
|
||||
entities: "エンティティ",
|
||||
businessRules: "ビジネスルール",
|
||||
crossDomain: "クロスドメイン",
|
||||
flows: "フロー",
|
||||
entryPoint: "エントリポイント",
|
||||
steps: "ステップ",
|
||||
implementation: "実装",
|
||||
},
|
||||
fileExplorer: {
|
||||
analyzedFiles: "分析済みファイル",
|
||||
filesFromGraph: "現在の知識グラフからのファイル",
|
||||
noFilePathsFound: "ファイルパスが見つかりません。",
|
||||
},
|
||||
filterPanel: {
|
||||
nodeTypes: "ノードタイプ",
|
||||
complexity: "複雑度",
|
||||
layers: "レイヤー",
|
||||
edgeCategories: "エッジカテゴリ",
|
||||
},
|
||||
personaSelector: {
|
||||
overview: "概要",
|
||||
overviewDesc: "高レベルアーキテクチャビュー",
|
||||
learn: "学習",
|
||||
learnDesc: "ガイド付き学習付き完全ダッシュボード",
|
||||
deepDive: "詳細",
|
||||
deepDiveDesc: "コード中心のチャット",
|
||||
},
|
||||
sidebar: {
|
||||
info: "情報",
|
||||
files: "ファイル",
|
||||
},
|
||||
mobile: {
|
||||
graph: "グラフ",
|
||||
info: "情報",
|
||||
files: "ファイル",
|
||||
},
|
||||
drawer: {
|
||||
controls: "コントロール",
|
||||
dashboard: "ダッシュボード",
|
||||
role: "ロール",
|
||||
view: "ビュー",
|
||||
diffOverlay: "差分オーバーレイ",
|
||||
nodeTypes: "ノードタイプ",
|
||||
layers: "レイヤー",
|
||||
tools: "ツール",
|
||||
path: "パス",
|
||||
help: "ヘルプ",
|
||||
structural: "構造",
|
||||
domain: "ドメイン",
|
||||
},
|
||||
domainView: {
|
||||
backToDomains: "ドメインに戻る",
|
||||
},
|
||||
detailLevel: {
|
||||
filesTitle: "ファイルのみ — アーキテクチャレベルの依存関係(高速)",
|
||||
classesTitle: "ファイル + クラス — 継承を含むコード構造",
|
||||
files: "ファイル",
|
||||
classes: "+クラス",
|
||||
fnTitle: "関数ノードを切り替え(レンダリングが遅くなる可能性)",
|
||||
fn: "fn",
|
||||
},
|
||||
nodeTypeLabels: {
|
||||
all: "すべて",
|
||||
code: "コード",
|
||||
config: "設定",
|
||||
docs: "ドキュメント",
|
||||
infra: "インフラ",
|
||||
data: "データ",
|
||||
domain: "ドメイン",
|
||||
knowledge: "ナレッジ",
|
||||
},
|
||||
tokenGate: {
|
||||
validating: "検証中...",
|
||||
continue: "続行",
|
||||
},
|
||||
diffToggle: {
|
||||
hideOverlay: "差分オーバーレイを非表示",
|
||||
showOverlay: "差分オーバーレイを表示",
|
||||
noData: "差分データが読み込まれていません",
|
||||
changed: "変更済み",
|
||||
affected: "影響あり",
|
||||
},
|
||||
learnPanel: {
|
||||
finish: "完了",
|
||||
next: "次へ",
|
||||
prev: "前へ",
|
||||
noTour: "ツアーがありません",
|
||||
noTourHint: "知識グラフからツアーを生成してコードベースのガイド付きウォークスルーを取得",
|
||||
projectTour: "プロジェクトツアー",
|
||||
steps: "ステップ",
|
||||
stepsTitle: "ステップ",
|
||||
guidedWalkthrough: "コードベースのガイド付きウォークスルー",
|
||||
startTour: "ツアー開始",
|
||||
tour: "ツアー",
|
||||
exitTour: "ツアー終了",
|
||||
},
|
||||
layer: {
|
||||
defaultName: "レイヤー",
|
||||
label: "レイヤー",
|
||||
},
|
||||
breadcrumb: {
|
||||
projectOverview: "プロジェクト概要",
|
||||
project: "プロジェクト",
|
||||
escBack: "Escで戻る",
|
||||
},
|
||||
warningBanner: {
|
||||
dropped: "削除済み",
|
||||
fatal: "致命的",
|
||||
},
|
||||
themePicker: {
|
||||
changeTheme: "テーマ変更",
|
||||
theme: "テーマ",
|
||||
accentColor: "アクセント色",
|
||||
headingFont: "見出しフォント",
|
||||
serif: "セリフ",
|
||||
sans: "サン",
|
||||
mono: "モノ",
|
||||
},
|
||||
codeViewer: {
|
||||
fullFile: "ファイル全体",
|
||||
lines: "行",
|
||||
linesLabel: "行",
|
||||
noFile: "ファイル未選択",
|
||||
loading: "ソース読み込み中...",
|
||||
openLarger: "大きなコードビューアを開く",
|
||||
closeExpanded: "展開したコードビューアを閉じる",
|
||||
closeViewer: "コードビューアを閉じる",
|
||||
sourceUnavailable: "ソースが利用できません",
|
||||
},
|
||||
customNode: {
|
||||
tested: "テスト済み",
|
||||
hasTests: "テストあり",
|
||||
},
|
||||
ariaLabels: {
|
||||
openMenu: "メニューを開く",
|
||||
closeMenu: "メニューを閉じる",
|
||||
settings: "設定",
|
||||
hideSearch: "検索を非表示",
|
||||
showSearch: "検索を表示",
|
||||
},
|
||||
nodeTypeFilter: {
|
||||
hide: "非表示",
|
||||
show: "表示",
|
||||
nodesLabel: "ノード",
|
||||
},
|
||||
keyboardShortcuts: {
|
||||
showHelp: "キーボードショートカットを表示",
|
||||
general: "一般",
|
||||
navigation: "ナビゲーション",
|
||||
tour: "ツアー",
|
||||
view: "ビュー",
|
||||
focusSearch: "検索バーにフォーカス",
|
||||
nextStep: "次のツアーステップ",
|
||||
prevStep: "前のツアーステップ",
|
||||
toggleDiff: "差分モード切り替え",
|
||||
toggleFilter: "フィルターパネル切り替え",
|
||||
toggleExport: "エクスポートメニュー切り替え",
|
||||
openPathFinder: "パスファインダーを開く",
|
||||
title: "キーボードショートカット",
|
||||
toggleHint: "いつでも ? を押してこのヘルプを切り替え",
|
||||
closeHint: "ESC を押して閉じる",
|
||||
escapeDesc: "パネルとモーダルを閉じる / 概要に戻る",
|
||||
},
|
||||
search: {
|
||||
placeholder: "ノード名、概要、タグで検索...",
|
||||
fuzzy: "ファジー",
|
||||
semantic: "セマンティック",
|
||||
result: "結果",
|
||||
},
|
||||
export: {
|
||||
label: "エクスポート",
|
||||
title: "グラフをエクスポート (E)",
|
||||
asPNG: "PNGでエクスポート",
|
||||
asSVG: "SVGでエクスポート",
|
||||
asJSON: "JSONでエクスポート",
|
||||
},
|
||||
edgeLabels: {
|
||||
imports: { forward: "インポート", backward: "インポートされる" },
|
||||
exports: { forward: "エクスポート", backward: "エクスポートされる" },
|
||||
contains: { forward: "含む", backward: "含まれる" },
|
||||
inherits: { forward: "継承", backward: "継承される" },
|
||||
implements: { forward: "実装", backward: "実装される" },
|
||||
calls: { forward: "呼び出す", backward: "呼び出される" },
|
||||
subscribes: { forward: "購読", backward: "購読される" },
|
||||
publishes: { forward: "公開", backward: "消費される" },
|
||||
middleware: { forward: "ミドルウェア", backward: "ミドルウェアを使用" },
|
||||
reads_from: { forward: "読み取り", backward: "読み取られる" },
|
||||
writes_to: { forward: "書き込み", backward: "書き込まれる" },
|
||||
transforms: { forward: "変換", backward: "変換される" },
|
||||
validates: { forward: "検証", backward: "検証される" },
|
||||
depends_on: { forward: "依存", backward: "依存される" },
|
||||
tested_by: { forward: "テストされる", backward: "テスト" },
|
||||
configures: { forward: "設定", backward: "設定される" },
|
||||
related: { forward: "関連", backward: "関連" },
|
||||
similar_to: { forward: "類似", backward: "類似" },
|
||||
deploys: { forward: "デプロイ", backward: "デプロイされる" },
|
||||
serves: { forward: "提供", backward: "提供される" },
|
||||
migrates: { forward: "移行", backward: "移行される" },
|
||||
documents: { forward: "ドキュメント化", backward: "ドキュメント化される" },
|
||||
provisions: { forward: "提供", backward: "提供される" },
|
||||
routes: { forward: "ルーティング", backward: "ルーティングされる" },
|
||||
defines_schema: { forward: "スキーマ定義", backward: "スキーマ定義される" },
|
||||
triggers: { forward: "トリガー", backward: "トリガーされる" },
|
||||
contains_flow: { forward: "フローを含む", backward: "フロー内" },
|
||||
flow_step: { forward: "フローステップ", backward: "ステップの" },
|
||||
cross_domain: { forward: "クロスドメイン", backward: "クロスドメインから" },
|
||||
cites: { forward: "引用", backward: "引用される" },
|
||||
contradicts: { forward: "矛盾", backward: "矛盾される" },
|
||||
builds_on: { forward: "基礎", backward: "基礎となる" },
|
||||
exemplifies: { forward: "例示", backward: "例示される" },
|
||||
categorized_under: { forward: "カテゴリ化", backward: "カテゴリ化する" },
|
||||
authored_by: { forward: "作成者", backward: "作成" },
|
||||
},
|
||||
pathFinder: {
|
||||
title: "ノード間のパスを検索 (P)",
|
||||
},
|
||||
};
|
||||
|
||||
export default ja;
|
||||
@@ -0,0 +1,272 @@
|
||||
export const ko = {
|
||||
common: {
|
||||
loading: "프로젝트 로딩 중...",
|
||||
noGraphLoaded: "지식 그래프가 로드되지 않음",
|
||||
selectNode: "노드를 선택하여 상세 정보 확인",
|
||||
back: "뒤로",
|
||||
focus: "포커스",
|
||||
unfocus: "포커스 해제",
|
||||
openCode: "코드 열기",
|
||||
file: "파일",
|
||||
tags: "태그",
|
||||
connections: "연결",
|
||||
filter: "필터",
|
||||
resetAll: "모두 재설정",
|
||||
analyzed: "분석 시간",
|
||||
startGuidedTour: "가이드 투어 시작",
|
||||
truncated: "(생략)",
|
||||
preview: "미리보기",
|
||||
doubleClickToOpen: "두 번 클릭하여 열기",
|
||||
appName: "Understand Anything",
|
||||
pressKeyboard: "? 키를 눌러 키보드 단축키 보기",
|
||||
path: "경로",
|
||||
theme: "테마",
|
||||
},
|
||||
projectOverview: {
|
||||
nodes: "노드",
|
||||
edges: "엣지",
|
||||
layers: "레이어",
|
||||
types: "타입",
|
||||
fileTypes: "파일 타입",
|
||||
code: "코드",
|
||||
config: "설정",
|
||||
docs: "문서",
|
||||
infra: "인프라",
|
||||
data: "데이터",
|
||||
domain: "도메인",
|
||||
knowledge: "지식",
|
||||
languages: "프로그래밍 언어",
|
||||
frameworks: "프레임워크",
|
||||
nodeTypeDistribution: "노드 타입 분포",
|
||||
complexityDistribution: "복잡도 분포",
|
||||
simple: "단순",
|
||||
moderate: "중간",
|
||||
complex: "복잡",
|
||||
mostConnectedNodes: "가장 많이 연결된 노드",
|
||||
avgConnectionsPerNode: "노드 평균 연결 수",
|
||||
},
|
||||
nodeInfo: {
|
||||
definedInThisFile: "이 파일에 정義",
|
||||
languageConcepts: "언어 개념",
|
||||
category: "카테고리",
|
||||
wikilinks: "Wikilinks",
|
||||
backlinks: "Backlinks",
|
||||
entities: "엔티티",
|
||||
businessRules: "비즈니스 규칙",
|
||||
crossDomain: "크로스 도메인",
|
||||
flows: "플로우",
|
||||
entryPoint: "진입점",
|
||||
steps: "단계",
|
||||
implementation: "구현",
|
||||
},
|
||||
fileExplorer: {
|
||||
analyzedFiles: "분석된 파일",
|
||||
filesFromGraph: "현재 지식 그래프의 파일",
|
||||
noFilePathsFound: "파일 경로를 찾을 수 없습니다.",
|
||||
},
|
||||
filterPanel: {
|
||||
nodeTypes: "노드 타입",
|
||||
complexity: "복잡도",
|
||||
layers: "레이어",
|
||||
edgeCategories: "엣지 카테고리",
|
||||
},
|
||||
personaSelector: {
|
||||
overview: "개요",
|
||||
overviewDesc: "고수준 아키텍처 뷰",
|
||||
learn: "학습",
|
||||
learnDesc: "가이드 학습 포함 완전 대시보드",
|
||||
deepDive: "심층",
|
||||
deepDiveDesc: "코드 중심 채팅",
|
||||
},
|
||||
sidebar: {
|
||||
info: "정보",
|
||||
files: "파일",
|
||||
},
|
||||
mobile: {
|
||||
graph: "그래프",
|
||||
info: "정보",
|
||||
files: "파일",
|
||||
},
|
||||
drawer: {
|
||||
controls: "컨트롤",
|
||||
dashboard: "대시보드",
|
||||
role: "역할",
|
||||
view: "보기",
|
||||
diffOverlay: "차분 오버레이",
|
||||
nodeTypes: "노드 타입",
|
||||
layers: "레이어",
|
||||
tools: "도구",
|
||||
path: "경로",
|
||||
help: "도움말",
|
||||
structural: "구조",
|
||||
domain: "도메인",
|
||||
},
|
||||
domainView: {
|
||||
backToDomains: "도메인으로 돌아가기",
|
||||
},
|
||||
detailLevel: {
|
||||
filesTitle: "파일만 — 아키텍처 레벨 의존성 (빠름)",
|
||||
classesTitle: "파일 + 클래스 — 상속 포함 코드 구조",
|
||||
files: "파일",
|
||||
classes: "+클래스",
|
||||
fnTitle: "함수 노드 토글 (렌더링 속도 저하 가능)",
|
||||
fn: "fn",
|
||||
},
|
||||
nodeTypeLabels: {
|
||||
all: "모두",
|
||||
code: "코드",
|
||||
config: "설정",
|
||||
docs: "문서",
|
||||
infra: "인프라",
|
||||
data: "데이터",
|
||||
domain: "도메인",
|
||||
knowledge: "지식",
|
||||
},
|
||||
tokenGate: {
|
||||
validating: "검증 중...",
|
||||
continue: "계속",
|
||||
},
|
||||
diffToggle: {
|
||||
hideOverlay: "차분 오버레이 숨기기",
|
||||
showOverlay: "차분 오버레이 표시",
|
||||
noData: "차분 데이터가 로드되지 않음",
|
||||
changed: "변경됨",
|
||||
affected: "영향받음",
|
||||
},
|
||||
learnPanel: {
|
||||
finish: "완료",
|
||||
next: "다음",
|
||||
prev: "이전",
|
||||
noTour: "투어 없음",
|
||||
noTourHint: "지식 그래프에서 투어를 생성하여 코드베이스의 가이드 워크스루를 얻으세요",
|
||||
projectTour: "프로젝트 투어",
|
||||
steps: "단계",
|
||||
stepsTitle: "단계",
|
||||
guidedWalkthrough: "코드베이스 가이드 워크스루",
|
||||
startTour: "투어 시작",
|
||||
tour: "투어",
|
||||
exitTour: "투어 종료",
|
||||
},
|
||||
layer: {
|
||||
defaultName: "레이어",
|
||||
label: "레이어",
|
||||
},
|
||||
breadcrumb: {
|
||||
projectOverview: "프로젝트 개요",
|
||||
project: "프로젝트",
|
||||
escBack: "Esc로 돌아가기",
|
||||
},
|
||||
warningBanner: {
|
||||
dropped: "삭제됨",
|
||||
fatal: "치명적",
|
||||
},
|
||||
themePicker: {
|
||||
changeTheme: "테마 변경",
|
||||
theme: "테마",
|
||||
accentColor: "강조색",
|
||||
headingFont: "제목 폰트",
|
||||
serif: "세리프",
|
||||
sans: "산스",
|
||||
mono: "모노",
|
||||
},
|
||||
codeViewer: {
|
||||
fullFile: "전체 파일",
|
||||
lines: "행",
|
||||
linesLabel: "행",
|
||||
noFile: "파일 선택 안 됨",
|
||||
loading: "소스 로딩 중...",
|
||||
openLarger: "더 큰 코드 뷰어 열기",
|
||||
closeExpanded: "확장된 코드 뷰어 닫기",
|
||||
closeViewer: "코드 뷰어 닫기",
|
||||
sourceUnavailable: "소스 사용 불가",
|
||||
},
|
||||
customNode: {
|
||||
tested: "테스트됨",
|
||||
hasTests: "테스트 있음",
|
||||
},
|
||||
ariaLabels: {
|
||||
openMenu: "메뉴 열기",
|
||||
closeMenu: "메뉴 닫기",
|
||||
settings: "설정",
|
||||
hideSearch: "검색 숨기기",
|
||||
showSearch: "검색 표시",
|
||||
},
|
||||
nodeTypeFilter: {
|
||||
hide: "숨기기",
|
||||
show: "표시",
|
||||
nodesLabel: "노드",
|
||||
},
|
||||
keyboardShortcuts: {
|
||||
showHelp: "키보드 단축키 표시",
|
||||
general: "일반",
|
||||
navigation: "탐색",
|
||||
tour: "투어",
|
||||
view: "보기",
|
||||
focusSearch: "검색창 포커스",
|
||||
nextStep: "다음 투어 단계",
|
||||
prevStep: "이전 투어 단계",
|
||||
toggleDiff: "차분 모드 전환",
|
||||
toggleFilter: "필터 패널 전환",
|
||||
toggleExport: "내보내기 메뉴 전환",
|
||||
openPathFinder: "경로 찾기 열기",
|
||||
title: "키보드 단축키",
|
||||
toggleHint: "언제든 ?를 눌러 이 도움말을 토글",
|
||||
closeHint: "ESC를 눌러 닫기",
|
||||
escapeDesc: "패널 및 모달 닫기 / 개요로 돌아가기",
|
||||
},
|
||||
search: {
|
||||
placeholder: "노드 이름, 요약, 태그로 검색...",
|
||||
fuzzy: "퍼지",
|
||||
semantic: "시맨틱",
|
||||
result: "결과",
|
||||
},
|
||||
export: {
|
||||
label: "내보내기",
|
||||
title: "그래프 내보내기 (E)",
|
||||
asPNG: "PNG로 내보내기",
|
||||
asSVG: "SVG로 내보내기",
|
||||
asJSON: "JSON으로 내보내기",
|
||||
},
|
||||
edgeLabels: {
|
||||
imports: { forward: "임포트", backward: "임포트됨" },
|
||||
exports: { forward: "내보내기", backward: "내보내기됨" },
|
||||
contains: { forward: "포함", backward: "포함됨" },
|
||||
inherits: { forward: "상속", backward: "상속됨" },
|
||||
implements: { forward: "구현", backward: "구현됨" },
|
||||
calls: { forward: "호출", backward: "호출됨" },
|
||||
subscribes: { forward: "구독", backward: "구독됨" },
|
||||
publishes: { forward: "게시", backward: "소비됨" },
|
||||
middleware: { forward: "미들웨어", backward: "미들웨어 사용" },
|
||||
reads_from: { forward: "읽기", backward: "읽기됨" },
|
||||
writes_to: { forward: "쓰기", backward: "쓰기됨" },
|
||||
transforms: { forward: "변환", backward: "변환됨" },
|
||||
validates: { forward: "검증", backward: "검증됨" },
|
||||
depends_on: { forward: "종속", backward: "종속됨" },
|
||||
tested_by: { forward: "테스트됨", backward: "테스트" },
|
||||
configures: { forward: "설정", backward: "설정됨" },
|
||||
related: { forward: "관련", backward: "관련" },
|
||||
similar_to: { forward: "유사", backward: "유사" },
|
||||
deploys: { forward: "배포", backward: "배포됨" },
|
||||
serves: { forward: "서비스", backward: "서비스됨" },
|
||||
migrates: { forward: "마이그레이션", backward: "마이그레이션됨" },
|
||||
documents: { forward: "문서화", backward: "문서화됨" },
|
||||
provisions: { forward: "제공", backward: "제공됨" },
|
||||
routes: { forward: "라우팅", backward: "라우팅됨" },
|
||||
defines_schema: { forward: "스키마 정의", backward: "스키마 정의됨" },
|
||||
triggers: { forward: "트리거", backward: "트리거됨" },
|
||||
contains_flow: { forward: "플로우 포함", backward: "플로우 내" },
|
||||
flow_step: { forward: "플로우 단계", backward: "단계의" },
|
||||
cross_domain: { forward: "크로스 도메인", backward: "크로스 도메인에서" },
|
||||
cites: { forward: "인용", backward: "인용됨" },
|
||||
contradicts: { forward: "반박", backward: "반박됨" },
|
||||
builds_on: { forward: "기반", backward: "기반됨" },
|
||||
exemplifies: { forward: "예시", backward: "예시됨" },
|
||||
categorized_under: { forward: "카테고리화", backward: "카테고리화함" },
|
||||
authored_by: { forward: "작성자", backward: "작성" },
|
||||
},
|
||||
pathFinder: {
|
||||
title: "노드 간 경로 찾기 (P)",
|
||||
},
|
||||
};
|
||||
|
||||
export default ko;
|
||||
@@ -0,0 +1,272 @@
|
||||
export const ru = {
|
||||
common: {
|
||||
loading: "Загрузка проекта...",
|
||||
noGraphLoaded: "Граф знаний не загружен",
|
||||
selectNode: "Выберите узел, чтобы увидеть подробности",
|
||||
back: "Назад",
|
||||
focus: "Фокус",
|
||||
unfocus: "Снять фокус",
|
||||
openCode: "Открыть код",
|
||||
file: "Файл",
|
||||
tags: "Теги",
|
||||
connections: "Связи",
|
||||
filter: "Фильтр",
|
||||
resetAll: "Сбросить всё",
|
||||
analyzed: "Проанализировано",
|
||||
startGuidedTour: "Начать обзор",
|
||||
truncated: "(сокращено)",
|
||||
preview: "Предпросмотр",
|
||||
doubleClickToOpen: "двойной клик, чтобы открыть",
|
||||
appName: "Understand Anything",
|
||||
pressKeyboard: "Нажмите ? для горячих клавиш",
|
||||
path: "Путь",
|
||||
theme: "Тема",
|
||||
},
|
||||
projectOverview: {
|
||||
nodes: "Узлы",
|
||||
edges: "Рёбра",
|
||||
layers: "Слои",
|
||||
types: "Типы",
|
||||
fileTypes: "Типы файлов",
|
||||
code: "Код",
|
||||
config: "Конфиг",
|
||||
docs: "Документация",
|
||||
infra: "Инфраструктура",
|
||||
data: "Данные",
|
||||
domain: "Домен",
|
||||
knowledge: "Знания",
|
||||
languages: "Языки",
|
||||
frameworks: "Фреймворки",
|
||||
nodeTypeDistribution: "Распределение типов узлов",
|
||||
complexityDistribution: "Распределение сложности",
|
||||
simple: "Простой",
|
||||
moderate: "Средний",
|
||||
complex: "Сложный",
|
||||
mostConnectedNodes: "Самые связанные узлы",
|
||||
avgConnectionsPerNode: "Среднее число связей на узел",
|
||||
},
|
||||
nodeInfo: {
|
||||
definedInThisFile: "Определено в этом файле",
|
||||
languageConcepts: "Концепции языка",
|
||||
category: "Категория",
|
||||
wikilinks: "Wiki-ссылки",
|
||||
backlinks: "Обратные ссылки",
|
||||
entities: "Сущности",
|
||||
businessRules: "Бизнес-правила",
|
||||
crossDomain: "Междоменные связи",
|
||||
flows: "Потоки",
|
||||
entryPoint: "Точка входа",
|
||||
steps: "Шаги",
|
||||
implementation: "Реализация",
|
||||
},
|
||||
fileExplorer: {
|
||||
analyzedFiles: "Проанализированные файлы",
|
||||
filesFromGraph: "файлы из текущего графа знаний",
|
||||
noFilePathsFound: "Пути файлов не найдены.",
|
||||
},
|
||||
filterPanel: {
|
||||
nodeTypes: "Типы узлов",
|
||||
complexity: "Сложность",
|
||||
layers: "Слои",
|
||||
edgeCategories: "Категории рёбер",
|
||||
},
|
||||
personaSelector: {
|
||||
overview: "Обзор",
|
||||
overviewDesc: "Высокоуровневый архитектурный вид",
|
||||
learn: "Обучение",
|
||||
learnDesc: "Полная панель с пошаговым обучением",
|
||||
deepDive: "Погружение",
|
||||
deepDiveDesc: "Фокус на коде с чатом",
|
||||
},
|
||||
sidebar: {
|
||||
info: "Информация",
|
||||
files: "Файлы",
|
||||
},
|
||||
mobile: {
|
||||
graph: "Граф",
|
||||
info: "Информация",
|
||||
files: "Файлы",
|
||||
},
|
||||
drawer: {
|
||||
controls: "Управление",
|
||||
dashboard: "Панель",
|
||||
role: "Роль",
|
||||
view: "Вид",
|
||||
diffOverlay: "Наложение изменений",
|
||||
nodeTypes: "Типы узлов",
|
||||
layers: "Слои",
|
||||
tools: "Инструменты",
|
||||
path: "Путь",
|
||||
help: "Помощь",
|
||||
structural: "Структура",
|
||||
domain: "Домен",
|
||||
},
|
||||
domainView: {
|
||||
backToDomains: "Назад к доменам",
|
||||
},
|
||||
detailLevel: {
|
||||
filesTitle: "Только файлы — зависимости архитектурного уровня (быстро)",
|
||||
classesTitle: "Файлы + классы — структура кода с наследованием",
|
||||
files: "Файлы",
|
||||
classes: "+Классы",
|
||||
fnTitle: "Переключить узлы функций (может замедлить отрисовку)",
|
||||
fn: "fn",
|
||||
},
|
||||
nodeTypeLabels: {
|
||||
all: "Все",
|
||||
code: "Код",
|
||||
config: "Конфиг",
|
||||
docs: "Документация",
|
||||
infra: "Инфраструктура",
|
||||
data: "Данные",
|
||||
domain: "Домен",
|
||||
knowledge: "Знания",
|
||||
},
|
||||
tokenGate: {
|
||||
validating: "Проверка...",
|
||||
continue: "Продолжить",
|
||||
},
|
||||
diffToggle: {
|
||||
hideOverlay: "Скрыть наложение изменений",
|
||||
showOverlay: "Показать наложение изменений",
|
||||
noData: "Данные об изменениях не загружены",
|
||||
changed: "Изменено",
|
||||
affected: "Затронуто",
|
||||
},
|
||||
learnPanel: {
|
||||
finish: "Завершить",
|
||||
next: "Далее",
|
||||
prev: "Назад",
|
||||
noTour: "Обзор недоступен",
|
||||
noTourHint: "Сгенерируйте обзор из графа знаний, чтобы получить пошаговое руководство по кодовой базе",
|
||||
projectTour: "Обзор проекта",
|
||||
steps: "шагов",
|
||||
stepsTitle: "Шаги",
|
||||
guidedWalkthrough: "Пошаговое знакомство с кодовой базой",
|
||||
startTour: "Начать обзор",
|
||||
tour: "Обзор",
|
||||
exitTour: "Завершить обзор",
|
||||
},
|
||||
layer: {
|
||||
defaultName: "Слой",
|
||||
label: "слои",
|
||||
},
|
||||
breadcrumb: {
|
||||
projectOverview: "Обзор проекта",
|
||||
project: "Проект",
|
||||
escBack: "Esc — назад",
|
||||
},
|
||||
warningBanner: {
|
||||
dropped: "Отброшено",
|
||||
fatal: "Критично",
|
||||
},
|
||||
themePicker: {
|
||||
changeTheme: "Сменить тему",
|
||||
theme: "Тема",
|
||||
accentColor: "Акцентный цвет",
|
||||
headingFont: "Шрифт заголовков",
|
||||
serif: "Серифный",
|
||||
sans: "Без засечек",
|
||||
mono: "Моноширинный",
|
||||
},
|
||||
codeViewer: {
|
||||
fullFile: "Весь файл",
|
||||
lines: "Строки",
|
||||
linesLabel: "строк",
|
||||
noFile: "Файл не выбран",
|
||||
loading: "Загрузка исходного кода...",
|
||||
openLarger: "Открыть увеличенный просмотрщик кода",
|
||||
closeExpanded: "Закрыть расширенный просмотрщик кода",
|
||||
closeViewer: "Закрыть просмотрщик кода",
|
||||
sourceUnavailable: "Исходный код недоступен",
|
||||
},
|
||||
customNode: {
|
||||
tested: "Покрыт тестами",
|
||||
hasTests: "Есть тесты",
|
||||
},
|
||||
ariaLabels: {
|
||||
openMenu: "Открыть меню",
|
||||
closeMenu: "Закрыть меню",
|
||||
settings: "Настройки",
|
||||
hideSearch: "Скрыть поиск",
|
||||
showSearch: "Показать поиск",
|
||||
},
|
||||
nodeTypeFilter: {
|
||||
hide: "Скрыть",
|
||||
show: "Показать",
|
||||
nodesLabel: "узлов",
|
||||
},
|
||||
keyboardShortcuts: {
|
||||
showHelp: "Показать горячие клавиши",
|
||||
general: "Общие",
|
||||
navigation: "Навигация",
|
||||
tour: "Обзор",
|
||||
view: "Вид",
|
||||
focusSearch: "Перейти к строке поиска",
|
||||
nextStep: "Следующий шаг обзора",
|
||||
prevStep: "Предыдущий шаг обзора",
|
||||
toggleDiff: "Переключить режим изменений",
|
||||
toggleFilter: "Переключить панель фильтров",
|
||||
toggleExport: "Переключить меню экспорта",
|
||||
openPathFinder: "Открыть поиск пути",
|
||||
title: "Горячие клавиши",
|
||||
toggleHint: "Нажмите ?, чтобы открыть или закрыть эту справку",
|
||||
closeHint: "Нажмите ESC, чтобы закрыть",
|
||||
escapeDesc: "Закрыть панели и модальные окна / вернуться к обзору",
|
||||
},
|
||||
search: {
|
||||
placeholder: "Поиск узлов по имени, описанию или тегам...",
|
||||
fuzzy: "Нечёткий",
|
||||
semantic: "Семантический",
|
||||
result: "результат",
|
||||
},
|
||||
export: {
|
||||
label: "Экспорт",
|
||||
title: "Экспортировать граф (E)",
|
||||
asPNG: "Экспортировать как PNG",
|
||||
asSVG: "Экспортировать как SVG",
|
||||
asJSON: "Экспортировать как JSON",
|
||||
},
|
||||
edgeLabels: {
|
||||
imports: { forward: "импортирует", backward: "импортируется" },
|
||||
exports: { forward: "экспортирует в", backward: "экспортируется" },
|
||||
contains: { forward: "содержит", backward: "содержится в" },
|
||||
inherits: { forward: "наследует от", backward: "наследуется" },
|
||||
implements: { forward: "реализует", backward: "реализуется" },
|
||||
calls: { forward: "вызывает", backward: "вызывается" },
|
||||
subscribes: { forward: "подписывается на", backward: "подписан" },
|
||||
publishes: { forward: "публикует в", backward: "получает события" },
|
||||
middleware: { forward: "middleware для", backward: "использует middleware" },
|
||||
reads_from: { forward: "читает из", backward: "читается" },
|
||||
writes_to: { forward: "пишет в", backward: "записывается" },
|
||||
transforms: { forward: "преобразует", backward: "преобразуется" },
|
||||
validates: { forward: "валидирует", backward: "валидируется" },
|
||||
depends_on: { forward: "зависит от", backward: "является зависимостью" },
|
||||
tested_by: { forward: "тестируется", backward: "тестирует" },
|
||||
configures: { forward: "конфигурирует", backward: "конфигурируется" },
|
||||
related: { forward: "связан с", backward: "связан с" },
|
||||
similar_to: { forward: "похож на", backward: "похож на" },
|
||||
deploys: { forward: "разворачивает", backward: "разворачивается" },
|
||||
serves: { forward: "обслуживает", backward: "обслуживается" },
|
||||
migrates: { forward: "мигрирует", backward: "мигрируется" },
|
||||
documents: { forward: "документирует", backward: "документируется" },
|
||||
provisions: { forward: "обеспечивает", backward: "обеспечивается" },
|
||||
routes: { forward: "маршрутизирует в", backward: "маршрутизируется из" },
|
||||
defines_schema: { forward: "определяет схему для", backward: "схема определена" },
|
||||
triggers: { forward: "запускает", backward: "запускается" },
|
||||
contains_flow: { forward: "содержит поток", backward: "поток в" },
|
||||
flow_step: { forward: "шаг потока", backward: "шаг" },
|
||||
cross_domain: { forward: "междоменно к", backward: "междоменно из" },
|
||||
cites: { forward: "цитирует", backward: "цитируется" },
|
||||
contradicts: { forward: "противоречит", backward: "опровергается" },
|
||||
builds_on: { forward: "основан на", backward: "основа для" },
|
||||
exemplifies: { forward: "иллюстрирует", backward: "иллюстрируется" },
|
||||
categorized_under: { forward: "относится к", backward: "категоризирует" },
|
||||
authored_by: { forward: "автор", backward: "автор" },
|
||||
},
|
||||
pathFinder: {
|
||||
title: "Найти путь между узлами (P)",
|
||||
},
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@@ -0,0 +1,272 @@
|
||||
export const zhTW = {
|
||||
common: {
|
||||
loading: "載入專案...",
|
||||
noGraphLoaded: "未載入知識圖谱",
|
||||
selectNode: "選擇節點查看詳情",
|
||||
back: "返回",
|
||||
focus: "聚焦",
|
||||
unfocus: "取消聚焦",
|
||||
openCode: "開啟程式碼",
|
||||
file: "檔案",
|
||||
tags: "標籤",
|
||||
connections: "連結",
|
||||
filter: "篩選",
|
||||
resetAll: "重置全部",
|
||||
analyzed: "分析時間",
|
||||
startGuidedTour: "開始導覽",
|
||||
truncated: "(已截斷)",
|
||||
preview: "預覽",
|
||||
doubleClickToOpen: "雙擊開啟",
|
||||
appName: "Understand Anything",
|
||||
pressKeyboard: "按 ? 查看鍵盤快捷鍵",
|
||||
path: "路徑",
|
||||
theme: "主題",
|
||||
},
|
||||
projectOverview: {
|
||||
nodes: "節點",
|
||||
edges: "邊",
|
||||
layers: "層級",
|
||||
types: "類型",
|
||||
fileTypes: "檔案類型",
|
||||
code: "程式碼",
|
||||
config: "配置",
|
||||
docs: "文件",
|
||||
infra: "基礎設施",
|
||||
data: "資料",
|
||||
domain: "領域",
|
||||
knowledge: "知識",
|
||||
languages: "程式語言",
|
||||
frameworks: "框架",
|
||||
nodeTypeDistribution: "節點類型分布",
|
||||
complexityDistribution: "複雜度分布",
|
||||
simple: "簡單",
|
||||
moderate: "中等",
|
||||
complex: "複雜",
|
||||
mostConnectedNodes: "連結最多的節點",
|
||||
avgConnectionsPerNode: "節點平均連結數",
|
||||
},
|
||||
nodeInfo: {
|
||||
definedInThisFile: "在此檔案中定義",
|
||||
languageConcepts: "語言概念",
|
||||
category: "分類",
|
||||
wikilinks: "維基連結",
|
||||
backlinks: "反向連結",
|
||||
entities: "實體",
|
||||
businessRules: "業務規則",
|
||||
crossDomain: "跨領域",
|
||||
flows: "流程",
|
||||
entryPoint: "入口點",
|
||||
steps: "步驟",
|
||||
implementation: "實作",
|
||||
},
|
||||
fileExplorer: {
|
||||
analyzedFiles: "已分析檔案",
|
||||
filesFromGraph: "來自目前知識圖谱的檔案",
|
||||
noFilePathsFound: "未找到檔案路徑。",
|
||||
},
|
||||
filterPanel: {
|
||||
nodeTypes: "節點類型",
|
||||
complexity: "複雜度",
|
||||
layers: "層級",
|
||||
edgeCategories: "邊類別",
|
||||
},
|
||||
personaSelector: {
|
||||
overview: "概覽",
|
||||
overviewDesc: "高層次架構視圖",
|
||||
learn: "學習",
|
||||
learnDesc: "完整儀表板與導覽學習",
|
||||
deepDive: "深入",
|
||||
deepDiveDesc: "程式碼聚焦與對話",
|
||||
},
|
||||
sidebar: {
|
||||
info: "資訊",
|
||||
files: "檔案",
|
||||
},
|
||||
mobile: {
|
||||
graph: "圖谱",
|
||||
info: "資訊",
|
||||
files: "檔案",
|
||||
},
|
||||
drawer: {
|
||||
controls: "控制",
|
||||
dashboard: "儀表板",
|
||||
role: "角色",
|
||||
view: "視圖",
|
||||
diffOverlay: "差異覆蓋",
|
||||
nodeTypes: "節點類型",
|
||||
layers: "層級",
|
||||
tools: "工具",
|
||||
path: "路徑",
|
||||
help: "幫助",
|
||||
structural: "結構",
|
||||
domain: "領域",
|
||||
},
|
||||
domainView: {
|
||||
backToDomains: "返回領域列表",
|
||||
},
|
||||
detailLevel: {
|
||||
filesTitle: "僅檔案 — 架構級依賴(快速)",
|
||||
classesTitle: "檔案 + 類別 — 程式碼結構及繼承關係",
|
||||
files: "檔案",
|
||||
classes: "+類別",
|
||||
fnTitle: "切換函數節點(可能降低渲染速度)",
|
||||
fn: "函數",
|
||||
},
|
||||
nodeTypeLabels: {
|
||||
all: "全部",
|
||||
code: "程式碼",
|
||||
config: "配置",
|
||||
docs: "文件",
|
||||
infra: "基礎設施",
|
||||
data: "資料",
|
||||
domain: "領域",
|
||||
knowledge: "知識",
|
||||
},
|
||||
tokenGate: {
|
||||
validating: "驗證中...",
|
||||
continue: "繼續",
|
||||
},
|
||||
diffToggle: {
|
||||
hideOverlay: "隱藏差異覆蓋",
|
||||
showOverlay: "顯示差異覆蓋",
|
||||
noData: "未載入差異資料",
|
||||
changed: "已修改",
|
||||
affected: "受影響",
|
||||
},
|
||||
learnPanel: {
|
||||
finish: "完成",
|
||||
next: "下一步",
|
||||
prev: "上一步",
|
||||
noTour: "無導覽可用",
|
||||
noTourHint: "從知識圖谱生成導覽以獲取程式碼庫的引導式講解",
|
||||
projectTour: "專案導覽",
|
||||
steps: "步",
|
||||
stepsTitle: "步驟",
|
||||
guidedWalkthrough: "程式碼庫引導式講解",
|
||||
startTour: "開始導覽",
|
||||
tour: "導覽",
|
||||
exitTour: "退出導覽",
|
||||
},
|
||||
layer: {
|
||||
defaultName: "層級",
|
||||
label: "層",
|
||||
},
|
||||
breadcrumb: {
|
||||
projectOverview: "專案概覽",
|
||||
project: "專案",
|
||||
escBack: "按 Esc 返回",
|
||||
},
|
||||
warningBanner: {
|
||||
dropped: "已捨棄",
|
||||
fatal: "致命錯誤",
|
||||
},
|
||||
themePicker: {
|
||||
changeTheme: "變更主題",
|
||||
theme: "主題",
|
||||
accentColor: "強調色",
|
||||
headingFont: "標題字型",
|
||||
serif: "襯線",
|
||||
sans: "無襯線",
|
||||
mono: "等寬",
|
||||
},
|
||||
codeViewer: {
|
||||
fullFile: "完整檔案",
|
||||
lines: "行",
|
||||
linesLabel: "行",
|
||||
noFile: "未選擇檔案",
|
||||
loading: "載入原始碼中...",
|
||||
openLarger: "開啟更大的程式碼檢視器",
|
||||
closeExpanded: "關閉展開的程式碼檢視器",
|
||||
closeViewer: "關閉程式碼檢視器",
|
||||
sourceUnavailable: "原始碼不可用",
|
||||
},
|
||||
customNode: {
|
||||
tested: "已測試",
|
||||
hasTests: "有測試",
|
||||
},
|
||||
ariaLabels: {
|
||||
openMenu: "開啟選單",
|
||||
closeMenu: "關閉選單",
|
||||
settings: "設定",
|
||||
hideSearch: "隱藏搜尋",
|
||||
showSearch: "顯示搜尋",
|
||||
},
|
||||
nodeTypeFilter: {
|
||||
hide: "隱藏",
|
||||
show: "顯示",
|
||||
nodesLabel: "節點",
|
||||
},
|
||||
keyboardShortcuts: {
|
||||
showHelp: "顯示鍵盤快捷鍵",
|
||||
general: "一般",
|
||||
navigation: "導航",
|
||||
tour: "導覽",
|
||||
view: "檢視",
|
||||
focusSearch: "聚焦搜尋列",
|
||||
nextStep: "下一步導覽",
|
||||
prevStep: "上一步導覽",
|
||||
toggleDiff: "切換差異模式",
|
||||
toggleFilter: "切換篩選面板",
|
||||
toggleExport: "切換匯出選單",
|
||||
openPathFinder: "開啟路徑尋找器",
|
||||
title: "鍵盤快捷鍵",
|
||||
toggleHint: "按 ? 隨時切換此幫助",
|
||||
closeHint: "按 ESC 關閉",
|
||||
escapeDesc: "關閉面板和彈窗 / 返回概覽",
|
||||
},
|
||||
search: {
|
||||
placeholder: "搜尋節點名稱、摘要或標籤...",
|
||||
fuzzy: "模糊",
|
||||
semantic: "語意",
|
||||
result: "結果",
|
||||
},
|
||||
export: {
|
||||
label: "匯出",
|
||||
title: "匯出圖谱 (E)",
|
||||
asPNG: "匯出為 PNG",
|
||||
asSVG: "匯出為 SVG",
|
||||
asJSON: "匯出為 JSON",
|
||||
},
|
||||
edgeLabels: {
|
||||
imports: { forward: "導入", backward: "被導入" },
|
||||
exports: { forward: "導出到", backward: "被導出" },
|
||||
contains: { forward: "包含", backward: "被包含" },
|
||||
inherits: { forward: "繼承自", backward: "被繼承" },
|
||||
implements: { forward: "實作", backward: "被實作" },
|
||||
calls: { forward: "呼叫", backward: "被呼叫" },
|
||||
subscribes: { forward: "訂閱", backward: "被訂閱" },
|
||||
publishes: { forward: "發布到", backward: "被消費" },
|
||||
middleware: { forward: "中介軟體", backward: "使用中介軟體" },
|
||||
reads_from: { forward: "讀取", backward: "被讀取" },
|
||||
writes_to: { forward: "寫入", backward: "被寫入" },
|
||||
transforms: { forward: "轉換", backward: "被轉換" },
|
||||
validates: { forward: "驗證", backward: "被驗證" },
|
||||
depends_on: { forward: "依賴", backward: "被依賴" },
|
||||
tested_by: { forward: "被測試", backward: "測試" },
|
||||
configures: { forward: "配置", backward: "被配置" },
|
||||
related: { forward: "相關", backward: "相關" },
|
||||
similar_to: { forward: "相似", backward: "相似" },
|
||||
deploys: { forward: "部署", backward: "被部署" },
|
||||
serves: { forward: "服務", backward: "被服務" },
|
||||
migrates: { forward: "遷移", backward: "被遷移" },
|
||||
documents: { forward: "文件化", backward: "被文件化" },
|
||||
provisions: { forward: "提供", backward: "被提供" },
|
||||
routes: { forward: "路由到", backward: "被路由" },
|
||||
defines_schema: { forward: "定義架構", backward: "架構被定義" },
|
||||
triggers: { forward: "觸發", backward: "被觸發" },
|
||||
contains_flow: { forward: "包含流程", backward: "流程所在" },
|
||||
flow_step: { forward: "流程步驟", backward: "步驟所属" },
|
||||
cross_domain: { forward: "跨領域到", backward: "跨領域来自" },
|
||||
cites: { forward: "引用", backward: "被引用" },
|
||||
contradicts: { forward: "反駁", backward: "被反駁" },
|
||||
builds_on: { forward: "基於", backward: "作為基礎" },
|
||||
exemplifies: { forward: "例證", backward: "被例證" },
|
||||
categorized_under: { forward: "归类於", backward: "归类" },
|
||||
authored_by: { forward: "作者", backward: "著作" },
|
||||
},
|
||||
pathFinder: {
|
||||
title: "尋找節點間路徑 (P)",
|
||||
},
|
||||
};
|
||||
|
||||
export default zhTW;
|
||||
@@ -0,0 +1,272 @@
|
||||
export const zh = {
|
||||
common: {
|
||||
loading: "加载项目...",
|
||||
noGraphLoaded: "未加载知识图谱",
|
||||
selectNode: "选择节点查看详情",
|
||||
back: "返回",
|
||||
focus: "聚焦",
|
||||
unfocus: "取消聚焦",
|
||||
openCode: "打开代码",
|
||||
file: "文件",
|
||||
tags: "标签",
|
||||
connections: "连接",
|
||||
filter: "筛选",
|
||||
resetAll: "重置全部",
|
||||
analyzed: "分析时间",
|
||||
startGuidedTour: "开始导览",
|
||||
truncated: "(已截断)",
|
||||
preview: "预览",
|
||||
doubleClickToOpen: "双击打开",
|
||||
appName: "Understand Anything",
|
||||
pressKeyboard: "按 ? 查看键盘快捷键",
|
||||
path: "路径",
|
||||
theme: "主题",
|
||||
},
|
||||
projectOverview: {
|
||||
nodes: "节点",
|
||||
edges: "边",
|
||||
layers: "层级",
|
||||
types: "类型",
|
||||
fileTypes: "文件类型",
|
||||
code: "代码",
|
||||
config: "配置",
|
||||
docs: "文档",
|
||||
infra: "基础设施",
|
||||
data: "数据",
|
||||
domain: "领域",
|
||||
knowledge: "知识",
|
||||
languages: "编程语言",
|
||||
frameworks: "框架",
|
||||
nodeTypeDistribution: "节点类型分布",
|
||||
complexityDistribution: "复杂度分布",
|
||||
simple: "简单",
|
||||
moderate: "中等",
|
||||
complex: "复杂",
|
||||
mostConnectedNodes: "连接最多的节点",
|
||||
avgConnectionsPerNode: "节点平均连接数",
|
||||
},
|
||||
nodeInfo: {
|
||||
definedInThisFile: "在此文件中定义",
|
||||
languageConcepts: "语言概念",
|
||||
category: "分类",
|
||||
wikilinks: "维基链接",
|
||||
backlinks: "反向链接",
|
||||
entities: "实体",
|
||||
businessRules: "业务规则",
|
||||
crossDomain: "跨领域",
|
||||
flows: "流程",
|
||||
entryPoint: "入口点",
|
||||
steps: "步骤",
|
||||
implementation: "实现",
|
||||
},
|
||||
fileExplorer: {
|
||||
analyzedFiles: "已分析文件",
|
||||
filesFromGraph: "来自当前知识图谱的文件",
|
||||
noFilePathsFound: "未找到文件路径。",
|
||||
},
|
||||
filterPanel: {
|
||||
nodeTypes: "节点类型",
|
||||
complexity: "复杂度",
|
||||
layers: "层级",
|
||||
edgeCategories: "边类别",
|
||||
},
|
||||
personaSelector: {
|
||||
overview: "概览",
|
||||
overviewDesc: "高层次架构视图",
|
||||
learn: "学习",
|
||||
learnDesc: "完整仪表盘与导览学习",
|
||||
deepDive: "深入",
|
||||
deepDiveDesc: "代码聚焦与对话",
|
||||
},
|
||||
sidebar: {
|
||||
info: "信息",
|
||||
files: "文件",
|
||||
},
|
||||
mobile: {
|
||||
graph: "图谱",
|
||||
info: "信息",
|
||||
files: "文件",
|
||||
},
|
||||
drawer: {
|
||||
controls: "控制",
|
||||
dashboard: "仪表盘",
|
||||
role: "角色",
|
||||
view: "视图",
|
||||
diffOverlay: "差异覆盖",
|
||||
nodeTypes: "节点类型",
|
||||
layers: "层级",
|
||||
tools: "工具",
|
||||
path: "路径",
|
||||
help: "帮助",
|
||||
structural: "结构",
|
||||
domain: "领域",
|
||||
},
|
||||
domainView: {
|
||||
backToDomains: "返回领域列表",
|
||||
},
|
||||
detailLevel: {
|
||||
filesTitle: "仅文件 — 架构级依赖(快速)",
|
||||
classesTitle: "文件 + 类 — 代码结构及继承关系",
|
||||
files: "文件",
|
||||
classes: "+类",
|
||||
fnTitle: "切换函数节点(可能降低渲染速度)",
|
||||
fn: "函数",
|
||||
},
|
||||
nodeTypeLabels: {
|
||||
all: "全部",
|
||||
code: "代码",
|
||||
config: "配置",
|
||||
docs: "文档",
|
||||
infra: "基础设施",
|
||||
data: "数据",
|
||||
domain: "领域",
|
||||
knowledge: "知识",
|
||||
},
|
||||
tokenGate: {
|
||||
validating: "验证中...",
|
||||
continue: "继续",
|
||||
},
|
||||
diffToggle: {
|
||||
hideOverlay: "隐藏差异覆盖",
|
||||
showOverlay: "显示差异覆盖",
|
||||
noData: "未加载差异数据",
|
||||
changed: "已修改",
|
||||
affected: "受影响",
|
||||
},
|
||||
learnPanel: {
|
||||
finish: "完成",
|
||||
next: "下一步",
|
||||
prev: "上一步",
|
||||
noTour: "无导览可用",
|
||||
noTourHint: "从知识图谱生成导览以获取代码库的引导式讲解",
|
||||
projectTour: "项目导览",
|
||||
steps: "步",
|
||||
stepsTitle: "步骤",
|
||||
guidedWalkthrough: "代码库引导式讲解",
|
||||
startTour: "开始导览",
|
||||
tour: "导览",
|
||||
exitTour: "退出导览",
|
||||
},
|
||||
layer: {
|
||||
defaultName: "层级",
|
||||
label: "层",
|
||||
},
|
||||
breadcrumb: {
|
||||
projectOverview: "项目概览",
|
||||
project: "项目",
|
||||
escBack: "按 Esc 返回",
|
||||
},
|
||||
warningBanner: {
|
||||
dropped: "已丢弃",
|
||||
fatal: "致命错误",
|
||||
},
|
||||
themePicker: {
|
||||
changeTheme: "更换主题",
|
||||
theme: "主题",
|
||||
accentColor: "强调色",
|
||||
headingFont: "标题字体",
|
||||
serif: "衬线",
|
||||
sans: "无衬线",
|
||||
mono: "等宽",
|
||||
},
|
||||
codeViewer: {
|
||||
fullFile: "完整文件",
|
||||
lines: "行",
|
||||
linesLabel: "行",
|
||||
noFile: "未选择文件",
|
||||
loading: "加载源码中...",
|
||||
openLarger: "打开更大的代码查看器",
|
||||
closeExpanded: "关闭展开的代码查看器",
|
||||
closeViewer: "关闭代码查看器",
|
||||
sourceUnavailable: "源码不可用",
|
||||
},
|
||||
customNode: {
|
||||
tested: "已测试",
|
||||
hasTests: "有测试",
|
||||
},
|
||||
ariaLabels: {
|
||||
openMenu: "打开菜单",
|
||||
closeMenu: "关闭菜单",
|
||||
settings: "设置",
|
||||
hideSearch: "隐藏搜索",
|
||||
showSearch: "显示搜索",
|
||||
},
|
||||
nodeTypeFilter: {
|
||||
hide: "隐藏",
|
||||
show: "显示",
|
||||
nodesLabel: "节点",
|
||||
},
|
||||
keyboardShortcuts: {
|
||||
showHelp: "显示键盘快捷键",
|
||||
general: "通用",
|
||||
navigation: "导航",
|
||||
tour: "导览",
|
||||
view: "视图",
|
||||
focusSearch: "聚焦搜索栏",
|
||||
nextStep: "下一步导览",
|
||||
prevStep: "上一步导览",
|
||||
toggleDiff: "切换差异模式",
|
||||
toggleFilter: "切换筛选面板",
|
||||
toggleExport: "切换导出菜单",
|
||||
openPathFinder: "打开路径查找器",
|
||||
title: "键盘快捷键",
|
||||
toggleHint: "按 ? 随时切换此帮助",
|
||||
closeHint: "按 ESC 关闭",
|
||||
escapeDesc: "关闭面板和弹窗 / 返回概览",
|
||||
},
|
||||
search: {
|
||||
placeholder: "搜索节点名称、摘要或标签...",
|
||||
fuzzy: "模糊",
|
||||
semantic: "语义",
|
||||
result: "结果",
|
||||
},
|
||||
export: {
|
||||
label: "导出",
|
||||
title: "导出图谱 (E)",
|
||||
asPNG: "导出为 PNG",
|
||||
asSVG: "导出为 SVG",
|
||||
asJSON: "导出为 JSON",
|
||||
},
|
||||
edgeLabels: {
|
||||
imports: { forward: "导入", backward: "被导入" },
|
||||
exports: { forward: "导出到", backward: "被导出" },
|
||||
contains: { forward: "包含", backward: "被包含" },
|
||||
inherits: { forward: "继承自", backward: "被继承" },
|
||||
implements: { forward: "实现", backward: "被实现" },
|
||||
calls: { forward: "调用", backward: "被调用" },
|
||||
subscribes: { forward: "订阅", backward: "被订阅" },
|
||||
publishes: { forward: "发布到", backward: "被消费" },
|
||||
middleware: { forward: "中间件", backward: "使用中间件" },
|
||||
reads_from: { forward: "读取", backward: "被读取" },
|
||||
writes_to: { forward: "写入", backward: "被写入" },
|
||||
transforms: { forward: "转换", backward: "被转换" },
|
||||
validates: { forward: "验证", backward: "被验证" },
|
||||
depends_on: { forward: "依赖", backward: "被依赖" },
|
||||
tested_by: { forward: "被测试", backward: "测试" },
|
||||
configures: { forward: "配置", backward: "被配置" },
|
||||
related: { forward: "相关", backward: "相关" },
|
||||
similar_to: { forward: "相似", backward: "相似" },
|
||||
deploys: { forward: "部署", backward: "被部署" },
|
||||
serves: { forward: "服务", backward: "被服务" },
|
||||
migrates: { forward: "迁移", backward: "被迁移" },
|
||||
documents: { forward: "文档化", backward: "被文档化" },
|
||||
provisions: { forward: "提供", backward: "被提供" },
|
||||
routes: { forward: "路由到", backward: "被路由" },
|
||||
defines_schema: { forward: "定义架构", backward: "架构被定义" },
|
||||
triggers: { forward: "触发", backward: "被触发" },
|
||||
contains_flow: { forward: "包含流程", backward: "流程所在" },
|
||||
flow_step: { forward: "流程步骤", backward: "步骤所属" },
|
||||
cross_domain: { forward: "跨领域到", backward: "跨领域来自" },
|
||||
cites: { forward: "引用", backward: "被引用" },
|
||||
contradicts: { forward: "反驳", backward: "被反驳" },
|
||||
builds_on: { forward: "基于", backward: "作为基础" },
|
||||
exemplifies: { forward: "例证", backward: "被例证" },
|
||||
categorized_under: { forward: "归类于", backward: "归类" },
|
||||
authored_by: { forward: "作者", backward: "著作" },
|
||||
},
|
||||
pathFinder: {
|
||||
title: "查找节点间路径 (P)",
|
||||
},
|
||||
};
|
||||
|
||||
export default zh;
|
||||
@@ -15,6 +15,7 @@ export type NodeType = "file" | "function" | "class" | "module" | "concept" | "c
|
||||
export type Complexity = "simple" | "moderate" | "complex";
|
||||
export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic" | "infrastructure" | "domain" | "knowledge";
|
||||
export type ViewMode = "structural" | "domain" | "knowledge";
|
||||
export type DetailLevel = "file" | "class";
|
||||
|
||||
export interface FilterState {
|
||||
nodeTypes: Set<NodeType>;
|
||||
@@ -146,6 +147,13 @@ interface DashboardStore {
|
||||
nodeTypeFilters: Record<NodeCategory, boolean>;
|
||||
toggleNodeTypeFilter: (category: NodeCategory) => void;
|
||||
|
||||
// Detail level: "file" shows only file nodes (architecture view),
|
||||
// "class" shows files + class nodes (code structure view) with optional function expansion.
|
||||
detailLevel: DetailLevel;
|
||||
setDetailLevel: (level: DetailLevel) => void;
|
||||
showFunctionsInClassView: boolean;
|
||||
toggleShowFunctionsInClassView: () => void;
|
||||
|
||||
setGraph: (graph: KnowledgeGraph) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
navigateToNode: (nodeId: string) => void;
|
||||
@@ -331,6 +339,29 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
pendingFocusContainer: null,
|
||||
})),
|
||||
|
||||
detailLevel: "file",
|
||||
setDetailLevel: (level) =>
|
||||
set({
|
||||
detailLevel: level,
|
||||
// Detail level changes which nodes are visible; cached positions stale.
|
||||
// Reset fn toggle so it doesn't resurrect when re-entering class view.
|
||||
showFunctionsInClassView: false,
|
||||
containerLayoutCache: new Map(),
|
||||
containerSizeMemory: new Map(),
|
||||
expandedContainers: new Set(),
|
||||
pendingFocusContainer: null,
|
||||
}),
|
||||
|
||||
showFunctionsInClassView: false,
|
||||
toggleShowFunctionsInClassView: () =>
|
||||
set((state) => ({
|
||||
showFunctionsInClassView: !state.showFunctionsInClassView,
|
||||
containerLayoutCache: new Map(),
|
||||
containerSizeMemory: new Map(),
|
||||
expandedContainers: new Set(),
|
||||
pendingFocusContainer: null,
|
||||
})),
|
||||
|
||||
setGraph: (graph) => {
|
||||
const searchEngine = new SearchEngine(graph.nodes);
|
||||
const query = get().searchQuery;
|
||||
|
||||
@@ -9,7 +9,7 @@ import crypto from "crypto";
|
||||
// Generate a one-time token when the server process starts.
|
||||
// This token is printed to the terminal and must be in the URL
|
||||
// to fetch knowledge-graph.json or diff-overlay.json.
|
||||
const ACCESS_TOKEN = crypto.randomBytes(16).toString("hex");
|
||||
const ACCESS_TOKEN = process.env.UNDERSTAND_ACCESS_TOKEN || crypto.randomBytes(16).toString("hex");
|
||||
const MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
||||
|
||||
function graphFileCandidates(fileName: string): string[] {
|
||||
@@ -252,6 +252,7 @@ export default defineConfig({
|
||||
pathname === "/domain-graph.json" ||
|
||||
pathname === "/diff-overlay.json" ||
|
||||
pathname === "/meta.json" ||
|
||||
pathname === "/config.json" ||
|
||||
pathname === "/file-content.json";
|
||||
|
||||
if (!isProtectedEndpoint) {
|
||||
@@ -272,6 +273,24 @@ export default defineConfig({
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/config.json") {
|
||||
const configCandidates = graphFileCandidates("config.json");
|
||||
for (const candidate of configCandidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(candidate, "utf-8"));
|
||||
sendJson(res, 200, raw);
|
||||
return;
|
||||
} catch {
|
||||
sendJson(res, 500, { error: "Failed to read config file" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
sendJson(res, 200, { autoUpdate: false, outputLanguage: "en" });
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName =
|
||||
pathname === "/diff-overlay.json"
|
||||
? "diff-overlay.json"
|
||||
|
||||
+97
@@ -100,6 +100,18 @@ importers:
|
||||
devlop:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
elkjs:
|
||||
specifier: ^0.9.3
|
||||
version: 0.9.3
|
||||
graphology:
|
||||
specifier: ^0.25.4
|
||||
version: 0.25.4(graphology-types@0.24.8)
|
||||
graphology-communities-louvain:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.2(graphology-types@0.24.8)
|
||||
graphology-types:
|
||||
specifier: ^0.24.8
|
||||
version: 0.24.8
|
||||
hast-util-to-jsx-runtime:
|
||||
specifier: ^2.3.6
|
||||
version: 2.3.6
|
||||
@@ -134,6 +146,9 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.3.0
|
||||
version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
@@ -143,6 +158,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^6.0.0
|
||||
version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
|
||||
vitest:
|
||||
specifier: ^3.1.0
|
||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -967,6 +985,9 @@ packages:
|
||||
electron-to-chromium@1.5.325:
|
||||
resolution: {integrity: sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==}
|
||||
|
||||
elkjs@0.9.3:
|
||||
resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -995,6 +1016,10 @@ packages:
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1036,6 +1061,29 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
graphology-communities-louvain@2.0.2:
|
||||
resolution: {integrity: sha512-zt+2hHVPYxjEquyecxWXoUoIuN/UvYzsvI7boDdMNz0rRvpESQ7+e+Ejv6wK7AThycbZXuQ6DkG8NPMCq6XwoA==}
|
||||
peerDependencies:
|
||||
graphology-types: '>=0.19.0'
|
||||
|
||||
graphology-indices@0.17.0:
|
||||
resolution: {integrity: sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==}
|
||||
peerDependencies:
|
||||
graphology-types: '>=0.20.0'
|
||||
|
||||
graphology-types@0.24.8:
|
||||
resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==}
|
||||
|
||||
graphology-utils@2.5.2:
|
||||
resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==}
|
||||
peerDependencies:
|
||||
graphology-types: '>=0.23.0'
|
||||
|
||||
graphology@0.25.4:
|
||||
resolution: {integrity: sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==}
|
||||
peerDependencies:
|
||||
graphology-types: '>=0.24.0'
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1315,6 +1363,9 @@ packages:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mnemonist@0.39.8:
|
||||
resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1334,9 +1385,15 @@ packages:
|
||||
node-releases@2.0.36:
|
||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||
|
||||
obliterator@2.0.5:
|
||||
resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
pandemonium@2.4.1:
|
||||
resolution: {integrity: sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==}
|
||||
|
||||
parse-entities@4.0.2:
|
||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||
|
||||
@@ -2526,6 +2583,8 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.325: {}
|
||||
|
||||
elkjs@0.9.3: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
@@ -2574,6 +2633,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
@@ -2605,6 +2666,32 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
graphology-communities-louvain@2.0.2(graphology-types@0.24.8):
|
||||
dependencies:
|
||||
graphology-indices: 0.17.0(graphology-types@0.24.8)
|
||||
graphology-types: 0.24.8
|
||||
graphology-utils: 2.5.2(graphology-types@0.24.8)
|
||||
mnemonist: 0.39.8
|
||||
pandemonium: 2.4.1
|
||||
|
||||
graphology-indices@0.17.0(graphology-types@0.24.8):
|
||||
dependencies:
|
||||
graphology-types: 0.24.8
|
||||
graphology-utils: 2.5.2(graphology-types@0.24.8)
|
||||
mnemonist: 0.39.8
|
||||
|
||||
graphology-types@0.24.8: {}
|
||||
|
||||
graphology-utils@2.5.2(graphology-types@0.24.8):
|
||||
dependencies:
|
||||
graphology-types: 0.24.8
|
||||
|
||||
graphology@0.25.4(graphology-types@0.24.8):
|
||||
dependencies:
|
||||
events: 3.3.0
|
||||
graphology-types: 0.24.8
|
||||
obliterator: 2.0.5
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
@@ -3000,6 +3087,10 @@ snapshots:
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
mnemonist@0.39.8:
|
||||
dependencies:
|
||||
obliterator: 2.0.5
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
@@ -3010,8 +3101,14 @@ snapshots:
|
||||
|
||||
node-releases@2.0.36: {}
|
||||
|
||||
obliterator@2.0.5: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pandemonium@2.4.1:
|
||||
dependencies:
|
||||
mnemonist: 0.39.8
|
||||
|
||||
parse-entities@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
|
||||
@@ -16,9 +16,79 @@ Extracts business domain knowledge — domains, business flows, and process step
|
||||
|
||||
## Instructions
|
||||
|
||||
### Phase 0: Resolve `PROJECT_ROOT`
|
||||
|
||||
Set `PROJECT_ROOT` to the current working directory.
|
||||
|
||||
**Worktree redirect.** If `PROJECT_ROOT` is inside a git worktree (not the main checkout), redirect output to the main repository root. Worktrees managed by Claude Code are ephemeral — `.understand-anything/` written there is destroyed when the session ends, taking the domain graph with it (issue #133). Detect a worktree by comparing `git rev-parse --git-dir` against `git rev-parse --git-common-dir`; in a normal checkout or submodule they resolve to the same path, in a worktree they differ and the parent of `--git-common-dir` is the main repo root.
|
||||
|
||||
```bash
|
||||
COMMON_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)
|
||||
GIT_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-dir 2>/dev/null)
|
||||
if [ -n "$COMMON_DIR" ] && [ -n "$GIT_DIR" ]; then
|
||||
COMMON_ABS=$(cd "$PROJECT_ROOT" && cd "$COMMON_DIR" 2>/dev/null && pwd -P)
|
||||
GIT_ABS=$(cd "$PROJECT_ROOT" && cd "$GIT_DIR" 2>/dev/null && pwd -P)
|
||||
if [ -n "$COMMON_ABS" ] && [ "$COMMON_ABS" != "$GIT_ABS" ]; then
|
||||
MAIN_ROOT=$(dirname "$COMMON_ABS")
|
||||
if [ -d "$MAIN_ROOT" ] && [ "${UNDERSTAND_NO_WORKTREE_REDIRECT:-0}" != "1" ]; then
|
||||
echo "[understand-domain] Detected git worktree at $PROJECT_ROOT"
|
||||
echo "[understand-domain] Redirecting output to main repo root: $MAIN_ROOT"
|
||||
echo "[understand-domain] (Set UNDERSTAND_NO_WORKTREE_REDIRECT=1 to keep PROJECT_ROOT as the worktree.)"
|
||||
PROJECT_ROOT="$MAIN_ROOT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
Use `$PROJECT_ROOT` (not the bare CWD) for every reference to "the current project" / `<project-root>` in subsequent phases.
|
||||
|
||||
**Important:** do **not** assume the plugin root is simply two directories above the skill path string. In many installations `~/.agents/skills/understand-domain` is a symlink into the real plugin checkout. Prefer runtime-provided plugin roots first (for Claude), then fall back to universal symlinks, skill symlink resolution, and common clone-based install paths.
|
||||
|
||||
Resolve the plugin root like this:
|
||||
|
||||
```bash
|
||||
SKILL_REAL=$(realpath ~/.agents/skills/understand-domain 2>/dev/null || readlink -f ~/.agents/skills/understand-domain 2>/dev/null || echo "")
|
||||
SELF_RELATIVE=$([ -n "$SKILL_REAL" ] && cd "$SKILL_REAL/../.." 2>/dev/null && pwd || echo "")
|
||||
COPILOT_SKILL_REAL=$(realpath ~/.copilot/skills/understand-domain 2>/dev/null || readlink -f ~/.copilot/skills/understand-domain 2>/dev/null || echo "")
|
||||
COPILOT_SELF_RELATIVE=$([ -n "$COPILOT_SKILL_REAL" ] && cd "$COPILOT_SKILL_REAL/../.." 2>/dev/null && pwd || echo "")
|
||||
|
||||
PLUGIN_ROOT=""
|
||||
for candidate in \
|
||||
"${CLAUDE_PLUGIN_ROOT}" \
|
||||
"$HOME/.understand-anything-plugin" \
|
||||
"$SELF_RELATIVE" \
|
||||
"$COPILOT_SELF_RELATIVE" \
|
||||
"$HOME/.codex/understand-anything/understand-anything-plugin" \
|
||||
"$HOME/.opencode/understand-anything/understand-anything-plugin" \
|
||||
"$HOME/.pi/understand-anything/understand-anything-plugin" \
|
||||
"$HOME/understand-anything/understand-anything-plugin"; do
|
||||
if [ -n "$candidate" ] && [ -f "$candidate/package.json" ] && [ -f "$candidate/pnpm-workspace.yaml" ]; then
|
||||
PLUGIN_ROOT="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PLUGIN_ROOT" ]; then
|
||||
echo "Error: Cannot find the understand-anything plugin root."
|
||||
echo "Checked:"
|
||||
echo " - ${CLAUDE_PLUGIN_ROOT:-<unset CLAUDE_PLUGIN_ROOT>}"
|
||||
echo " - $HOME/.understand-anything-plugin"
|
||||
echo " - ${SELF_RELATIVE:-<unresolved path derived from ~/.agents/skills/understand-domain>}"
|
||||
echo " - ${COPILOT_SELF_RELATIVE:-<unresolved path derived from ~/.copilot/skills/understand-domain>}"
|
||||
echo " - $HOME/.codex/understand-anything/understand-anything-plugin"
|
||||
echo " - $HOME/.opencode/understand-anything/understand-anything-plugin"
|
||||
echo " - $HOME/.pi/understand-anything/understand-anything-plugin"
|
||||
echo " - $HOME/understand-anything/understand-anything-plugin"
|
||||
echo "Make sure the plugin is installed correctly."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
Use `$PLUGIN_ROOT` for every reference to agent definitions in subsequent phases.
|
||||
|
||||
### Phase 1: Detect Existing Graph
|
||||
|
||||
1. Check if `.understand-anything/knowledge-graph.json` exists in the current project
|
||||
1. Check if `$PROJECT_ROOT/.understand-anything/knowledge-graph.json` exists
|
||||
2. If it exists AND `--full` was NOT passed → proceed to Phase 3 (derive from graph)
|
||||
3. Otherwise → proceed to Phase 2 (lightweight scan)
|
||||
|
||||
@@ -26,11 +96,11 @@ Extracts business domain knowledge — domains, business flows, and process step
|
||||
|
||||
The preprocessing script does NOT produce a domain graph — it produces **raw material** (file tree, entry points, exports/imports) so the domain-analyzer agent can focus on the actual domain analysis instead of spending dozens of tool calls exploring the codebase. Think of it as a cheat sheet: cheap Python preprocessing → expensive LLM gets a clean, small input → better results for less cost.
|
||||
|
||||
1. Run the preprocessing script bundled with this skill:
|
||||
1. Run the preprocessing script bundled with this skill, passing `$PROJECT_ROOT` from Phase 0:
|
||||
```
|
||||
python ./extract-domain-context.py <project-root>
|
||||
python ./extract-domain-context.py "$PROJECT_ROOT"
|
||||
```
|
||||
This outputs `<project-root>/.understand-anything/intermediate/domain-context.json` containing:
|
||||
This outputs `$PROJECT_ROOT/.understand-anything/intermediate/domain-context.json` containing:
|
||||
- File tree (respecting `.gitignore`)
|
||||
- Detected entry points (HTTP routes, CLI commands, event handlers, cron jobs, exported handlers)
|
||||
- File signatures (exports, imports per file)
|
||||
@@ -41,7 +111,7 @@ The preprocessing script does NOT produce a domain graph — it produces **raw m
|
||||
|
||||
### Phase 3: Derive from Existing Graph (Path 2)
|
||||
|
||||
1. Read `.understand-anything/knowledge-graph.json`
|
||||
1. Read `$PROJECT_ROOT/.understand-anything/knowledge-graph.json`
|
||||
2. Format the graph data as structured context:
|
||||
- All nodes with their types, names, summaries, and tags
|
||||
- All edges with their types (especially `calls`, `imports`, `contains`)
|
||||
@@ -52,17 +122,17 @@ The preprocessing script does NOT produce a domain graph — it produces **raw m
|
||||
|
||||
### Phase 4: Domain Analysis
|
||||
|
||||
1. Read the domain-analyzer agent prompt from `agents/domain-analyzer.md`
|
||||
1. Read the domain-analyzer agent prompt from `$PLUGIN_ROOT/agents/domain-analyzer.md`
|
||||
2. Dispatch a subagent with the domain-analyzer prompt + the context from Phase 2 or 3
|
||||
3. The agent writes its output to `.understand-anything/intermediate/domain-analysis.json`
|
||||
3. The agent writes its output to `$PROJECT_ROOT/.understand-anything/intermediate/domain-analysis.json`
|
||||
|
||||
### Phase 5: Validate and Save
|
||||
|
||||
1. Read the domain analysis output
|
||||
2. Validate using the standard graph validation pipeline (the schema now supports domain/flow/step types)
|
||||
3. If validation fails, log warnings but save what's valid (error tolerance)
|
||||
4. Save to `.understand-anything/domain-graph.json`
|
||||
5. Clean up `.understand-anything/intermediate/domain-analysis.json` and `.understand-anything/intermediate/domain-context.json`
|
||||
4. Save to `$PROJECT_ROOT/.understand-anything/domain-graph.json`
|
||||
5. Clean up `$PROJECT_ROOT/.understand-anything/intermediate/domain-analysis.json` and `$PROJECT_ROOT/.understand-anything/intermediate/domain-context.json`
|
||||
|
||||
### Phase 6: Launch Dashboard
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ def build_name_to_stem_map(wiki_root: Path) -> dict[str, str]:
|
||||
basename_counts: dict[str, int] = {}
|
||||
for md_file in wiki_root.rglob("*.md"):
|
||||
rel = md_file.relative_to(wiki_root)
|
||||
stem = str(rel.with_suffix("")) # e.g., "decisions/decision-foo"
|
||||
stem = rel.with_suffix("").as_posix() # e.g., "decisions/decision-foo"
|
||||
basename = md_file.stem # e.g., "decision-foo"
|
||||
# Full relative path always maps uniquely
|
||||
name_map[stem.lower()] = stem
|
||||
@@ -313,7 +313,7 @@ def parse_wiki(root: Path) -> dict:
|
||||
article_ids: set[str] = set()
|
||||
for md_file in sorted(wiki_root.rglob("*.md")):
|
||||
rel = md_file.relative_to(wiki_root)
|
||||
stem = str(rel.with_suffix(""))
|
||||
stem = rel.with_suffix("").as_posix()
|
||||
# Only filter infra files at root level (no parent directory)
|
||||
if rel.parent == Path(".") and rel.name.lower() in INFRA_FILES:
|
||||
continue
|
||||
@@ -327,7 +327,7 @@ def parse_wiki(root: Path) -> dict:
|
||||
|
||||
for md_file in sorted(wiki_root.rglob("*.md")):
|
||||
rel = md_file.relative_to(wiki_root)
|
||||
stem = str(rel.with_suffix(""))
|
||||
stem = rel.with_suffix("").as_posix()
|
||||
basename = md_file.stem
|
||||
|
||||
# Skip infrastructure files only at wiki root level
|
||||
@@ -381,7 +381,7 @@ def parse_wiki(root: Path) -> dict:
|
||||
"complexity": complexity,
|
||||
"knowledgeMeta": {
|
||||
"wikilinks": [wl["target"] for wl in wikilinks],
|
||||
"category": category or None,
|
||||
**({"category": category} if category else {}),
|
||||
"content": text[:3000], # First 3000 chars for LLM analysis
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: understand
|
||||
description: Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships
|
||||
argument-hint: ["[path] [--full|--auto-update|--no-auto-update|--review]"]
|
||||
argument-hint: ["[path] [--full|--auto-update|--no-auto-update|--review|--language <lang>]"]
|
||||
---
|
||||
|
||||
# /understand
|
||||
@@ -15,6 +15,7 @@ Analyze the current codebase and produce a `knowledge-graph.json` file in `.unde
|
||||
- `--auto-update` — Enable automatic graph updates on commit (writes `autoUpdate: true` to `.understand-anything/config.json`)
|
||||
- `--no-auto-update` — Disable automatic graph updates (writes `autoUpdate: false` to `.understand-anything/config.json`)
|
||||
- `--review` — Run full LLM graph-reviewer instead of inline deterministic validation
|
||||
- `--language <lang>` — Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in the specified language. Accepts ISO 639-1 codes (`zh`, `ja`, `ko`, `en`, `es`, `fr`, `de`, etc.) or friendly names (`chinese`, `japanese`, `korean`, `english`, `spanish`, etc.). Locale variants supported: `zh-TW`, `zh-HK`, etc. Defaults to `en` (English). Stores preference in `.understand-anything/config.json` for consistency across incremental updates.
|
||||
- A directory path (e.g. `/path/to/repo` or `../other-project`) — Analyze the given directory instead of the current working directory
|
||||
|
||||
---
|
||||
@@ -29,6 +30,27 @@ Determine whether to run a full analysis or incremental update.
|
||||
- Verify the resolved path exists and is a directory (run `test -d <path>`). If it does not exist or is not a directory, report an error to the user and **STOP**.
|
||||
- Set `PROJECT_ROOT` to the resolved absolute path.
|
||||
- If no directory path argument is found, set `PROJECT_ROOT` to the current working directory.
|
||||
- **Worktree redirect.** If `PROJECT_ROOT` is inside a git worktree (not the main checkout), redirect output to the main repository root. Worktrees managed by Claude Code are ephemeral — `.understand-anything/` written there is destroyed when the session ends, taking the knowledge graph with it (issue #133). Detect a worktree by comparing `git rev-parse --git-dir` against `git rev-parse --git-common-dir`; in a normal checkout or submodule they resolve to the same path, in a worktree they differ and the parent of `--git-common-dir` is the main repo root.
|
||||
|
||||
```bash
|
||||
COMMON_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)
|
||||
GIT_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-dir 2>/dev/null)
|
||||
if [ -n "$COMMON_DIR" ] && [ -n "$GIT_DIR" ]; then
|
||||
COMMON_ABS=$(cd "$PROJECT_ROOT" && cd "$COMMON_DIR" 2>/dev/null && pwd -P)
|
||||
GIT_ABS=$(cd "$PROJECT_ROOT" && cd "$GIT_DIR" 2>/dev/null && pwd -P)
|
||||
if [ -n "$COMMON_ABS" ] && [ "$COMMON_ABS" != "$GIT_ABS" ]; then
|
||||
MAIN_ROOT=$(dirname "$COMMON_ABS")
|
||||
if [ -d "$MAIN_ROOT" ] && [ "${UNDERSTAND_NO_WORKTREE_REDIRECT:-0}" != "1" ]; then
|
||||
echo "[understand] Detected git worktree at $PROJECT_ROOT"
|
||||
echo "[understand] Redirecting output to main repo root: $MAIN_ROOT"
|
||||
echo "[understand] (Set UNDERSTAND_NO_WORKTREE_REDIRECT=1 to keep PROJECT_ROOT as the worktree.)"
|
||||
PROJECT_ROOT="$MAIN_ROOT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
Set `UNDERSTAND_NO_WORKTREE_REDIRECT=1` if you intentionally want a per-worktree graph (rare — most users want the redirect).
|
||||
1.5. **Ensure the plugin is built.** Later phases invoke Node scripts that import `@understand-anything/core`. On a fresh install `packages/core/dist/` does not exist yet — build once.
|
||||
|
||||
**Important:** do **not** assume the plugin root is simply two directories above the skill path string. In many installations `~/.agents/skills/understand` is a symlink into the real plugin checkout. Prefer runtime-provided plugin roots first (for Claude), then fall back to universal symlinks, skill symlink resolution, and common clone-based install paths.
|
||||
@@ -89,11 +111,27 @@ Determine whether to run a full analysis or incremental update.
|
||||
mkdir -p $PROJECT_ROOT/.understand-anything/tmp
|
||||
```
|
||||
3.5. **Auto-update configuration:**
|
||||
- If `--auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": true}` to `$PROJECT_ROOT/.understand-anything/config.json`
|
||||
- If `--no-auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": false}` to `$PROJECT_ROOT/.understand-anything/config.json`
|
||||
- These flags only set the config — analysis proceeds normally regardless.
|
||||
- If `--auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": true}` to `$PROJECT_ROOT/.understand-anything/config.json`
|
||||
- If `--no-auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": false}` to `$PROJECT_ROOT/.understand-anything/config.json`
|
||||
- These flags only set the config — analysis proceeds normally regardless.
|
||||
|
||||
4. **Check for subdomain knowledge graphs to merge:**
|
||||
3.6. **Language configuration:**
|
||||
- Parse `$ARGUMENTS` for `--language <lang>` flag. If found, extract the language code.
|
||||
- **Language code normalization:** Map friendly names to ISO codes:
|
||||
- `chinese` → `zh`, `japanese` → `ja`, `korean` → `ko`, `english` → `en`, `spanish` → `es`, `french` → `fr`, `german` → `de`, `portuguese` → `pt`, `russian` → `ru`, `arabic` → `ar`, etc.
|
||||
- Locale variants: `zh-TW`, `zh-HK`, `zh-CN`, `pt-BR`, etc. are preserved as-is.
|
||||
- If `--language` is NOT specified:
|
||||
- Check `$PROJECT_ROOT/.understand-anything/config.json` for an existing `outputLanguage` field. If present, use that.
|
||||
- If no stored preference, default to `en` (English).
|
||||
- If `--language` IS specified:
|
||||
- Update `$PROJECT_ROOT/.understand-anything/config.json` with the new language: merge `{"outputLanguage": "<lang>"}` into existing config.
|
||||
- Store as `$OUTPUT_LANGUAGE` for use throughout all phases.
|
||||
- **Language directive template:** Store as `$LANGUAGE_DIRECTIVE`:
|
||||
```markdown
|
||||
> **Language directive**: Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in **{language}**. Maintain technical accuracy while using natural, native-level phrasing in the target language. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel").
|
||||
```
|
||||
|
||||
4. **Check for subdomain knowledge graphs to merge:**
|
||||
List all `*knowledge-graph*.json` files in `$PROJECT_ROOT/.understand-anything/` **excluding** `knowledge-graph.json` itself (e.g. `frontend-knowledge-graph.json`, `backend-knowledge-graph.json`). If any subdomain graphs exist, run the merge script bundled with this skill (located next to this SKILL.md file — use the skill directory path, not the project root):
|
||||
```bash
|
||||
python <SKILL_DIR>/merge-subdomain-graphs.py $PROJECT_ROOT
|
||||
@@ -190,6 +228,8 @@ Dispatch a subagent using the `project-scanner` agent definition (at `agents/pro
|
||||
> ```
|
||||
>
|
||||
> Use this context to produce more accurate project name, description, and framework detection. The README and manifest are authoritative — prefer their information over heuristics.
|
||||
>
|
||||
> $LANGUAGE_DIRECTIVE
|
||||
|
||||
Pass these parameters in the dispatch prompt:
|
||||
|
||||
@@ -236,6 +276,8 @@ For each batch, dispatch a subagent using the `file-analyzer` agent definition (
|
||||
>
|
||||
> Project: `<projectName>` — `<projectDescription>`
|
||||
> Languages: `<languages from Phase 1>`
|
||||
>
|
||||
> $LANGUAGE_DIRECTIVE
|
||||
|
||||
Before dispatching each batch, construct `batchImportData` from `$IMPORT_MAP`:
|
||||
```json
|
||||
@@ -327,9 +369,10 @@ After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermedi
|
||||
## Phase 4 — ARCHITECTURE
|
||||
|
||||
**Build the combined prompt template:**
|
||||
1. Use the `architecture-analyzer` agent definition (at `agents/architecture-analyzer.md`).
|
||||
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`, `markdown`, `dockerfile`, `yaml`, `sql`, `terraform`, `graphql`, `protobuf`, `shell`, `html`, `css`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`, `./languages/dockerfile.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. **Include non-code language snippets** — they provide edge patterns and summary styles for non-code files.
|
||||
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
|
||||
1. Use the `architecture-analyzer` agent definition (at `agents/architecture-analyzer.md`).
|
||||
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`, `markdown`, `dockerfile`, `yaml`, `sql`, `terraform`, `graphql`, `protobuf`, `shell`, `html`, `css`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`, `./languages/dockerfile.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. **Include non-code language snippets** — they provide edge patterns and summary styles for non-code files.
|
||||
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
|
||||
4. **Output locale injection:** If `$OUTPUT_LANGUAGE` is NOT `en` (English), read the locale guidance file at `./locales/<language-code>.md` (e.g., `./locales/zh.md`, `./locales/ja.md`, `./locales/ko.md`) and append its content after the framework addendums under a `## Output Language Guidelines` header. This provides language-specific guidance for tag naming conventions, summary style, and layer name translations. If the locale file does not exist for the specified language, skip silently — the `$LANGUAGE_DIRECTIVE` still applies. These files are in the `locales/` subdirectory next to this SKILL.md file.
|
||||
|
||||
Append the language/framework context and the following additional context to the agent's prompt:
|
||||
|
||||
@@ -343,6 +386,8 @@ Append the language/framework context and the following additional context to th
|
||||
> ```
|
||||
>
|
||||
> Use the directory tree, language context, and framework addendums (appended above) to inform layer assignments. Directory structure is strong evidence for layer boundaries. Non-code files (config, docs, infrastructure, data) should be assigned to appropriate layers — see the prompt template for guidance.
|
||||
>
|
||||
> $LANGUAGE_DIRECTIVE
|
||||
|
||||
Pass these parameters in the dispatch prompt:
|
||||
|
||||
@@ -416,6 +461,8 @@ Dispatch a subagent using the `tour-builder` agent definition (at `agents/tour-b
|
||||
> Project entry point: `$ENTRY_POINT`
|
||||
>
|
||||
> Use the README to align the tour narrative with the project's own documentation. Start the tour from the entry point if one was detected. The tour should tell the same story the README tells, but through the lens of actual code structure.
|
||||
>
|
||||
> $LANGUAGE_DIRECTIVE
|
||||
|
||||
Pass these parameters in the dispatch prompt:
|
||||
|
||||
@@ -635,7 +682,30 @@ Pass these parameters in the dispatch prompt:
|
||||
|
||||
1. Write the final knowledge graph to `$PROJECT_ROOT/.understand-anything/knowledge-graph.json`.
|
||||
|
||||
2. Write metadata to `$PROJECT_ROOT/.understand-anything/meta.json`:
|
||||
2. **Generate structural fingerprints baseline.** This creates the basis for future automatic incremental updates and **must succeed before `meta.json` is written** — otherwise auto-update sees a fresh commit hash with no fingerprints to compare against, classifies every file as STRUCTURAL, and escalates to `FULL_UPDATE` on every subsequent commit (issue #152).
|
||||
|
||||
Write the input file:
|
||||
```bash
|
||||
cat > $PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json <<EOF
|
||||
{
|
||||
"projectRoot": "$PROJECT_ROOT",
|
||||
"sourceFilePaths": [<all source file paths from Phase 1, as JSON array>],
|
||||
"gitCommitHash": "<current commit hash>"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Then invoke the bundled script (located next to this SKILL.md):
|
||||
```bash
|
||||
node <SKILL_DIR>/build-fingerprints.mjs \
|
||||
$PROJECT_ROOT/.understand-anything/intermediate/fingerprint-input.json
|
||||
```
|
||||
|
||||
The script uses `TreeSitterPlugin + PluginRegistry` exactly like `extract-structure.mjs`, so the baseline matches the comparison logic used during auto-updates.
|
||||
|
||||
**If the script exits non-zero or stdout does not include `Fingerprints baseline:`, abort Phase 7 and report the error. Do NOT proceed to step 3 (writing `meta.json`).**
|
||||
|
||||
3. Write metadata to `$PROJECT_ROOT/.understand-anything/meta.json` (only after step 2 succeeded):
|
||||
```json
|
||||
{
|
||||
"lastAnalyzedAt": "<ISO 8601 timestamp>",
|
||||
@@ -645,25 +715,13 @@ Pass these parameters in the dispatch prompt:
|
||||
}
|
||||
```
|
||||
|
||||
2.5. **Generate structural fingerprints** for all analyzed files and save to `$PROJECT_ROOT/.understand-anything/fingerprints.json`. This creates the baseline for future automatic incremental updates.
|
||||
|
||||
Write and execute a Node.js script that uses the core fingerprint module (tree-sitter-based, not regex):
|
||||
```javascript
|
||||
import { buildFingerprintStore } from '@understand-anything/core';
|
||||
import { saveFingerprints } from '@understand-anything/core';
|
||||
|
||||
const store = await buildFingerprintStore('<PROJECT_ROOT>', sourceFilePaths);
|
||||
saveFingerprints('<PROJECT_ROOT>', store);
|
||||
```
|
||||
Where `sourceFilePaths` is the list of all analyzed source file paths from Phase 1. This uses the same tree-sitter analysis pipeline as the main fingerprint engine, ensuring the baseline matches the comparison logic used during auto-updates.
|
||||
|
||||
3. Clean up intermediate files:
|
||||
4. Clean up intermediate files:
|
||||
```bash
|
||||
rm -rf $PROJECT_ROOT/.understand-anything/intermediate
|
||||
rm -rf $PROJECT_ROOT/.understand-anything/tmp
|
||||
```
|
||||
|
||||
4. Report a summary to the user containing:
|
||||
5. Report a summary to the user containing:
|
||||
- Project name and description
|
||||
- Files analyzed / total files (with breakdown by fileCategory: code, config, docs, infra, data, script, markup)
|
||||
- Nodes created (broken down by type: file, function, class, config, document, service, table, endpoint, pipeline, schema, resource)
|
||||
@@ -673,7 +731,7 @@ Pass these parameters in the dispatch prompt:
|
||||
- Any warnings from the reviewer
|
||||
- Path to the output file: `$PROJECT_ROOT/.understand-anything/knowledge-graph.json`
|
||||
|
||||
5. Only automatically launch the dashboard by invoking the `/understand-dashboard` skill if final graph validation passed after normalization/review fixes.
|
||||
6. Only automatically launch the dashboard by invoking the `/understand-dashboard` skill if final graph validation passed after normalization/review fixes.
|
||||
If final validation did not pass, report that the graph was saved with warnings and dashboard launch was skipped.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-fingerprints.mjs
|
||||
*
|
||||
* Builds the structural-fingerprint baseline used by auto-update's
|
||||
* incremental change detection. Runs once per /understand full rebuild
|
||||
* (Phase 7 step 2.5), generating .understand-anything/fingerprints.json.
|
||||
*
|
||||
* Replaces the LLM-written fingerprint script that previously sat in
|
||||
* SKILL.md as a code example — that example had the wrong signature
|
||||
* for buildFingerprintStore() and never successfully produced a baseline,
|
||||
* which silently broke auto-update for every install (see issue #152).
|
||||
*
|
||||
* Usage:
|
||||
* node build-fingerprints.mjs <input.json>
|
||||
*
|
||||
* Input JSON:
|
||||
* { projectRoot: string, sourceFilePaths: string[], gitCommitHash: string }
|
||||
*
|
||||
* Writes: <projectRoot>/.understand-anything/fingerprints.json
|
||||
* Exit code: 0 on success (including 0 files analyzed); non-zero on error.
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
// skills/understand/ -> plugin root is two dirs up
|
||||
const pluginRoot = resolve(__dirname, '../..');
|
||||
const require = createRequire(resolve(pluginRoot, 'package.json'));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolve @understand-anything/core (matches extract-structure.mjs).
|
||||
// pathToFileURL() is required for Windows: dynamic import() of a raw
|
||||
// "C:\..." path throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
||||
// ---------------------------------------------------------------------------
|
||||
let core;
|
||||
try {
|
||||
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
|
||||
} catch {
|
||||
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
|
||||
}
|
||||
|
||||
const {
|
||||
TreeSitterPlugin,
|
||||
PluginRegistry,
|
||||
builtinLanguageConfigs,
|
||||
registerAllParsers,
|
||||
buildFingerprintStore,
|
||||
saveFingerprints,
|
||||
} = core;
|
||||
|
||||
async function main() {
|
||||
const [, , inputPath] = process.argv;
|
||||
if (!inputPath) {
|
||||
process.stderr.write('Usage: node build-fingerprints.mjs <input.json>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { projectRoot, sourceFilePaths, gitCommitHash } = JSON.parse(
|
||||
readFileSync(inputPath, 'utf-8'),
|
||||
);
|
||||
|
||||
if (!projectRoot || !Array.isArray(sourceFilePaths) || typeof gitCommitHash !== 'string') {
|
||||
throw new Error(
|
||||
'Invalid input: requires { projectRoot: string, sourceFilePaths: string[], gitCommitHash: string }',
|
||||
);
|
||||
}
|
||||
|
||||
// Create tree-sitter plugin with all configs that have WASM grammars,
|
||||
// mirroring extract-structure.mjs so the baseline matches the comparison
|
||||
// logic used during auto-updates.
|
||||
const tsConfigs = builtinLanguageConfigs.filter((c) => c.treeSitter);
|
||||
const tsPlugin = new TreeSitterPlugin(tsConfigs);
|
||||
await tsPlugin.init();
|
||||
|
||||
const registry = new PluginRegistry();
|
||||
registry.register(tsPlugin);
|
||||
registerAllParsers(registry);
|
||||
|
||||
const store = buildFingerprintStore(projectRoot, sourceFilePaths, registry, gitCommitHash);
|
||||
saveFingerprints(projectRoot, store);
|
||||
|
||||
const fileCount = Object.keys(store.files).length;
|
||||
process.stdout.write(`Fingerprints baseline: ${fileCount} files\n`);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -28,13 +28,17 @@ const require = createRequire(resolve(pluginRoot, 'package.json'));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolve @understand-anything/core
|
||||
//
|
||||
// Node ESM dynamic import() requires a file:// URL on Windows; passing a raw
|
||||
// absolute path like "C:\..." throws ERR_UNSUPPORTED_ESM_URL_SCHEME because the
|
||||
// loader parses "C:" as a URL scheme. Wrap both resolutions in pathToFileURL().
|
||||
// ---------------------------------------------------------------------------
|
||||
let core;
|
||||
try {
|
||||
core = await import(require.resolve('@understand-anything/core'));
|
||||
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
|
||||
} catch {
|
||||
// Fallback: direct path for installed plugin cache layouts
|
||||
core = await import(resolve(pluginRoot, 'packages/core/dist/index.js'));
|
||||
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
|
||||
}
|
||||
|
||||
const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# English Output Guidelines
|
||||
|
||||
This file provides language-specific guidance for generating knowledge graph content in English.
|
||||
|
||||
## Tag Conventions
|
||||
|
||||
Use lowercase, hyphenated tags in English:
|
||||
|
||||
| Pattern | Recommended Tags |
|
||||
|---------|-----------------|
|
||||
| Entry point file | `entry-point`, `barrel`, `exports` |
|
||||
| Utility functions | `utility`, `helpers`, `common` |
|
||||
| API handlers | `api-handler`, `controller`, `endpoint` |
|
||||
| Data models | `data-model`, `entity`, `schema` |
|
||||
| Test files | `test`, `spec`, `unit-test` |
|
||||
| Configuration | `configuration`, `build-system`, `settings` |
|
||||
| Infrastructure | `infrastructure`, `deployment`, `containerization` |
|
||||
| Documentation | `documentation`, `guide`, `reference` |
|
||||
|
||||
## Summary Style
|
||||
|
||||
Write 1-2 sentence summaries that:
|
||||
- Describe **purpose** and **role** in the project
|
||||
- Use active voice ("Provides...", "Handles...", "Manages...")
|
||||
- Avoid restating the filename
|
||||
|
||||
**Examples:**
|
||||
- Good: "Provides date formatting and string sanitization helpers used across the API layer."
|
||||
- Bad: "The utils file contains utility functions."
|
||||
|
||||
## Technical Terms
|
||||
|
||||
Keep these terms in English (no translation needed):
|
||||
- `middleware`, `hook`, `barrel`, `entry-point`
|
||||
- `ORM`, `REST API`, `CI/CD`, `CRUD`
|
||||
- `singleton`, `factory`, `observer`
|
||||
- `middleware`, `interceptor`, `guard`
|
||||
|
||||
## Layer Names
|
||||
|
||||
Use standard English layer names:
|
||||
- `API Layer`, `Service Layer`, `Data Layer`, `UI Layer`
|
||||
- `Infrastructure`, `Configuration`, `Documentation`
|
||||
- `Utility Layer`, `Middleware Layer`, `Test Layer`
|
||||
@@ -0,0 +1,49 @@
|
||||
# 日本語出力ガイドライン (Japanese)
|
||||
|
||||
本ファイルは、日本語でナレッジグラフコンテンツを生成する際の言語固有のガイドラインを提供します。
|
||||
|
||||
## タグの命名規則
|
||||
|
||||
日本語タグまたは英語の一般的な技術用語を使用:
|
||||
|
||||
| パターン | 推奨タグ |
|
||||
|---------|---------|
|
||||
| エントリーポイント | `入口点`, `barrel`, `exports` または `entry-point` |
|
||||
| ユーティリティ | `ユーティリティ`, `helpers`, `utility` |
|
||||
| APIハンドラー | `api-handler`, `controller`, `endpoint` |
|
||||
| データモデル | `データモデル`, `entity`, `schema` または `data-model` |
|
||||
| テストファイル | `テスト`, `unit-test`, `test` |
|
||||
| 設定ファイル | `設定`, `build-system`, `configuration` |
|
||||
| インフラ | `インフラ`, `deployment`, `infrastructure` |
|
||||
| ドキュメント | `ドキュメント`, `guide`, `documentation` |
|
||||
|
||||
**混合戦略:** 一般的な技術用語は英語を保持(`middleware`, `api-handler`など)、説明用タグは日本語を使用可能。
|
||||
|
||||
## サマリーのスタイル
|
||||
|
||||
1-2文のサマリーを日本語で記述:
|
||||
- ファイルの**目的**と**役割**を説明
|
||||
- 能動態を使用(「提供する...」「処理する...」「管理する...」)
|
||||
- ファイル名の繰り返しを避ける
|
||||
|
||||
**例:**
|
||||
- 良い: "API層全体で使用される日付フォーマットと文字列サニタイズのヘルパー関数を提供。"
|
||||
- 悪い: "utilsファイルにはユーティリティ関数が含まれています。"
|
||||
|
||||
## 技術用語
|
||||
|
||||
以下の用語は英語を保持(標準翻訳がない場合):
|
||||
- `middleware`, `hook`, `barrel`, `entry-point`
|
||||
- `ORM`, `REST API`, `CI/CD`, `CRUD`
|
||||
- `singleton`, `factory`, `observer`
|
||||
- `interceptor`, `guard`
|
||||
|
||||
## レイヤー名
|
||||
|
||||
日本語のレイヤー名を使用:
|
||||
- `API層`, `サービス層`, `データ層`, `UI層`
|
||||
- `インフラ`, `設定`, `ドキュメント`
|
||||
- `ユーティリティ層`, `ミドルウェア層`, `テスト層`
|
||||
|
||||
または英語を保持(チームの慣習に従う):
|
||||
- `API Layer`, `Service Layer`, `Data Layer`
|
||||
@@ -0,0 +1,49 @@
|
||||
# 한국어 출력 가이드라인 (Korean)
|
||||
|
||||
이 파일은 한국어로 지식 그래프 콘텐츠를 생성할 때의 언어별 가이드를 제공합니다.
|
||||
|
||||
## 태그 명명 규칙
|
||||
|
||||
한국어 태그 또는 영어 일반 기술 용어 사용:
|
||||
|
||||
| 패턴 | 추천 태그 |
|
||||
|------|---------|
|
||||
| 진입점 파일 | `진입점`, `barrel`, `exports` 또는 `entry-point` |
|
||||
| 유틸리티 함수 | `유틸리티`, `helpers`, `utility` |
|
||||
| API 핸들러 | `api-handler`, `controller`, `endpoint` |
|
||||
| 데이터 모델 | `데이터모델`, `entity`, `schema` 또는 `data-model` |
|
||||
| 테스트 파일 | `테스트`, `unit-test`, `test` |
|
||||
| 설정 파일 | `설정`, `build-system`, `configuration` |
|
||||
| 인프라 | `인프라`, `deployment`, `infrastructure` |
|
||||
| 문서 | `문서`, `guide`, `documentation` |
|
||||
|
||||
**혼합 전략:** 일반 기술 용어는 영어 유지 (`middleware`, `api-handler` 등), 설명용 태그는 한국어 사용 가능.
|
||||
|
||||
## 요약 스타일
|
||||
|
||||
1-2문장 요약을 한국어로 작성:
|
||||
- 파일의 **목적**과 **역할** 설명
|
||||
- 능동태 사용 ("제공하는...", "처리하는...", "관리하는...")
|
||||
- 파일명 반복 피하기
|
||||
|
||||
**예시:**
|
||||
- 좋음: "API 레이어 전체에서 사용되는 날짜 포맷 및 문자열 정제 헬per 함수를 제공."
|
||||
- 나쁨: "utils 파일에는 유틸리티 함수가 포함되어 있습니다."
|
||||
|
||||
## 기술 용어
|
||||
|
||||
다음 용어는 영어 유지 (표준 번역 없음):
|
||||
- `middleware`, `hook`, `barrel`, `entry-point`
|
||||
- `ORM`, `REST API`, `CI/CD`, `CRUD`
|
||||
- `singleton`, `factory`, `observer`
|
||||
- `interceptor`, `guard`
|
||||
|
||||
## 레이어 이름
|
||||
|
||||
한국어 레이어 이름 사용:
|
||||
- `API 레이어`, `서비스 레이어`, `데이터 레이어`, `UI 레이어`
|
||||
- `인프라`, `설정`, `문서`
|
||||
- `유틸리티 레이어`, `미들웨어 레이어`, `테스트 레이어`
|
||||
|
||||
또는 영어 유지 (팀 관습에 따라):
|
||||
- `API Layer`, `Service Layer`, `Data Layer`
|
||||
@@ -0,0 +1,49 @@
|
||||
# Руководство по выводу на русском языке (Russian)
|
||||
|
||||
Этот файл содержит языковые рекомендации для генерации контента графа знаний на русском языке.
|
||||
|
||||
## Соглашения по тегам
|
||||
|
||||
Используйте русские теги или общепринятые английские технические термины:
|
||||
|
||||
| Шаблон | Рекомендуемые теги |
|
||||
|--------|--------------------|
|
||||
| Точка входа | `точка-входа`, `barrel`, `exports` или `entry-point` |
|
||||
| Утилитарные функции | `утилиты`, `helpers`, `common` или `utility` |
|
||||
| API-обработчики | `api-handler`, `контроллер`, `endpoint` |
|
||||
| Модели данных | `модель-данных`, `entity`, `schema` или `data-model` |
|
||||
| Тестовые файлы | `тесты`, `unit-test`, `test` |
|
||||
| Конфигурационные файлы | `конфигурация`, `build-system`, `settings` или `configuration` |
|
||||
| Инфраструктура | `инфраструктура`, `deployment`, `контейнеризация` или `infrastructure` |
|
||||
| Документация | `документация`, `руководство`, `documentation` |
|
||||
|
||||
**Смешанная стратегия:** общепринятые технические термины оставляйте на английском (например, `middleware`, `api-handler`), описательные теги можно писать на русском.
|
||||
|
||||
## Стиль резюме
|
||||
|
||||
Пишите резюме на русском в 1–2 предложениях:
|
||||
- Описывайте **назначение** и **роль** файла
|
||||
- Используйте активный залог («предоставляет…», «обрабатывает…», «управляет…»)
|
||||
- Избегайте повторения имени файла
|
||||
|
||||
**Примеры:**
|
||||
- Хорошо: «Предоставляет вспомогательные функции для форматирования дат и очистки строк, широко используемые на API-слое.»
|
||||
- Плохо: «Файл utils содержит утилитарные функции.»
|
||||
|
||||
## Технические термины
|
||||
|
||||
Следующие термины рекомендуется оставлять на английском (устоявшегося перевода нет):
|
||||
- `middleware`, `hook`, `barrel`, `entry-point`
|
||||
- `ORM`, `REST API`, `CI/CD`, `CRUD`
|
||||
- `singleton`, `factory`, `observer`
|
||||
- `interceptor`, `guard`
|
||||
|
||||
## Имена слоёв
|
||||
|
||||
Используйте русские имена слоёв:
|
||||
- `API-слой`, `сервисный слой`, `слой данных`, `UI-слой`
|
||||
- `инфраструктура`, `конфигурация`, `документация`
|
||||
- `утилитарный слой`, `слой middleware`, `тестовый слой`
|
||||
|
||||
Или оставляйте английские (по договорённости команды):
|
||||
- `API Layer`, `Service Layer`, `Data Layer`
|
||||
@@ -0,0 +1,49 @@
|
||||
# 繁體中文輸出指南 (Chinese Traditional)
|
||||
|
||||
本文件提供生成繁體中文知識圖譜內容的語言指導。
|
||||
|
||||
## 標籤約定
|
||||
|
||||
推薦使用繁體中文標籤或英文通用技術術語:
|
||||
|
||||
| 模式 | 推薦標籤 |
|
||||
|------|---------|
|
||||
| 入口檔案 | `入口點`, `barrel`, `匯出` 或 `entry-point` |
|
||||
| 工具函數 | `工具函數`, `helpers`, `common` 或 `utility` |
|
||||
| API處理器 | `api-handler`, `控制器`, `端點` |
|
||||
| 資料模型 | `資料模型`, `entity`, `schema` 或 `data-model` |
|
||||
| 測試檔案 | `測試`, `單元測試`, `test` |
|
||||
| 設定檔 | `設定`, `建構系統`, `settings` 或 `configuration` |
|
||||
| 基礎架構 | `基礎架構`, `部署`, `容器化` 或 `infrastructure` |
|
||||
| 文件 | `文件`, `指南`, `參考` 或 `documentation` |
|
||||
|
||||
**混合策略:** 通用技術術語保留英文(如 `middleware`, `api-handler`),描述性標籤可使用繁體中文。
|
||||
|
||||
## 摘要風格
|
||||
|
||||
用繁體中文撰寫1-2句摘要:
|
||||
- 描述檔案的**目的**和**作用**
|
||||
- 使用主動語態("提供...", "處理...", "管理...")
|
||||
- 避免重複檔名
|
||||
|
||||
**範例:**
|
||||
- 好: "提供日期格式化和字串清洗工具函數,被 API 層廣泛使用。"
|
||||
- 差: "utils 檔案包含工具函數。"
|
||||
|
||||
## 技術術語
|
||||
|
||||
以下術語建議保留英文(暫無標準翻譯):
|
||||
- `middleware`, `hook`, `barrel`, `entry-point`
|
||||
- `ORM`, `REST API`, `CI/CD`, `CRUD`
|
||||
- `singleton`, `factory`, `observer`
|
||||
- `interceptor`, `guard`
|
||||
|
||||
## 層級名稱
|
||||
|
||||
使用繁體中文層級名稱:
|
||||
- `API 層`, `服務層`, `資料層`, `UI 層`
|
||||
- `基礎架構`, `設定`, `文件`
|
||||
- `工具層`, `中介軟體層`, `測試層`
|
||||
|
||||
或保留英文(根據團隊習慣):
|
||||
- `API Layer`, `Service Layer`, `Data Layer`
|
||||
@@ -0,0 +1,49 @@
|
||||
# 中文输出指南 (Chinese Simplified)
|
||||
|
||||
本文件提供生成中文知识图谱内容的语言指导。
|
||||
|
||||
## 标签约定
|
||||
|
||||
推荐使用中文标签或英文通用技术术语:
|
||||
|
||||
| 模式 | 推荐标签 |
|
||||
|------|---------|
|
||||
| 入口文件 | `入口点`, `barrel`, `导出` 或 `entry-point` |
|
||||
| 工具函数 | `工具函数`, `helpers`, `common` 或 `utility` |
|
||||
| API处理器 | `api-handler`, `控制器`, `端点` |
|
||||
| 数据模型 | `数据模型`, `entity`, `schema` 或 `data-model` |
|
||||
| 测试文件 | `测试`, `单元测试`, `test` |
|
||||
| 配置文件 | `配置`, `构建系统`, `settings` 或 `configuration` |
|
||||
| 基础设施 | `基础设施`, `部署`, `容器化` 或 `infrastructure` |
|
||||
| 文档 | `文档`, `指南`, `参考` 或 `documentation` |
|
||||
|
||||
**混合策略:** 通用技术术语保留英文(如 `middleware`, `api-handler`),描述性标签可使用中文。
|
||||
|
||||
## 摘要风格
|
||||
|
||||
用中文撰写1-2句摘要:
|
||||
- 描述文件的**目的**和**作用**
|
||||
- 使用主动语态("提供...", "处理...", "管理...")
|
||||
- 避免重复文件名
|
||||
|
||||
**示例:**
|
||||
- 好: "提供日期格式化和字符串清洗工具函数,被 API 层广泛使用。"
|
||||
- 差: "utils 文件包含工具函数。"
|
||||
|
||||
## 技术术语
|
||||
|
||||
以下术语建议保留英文(暂无标准翻译):
|
||||
- `middleware`, `hook`, `barrel`, `entry-point`
|
||||
- `ORM`, `REST API`, `CI/CD`, `CRUD`
|
||||
- `singleton`, `factory`, `observer`
|
||||
- `interceptor`, `guard`
|
||||
|
||||
## 层级名称
|
||||
|
||||
使用中文层级名称:
|
||||
- `API 层`, `服务层`, `数据层`, `UI 层`
|
||||
- `基础设施`, `配置`, `文档`
|
||||
- `工具层`, `中间件层`, `测试层`
|
||||
|
||||
或保留英文(根据团队习惯):
|
||||
- `API Layer`, `Service Layer`, `Data Layer`
|
||||
@@ -106,6 +106,21 @@ _TEST_NAME_PATTERNS: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {
|
||||
}
|
||||
|
||||
|
||||
# Mirrors packages/core/src/schema.ts so the dashboard validator has nothing
|
||||
# left to auto-correct for the `direction` field on merged graphs.
|
||||
_DIRECTION_ALIASES: dict[str, str] = {"both": "bidirectional", "mutual": "bidirectional"}
|
||||
_VALID_DIRECTIONS: frozenset[str] = frozenset({"forward", "backward", "bidirectional"})
|
||||
|
||||
|
||||
def normalize_direction(value: Any) -> str:
|
||||
"""Canonicalize an edge `direction` value to one of the schema enum members."""
|
||||
candidate = value.lower() if isinstance(value, str) else ""
|
||||
candidate = _DIRECTION_ALIASES.get(candidate, candidate)
|
||||
if candidate not in _VALID_DIRECTIONS:
|
||||
return "forward"
|
||||
return candidate
|
||||
|
||||
|
||||
def _num(v: Any) -> float:
|
||||
"""Coerce a value to float for safe comparison (handles string weights)."""
|
||||
try:
|
||||
@@ -803,7 +818,8 @@ def merge_and_normalize(batches: list[dict[str, Any]]) -> tuple[dict[str, Any],
|
||||
src = edge.get("source", "")
|
||||
tgt = edge.get("target", "")
|
||||
etype = edge.get("type", "")
|
||||
direction = edge.get("direction", "forward")
|
||||
direction = normalize_direction(edge.get("direction"))
|
||||
edge["direction"] = direction
|
||||
|
||||
if src not in node_ids or tgt not in node_ids:
|
||||
missing = []
|
||||
|
||||
@@ -870,5 +870,76 @@ class MergeIntegrationTests(unittest.TestCase):
|
||||
self.assertIn("tested", prod_node["tags"])
|
||||
|
||||
|
||||
class NormalizeDirectionTests(unittest.TestCase):
|
||||
"""`direction` canonicalization mirrors the dashboard schema validator."""
|
||||
|
||||
def test_missing_defaults_to_forward(self) -> None:
|
||||
self.assertEqual(mbg.normalize_direction(None), "forward")
|
||||
self.assertEqual(mbg.normalize_direction(""), "forward")
|
||||
|
||||
def test_valid_values_pass_through(self) -> None:
|
||||
for value in ("forward", "backward", "bidirectional"):
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(mbg.normalize_direction(value), value)
|
||||
|
||||
def test_case_is_normalized(self) -> None:
|
||||
self.assertEqual(mbg.normalize_direction("Forward"), "forward")
|
||||
self.assertEqual(mbg.normalize_direction("BIDIRECTIONAL"), "bidirectional")
|
||||
|
||||
def test_aliases_are_mapped(self) -> None:
|
||||
self.assertEqual(mbg.normalize_direction("both"), "bidirectional")
|
||||
self.assertEqual(mbg.normalize_direction("Mutual"), "bidirectional")
|
||||
|
||||
def test_unknown_values_fall_back_to_forward(self) -> None:
|
||||
self.assertEqual(mbg.normalize_direction("sideways"), "forward")
|
||||
self.assertEqual(mbg.normalize_direction(42), "forward")
|
||||
|
||||
|
||||
class MergeEdgeDirectionTests(unittest.TestCase):
|
||||
"""End-to-end: merge_and_normalize persists a canonical `direction`."""
|
||||
|
||||
def _two_node_batch(self, edge: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"nodes": [_file_node("src/a.ts"), _file_node("src/b.ts")],
|
||||
"edges": [edge],
|
||||
}
|
||||
|
||||
def test_missing_direction_is_persisted_as_forward(self) -> None:
|
||||
# Reproduces issue #140: edges without a `direction` field still
|
||||
# reach the final graph and trigger dashboard auto-corrections.
|
||||
batch = self._two_node_batch({
|
||||
"source": "file:src/a.ts",
|
||||
"target": "file:src/b.ts",
|
||||
"type": "depends_on",
|
||||
"weight": 0.5,
|
||||
})
|
||||
|
||||
assembled, _report = mbg.merge_and_normalize([batch])
|
||||
|
||||
edges = [e for e in assembled["edges"] if e["type"] == "depends_on"]
|
||||
self.assertEqual(len(edges), 1)
|
||||
self.assertEqual(edges[0]["direction"], "forward")
|
||||
|
||||
def test_alias_is_canonicalized_before_dedup(self) -> None:
|
||||
# `"both"` and `"bidirectional"` describe the same relationship; without
|
||||
# canonicalization they get separate dedup keys and leak duplicates.
|
||||
batch = {
|
||||
"nodes": [_file_node("src/a.ts"), _file_node("src/b.ts")],
|
||||
"edges": [
|
||||
{"source": "file:src/a.ts", "target": "file:src/b.ts",
|
||||
"type": "depends_on", "direction": "both", "weight": 0.3},
|
||||
{"source": "file:src/a.ts", "target": "file:src/b.ts",
|
||||
"type": "depends_on", "direction": "bidirectional", "weight": 0.9},
|
||||
],
|
||||
}
|
||||
|
||||
assembled, _report = mbg.merge_and_normalize([batch])
|
||||
|
||||
edges = [e for e in assembled["edges"] if e["type"] == "depends_on"]
|
||||
self.assertEqual(len(edges), 1)
|
||||
self.assertEqual(edges[0]["direction"], "bidirectional")
|
||||
self.assertEqual(edges[0]["weight"], 0.9)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Validates the worktree-redirect bash snippet embedded in
|
||||
// `skills/understand/SKILL.md` Phase 0 step 1 and
|
||||
// `skills/understand-domain/SKILL.md` Phase 0.
|
||||
//
|
||||
// If you edit the snippet in either SKILL.md, mirror the change to RESOLVE_SNIPPET
|
||||
// below — there is no shared script to source (per-skill convention in this repo).
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const RESOLVE_SNIPPET = `
|
||||
COMMON_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)
|
||||
GIT_DIR=$(git -C "$PROJECT_ROOT" rev-parse --git-dir 2>/dev/null)
|
||||
if [ -n "$COMMON_DIR" ] && [ -n "$GIT_DIR" ]; then
|
||||
COMMON_ABS=$(cd "$PROJECT_ROOT" && cd "$COMMON_DIR" 2>/dev/null && pwd -P)
|
||||
GIT_ABS=$(cd "$PROJECT_ROOT" && cd "$GIT_DIR" 2>/dev/null && pwd -P)
|
||||
if [ -n "$COMMON_ABS" ] && [ "$COMMON_ABS" != "$GIT_ABS" ]; then
|
||||
MAIN_ROOT=$(dirname "$COMMON_ABS")
|
||||
if [ -d "$MAIN_ROOT" ] && [ "\${UNDERSTAND_NO_WORKTREE_REDIRECT:-0}" != "1" ]; then
|
||||
PROJECT_ROOT="$MAIN_ROOT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "$PROJECT_ROOT"
|
||||
`;
|
||||
|
||||
function runResolve(projectRoot, env = {}) {
|
||||
// No `set -e` — the snippet relies on `git ... 2>/dev/null` returning empty
|
||||
// strings when not in a git repo; `set -e` would short-circuit instead.
|
||||
const script = `PROJECT_ROOT=${JSON.stringify(projectRoot)}\n${RESOLVE_SNIPPET}`;
|
||||
return execFileSync("bash", ["-c", script], {
|
||||
env: { ...process.env, ...env },
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
}
|
||||
|
||||
let tmpRoot;
|
||||
let mainRepo;
|
||||
let worktree;
|
||||
let subdir;
|
||||
|
||||
beforeAll(() => {
|
||||
tmpRoot = realpathSync(mkdtempSync(join(tmpdir(), "ua-wt-")));
|
||||
mainRepo = join(tmpRoot, "main");
|
||||
worktree = join(tmpRoot, "wt");
|
||||
subdir = join(worktree, "src", "deep");
|
||||
|
||||
execFileSync("git", ["init", "-q", "-b", "main", mainRepo]);
|
||||
execFileSync("git", ["-C", mainRepo, "config", "user.email", "t@t"]);
|
||||
execFileSync("git", ["-C", mainRepo, "config", "user.name", "t"]);
|
||||
writeFileSync(join(mainRepo, "README.md"), "main\n");
|
||||
execFileSync("git", ["-C", mainRepo, "add", "."]);
|
||||
execFileSync("git", ["-C", mainRepo, "commit", "-q", "-m", "init"]);
|
||||
execFileSync("git", ["-C", mainRepo, "worktree", "add", "-q", worktree]);
|
||||
mkdirSync(subdir, { recursive: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (tmpRoot) rmSync(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("worktree-redirect snippet (issue #133)", () => {
|
||||
it("leaves PROJECT_ROOT alone in a normal checkout", () => {
|
||||
expect(runResolve(mainRepo)).toBe(mainRepo);
|
||||
});
|
||||
|
||||
it("redirects PROJECT_ROOT to the main repo when started in a worktree", () => {
|
||||
expect(runResolve(worktree)).toBe(mainRepo);
|
||||
});
|
||||
|
||||
it("redirects from a subdirectory inside a worktree", () => {
|
||||
expect(runResolve(subdir)).toBe(mainRepo);
|
||||
});
|
||||
|
||||
it("respects UNDERSTAND_NO_WORKTREE_REDIRECT=1", () => {
|
||||
expect(runResolve(worktree, { UNDERSTAND_NO_WORKTREE_REDIRECT: "1" })).toBe(worktree);
|
||||
});
|
||||
|
||||
it("leaves PROJECT_ROOT alone when not inside a git repo", () => {
|
||||
// Use a path under the resolved tmp root so we never accidentally land
|
||||
// inside a parent git repo (e.g. when /tmp is symlinked into one).
|
||||
const nonGit = join(tmpRoot, "no-git");
|
||||
mkdirSync(nonGit, { recursive: true });
|
||||
expect(runResolve(nonGit)).toBe(nonGit);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user