diff --git a/README.md b/README.md index e699e634..896c063b 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ - + @@ -101,79 +101,62 @@ Every diagramming tool makes a compromise. OpenFlowKit doesn't. | **Lucidchart / Miro** | Cloud lock-in โ€” expensive, account required, your data lives on their servers | | **PlantUML** | Server-dependent rendering โ€” no visual editor, no local-first model | -OpenFlowKit is the **only MIT-licensed tool** that combines a real workspace home, a professional visual canvas, bidirectional diagram-as-code, AI generation from 9 providers, deterministic and AI-assisted imports, asset libraries for technical diagrams, and cinematic animated export โ€” with zero server-side storage. +OpenFlowKit is the **only MIT-licensed tool** that combines a real workspace home, a professional visual canvas, bidirectional diagram-as-code, AI generation from 9 providers, **automatic icon assignment from 1,100+ tech icons**, and cinematic animated export โ€” with zero server-side storage. --- ## Feature highlights -| | OpenFlowKit | Excalidraw | Draw.io | Mermaid | Lucidchart | -| ------------------------------ | :---------: | :--------: | :-----: | :-----: | :--------: | -| Visual canvas editor | โœ… | โœ… | โœ… | โŒ | โœ… | -| Bidirectional diagram-as-code | โœ… | โŒ | โŒ | โœ… | โŒ | -| AI generation (9 providers) `Beta` | โœ… | โŒ | โŒ | โŒ | Limited | -| SQL โ†’ ERD (native parser) | โœ… | โŒ | โŒ | โŒ | โŒ | -| Terraform / K8s import `Beta` | โœ… | โŒ | โŒ | โŒ | โŒ | -| AWS / Azure / GCP / CNCF icons | โœ… | โŒ | โœ… | Partial | โœ… | +| | OpenFlowKit | Excalidraw | Draw.io | Mermaid | Lucidchart | +| ------------------------------------ | :---------: | :--------: | :-----: | :-----: | :--------: | +| Visual canvas editor | โœ… | โœ… | โœ… | โŒ | โœ… | +| Bidirectional diagram-as-code | โœ… | โŒ | โŒ | โœ… | โŒ | +| AI generation (9 providers) `Beta` | โœ… | โŒ | โŒ | โŒ | Limited | +| Mermaid import (7 types) | โœ… | โŒ | โš ๏ธ | โœ… | โŒ | +| Auto-icon assignment (1,100+) | โœ… | โŒ | โŒ | โŒ | โŒ | +| AWS / Azure / GCP / CNCF icons | โœ… | โŒ | โœ… | Partial | โœ… | | Real-time collaboration (P2P) `Beta` | โœ… | โœ… | โŒ | โŒ | โœ… (cloud) | -| Cinematic animated export | โœ… | โŒ | โŒ | โŒ | โŒ | -| Figma export (editable SVG) | โœ… | โŒ | โŒ | โŒ | โŒ | -| No account required | โœ… | โœ… | โœ… | โœ… | โŒ | -| Open source (MIT) | โœ… | โœ… | โœ… | โœ… | โŒ | +| Cinematic animated export | โœ… | โŒ | โŒ | โŒ | โŒ | +| Figma export (editable SVG) | โœ… | โŒ | โŒ | โŒ | โŒ | +| No account required | โœ… | โœ… | โœ… | โœ… | โŒ | +| Open source (MIT) | โœ… | โœ… | โœ… | โœ… | โŒ | --- -## Code โ†’ Diagram +## Paste Mermaid โ†’ Beautiful Diagrams -Drop in your existing artifacts. Many formats are handled by **deterministic native parsers** that run entirely in your browser. AI-powered imports help when the source needs interpretation or when you want a richer first-pass architecture draft. +Paste any Mermaid flowchart, state diagram, class diagram, ER diagram, sequence diagram, mindmap, or journey. OpenFlowKit renders it on a visual canvas โ€” and automatically assigns the correct branded icon to every technology node. -**Native parsers (no API key needed):** - -```sql -CREATE TABLE orders ( - id BIGINT PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - status ENUM('pending','paid','shipped') NOT NULL -); +``` +flowchart TD + API[Express API] --> DB[(PostgreSQL)] + DB --> Cache[Redis Cache] + Cache --> Queue[RabbitMQ] ``` -โ†’ Typed ERD with inferred foreign-key edges and cardinalities. Rendered in milliseconds, no server involved. +Paste this โ†’ you get the Express wordmark, PostgreSQL elephant, Redis logo, and RabbitMQ icon โ€” all auto-detected, all beautifully laid out. No other tool does this. -```yaml -# deployment.yaml -apiVersion: apps/v1 -kind: Deployment -spec: - replicas: 3 ---- -apiVersion: v1 -kind: Service -selector: - app: api ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -spec: - rules: - - host: api.example.com -``` +**1,100+ icons** from developer, AWS, Azure, CNCF, and GCP catalogs are matched automatically based on node labels. No manual drag-and-drop. No configuration. + +### How it works + +1. **Paste Mermaid** on the canvas or in the code panel +2. **Semantic classifier** detects technology names (PostgreSQL, Redis, Express, Lambda, etc.) +3. **Icon matcher** searches 1,100+ icons across all catalogs โ€” exact match, then alias, then substring +4. **Enricher** assigns colors, icons, and provider SVGs to every node +5. **ELK layout** arranges everything cleanly -โ†’ Kubernetes architecture with Deployment โ†’ Service โ†’ Ingress connections. +### AI generation (API key required) -**AI-powered imports (API key required):** +Describe your system in plain English. AI generates a diagram on the canvas with correct icons applied automatically. -Paste source code, infrastructure, or API specs and hit generate โ€” the diagram lands directly on your canvas. AWS, Azure, GCP, and CNCF icons are automatically applied when the AI detects cloud services in your input. +| Prompt | Output | +| ----------------------------------------- | --------------------------------------- | +| "Node.js API with PostgreSQL and Redis" | 3 nodes with correct icons | +| "AWS Lambda โ†’ SQS โ†’ DynamoDB" | 3 nodes with AWS icons | +| "React frontend โ†’ Express โ†’ MongoDB โ†’ S3" | 4 nodes across developer + AWS catalogs | -| Source | Engine | API key? | -| ----------------------------------- | ------------------------- | :------: | -| SQL DDL | **Native parser** | **No** | -| Terraform `.tfstate` | **Native parser** | **No** | -| Terraform HCL | AI-assisted | Yes | -| Kubernetes YAML / Helm | **Native parser** | **No** | -| OpenAPI / Swagger YAML/JSON | **Native parser** | **No** | -| OpenAPI source text โ†’ richer flow | AI-assisted | Yes | -| Source code (single file) | AI-assisted ยท 9 languages | Yes | -| Mermaid | **Native parser** | **No** | +9 providers supported: Google Gemini, OpenAI, Anthropic Claude, Groq, Mistral, NVIDIA NIM, Cerebras, OpenRouter, or any custom OpenAI-compatible endpoint. --- @@ -224,9 +207,10 @@ flowchart TB auth --> db ``` -- Mermaid-compatible syntax +- Mermaid-compatible syntax โ€” paste any Mermaid and it renders with auto-assigned icons +- Specify icons directly: `{ archProvider: "developer", archResourceType: "database-postgresql" }` +- Auto-icon resolution: nodes are enriched with the correct branded icon based on their label - Export to Mermaid, PlantUML, or JSON -- Paste any Mermaid diagram and it renders immediately - Version snapshots โ€” restore any previous state --- diff --git a/assets/third-party-icons/developer/processed/Analytics/databricks.svg b/assets/third-party-icons/developer/processed/Analytics/databricks.svg new file mode 100644 index 00000000..d67b1705 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/databricks.svg @@ -0,0 +1 @@ +Databricks \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/flink.svg b/assets/third-party-icons/developer/processed/Analytics/flink.svg new file mode 100644 index 00000000..254b43f2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/flink.svg @@ -0,0 +1 @@ +Apache Flink \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/hadoop.svg b/assets/third-party-icons/developer/processed/Analytics/hadoop.svg new file mode 100644 index 00000000..a03f6844 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/hadoop.svg @@ -0,0 +1 @@ +Apache Hadoop \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/hive.svg b/assets/third-party-icons/developer/processed/Analytics/hive.svg new file mode 100644 index 00000000..a1b23393 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/hive.svg @@ -0,0 +1 @@ +Apache Hive \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/solr.svg b/assets/third-party-icons/developer/processed/Analytics/solr.svg new file mode 100644 index 00000000..75600aaa --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/solr.svg @@ -0,0 +1 @@ +Apache Solr \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/spark.svg b/assets/third-party-icons/developer/processed/Analytics/spark.svg new file mode 100644 index 00000000..106fc42e --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/spark.svg @@ -0,0 +1 @@ +Apache Spark \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/superset.svg b/assets/third-party-icons/developer/processed/Analytics/superset.svg new file mode 100644 index 00000000..735b4b42 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/superset.svg @@ -0,0 +1 @@ +Apache Superset \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Analytics/trino.svg b/assets/third-party-icons/developer/processed/Analytics/trino.svg new file mode 100644 index 00000000..2af274c8 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Analytics/trino.svg @@ -0,0 +1 @@ +Trino \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/dapr.svg b/assets/third-party-icons/developer/processed/Backend/dapr.svg new file mode 100644 index 00000000..7ea3d977 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/dapr.svg @@ -0,0 +1 @@ +Dapr \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/dotnet.svg b/assets/third-party-icons/developer/processed/Backend/dotnet.svg new file mode 100644 index 00000000..c4b482ec --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/dotnet.svg @@ -0,0 +1 @@ +.NET \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/phoenix.svg b/assets/third-party-icons/developer/processed/Backend/phoenix.svg new file mode 100644 index 00000000..e216b465 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/phoenix.svg @@ -0,0 +1 @@ +Phoenix Framework \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Backend/quarkus.svg b/assets/third-party-icons/developer/processed/Backend/quarkus.svg new file mode 100644 index 00000000..892f6f44 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Backend/quarkus.svg @@ -0,0 +1 @@ +Quarkus \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Container/containerd.svg b/assets/third-party-icons/developer/processed/Container/containerd.svg new file mode 100644 index 00000000..6e87c072 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Container/containerd.svg @@ -0,0 +1 @@ +containerd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Container/k3s.svg b/assets/third-party-icons/developer/processed/Container/k3s.svg new file mode 100644 index 00000000..c4dbcbe3 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Container/k3s.svg @@ -0,0 +1 @@ +K3s \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Container/lxc.svg b/assets/third-party-icons/developer/processed/Container/lxc.svg new file mode 100644 index 00000000..08d405b4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Container/lxc.svg @@ -0,0 +1 @@ +Linux Containers \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/cockroachdb.svg b/assets/third-party-icons/developer/processed/Database/cockroachdb.svg new file mode 100644 index 00000000..6929bf3f --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/cockroachdb.svg @@ -0,0 +1 @@ +Cockroach Labs \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/couchbase.svg b/assets/third-party-icons/developer/processed/Database/couchbase.svg new file mode 100644 index 00000000..6e57b7e3 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/couchbase.svg @@ -0,0 +1 @@ +Couchbase \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/couchdb.svg b/assets/third-party-icons/developer/processed/Database/couchdb.svg new file mode 100644 index 00000000..0f6297a9 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/couchdb.svg @@ -0,0 +1 @@ +Apache CouchDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/druid.svg b/assets/third-party-icons/developer/processed/Database/druid.svg new file mode 100644 index 00000000..a3af5cba --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/druid.svg @@ -0,0 +1 @@ +Apache Druid \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/duckdb.svg b/assets/third-party-icons/developer/processed/Database/duckdb.svg new file mode 100644 index 00000000..ac31e6f9 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/duckdb.svg @@ -0,0 +1 @@ +DuckDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/influxdb.svg b/assets/third-party-icons/developer/processed/Database/influxdb.svg new file mode 100644 index 00000000..9ad681b4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/influxdb.svg @@ -0,0 +1 @@ +InfluxDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/neo4j.svg b/assets/third-party-icons/developer/processed/Database/neo4j.svg new file mode 100644 index 00000000..b4194ce8 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/neo4j.svg @@ -0,0 +1 @@ +Neo4j \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Database/scylladb.svg b/assets/third-party-icons/developer/processed/Database/scylladb.svg new file mode 100644 index 00000000..f6da258d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Database/scylladb.svg @@ -0,0 +1 @@ +ScyllaDB \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/airflow.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/airflow.svg new file mode 100644 index 00000000..fd6c19c2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/airflow.svg @@ -0,0 +1 @@ +Apache Airflow \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/ansible.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ansible.svg new file mode 100644 index 00000000..2a121a2a --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ansible.svg @@ -0,0 +1 @@ +Ansible \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/argocd.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/argocd.svg new file mode 100644 index 00000000..34239b81 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/argocd.svg @@ -0,0 +1 @@ +Argo \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/bentoml.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/bentoml.svg new file mode 100644 index 00000000..232cc2fb --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/bentoml.svg @@ -0,0 +1 @@ +BentoML \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/ceph.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ceph.svg new file mode 100644 index 00000000..36810803 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/ceph.svg @@ -0,0 +1 @@ +Ceph \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/chef.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/chef.svg new file mode 100644 index 00000000..f0fd1b8d --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/chef.svg @@ -0,0 +1 @@ +Chef \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/droneci.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/droneci.svg new file mode 100644 index 00000000..47b8f613 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/droneci.svg @@ -0,0 +1 @@ +Drone \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/flux.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/flux.svg new file mode 100644 index 00000000..7ba68c5e --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/flux.svg @@ -0,0 +1 @@ +Flux \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/harbor.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/harbor.svg new file mode 100644 index 00000000..8dc9fa9d --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/harbor.svg @@ -0,0 +1 @@ +Harbor \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/jfrog.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/jfrog.svg new file mode 100644 index 00000000..1f7aae01 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/jfrog.svg @@ -0,0 +1 @@ +JFrog \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/keycloak.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/keycloak.svg new file mode 100644 index 00000000..a2c6a28d --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/keycloak.svg @@ -0,0 +1 @@ +Keycloak \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/minio.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/minio.svg new file mode 100644 index 00000000..12f8ffdd --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/minio.svg @@ -0,0 +1 @@ +MinIO \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/mlflow.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/mlflow.svg new file mode 100644 index 00000000..8d1b11f4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/mlflow.svg @@ -0,0 +1 @@ +MLflow \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/nomad.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/nomad.svg new file mode 100644 index 00000000..25dae0d2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/nomad.svg @@ -0,0 +1 @@ +Nomad \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/oauth2.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/oauth2.svg new file mode 100644 index 00000000..32a6dc3f --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/oauth2.svg @@ -0,0 +1 @@ +Auth0 \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/prefect.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/prefect.svg new file mode 100644 index 00000000..718c735a --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/prefect.svg @@ -0,0 +1 @@ +Prefect \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/puppet.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/puppet.svg new file mode 100644 index 00000000..313bf0d9 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/puppet.svg @@ -0,0 +1 @@ +Puppet \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/teamcity.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/teamcity.svg new file mode 100644 index 00000000..3a481926 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/teamcity.svg @@ -0,0 +1 @@ +TeamCity \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/temporal.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/temporal.svg new file mode 100644 index 00000000..f815f900 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/temporal.svg @@ -0,0 +1 @@ +Temporal \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/travisci.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/travisci.svg new file mode 100644 index 00000000..723a1b3b --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/travisci.svg @@ -0,0 +1 @@ +Travis CI \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/vault.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/vault.svg new file mode 100644 index 00000000..adfcbdb0 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/vault.svg @@ -0,0 +1 @@ +Vault \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/DevOps-AI-ML/wandb.svg b/assets/third-party-icons/developer/processed/DevOps-AI-ML/wandb.svg new file mode 100644 index 00000000..b2f5d554 --- /dev/null +++ b/assets/third-party-icons/developer/processed/DevOps-AI-ML/wandb.svg @@ -0,0 +1 @@ +Weights & Biases \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/analytics.svg b/assets/third-party-icons/developer/processed/GCP/analytics.svg new file mode 100644 index 00000000..8b68c100 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/analytics.svg @@ -0,0 +1 @@ +Google Analytics \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/bigquery.svg b/assets/third-party-icons/developer/processed/GCP/bigquery.svg new file mode 100644 index 00000000..aa40433b --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/bigquery.svg @@ -0,0 +1 @@ +Google BigQuery \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/cloud-composer.svg b/assets/third-party-icons/developer/processed/GCP/cloud-composer.svg new file mode 100644 index 00000000..4ff2211e --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/cloud-composer.svg @@ -0,0 +1 @@ +Google Cloud Composer \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/cloud-spanner.svg b/assets/third-party-icons/developer/processed/GCP/cloud-spanner.svg new file mode 100644 index 00000000..d3ed6610 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/cloud-spanner.svg @@ -0,0 +1 @@ +Google Cloud Spanner \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/cloud-storage.svg b/assets/third-party-icons/developer/processed/GCP/cloud-storage.svg new file mode 100644 index 00000000..90978b76 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/cloud-storage.svg @@ -0,0 +1 @@ +Google Cloud Storage \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/colab.svg b/assets/third-party-icons/developer/processed/GCP/colab.svg new file mode 100644 index 00000000..434396ea --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/colab.svg @@ -0,0 +1 @@ +Google Colab \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/dataflow.svg b/assets/third-party-icons/developer/processed/GCP/dataflow.svg new file mode 100644 index 00000000..46b1e5f7 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/dataflow.svg @@ -0,0 +1 @@ +Google Dataflow \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/dataproc.svg b/assets/third-party-icons/developer/processed/GCP/dataproc.svg new file mode 100644 index 00000000..1ec18f35 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/dataproc.svg @@ -0,0 +1 @@ +Google Dataproc \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/docs.svg b/assets/third-party-icons/developer/processed/GCP/docs.svg new file mode 100644 index 00000000..12077819 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/docs.svg @@ -0,0 +1 @@ +Google Docs \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/drive.svg b/assets/third-party-icons/developer/processed/GCP/drive.svg new file mode 100644 index 00000000..7263ef31 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/drive.svg @@ -0,0 +1 @@ +Google Drive \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/gemini.svg b/assets/third-party-icons/developer/processed/GCP/gemini.svg new file mode 100644 index 00000000..e15e53ce --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/gemini.svg @@ -0,0 +1 @@ +Google Gemini \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/google-ads.svg b/assets/third-party-icons/developer/processed/GCP/google-ads.svg new file mode 100644 index 00000000..d0ad5413 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/google-ads.svg @@ -0,0 +1 @@ +Google Ads \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/google-cloud.svg b/assets/third-party-icons/developer/processed/GCP/google-cloud.svg new file mode 100644 index 00000000..9c069578 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/google-cloud.svg @@ -0,0 +1 @@ +Google Cloud \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/looker.svg b/assets/third-party-icons/developer/processed/GCP/looker.svg new file mode 100644 index 00000000..31db1e5c --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/looker.svg @@ -0,0 +1 @@ +Looker \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/maps.svg b/assets/third-party-icons/developer/processed/GCP/maps.svg new file mode 100644 index 00000000..2c928cd7 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/maps.svg @@ -0,0 +1 @@ +Google Maps \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/pubsub.svg b/assets/third-party-icons/developer/processed/GCP/pubsub.svg new file mode 100644 index 00000000..1e39cc62 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/pubsub.svg @@ -0,0 +1 @@ +Google Pub/Sub \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/sheets.svg b/assets/third-party-icons/developer/processed/GCP/sheets.svg new file mode 100644 index 00000000..3dce7189 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/sheets.svg @@ -0,0 +1 @@ +Google Sheets \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/GCP/slides.svg b/assets/third-party-icons/developer/processed/GCP/slides.svg new file mode 100644 index 00000000..398312b2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/GCP/slides.svg @@ -0,0 +1 @@ +Google Slides \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/caddy.svg b/assets/third-party-icons/developer/processed/Infra/caddy.svg new file mode 100644 index 00000000..c9cd0a5e --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/caddy.svg @@ -0,0 +1 @@ +Caddy \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/consul.svg b/assets/third-party-icons/developer/processed/Infra/consul.svg new file mode 100644 index 00000000..9d494e06 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/consul.svg @@ -0,0 +1 @@ +Consul \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/envoy.svg b/assets/third-party-icons/developer/processed/Infra/envoy.svg new file mode 100644 index 00000000..c60a821d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/envoy.svg @@ -0,0 +1 @@ +Envoy Proxy \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/etcd.svg b/assets/third-party-icons/developer/processed/Infra/etcd.svg new file mode 100644 index 00000000..cb9f3cee --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/etcd.svg @@ -0,0 +1 @@ +etcd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/gunicorn.svg b/assets/third-party-icons/developer/processed/Infra/gunicorn.svg new file mode 100644 index 00000000..11396b80 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/gunicorn.svg @@ -0,0 +1 @@ +Gunicorn \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/istio.svg b/assets/third-party-icons/developer/processed/Infra/istio.svg new file mode 100644 index 00000000..580b9fd5 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/istio.svg @@ -0,0 +1 @@ +Istio \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/kong.svg b/assets/third-party-icons/developer/processed/Infra/kong.svg new file mode 100644 index 00000000..d323c3a4 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/kong.svg @@ -0,0 +1 @@ +Kong \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/letsencrypt.svg b/assets/third-party-icons/developer/processed/Infra/letsencrypt.svg new file mode 100644 index 00000000..c4bdcf6d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/letsencrypt.svg @@ -0,0 +1 @@ +Let's Encrypt \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/linkerd.svg b/assets/third-party-icons/developer/processed/Infra/linkerd.svg new file mode 100644 index 00000000..512d0d82 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/linkerd.svg @@ -0,0 +1 @@ +Linkerd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/nginx.svg b/assets/third-party-icons/developer/processed/Infra/nginx.svg new file mode 100644 index 00000000..e52ea9fe --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/nginx.svg @@ -0,0 +1 @@ +NGINX \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/tomcat.svg b/assets/third-party-icons/developer/processed/Infra/tomcat.svg new file mode 100644 index 00000000..4de01fca --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/tomcat.svg @@ -0,0 +1 @@ +Apache Tomcat \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Infra/traefik.svg b/assets/third-party-icons/developer/processed/Infra/traefik.svg new file mode 100644 index 00000000..0135fda5 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Infra/traefik.svg @@ -0,0 +1 @@ +Traefik Proxy \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/cpp.svg b/assets/third-party-icons/developer/processed/Languages/cpp.svg new file mode 100644 index 00000000..3d1b49e2 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/cpp.svg @@ -0,0 +1 @@ +C++ \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/dart.svg b/assets/third-party-icons/developer/processed/Languages/dart.svg new file mode 100644 index 00000000..990da555 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/dart.svg @@ -0,0 +1 @@ +Dart \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/latex.svg b/assets/third-party-icons/developer/processed/Languages/latex.svg new file mode 100644 index 00000000..5bab7a8b --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/latex.svg @@ -0,0 +1 @@ +LaTeX \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/lua.svg b/assets/third-party-icons/developer/processed/Languages/lua.svg new file mode 100644 index 00000000..5f4c6521 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/lua.svg @@ -0,0 +1 @@ +Lua \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Languages/zig.svg b/assets/third-party-icons/developer/processed/Languages/zig.svg new file mode 100644 index 00000000..d8504941 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Languages/zig.svg @@ -0,0 +1 @@ +Zig \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Logging/fluentbit.svg b/assets/third-party-icons/developer/processed/Logging/fluentbit.svg new file mode 100644 index 00000000..cc0c5ffc --- /dev/null +++ b/assets/third-party-icons/developer/processed/Logging/fluentbit.svg @@ -0,0 +1 @@ +Fluent Bit \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Logging/fluentd.svg b/assets/third-party-icons/developer/processed/Logging/fluentd.svg new file mode 100644 index 00000000..38d9169d --- /dev/null +++ b/assets/third-party-icons/developer/processed/Logging/fluentd.svg @@ -0,0 +1 @@ +Fluentd \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Logging/graylog.svg b/assets/third-party-icons/developer/processed/Logging/graylog.svg new file mode 100644 index 00000000..d49fc873 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Logging/graylog.svg @@ -0,0 +1 @@ +Graylog \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/datadog.svg b/assets/third-party-icons/developer/processed/Monitoring/datadog.svg new file mode 100644 index 00000000..ab437319 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/datadog.svg @@ -0,0 +1 @@ +Datadog \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/dynatrace.svg b/assets/third-party-icons/developer/processed/Monitoring/dynatrace.svg new file mode 100644 index 00000000..d83ff94f --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/dynatrace.svg @@ -0,0 +1 @@ +Dynatrace \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/jaeger.svg b/assets/third-party-icons/developer/processed/Monitoring/jaeger.svg new file mode 100644 index 00000000..74155a60 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/jaeger.svg @@ -0,0 +1 @@ +Jaeger \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/newrelic.svg b/assets/third-party-icons/developer/processed/Monitoring/newrelic.svg new file mode 100644 index 00000000..f535b78b --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/newrelic.svg @@ -0,0 +1 @@ +New Relic \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/prometheus.svg b/assets/third-party-icons/developer/processed/Monitoring/prometheus.svg new file mode 100644 index 00000000..7dd2b46f --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/prometheus.svg @@ -0,0 +1 @@ +Prometheus \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/sentry.svg b/assets/third-party-icons/developer/processed/Monitoring/sentry.svg new file mode 100644 index 00000000..47888975 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/sentry.svg @@ -0,0 +1 @@ +Sentry \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Monitoring/splunk.svg b/assets/third-party-icons/developer/processed/Monitoring/splunk.svg new file mode 100644 index 00000000..e84d30db --- /dev/null +++ b/assets/third-party-icons/developer/processed/Monitoring/splunk.svg @@ -0,0 +1 @@ +Splunk \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Queue/celery.svg b/assets/third-party-icons/developer/processed/Queue/celery.svg new file mode 100644 index 00000000..9dd7df66 --- /dev/null +++ b/assets/third-party-icons/developer/processed/Queue/celery.svg @@ -0,0 +1 @@ +Celery \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Queue/nats.svg b/assets/third-party-icons/developer/processed/Queue/nats.svg new file mode 100644 index 00000000..1c31020c --- /dev/null +++ b/assets/third-party-icons/developer/processed/Queue/nats.svg @@ -0,0 +1 @@ +NATS.io \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/Queue/rabbitmq.svg b/assets/third-party-icons/developer/processed/Queue/rabbitmq.svg new file mode 100644 index 00000000..f8b90f5b --- /dev/null +++ b/assets/third-party-icons/developer/processed/Queue/rabbitmq.svg @@ -0,0 +1 @@ +RabbitMQ \ No newline at end of file diff --git a/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json b/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json index 69314059..a7f69957 100644 --- a/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json +++ b/assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json @@ -3193,6 +3193,936 @@ "defaultHeight": 96, "nodeType": "custom", "defaultData": {} + }, + { + "id": "infra-nginx", + "label": "Nginx", + "category": "Infra", + "svgContent": "NGINX", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-traefik", + "label": "Traefik", + "category": "Infra", + "svgContent": "Traefik Proxy", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-envoy", + "label": "Envoy", + "category": "Infra", + "svgContent": "Envoy Proxy", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-istio", + "label": "Istio", + "category": "Infra", + "svgContent": "Istio", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-consul", + "label": "Consul", + "category": "Infra", + "svgContent": "Consul", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-kong", + "label": "Kong", + "category": "Infra", + "svgContent": "Kong", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-etcd", + "label": "etcd", + "category": "Infra", + "svgContent": "etcd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-linkerd", + "label": "Linkerd", + "category": "Infra", + "svgContent": "Linkerd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-caddy", + "label": "Caddy", + "category": "Infra", + "svgContent": "Caddy", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-gunicorn", + "label": "Gunicorn", + "category": "Infra", + "svgContent": "Gunicorn", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-tomcat", + "label": "Tomcat", + "category": "Infra", + "svgContent": "Apache Tomcat", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-prometheus", + "label": "Prometheus", + "category": "Monitoring", + "svgContent": "Prometheus", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-sentry", + "label": "Sentry", + "category": "Monitoring", + "svgContent": "Sentry", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-newrelic", + "label": "New Relic", + "category": "Monitoring", + "svgContent": "New Relic", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-datadog", + "label": "Datadog", + "category": "Monitoring", + "svgContent": "Datadog", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-dynatrace", + "label": "Dynatrace", + "category": "Monitoring", + "svgContent": "Dynatrace", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-splunk", + "label": "Splunk", + "category": "Monitoring", + "svgContent": "Splunk", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-influxdb", + "label": "InfluxDB", + "category": "Database", + "svgContent": "InfluxDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-cockroachdb", + "label": "CockroachDB", + "category": "Database", + "svgContent": "Cockroach Labs", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-neo4j", + "label": "Neo4j", + "category": "Database", + "svgContent": "Neo4j", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-couchdb", + "label": "CouchDB", + "category": "Database", + "svgContent": "Apache CouchDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-couchbase", + "label": "Couchbase", + "category": "Database", + "svgContent": "Couchbase", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-duckdb", + "label": "DuckDB", + "category": "Database", + "svgContent": "DuckDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-scylla", + "label": "ScyllaDB", + "category": "Database", + "svgContent": "ScyllaDB", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "database-druid", + "label": "Apache Druid", + "category": "Database", + "svgContent": "Apache Druid", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "queue-rabbitmq", + "label": "RabbitMQ", + "category": "Queue", + "svgContent": "RabbitMQ", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "queue-celery", + "label": "Celery", + "category": "Queue", + "svgContent": "Celery", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "container-containerd", + "label": "containerd", + "category": "Container", + "svgContent": "containerd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "container-k3s", + "label": "K3s", + "category": "Container", + "svgContent": "K3s", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "container-lxc", + "label": "LXC", + "category": "Container", + "svgContent": "Linux Containers", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "logging-fluentbit", + "label": "Fluent Bit", + "category": "Logging", + "svgContent": "Fluent Bit", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "logging-fluentd", + "label": "Fluentd", + "category": "Logging", + "svgContent": "Fluentd", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "logging-graylog", + "label": "Graylog", + "category": "Logging", + "svgContent": "Graylog", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ci-travisci", + "label": "Travis CI", + "category": "CI-CD", + "svgContent": "Travis CI", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ci-teamcity", + "label": "TeamCity", + "category": "CI-CD", + "svgContent": "TeamCity", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ci-droneci", + "label": "Drone CI", + "category": "CI-CD", + "svgContent": "Drone", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gitops-argocd", + "label": "Argo CD", + "category": "GitOps", + "svgContent": "Argo", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gitops-flux", + "label": "Flux", + "category": "GitOps", + "svgContent": "Flux", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-puppet", + "label": "Puppet", + "category": "IaC", + "svgContent": "Puppet", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-ansible", + "label": "Ansible", + "category": "IaC", + "svgContent": "Ansible", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-chef", + "label": "Chef", + "category": "IaC", + "svgContent": "Chef", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "storage-ceph", + "label": "Ceph", + "category": "Storage", + "svgContent": "Ceph", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "storage-minio", + "label": "MinIO", + "category": "Storage", + "svgContent": "MinIO", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "auth-vault", + "label": "Vault", + "category": "Security", + "svgContent": "Vault", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "auth-keycloak", + "label": "Keycloak", + "category": "Security", + "svgContent": "Keycloak", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "auth-oauth2", + "label": "OAuth 2.0", + "category": "Security", + "svgContent": "Auth0", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "infra-letsencrypt", + "label": "Let's Encrypt", + "category": "Infra", + "svgContent": "Let's Encrypt", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "workflow-airflow", + "label": "Apache Airflow", + "category": "Workflow", + "svgContent": "Apache Airflow", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "workflow-prefect", + "label": "Prefect", + "category": "Workflow", + "svgContent": "Prefect", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "workflow-temporal", + "label": "Temporal", + "category": "Workflow", + "svgContent": "Temporal", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "registry-harbor", + "label": "Harbor", + "category": "Registry", + "svgContent": "Harbor", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "registry-jfrog", + "label": "JFrog", + "category": "Registry", + "svgContent": "JFrog", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-spark", + "label": "Apache Spark", + "category": "Analytics", + "svgContent": "Apache Spark", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-flink", + "label": "Apache Flink", + "category": "Analytics", + "svgContent": "Apache Flink", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-hadoop", + "label": "Hadoop", + "category": "Analytics", + "svgContent": "Apache Hadoop", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-hive", + "label": "Apache Hive", + "category": "Analytics", + "svgContent": "Apache Hive", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-databricks", + "label": "Databricks", + "category": "Analytics", + "svgContent": "Databricks", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-superset", + "label": "Superset", + "category": "Analytics", + "svgContent": "Apache Superset", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-trino", + "label": "Trino", + "category": "Analytics", + "svgContent": "Trino", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "analytics-solr", + "label": "Apache Solr", + "category": "Analytics", + "svgContent": "Apache Solr", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ml-mlflow", + "label": "MLflow", + "category": "ML", + "svgContent": "MLflow", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ml-wandb", + "label": "Weights & Biases", + "category": "ML", + "svgContent": "Weights & Biases", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "ml-bentoml", + "label": "BentoML", + "category": "ML", + "svgContent": "BentoML", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-dotnet", + "label": ".NET", + "category": "Backend", + "svgContent": ".NET", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-quarkus", + "label": "Quarkus", + "category": "Backend", + "svgContent": "Quarkus", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-phoenix", + "label": "Phoenix", + "category": "Backend", + "svgContent": "Phoenix Framework", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "backend-dapr", + "label": "Dapr", + "category": "Backend", + "svgContent": "Dapr", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-cpp", + "label": "C++", + "category": "Languages", + "svgContent": "C++", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-latex", + "label": "LaTeX", + "category": "Languages", + "svgContent": "LaTeX", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-zig", + "label": "Zig", + "category": "Languages", + "svgContent": "Zig", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-lua", + "label": "Lua", + "category": "Languages", + "svgContent": "Lua", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "lang-dart", + "label": "Dart", + "category": "Languages", + "svgContent": "Dart", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "monitoring-jaeger", + "label": "Jaeger", + "category": "Monitoring", + "svgContent": "Jaeger", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "queue-nats", + "label": "NATS", + "category": "Queue", + "svgContent": "NATS.io", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "iac-nomad", + "label": "Nomad", + "category": "IaC", + "svgContent": "Nomad", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-googlecloud", + "label": "Google Cloud", + "category": "GCP", + "svgContent": "Google Cloud", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-bigquery", + "label": "BigQuery", + "category": "GCP", + "svgContent": "Google BigQuery", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-cloudstorage", + "label": "Cloud Storage", + "category": "GCP", + "svgContent": "Google Cloud Storage", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-pubsub", + "label": "Cloud Pub/Sub", + "category": "GCP", + "svgContent": "Google Pub/Sub", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-spanner", + "label": "Cloud Spanner", + "category": "GCP", + "svgContent": "Google Cloud Spanner", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-composer", + "label": "Cloud Composer", + "category": "GCP", + "svgContent": "Google Cloud Composer", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-dataflow", + "label": "Dataflow", + "category": "GCP", + "svgContent": "Google Dataflow", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-dataproc", + "label": "Dataproc", + "category": "GCP", + "svgContent": "Google Dataproc", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-gemini", + "label": "Gemini", + "category": "GCP", + "svgContent": "Google Gemini", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-colab", + "label": "Google Colab", + "category": "GCP", + "svgContent": "Google Colab", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-maps", + "label": "Google Maps", + "category": "GCP", + "svgContent": "Google Maps", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-analytics", + "label": "Google Analytics", + "category": "GCP", + "svgContent": "Google Analytics", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-ads", + "label": "Google Ads", + "category": "GCP", + "svgContent": "Google Ads", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-drive", + "label": "Google Drive", + "category": "GCP", + "svgContent": "Google Drive", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-docs", + "label": "Google Docs", + "category": "GCP", + "svgContent": "Google Docs", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-sheets", + "label": "Google Sheets", + "category": "GCP", + "svgContent": "Google Sheets", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-slides", + "label": "Google Slides", + "category": "GCP", + "svgContent": "Google Slides", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} + }, + { + "id": "gcp-looker", + "label": "Looker", + "category": "GCP", + "svgContent": "Looker", + "defaultWidth": 160, + "defaultHeight": 96, + "nodeType": "custom", + "defaultData": {} } ] -} +} \ No newline at end of file diff --git a/assets/third-party-icons/google-cloud/SOURCE.md b/assets/third-party-icons/gcp/SOURCE.md similarity index 100% rename from assets/third-party-icons/google-cloud/SOURCE.md rename to assets/third-party-icons/gcp/SOURCE.md diff --git a/assets/third-party-icons/google-cloud/processed/.gitkeep b/assets/third-party-icons/gcp/processed/.gitkeep similarity index 100% rename from assets/third-party-icons/google-cloud/processed/.gitkeep rename to assets/third-party-icons/gcp/processed/.gitkeep diff --git a/assets/third-party-icons/gcp/processed/Core/analytics.svg b/assets/third-party-icons/gcp/processed/Core/analytics.svg new file mode 100644 index 00000000..8b68c100 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/analytics.svg @@ -0,0 +1 @@ +Google Analytics \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/bigquery.svg b/assets/third-party-icons/gcp/processed/Core/bigquery.svg new file mode 100644 index 00000000..aa40433b --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/bigquery.svg @@ -0,0 +1 @@ +Google BigQuery \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/cloud-composer.svg b/assets/third-party-icons/gcp/processed/Core/cloud-composer.svg new file mode 100644 index 00000000..4ff2211e --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/cloud-composer.svg @@ -0,0 +1 @@ +Google Cloud Composer \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/cloud-spanner.svg b/assets/third-party-icons/gcp/processed/Core/cloud-spanner.svg new file mode 100644 index 00000000..d3ed6610 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/cloud-spanner.svg @@ -0,0 +1 @@ +Google Cloud Spanner \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/cloud-storage.svg b/assets/third-party-icons/gcp/processed/Core/cloud-storage.svg new file mode 100644 index 00000000..90978b76 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/cloud-storage.svg @@ -0,0 +1 @@ +Google Cloud Storage \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/colab.svg b/assets/third-party-icons/gcp/processed/Core/colab.svg new file mode 100644 index 00000000..434396ea --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/colab.svg @@ -0,0 +1 @@ +Google Colab \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/dataflow.svg b/assets/third-party-icons/gcp/processed/Core/dataflow.svg new file mode 100644 index 00000000..46b1e5f7 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/dataflow.svg @@ -0,0 +1 @@ +Google Dataflow \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/dataproc.svg b/assets/third-party-icons/gcp/processed/Core/dataproc.svg new file mode 100644 index 00000000..1ec18f35 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/dataproc.svg @@ -0,0 +1 @@ +Google Dataproc \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/docs.svg b/assets/third-party-icons/gcp/processed/Core/docs.svg new file mode 100644 index 00000000..12077819 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/docs.svg @@ -0,0 +1 @@ +Google Docs \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/drive.svg b/assets/third-party-icons/gcp/processed/Core/drive.svg new file mode 100644 index 00000000..7263ef31 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/drive.svg @@ -0,0 +1 @@ +Google Drive \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/gemini.svg b/assets/third-party-icons/gcp/processed/Core/gemini.svg new file mode 100644 index 00000000..e15e53ce --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/gemini.svg @@ -0,0 +1 @@ +Google Gemini \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/google-ads.svg b/assets/third-party-icons/gcp/processed/Core/google-ads.svg new file mode 100644 index 00000000..d0ad5413 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/google-ads.svg @@ -0,0 +1 @@ +Google Ads \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/google-cloud.svg b/assets/third-party-icons/gcp/processed/Core/google-cloud.svg new file mode 100644 index 00000000..9c069578 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/google-cloud.svg @@ -0,0 +1 @@ +Google Cloud \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/looker.svg b/assets/third-party-icons/gcp/processed/Core/looker.svg new file mode 100644 index 00000000..31db1e5c --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/looker.svg @@ -0,0 +1 @@ +Looker \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/maps.svg b/assets/third-party-icons/gcp/processed/Core/maps.svg new file mode 100644 index 00000000..2c928cd7 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/maps.svg @@ -0,0 +1 @@ +Google Maps \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/pubsub.svg b/assets/third-party-icons/gcp/processed/Core/pubsub.svg new file mode 100644 index 00000000..1e39cc62 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/pubsub.svg @@ -0,0 +1 @@ +Google Pub/Sub \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/sheets.svg b/assets/third-party-icons/gcp/processed/Core/sheets.svg new file mode 100644 index 00000000..3dce7189 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/sheets.svg @@ -0,0 +1 @@ +Google Sheets \ No newline at end of file diff --git a/assets/third-party-icons/gcp/processed/Core/slides.svg b/assets/third-party-icons/gcp/processed/Core/slides.svg new file mode 100644 index 00000000..398312b2 --- /dev/null +++ b/assets/third-party-icons/gcp/processed/Core/slides.svg @@ -0,0 +1 @@ +Google Slides \ No newline at end of file diff --git a/assets/third-party-icons/google-cloud/raw/.gitkeep b/assets/third-party-icons/gcp/raw/.gitkeep similarity index 100% rename from assets/third-party-icons/google-cloud/raw/.gitkeep rename to assets/third-party-icons/gcp/raw/.gitkeep diff --git a/scripts/shape-pack/add-missing-devicons.mjs b/scripts/shape-pack/add-missing-devicons.mjs new file mode 100644 index 00000000..bd6c101a --- /dev/null +++ b/scripts/shape-pack/add-missing-devicons.mjs @@ -0,0 +1,221 @@ +/** + * Fetches missing infrastructure/dev tool icons from Simple Icons CDN + * and appends them to the developer-icons-v1 manifest. + * + * Simple Icons: https://simpleicons.org โ€” CC0 license for most icons. + * Run: node scripts/shape-pack/add-missing-devicons.mjs + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const MANIFEST_PATH = path.resolve(__dirname, '../../assets/third-party-icons/developer/processed/developer-icons-v1.manifest.json'); + +// Missing icons: { id suffix, display label, category, simpleicons slug } +const MISSING_ICONS = [ + // Network / Proxy / Service Mesh + { id: 'infra-nginx', label: 'Nginx', category: 'Infra', slug: 'nginx' }, + { id: 'infra-traefik', label: 'Traefik', category: 'Infra', slug: 'traefikproxy' }, + { id: 'infra-haproxy', label: 'HAProxy', category: 'Infra', slug: 'haproxy' }, + { id: 'infra-envoy', label: 'Envoy', category: 'Infra', slug: 'envoyproxy' }, + { id: 'infra-istio', label: 'Istio', category: 'Infra', slug: 'istio' }, + { id: 'infra-consul', label: 'Consul', category: 'Infra', slug: 'consul' }, + { id: 'infra-kong', label: 'Kong', category: 'Infra', slug: 'kong' }, + { id: 'infra-etcd', label: 'etcd', category: 'Infra', slug: 'etcd' }, + { id: 'infra-linkerd', label: 'Linkerd', category: 'Infra', slug: 'linkerd' }, + { id: 'infra-zookeeper', label: 'Zookeeper', category: 'Infra', slug: 'apachezookeeper' }, + { id: 'infra-caddy', label: 'Caddy', category: 'Infra', slug: 'caddy' }, + { id: 'infra-gunicorn', label: 'Gunicorn', category: 'Infra', slug: 'gunicorn' }, + { id: 'infra-tomcat', label: 'Tomcat', category: 'Infra', slug: 'apachetomcat' }, + + // Monitoring / Observability + { id: 'monitoring-prometheus', label: 'Prometheus', category: 'Monitoring', slug: 'prometheus' }, + { id: 'monitoring-sentry', label: 'Sentry', category: 'Monitoring', slug: 'sentry' }, + { id: 'monitoring-newrelic', label: 'New Relic', category: 'Monitoring', slug: 'newrelic' }, + { id: 'monitoring-datadog', label: 'Datadog', category: 'Monitoring', slug: 'datadog' }, + { id: 'monitoring-dynatrace', label: 'Dynatrace', category: 'Monitoring', slug: 'dynatrace' }, + { id: 'monitoring-splunk', label: 'Splunk', category: 'Monitoring', slug: 'splunk' }, + { id: 'monitoring-nagios', label: 'Nagios', category: 'Monitoring', slug: 'nagios' }, + { id: 'monitoring-zabbix', label: 'Zabbix', category: 'Monitoring', slug: 'zabbix' }, + { id: 'monitoring-jaeger', label: 'Jaeger', category: 'Monitoring', slug: 'jaegertracing' }, + + // Databases + { id: 'database-influxdb', label: 'InfluxDB', category: 'Database', slug: 'influxdb' }, + { id: 'database-cockroachdb', label: 'CockroachDB', category: 'Database', slug: 'cockroachlabs' }, + { id: 'database-neo4j', label: 'Neo4j', category: 'Database', slug: 'neo4j' }, + { id: 'database-couchdb', label: 'CouchDB', category: 'Database', slug: 'apachecouchdb' }, + { id: 'database-couchbase', label: 'Couchbase', category: 'Database', slug: 'couchbase' }, + { id: 'database-duckdb', label: 'DuckDB', category: 'Database', slug: 'duckdb' }, + { id: 'database-qdrant', label: 'Qdrant', category: 'Database', slug: 'qdrant' }, + { id: 'database-scylla', label: 'ScyllaDB', category: 'Database', slug: 'scylladb' }, + { id: 'database-druid', label: 'Apache Druid', category: 'Database', slug: 'apachedruid' }, + + // Queues / Messaging + { id: 'queue-rabbitmq', label: 'RabbitMQ', category: 'Queue', slug: 'rabbitmq' }, + { id: 'queue-nats', label: 'NATS', category: 'Queue', slug: 'nats-io' }, + { id: 'queue-activemq', label: 'ActiveMQ', category: 'Queue', slug: 'apacheactivemq' }, + { id: 'queue-celery', label: 'Celery', category: 'Queue', slug: 'celery' }, + { id: 'queue-emqx', label: 'EMQX', category: 'Queue', slug: 'emqx' }, + + // Containers / Runtime + { id: 'container-containerd', label: 'containerd', category: 'Container', slug: 'containerd' }, + { id: 'container-k3s', label: 'K3s', category: 'Container', slug: 'k3s' }, + { id: 'container-lxc', label: 'LXC', category: 'Container', slug: 'linuxcontainers' }, + + // Logging + { id: 'logging-loki', label: 'Loki', category: 'Logging', slug: 'grafanaloki' }, + { id: 'logging-fluentbit', label: 'Fluent Bit', category: 'Logging', slug: 'fluentbit' }, + { id: 'logging-fluentd', label: 'Fluentd', category: 'Logging', slug: 'fluentd' }, + { id: 'logging-graylog', label: 'Graylog', category: 'Logging', slug: 'graylog' }, + + // CI + { id: 'ci-travisci', label: 'Travis CI', category: 'CI-CD', slug: 'travisci' }, + { id: 'ci-teamcity', label: 'TeamCity', category: 'CI-CD', slug: 'teamcity' }, + { id: 'ci-droneci', label: 'Drone CI', category: 'CI-CD', slug: 'drone' }, + + // GitOps + { id: 'gitops-argocd', label: 'Argo CD', category: 'GitOps', slug: 'argo' }, + { id: 'gitops-flux', label: 'Flux', category: 'GitOps', slug: 'flux' }, + + // IaC / Infra Management + { id: 'iac-puppet', label: 'Puppet', category: 'IaC', slug: 'puppet' }, + { id: 'iac-ansible', label: 'Ansible', category: 'IaC', slug: 'ansible' }, + { id: 'iac-chef', label: 'Chef', category: 'IaC', slug: 'chef' }, + { id: 'iac-nomad', label: 'Nomad', category: 'IaC', slug: 'hashicorpnomad' }, + + // Storage + { id: 'storage-ceph', label: 'Ceph', category: 'Storage', slug: 'ceph' }, + { id: 'storage-portworx', label: 'Portworx', category: 'Storage', slug: 'portworx' }, + { id: 'storage-glusterfs', label: 'GlusterFS', category: 'Storage', slug: 'gluster' }, + { id: 'storage-minio', label: 'MinIO', category: 'Storage', slug: 'minio' }, + + // Cache / In-Memory + { id: 'cache-memcached', label: 'Memcached', category: 'Cache', slug: 'memcached' }, + { id: 'cache-hazelcast', label: 'Hazelcast', category: 'Cache', slug: 'hazelcast' }, + + // Auth / Identity + { id: 'auth-vault', label: 'Vault', category: 'Security', slug: 'vault' }, + { id: 'auth-keycloak', label: 'Keycloak', category: 'Security', slug: 'keycloak' }, + { id: 'auth-oauth2', label: 'OAuth 2.0', category: 'Security', slug: 'auth0' }, + + // Certs + { id: 'infra-letsencrypt', label: "Let's Encrypt", category: 'Infra', slug: 'letsencrypt' }, + + // Workflow / Orchestration + { id: 'workflow-airflow', label: 'Apache Airflow', category: 'Workflow', slug: 'apacheairflow' }, + { id: 'workflow-kubeflow', label: 'Kubeflow', category: 'Workflow', slug: 'kubeflow' }, + { id: 'workflow-prefect', label: 'Prefect', category: 'Workflow', slug: 'prefect' }, + { id: 'workflow-temporal', label: 'Temporal', category: 'Workflow', slug: 'temporal' }, + + // Registry + { id: 'registry-harbor', label: 'Harbor', category: 'Registry', slug: 'harbor' }, + { id: 'registry-jfrog', label: 'JFrog', category: 'Registry', slug: 'jfrog' }, + + // Analytics / Data + { id: 'analytics-spark', label: 'Apache Spark', category: 'Analytics', slug: 'apachespark' }, + { id: 'analytics-flink', label: 'Apache Flink', category: 'Analytics', slug: 'apacheflink' }, + { id: 'analytics-hadoop', label: 'Hadoop', category: 'Analytics', slug: 'apachehadoop' }, + { id: 'analytics-hive', label: 'Apache Hive', category: 'Analytics', slug: 'apachehive' }, + { id: 'analytics-databricks', label: 'Databricks', category: 'Analytics', slug: 'databricks' }, + { id: 'analytics-dbt', label: 'dbt', category: 'Analytics', slug: 'dbt' }, + { id: 'analytics-superset', label: 'Superset', category: 'Analytics', slug: 'apachesuperset' }, + { id: 'analytics-tableau', label: 'Tableau', category: 'Analytics', slug: 'tableau' }, + { id: 'analytics-powerbi', label: 'Power BI', category: 'Analytics', slug: 'powerbi' }, + { id: 'analytics-trino', label: 'Trino', category: 'Analytics', slug: 'trino' }, + { id: 'analytics-solr', label: 'Apache Solr', category: 'Analytics', slug: 'apachesolr' }, + + // MLOps + { id: 'ml-mlflow', label: 'MLflow', category: 'ML', slug: 'mlflow' }, + { id: 'ml-wandb', label: 'Weights & Biases', category: 'ML', slug: 'weightsandbiases' }, + { id: 'ml-bentoml', label: 'BentoML', category: 'ML', slug: 'bentoml' }, + + // Frameworks + { id: 'backend-dotnet', label: '.NET', category: 'Backend', slug: 'dotnet' }, + { id: 'backend-quarkus', label: 'Quarkus', category: 'Backend', slug: 'quarkus' }, + { id: 'backend-micronaut', label: 'Micronaut', category: 'Backend', slug: 'micronaut' }, + { id: 'backend-phoenix', label: 'Phoenix', category: 'Backend', slug: 'phoenixframework' }, + { id: 'backend-camel', label: 'Apache Camel', category: 'Backend', slug: 'apachecamel' }, + { id: 'backend-dapr', label: 'Dapr', category: 'Backend', slug: 'dapr' }, + + // Languages + { id: 'lang-cpp', label: 'C++', category: 'Languages', slug: 'cplusplus' }, + { id: 'lang-latex', label: 'LaTeX', category: 'Languages', slug: 'latex' }, + { id: 'lang-matlab', label: 'MATLAB', category: 'Languages', slug: 'matlab' }, + { id: 'lang-zig', label: 'Zig', category: 'Languages', slug: 'zig' }, + { id: 'lang-lua', label: 'Lua', category: 'Languages', slug: 'lua' }, + { id: 'lang-dart', label: 'Dart', category: 'Languages', slug: 'dart' }, +]; + +async function fetchSvg(slug) { + const url = `https://cdn.simpleicons.org/${slug}`; + try { + const res = await fetch(url); + if (!res.ok) return null; + const svg = await res.text(); + // Simple Icons SVGs are black by default โ€” good for our use case + return svg.trim(); + } catch { + return null; + } +} + +async function main() { + console.log(`Reading manifest from ${MANIFEST_PATH}`); + const raw = await fs.readFile(MANIFEST_PATH, 'utf8'); + const manifest = JSON.parse(raw); + + const existingIds = new Set(manifest.shapes.map(s => s.id)); + console.log(`Existing shapes: ${manifest.shapes.length}`); + + let added = 0; + let failed = []; + + for (const icon of MISSING_ICONS) { + if (existingIds.has(icon.id)) { + console.log(` SKIP (exists): ${icon.id}`); + continue; + } + + process.stdout.write(` Fetching ${icon.label} (${icon.slug})... `); + const svgContent = await fetchSvg(icon.slug); + + if (!svgContent) { + console.log(`FAILED`); + failed.push(icon); + continue; + } + + manifest.shapes.push({ + id: icon.id, + label: icon.label, + category: icon.category, + svgContent, + defaultWidth: 160, + defaultHeight: 96, + nodeType: 'custom', + defaultData: {}, + }); + + existingIds.add(icon.id); + added++; + console.log(`OK`); + + // Small delay to be polite to CDN + await new Promise(r => setTimeout(r, 50)); + } + + await fs.writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2)); + console.log(`\nDone. Added ${added} icons. Total: ${manifest.shapes.length}`); + + if (failed.length > 0) { + console.log(`\nFailed to fetch (${failed.length}):`); + failed.forEach(f => console.log(` - ${f.label} (slug: ${f.slug})`)); + } +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/components/ExportMenuPanel.tsx b/src/components/ExportMenuPanel.tsx index 35ea77d7..d71d820b 100644 --- a/src/components/ExportMenuPanel.tsx +++ b/src/components/ExportMenuPanel.tsx @@ -11,10 +11,8 @@ import { GitBranch, Image, Share2, - Wand2, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { APP_NAME } from '@/lib/brand'; import { type CinematicExportResolution, type CinematicExportSpeed, @@ -177,16 +175,6 @@ export function ExportMenuPanel({ Icon: FileJson, actions: ['download', 'copy'], }, - { - key: 'openflow', - label: t('export.openflowdslLabel', { - appName: APP_NAME, - defaultValue: `${APP_NAME} DSL`, - }), - hint: t('export.actionCopy', 'Copy'), - Icon: Wand2, - actions: ['download', 'copy'], - }, { key: 'mermaid', label: t('export.mermaid', 'Mermaid'), diff --git a/src/components/StudioCodePanel.tsx b/src/components/StudioCodePanel.tsx index e35d0b43..a4ff5762 100644 --- a/src/components/StudioCodePanel.tsx +++ b/src/components/StudioCodePanel.tsx @@ -1,9 +1,7 @@ import React, { useRef } from 'react'; import { AlertCircle, - BookOpen, CheckCircle2, - CircleHelp, Play, RotateCcw, Zap, @@ -12,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import { Button } from './ui/Button'; import { Textarea } from './ui/Textarea'; import { useFlowStore } from '@/store'; -import { APP_NAME, IS_BEVELED } from '@/lib/brand'; +import { IS_BEVELED } from '@/lib/brand'; import { useMermaidDiagnosticsActions } from '@/store/selectionHooks'; import { useToast } from './ui/ToastContext'; import type { FlowEdge, FlowNode } from '@/lib/types'; @@ -21,7 +19,6 @@ import { useStudioCodePanelController, type DraftPreviewState, } from './studio-code-panel/useStudioCodePanelController'; -import { Tooltip } from './Tooltip'; interface CodeModeOption { id: StudioCodeMode; @@ -29,7 +26,6 @@ interface CodeModeOption { } const MODE_OPTIONS: CodeModeOption[] = [ - { id: 'openflow', label: `${APP_NAME} DSL` }, { id: 'mermaid', label: 'Mermaid' }, ]; @@ -168,28 +164,16 @@ export function StudioCodePanel({ ))} - {mode === 'mermaid' ? ( -
- -
- ) : ( - - - - )} +
+ +
@@ -204,7 +188,7 @@ export function StudioCodePanel({ placeholder={ mode === 'mermaid' ? t('commandBar.code.mermaidPlaceholder') - : t('commandBar.code.dslPlaceholder', { appName: APP_NAME }) + : t('commandBar.code.dslPlaceholder') } /> @@ -260,19 +244,7 @@ export function StudioCodePanel({ ) : null} - {mode === 'openflow' ? ( - - - {t('commandBar.code.syntaxGuide')} - - ) : ( -
- )} +
fallback ?? key) as TFunction< describe('importViewModel', () => { it('returns translated category labels from the shared definitions', () => { - expect(getImportCategoryLabel(t, 'sql')).toBe('SQL'); - expect(getImportCategoryLabel(t, 'codebase')).toBe('Repo'); + expect(getImportCategoryLabel(t, 'infra')).toBe('Infra'); expect(getImportCategoryDefinition('infra').hasNative).toBe(true); - expect(getImportCategoryDefinition('openapi').hasNative).toBe(false); expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'mermaid')).toBe( false ); + // sql, openapi, codebase are hidden behind feature flags (default off) + expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'sql')).toBe(false); + expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'codebase')).toBe( + false + ); }); it('builds placeholders and options for the import view controls', () => { @@ -38,7 +41,6 @@ describe('importViewModel', () => { 'terraform-state', 'kubernetes', 'docker-compose', - 'terraform-hcl', ]); expect(languageOptions.some((option) => option.value === 'typescript')).toBe(true); }); diff --git a/src/components/command-bar/importViewModel.ts b/src/components/command-bar/importViewModel.ts index cc26aaa8..076c346e 100644 --- a/src/components/command-bar/importViewModel.ts +++ b/src/components/command-bar/importViewModel.ts @@ -2,6 +2,7 @@ import type { TFunction } from 'i18next'; import type { SelectOption } from '@/components/ui/Select'; import { LANGUAGE_LABELS } from '@/hooks/ai-generation/codeToArchitecture'; import type { ImportCategory } from './importDetection'; +import { ROLLOUT_FLAGS, type RolloutFlagKey } from '@/config/rolloutFlags'; export interface ImportCategoryDefinition { id: ImportCategory; @@ -9,15 +10,17 @@ export interface ImportCategoryDefinition { labelKey: string; hasNative: boolean; hasAI: boolean; + featureFlag?: RolloutFlagKey; } -export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ +const ALL_IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ { id: 'sql', fallbackLabel: 'SQL', labelKey: 'commandBar.import.categories.sql', hasNative: true, hasAI: true, + featureFlag: 'importSql', }, { id: 'infra', @@ -32,6 +35,7 @@ export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ labelKey: 'commandBar.import.categories.openapi', hasNative: false, hasAI: true, + featureFlag: 'importOpenApi', }, { id: 'code', @@ -46,9 +50,15 @@ export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [ labelKey: 'commandBar.import.categories.codebase', hasNative: true, hasAI: true, + featureFlag: 'importCodebase', }, ]; +export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = + ALL_IMPORT_CATEGORY_DEFINITIONS.filter( + (cat) => !cat.featureFlag || ROLLOUT_FLAGS[cat.featureFlag] + ); + export function createLanguageOptions(): SelectOption[] { return Object.entries(LANGUAGE_LABELS).map(([value, label]) => ({ value, @@ -76,7 +86,7 @@ export function getImportPlaceholders( } export function getInfraFormatOptions(t: TFunction<'translation', undefined>): SelectOption[] { - return [ + const options: SelectOption[] = [ { value: 'terraform-state', label: t('commandBar.import.infraFormats.terraformState', 'Terraform State (.tfstate)'), @@ -89,11 +99,16 @@ export function getInfraFormatOptions(t: TFunction<'translation', undefined>): S value: 'docker-compose', label: t('commandBar.import.infraFormats.dockerCompose', 'Docker Compose'), }, - { + ]; + + if (ROLLOUT_FLAGS.importInfraTerraformHcl) { + options.push({ value: 'terraform-hcl', label: t('commandBar.import.infraFormats.terraformHcl', 'Terraform HCL (AI)'), - }, - ]; + }); + } + + return options; } export function getImportCategoryLabel( diff --git a/src/components/command-bar/useCommandBarCommands.test.tsx b/src/components/command-bar/useCommandBarCommands.test.tsx index b2814d1e..cd7dcb58 100644 --- a/src/components/command-bar/useCommandBarCommands.test.tsx +++ b/src/components/command-bar/useCommandBarCommands.test.tsx @@ -36,7 +36,6 @@ describe('useCommandBarCommands', () => { 'search-nodes', 'layout', 'architecture-rules', - 'studio-openflow', 'studio-mermaid', 'toggle-grid', 'toggle-snap', @@ -50,16 +49,14 @@ describe('useCommandBarCommands', () => { expect(result.current.find((command) => command.id === 'templates')?.tier).toBe('core'); expect(result.current.find((command) => command.id === 'layout')?.tier).toBe('core'); expect(result.current.find((command) => command.id === 'assets')?.tier).toBe('advanced'); - expect(result.current.find((command) => command.id === 'studio-openflow')?.tier).toBe('advanced'); result.current.find((command) => command.id === 'studio-ai')?.action?.(); result.current.find((command) => command.id === 'architecture-rules')?.action?.(); - result.current.find((command) => command.id === 'studio-openflow')?.action?.(); result.current.find((command) => command.id === 'studio-mermaid')?.action?.(); expect(onOpenStudioAI).toHaveBeenCalledTimes(1); expect(onOpenArchitectureRules).toHaveBeenCalledTimes(1); - expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(1); + expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(0); expect(onOpenStudioMermaid).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/command-bar/useCommandBarCommands.tsx b/src/components/command-bar/useCommandBarCommands.tsx index 5e61b6e3..bf7e274e 100644 --- a/src/components/command-bar/useCommandBarCommands.tsx +++ b/src/components/command-bar/useCommandBarCommands.tsx @@ -3,7 +3,6 @@ import { ArrowRight, Code2, Compass, - FileCode, Import, Search, Settings, @@ -12,7 +11,7 @@ import { Workflow, } from 'lucide-react'; import { useFlowStore } from '@/store'; -import { APP_NAME, FLOWPILOT_NAME } from '@/lib/brand'; +import { FLOWPILOT_NAME } from '@/lib/brand'; import type { CommandItem, CommandBarProps } from './types'; import { AssetsIcon } from '../icons/AssetsIcon'; @@ -32,7 +31,7 @@ export function useCommandBarCommands({ onUndo, onRedo, onOpenStudioAI, - onOpenStudioOpenFlow, + onOpenStudioOpenFlow: _onOpenStudioOpenFlow, onOpenStudioMermaid, onOpenArchitectureRules, hasImport = false, @@ -136,15 +135,6 @@ export function useCommandBarCommands({ description: 'Open architecture guardrails and rule templates', action: onOpenArchitectureRules, }, - { - id: 'studio-openflow', - label: 'Edit Flow DSL', - icon: , - tier: 'advanced', - type: 'action', - description: `Open ${APP_NAME} DSL in Studio`, - action: onOpenStudioOpenFlow, - }, { id: 'studio-mermaid', label: 'Edit Mermaid Code', @@ -194,7 +184,6 @@ export function useCommandBarCommands({ onOpenArchitectureRules, onOpenStudioAI, onOpenStudioMermaid, - onOpenStudioOpenFlow, onRedo, onUndo, settings, diff --git a/src/components/flow-canvas/flowCanvasTypes.test.ts b/src/components/flow-canvas/flowCanvasTypes.test.ts index f4591361..c7aa6a74 100644 --- a/src/components/flow-canvas/flowCanvasTypes.test.ts +++ b/src/components/flow-canvas/flowCanvasTypes.test.ts @@ -19,6 +19,7 @@ describe('flowCanvasNodeTypes', () => { "mindmap", "mobile", "process", + "section", "sequence_note", "sequence_participant", "start", diff --git a/src/components/flow-canvas/flowCanvasTypes.tsx b/src/components/flow-canvas/flowCanvasTypes.tsx index 3e0fbad8..6ebf0bbf 100644 --- a/src/components/flow-canvas/flowCanvasTypes.tsx +++ b/src/components/flow-canvas/flowCanvasTypes.tsx @@ -20,6 +20,7 @@ import JourneyNode from '@/components/custom-nodes/JourneyNode'; import ArchitectureNode from '@/components/custom-nodes/ArchitectureNode'; import SequenceParticipantNode from '@/components/custom-nodes/SequenceParticipantNode'; import SequenceNoteNode from '@/components/custom-nodes/SequenceNoteNode'; +import SectionNode from '@/components/SectionNode'; export const flowCanvasNodeTypes: NodeTypes = { start: CustomNode, @@ -34,6 +35,7 @@ export const flowCanvasNodeTypes: NodeTypes = { architecture: ArchitectureNode, annotation: AnnotationNode, text: TextNode, + section: SectionNode, swimlane: SwimlaneNode, image: ImageNode, browser: BrowserNode, diff --git a/src/components/flow-canvas/useFlowCanvasPaste.ts b/src/components/flow-canvas/useFlowCanvasPaste.ts index 8479c71b..8f6fb830 100644 --- a/src/components/flow-canvas/useFlowCanvasPaste.ts +++ b/src/components/flow-canvas/useFlowCanvasPaste.ts @@ -2,161 +2,182 @@ import { useCallback } from 'react'; import { useFlowStore } from '@/store'; import type { FlowEdge, FlowNode } from '@/lib/types'; import type { MermaidDiagnosticsSnapshot } from '@/store/types'; -import { createPastedTextNode, isEditablePasteTarget, resolveLayoutDirection } from './pasteHelpers'; +import { + createPastedTextNode, + isEditablePasteTarget, + resolveLayoutDirection, +} from './pasteHelpers'; import { detectMermaidDiagramType } from '@/services/mermaid/detectDiagramType'; import { normalizeParseDiagnostics } from '@/services/mermaid/diagnosticFormatting'; import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; +import { enrichNodesWithIcons } from '@/lib/nodeEnricher'; import { assignSmartHandles } from '@/services/smartEdgeRouting'; type SetFlowNodes = (payload: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void; type SetFlowEdges = (payload: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void; -type AddToast = (message: string, type?: 'success' | 'error' | 'info' | 'warning', duration?: number) => void; +type AddToast = ( + message: string, + type?: 'success' | 'error' | 'info' | 'warning', + duration?: number +) => void; interface UseFlowCanvasPasteParams { - architectureStrictMode: boolean; - activeTabId: string; - fitView: (options?: { duration?: number; padding?: number }) => void; - updateTab: (tabId: string, updates: Partial<{ diagramType: string }>) => void; - recordHistory: () => void; - setNodes: SetFlowNodes; - setEdges: SetFlowEdges; - setSelectedNodeId: (id: string | null) => void; - setMermaidDiagnostics: (payload: MermaidDiagnosticsSnapshot | null) => void; - clearMermaidDiagnostics: () => void; - addToast: AddToast; - strictModePasteBlockedMessage: string; - pasteSelection: (center?: { x: number; y: number }) => void; - getLastInteractionFlowPosition: () => { x: number; y: number } | null; - getCanvasCenterFlowPosition: () => { x: number; y: number }; + architectureStrictMode: boolean; + activeTabId: string; + fitView: (options?: { duration?: number; padding?: number }) => void; + updateTab: (tabId: string, updates: Partial<{ diagramType: string }>) => void; + recordHistory: () => void; + setNodes: SetFlowNodes; + setEdges: SetFlowEdges; + setSelectedNodeId: (id: string | null) => void; + setMermaidDiagnostics: (payload: MermaidDiagnosticsSnapshot | null) => void; + clearMermaidDiagnostics: () => void; + addToast: AddToast; + strictModePasteBlockedMessage: string; + pasteSelection: (center?: { x: number; y: number }) => void; + getLastInteractionFlowPosition: () => { x: number; y: number } | null; + getCanvasCenterFlowPosition: () => { x: number; y: number }; } export function useFlowCanvasPaste({ - architectureStrictMode, - activeTabId, - fitView, - updateTab, - recordHistory, - setNodes, - setEdges, - setSelectedNodeId, - setMermaidDiagnostics, - clearMermaidDiagnostics, - addToast, - strictModePasteBlockedMessage, - pasteSelection, - getLastInteractionFlowPosition, - getCanvasCenterFlowPosition, + architectureStrictMode, + activeTabId, + fitView, + updateTab, + recordHistory, + setNodes, + setEdges, + setSelectedNodeId, + setMermaidDiagnostics, + clearMermaidDiagnostics, + addToast, + strictModePasteBlockedMessage, + pasteSelection, + getLastInteractionFlowPosition, + getCanvasCenterFlowPosition, }: UseFlowCanvasPasteParams) { - const handleCanvasPaste = useCallback(async (event: React.ClipboardEvent): Promise => { - if (isEditablePasteTarget(event.target)) return; + const handleCanvasPaste = useCallback( + async (event: React.ClipboardEvent): Promise => { + if (isEditablePasteTarget(event.target)) return; - const rawText = event.clipboardData.getData('text/plain'); - const pastedText = rawText.trim(); + const rawText = event.clipboardData.getData('text/plain'); + const pastedText = rawText.trim(); - if (!pastedText) { - pasteSelection(getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition()); - return; - } + if (!pastedText) { + pasteSelection(getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition()); + return; + } - event.preventDefault(); - - const maybeMermaidType = detectMermaidDiagramType(pastedText); - if (maybeMermaidType) { - const result = parseMermaidByType(pastedText, { architectureStrictMode }); - const diagnostics = normalizeParseDiagnostics(result.diagnostics); - - if (!result.error) { - if (diagnostics.length > 0) { - setMermaidDiagnostics({ - source: 'paste', - diagramType: result.diagramType, - diagnostics, - updatedAt: Date.now(), - }); - addToast(`Imported with ${diagnostics.length} diagnostic warning(s).`, 'warning'); - } else { - clearMermaidDiagnostics(); - } + event.preventDefault(); - recordHistory(); - - if (result.nodes.length > 0) { - try { - const { getElkLayout } = await import('@/services/elkLayout'); - const layoutDirection = resolveLayoutDirection(result); - const { nodes: layoutedNodes, edges: layoutedEdges } = await getElkLayout(result.nodes, result.edges, { - direction: layoutDirection, - algorithm: 'layered', - spacing: 'normal', - }); - const smartEdges = assignSmartHandles(layoutedNodes, layoutedEdges); - setNodes(layoutedNodes); - setEdges(smartEdges); - } catch { - setNodes(result.nodes); - setEdges(result.edges); - } - } else { - setNodes(result.nodes); - setEdges(result.edges); - } + const maybeMermaidType = detectMermaidDiagramType(pastedText); + if (maybeMermaidType) { + const result = parseMermaidByType(pastedText, { architectureStrictMode }); + const diagnostics = normalizeParseDiagnostics(result.diagnostics); - if ('diagramType' in result && result.diagramType) { - updateTab(activeTabId, { diagramType: result.diagramType }); + if (!result.error) { + if (diagnostics.length > 0) { + setMermaidDiagnostics({ + source: 'paste', + diagramType: result.diagramType, + diagnostics, + updatedAt: Date.now(), + }); + addToast(`Imported with ${diagnostics.length} diagnostic warning(s).`, 'warning'); + } else { + clearMermaidDiagnostics(); + } + + recordHistory(); + + if (result.nodes.length > 0) { + const enrichedNodes = await enrichNodesWithIcons(result.nodes); + try { + const { getElkLayout, clearLayoutCache } = await import('@/services/elkLayout'); + clearLayoutCache(); + const layoutDirection = resolveLayoutDirection(result); + const { nodes: layoutedNodes, edges: layoutedEdges } = await getElkLayout( + enrichedNodes, + result.edges, + { + direction: layoutDirection, + algorithm: 'layered', + spacing: 'normal', } - - window.setTimeout(() => fitView({ duration: 600, padding: 0.2 }), 80); - return; + ); + const smartEdges = assignSmartHandles(layoutedNodes, layoutedEdges); + setNodes(layoutedNodes); + setEdges(smartEdges); + } catch { + setNodes(enrichedNodes); + setEdges(result.edges); } + } else { + setNodes(result.nodes); + setEdges(result.edges); + } - setMermaidDiagnostics({ - source: 'paste', - diagramType: result.diagramType ?? maybeMermaidType, - diagnostics, - error: result.error, - updatedAt: Date.now(), - }); + if ('diagramType' in result && result.diagramType) { + updateTab(activeTabId, { diagramType: result.diagramType }); + } - if (maybeMermaidType === 'architecture' && architectureStrictMode && result.error.includes('strict mode rejected')) { - addToast(strictModePasteBlockedMessage, 'error'); - return; - } + window.setTimeout(() => fitView({ duration: 600, padding: 0.2 }), 80); + return; + } - addToast(result.error, 'error'); - return; + setMermaidDiagnostics({ + source: 'paste', + diagramType: result.diagramType ?? maybeMermaidType, + diagnostics, + error: result.error, + updatedAt: Date.now(), + }); + + if ( + maybeMermaidType === 'architecture' && + architectureStrictMode && + result.error.includes('strict mode rejected') + ) { + addToast(strictModePasteBlockedMessage, 'error'); + return; } - const pasteFlowPosition = - getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition(); - - recordHistory(); - const { activeLayerId } = useFlowStore.getState(); - const newTextNode = createPastedTextNode(pastedText, pasteFlowPosition, activeLayerId); - - setNodes((existingNodes) => [ - ...existingNodes.map((node) => ({ ...node, selected: false })), - { ...newTextNode, selected: true }, - ]); - setSelectedNodeId(newTextNode.id); - }, [ - activeTabId, - addToast, - architectureStrictMode, - clearMermaidDiagnostics, - fitView, - getCanvasCenterFlowPosition, - pasteSelection, - getLastInteractionFlowPosition, - recordHistory, - setEdges, - setMermaidDiagnostics, - setNodes, - setSelectedNodeId, - strictModePasteBlockedMessage, - updateTab, - ]); - - return { - handleCanvasPaste, - }; + addToast(result.error, 'error'); + return; + } + + const pasteFlowPosition = getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition(); + + recordHistory(); + const { activeLayerId } = useFlowStore.getState(); + const newTextNode = createPastedTextNode(pastedText, pasteFlowPosition, activeLayerId); + + setNodes((existingNodes) => [ + ...existingNodes.map((node) => ({ ...node, selected: false })), + { ...newTextNode, selected: true }, + ]); + setSelectedNodeId(newTextNode.id); + }, + [ + activeTabId, + addToast, + architectureStrictMode, + clearMermaidDiagnostics, + fitView, + getCanvasCenterFlowPosition, + pasteSelection, + getLastInteractionFlowPosition, + recordHistory, + setEdges, + setMermaidDiagnostics, + setNodes, + setSelectedNodeId, + strictModePasteBlockedMessage, + updateTab, + ] + ); + + return { + handleCanvasPaste, + }; } diff --git a/src/config/rolloutFlags.ts b/src/config/rolloutFlags.ts index dd5b2c1a..712388b9 100644 --- a/src/config/rolloutFlags.ts +++ b/src/config/rolloutFlags.ts @@ -1,65 +1,97 @@ export type RolloutFlagKey = - | 'relationSemanticsV1' - | 'documentModelV2' - | 'collaborationEnabled' - | 'architectureLintEnabled'; + | 'relationSemanticsV1' + | 'documentModelV2' + | 'collaborationEnabled' + | 'architectureLintEnabled' + | 'importSql' + | 'importOpenApi' + | 'importInfraTerraformHcl' + | 'importCodebase'; interface RolloutFlagDefinition { - key: RolloutFlagKey; - envVar: string; - defaultEnabled: boolean; - description: string; + key: RolloutFlagKey; + envVar: string; + defaultEnabled: boolean; + description: string; } const ROLLOUT_FLAG_DEFINITIONS: Record = { - relationSemanticsV1: { - key: 'relationSemanticsV1', - envVar: 'VITE_RELATION_SEMANTICS_V1', - defaultEnabled: false, - description: 'Class/ER relation marker and routing semantics rollout', - }, - documentModelV2: { - key: 'documentModelV2', - envVar: 'VITE_DOCUMENT_MODEL_V2', - defaultEnabled: false, - description: 'Extended document metadata for scenes, exports, and bindings', - }, - collaborationEnabled: { - key: 'collaborationEnabled', - envVar: 'VITE_COLLABORATION_ENABLED', - defaultEnabled: true, - description: 'WebRTC peer collaboration (beta)', - }, - architectureLintEnabled: { - key: 'architectureLintEnabled', - envVar: 'VITE_ARCHITECTURE_LINT_ENABLED', - defaultEnabled: true, - description: 'Architecture diagram lint rules panel', - }, + relationSemanticsV1: { + key: 'relationSemanticsV1', + envVar: 'VITE_RELATION_SEMANTICS_V1', + defaultEnabled: false, + description: 'Class/ER relation marker and routing semantics rollout', + }, + documentModelV2: { + key: 'documentModelV2', + envVar: 'VITE_DOCUMENT_MODEL_V2', + defaultEnabled: false, + description: 'Extended document metadata for scenes, exports, and bindings', + }, + collaborationEnabled: { + key: 'collaborationEnabled', + envVar: 'VITE_COLLABORATION_ENABLED', + defaultEnabled: true, + description: 'WebRTC peer collaboration (beta)', + }, + architectureLintEnabled: { + key: 'architectureLintEnabled', + envVar: 'VITE_ARCHITECTURE_LINT_ENABLED', + defaultEnabled: true, + description: 'Architecture diagram lint rules panel', + }, + importSql: { + key: 'importSql', + envVar: 'VITE_IMPORT_SQL', + defaultEnabled: false, + description: 'SQL DDL importer (hidden โ€” unreliable for complex schemas)', + }, + importOpenApi: { + key: 'importOpenApi', + envVar: 'VITE_IMPORT_OPENAPI', + defaultEnabled: false, + description: 'OpenAPI/Swagger importer (hidden โ€” JSON-only, no YAML)', + }, + importInfraTerraformHcl: { + key: 'importInfraTerraformHcl', + envVar: 'VITE_IMPORT_INFRA_TERRAFORM_HCL', + defaultEnabled: false, + description: 'Terraform HCL importer (hidden โ€” AI-only, hallucination-prone)', + }, + importCodebase: { + key: 'importCodebase', + envVar: 'VITE_IMPORT_CODEBASE', + defaultEnabled: false, + description: 'Repo/codebase analyzer importer (hidden โ€” niche, heavy)', + }, }; function readBooleanEnvFlag(envValue: string | undefined, defaultEnabled: boolean): boolean { - if (envValue === '1') { - return true; - } - if (envValue === '0') { - return false; - } - return defaultEnabled; + if (envValue === '1') { + return true; + } + if (envValue === '0') { + return false; + } + return defaultEnabled; } export function isRolloutFlagEnabled(key: RolloutFlagKey): boolean { - const definition = ROLLOUT_FLAG_DEFINITIONS[key]; - if (!definition.envVar) { - return definition.defaultEnabled; - } - const envValue = import.meta.env[definition.envVar as keyof ImportMetaEnv] as string | undefined; - return readBooleanEnvFlag(envValue, definition.defaultEnabled); + const definition = ROLLOUT_FLAG_DEFINITIONS[key]; + if (!definition.envVar) { + return definition.defaultEnabled; + } + const envValue = import.meta.env[definition.envVar as keyof ImportMetaEnv] as string | undefined; + return readBooleanEnvFlag(envValue, definition.defaultEnabled); } export const ROLLOUT_FLAGS: Record = { - relationSemanticsV1: isRolloutFlagEnabled('relationSemanticsV1'), - documentModelV2: isRolloutFlagEnabled('documentModelV2'), - collaborationEnabled: isRolloutFlagEnabled('collaborationEnabled'), - architectureLintEnabled: isRolloutFlagEnabled('architectureLintEnabled'), + relationSemanticsV1: isRolloutFlagEnabled('relationSemanticsV1'), + documentModelV2: isRolloutFlagEnabled('documentModelV2'), + collaborationEnabled: isRolloutFlagEnabled('collaborationEnabled'), + architectureLintEnabled: isRolloutFlagEnabled('architectureLintEnabled'), + importSql: isRolloutFlagEnabled('importSql'), + importOpenApi: isRolloutFlagEnabled('importOpenApi'), + importInfraTerraformHcl: isRolloutFlagEnabled('importInfraTerraformHcl'), + importCodebase: isRolloutFlagEnabled('importCodebase'), }; diff --git a/src/hooks/ai-generation/requestLifecycle.ts b/src/hooks/ai-generation/requestLifecycle.ts index 3a6cab81..e374c271 100644 --- a/src/hooks/ai-generation/requestLifecycle.ts +++ b/src/hooks/ai-generation/requestLifecycle.ts @@ -3,13 +3,13 @@ import { serializeCanvasContextForAI } from '@/services/ai/contextSerializer'; import { generateDiagramFromChat, type ChatMessage } from '@/services/aiService'; import type { FlowEdge, FlowNode, GlobalEdgeOptions } from '@/lib/types'; import type { AISettings } from '@/store/types'; +import { buildIdMap, parseDslOrThrow, toFinalEdges, toFinalNodes } from './graphComposer'; import { - buildIdMap, - parseDslOrThrow, - toFinalEdges, - toFinalNodes, -} from './graphComposer'; -import { applyAIResultToCanvas, positionNewNodesSmartly, restoreExistingPositions } from './positionPreservingApply'; + applyAIResultToCanvas, + positionNewNodesSmartly, + restoreExistingPositions, +} from './positionPreservingApply'; +import { enrichNodesWithIcons } from '@/lib/nodeEnricher'; interface GenerateAIFlowResultParams { chatMessages: ChatMessage[]; @@ -34,7 +34,12 @@ function isRetryableError(error: unknown): boolean { if (error instanceof Error) { const msg = error.message.toLowerCase(); // Retry on rate-limit and network errors, not on auth or parse errors - return msg.includes('429') || msg.includes('rate') || msg.includes('network') || msg.includes('fetch'); + return ( + msg.includes('429') || + msg.includes('rate') || + msg.includes('network') || + msg.includes('fetch') + ); } return false; } @@ -42,7 +47,7 @@ function isRetryableError(error: unknown): boolean { async function withRetry( fn: () => Promise, signal: AbortSignal | undefined, - onRetry?: (attempt: number) => void, + onRetry?: (attempt: number) => void ): Promise { for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { if (signal?.aborted) throw new DOMException('Aborted', 'AbortError'); @@ -69,9 +74,11 @@ export interface GenerateAIFlowResult { export function buildUserChatMessage(prompt: string, imageBase64?: string): ChatMessage { return { role: 'user', - parts: [{ - text: imageBase64 ? `${prompt} [Image Attached]` : prompt, - }], + parts: [ + { + text: imageBase64 ? `${prompt} [Image Attached]` : prompt, + }, + ], }; } @@ -82,11 +89,7 @@ export function appendChatExchange( editMode = false ): ChatMessage[] { const modelText = editMode ? '[Diagram updated]' : dslText; - return [ - ...previousMessages, - userMessage, - { role: 'model', parts: [{ text: modelText }] }, - ]; + return [...previousMessages, userMessage, { role: 'model', parts: [{ text: modelText }] }]; } function buildSelectionPromptSuffix(selectedNodeIds: string[], nodes: FlowNode[]): string { @@ -128,22 +131,23 @@ export async function generateAIFlowResult({ for (let attempt = 0; attempt <= 1; attempt++) { dslText = await withRetry( - () => generateDiagramFromChat( - chatMessages, - activePrompt, - currentGraph, - imageBase64, - aiSettings.apiKey, - aiSettings.model, - aiSettings.provider || 'gemini', - aiSettings.customBaseUrl, - isEditMode, - onChunk, - signal, - aiSettings.temperature, - ), + () => + generateDiagramFromChat( + chatMessages, + activePrompt, + currentGraph, + imageBase64, + aiSettings.apiKey, + aiSettings.model, + aiSettings.provider || 'gemini', + aiSettings.customBaseUrl, + isEditMode, + onChunk, + signal, + aiSettings.temperature + ), signal, - onRetry, + onRetry ); try { parsed = parseDslOrThrow(dslText); @@ -157,7 +161,7 @@ export async function generateAIFlowResult({ } parsed = parsed!; const idMap = buildIdMap(parsed.nodes, nodes); - const finalNodes = toFinalNodes(parsed.nodes, idMap); + const finalNodes = await enrichNodesWithIcons(toFinalNodes(parsed.nodes, idMap)); const finalEdges = toFinalEdges(parsed.edges, idMap, globalEdgeOptions); const isEmptyCanvas = nodes.length === 0; @@ -167,7 +171,12 @@ export async function generateAIFlowResult({ finalEdges, { direction: 'TB', algorithm: 'mrtree', spacing: 'loose' } ); - return { dslText, userMessage: buildUserChatMessage(prompt, imageBase64), layoutedNodes, layoutedEdges }; + return { + dslText, + userMessage: buildUserChatMessage(prompt, imageBase64), + layoutedNodes, + layoutedEdges, + }; } // Position-preserving apply: matched nodes keep their positions, new nodes get ELK positions @@ -188,7 +197,12 @@ export async function generateAIFlowResult({ } // Smart placement: position new nodes near their existing neighbors - const smartPositioned = positionNewNodesSmartly(mergedNodes, mergedEdges, newNodeIds, existingById); + const smartPositioned = positionNewNodesSmartly( + mergedNodes, + mergedEdges, + newNodeIds, + existingById + ); const unplacedIds = [...newNodeIds].filter((id) => { const node = smartPositioned.find((n) => n.id === id); return !node?.position || (node.position.x === 0 && node.position.y === 0); diff --git a/src/hooks/useFlowEditorCallbacks.ts b/src/hooks/useFlowEditorCallbacks.ts index def03d94..7f462b23 100644 --- a/src/hooks/useFlowEditorCallbacks.ts +++ b/src/hooks/useFlowEditorCallbacks.ts @@ -1,155 +1,185 @@ import { startTransition, useCallback, useRef } from 'react'; import type { FlowEdge, FlowNode, FlowSnapshot } from '@/lib/types'; +import { enrichNodesWithIcons } from '@/lib/nodeEnricher'; import { useFlowStore } from '@/store'; import { composeDiagramForDisplay } from '@/services/composeDiagramForDisplay'; interface UseFlowEditorCallbacksParams { - addPage: () => string; - closePage: (pageId: string) => void; - reorderPage: (draggedPageId: string, targetPageId: string) => void; - updatePage: (pageId: string, update: Partial<{ name: string }>) => void; - navigate: (path: string) => void; - pagesLength: number; - cannotCloseLastTabMessage: string; - setNodes: (nodes: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void; - setEdges: (edges: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void; - restoreSnapshot: (snapshot: FlowSnapshot, setNodes: UseFlowEditorCallbacksParams['setNodes'], setEdges: UseFlowEditorCallbacksParams['setEdges']) => void; - recordHistory: () => void; - fitView: (options?: { duration?: number; padding?: number }) => void; - screenToFlowPosition: (position: { x: number; y: number }) => { x: number; y: number }; + addPage: () => string; + closePage: (pageId: string) => void; + reorderPage: (draggedPageId: string, targetPageId: string) => void; + updatePage: (pageId: string, update: Partial<{ name: string }>) => void; + navigate: (path: string) => void; + pagesLength: number; + cannotCloseLastTabMessage: string; + setNodes: (nodes: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void; + setEdges: (edges: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void; + restoreSnapshot: ( + snapshot: FlowSnapshot, + setNodes: UseFlowEditorCallbacksParams['setNodes'], + setEdges: UseFlowEditorCallbacksParams['setEdges'] + ) => void; + recordHistory: () => void; + fitView: (options?: { duration?: number; padding?: number }) => void; + screenToFlowPosition: (position: { x: number; y: number }) => { x: number; y: number }; } interface UseFlowEditorCallbacksResult { - getCenter: () => { x: number; y: number }; - handleSwitchPage: (pageId: string) => void; - handleAddPage: () => void; - handleClosePage: (pageId: string) => void; - handleRenamePage: (pageId: string, newName: string) => void; - handleReorderPage: (draggedPageId: string, targetPageId: string) => void; - selectAll: () => void; - handleRestoreSnapshot: (snapshot: FlowSnapshot) => void; - handleCommandBarApply: (newNodes: FlowNode[], newEdges: FlowEdge[]) => void; + getCenter: () => { x: number; y: number }; + handleSwitchPage: (pageId: string) => void; + handleAddPage: () => void; + handleClosePage: (pageId: string) => void; + handleRenamePage: (pageId: string, newName: string) => void; + handleReorderPage: (draggedPageId: string, targetPageId: string) => void; + selectAll: () => void; + handleRestoreSnapshot: (snapshot: FlowSnapshot) => void; + handleCommandBarApply: (newNodes: FlowNode[], newEdges: FlowEdge[]) => void; } export function useFlowEditorCallbacks({ - addPage, - closePage, - reorderPage, - updatePage, - navigate, - pagesLength, - cannotCloseLastTabMessage, - setNodes, - setEdges, - restoreSnapshot, - recordHistory, - fitView, - screenToFlowPosition, + addPage, + closePage, + reorderPage, + updatePage, + navigate, + pagesLength, + cannotCloseLastTabMessage, + setNodes, + setEdges, + restoreSnapshot, + recordHistory, + fitView, + screenToFlowPosition, }: UseFlowEditorCallbacksParams): UseFlowEditorCallbacksResult { - const stabilizationRunIdRef = useRef(0); - - const getCenter = useCallback(() => { - const centerX = window.innerWidth / 2; - const centerY = window.innerHeight / 2; - return screenToFlowPosition({ x: centerX, y: centerY }); - }, [screenToFlowPosition]); - - const handleSwitchPage = useCallback((pageId: string) => { - navigate(`/flow/${pageId}`); - }, [navigate]); - - const handleAddPage = useCallback(() => { - const newId = addPage(); - navigate(`/flow/${newId}`); - }, [addPage, navigate]); - - const handleClosePage = useCallback((pageId: string) => { - if (pagesLength === 1) { - alert(cannotCloseLastTabMessage); + const stabilizationRunIdRef = useRef(0); + + const getCenter = useCallback(() => { + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + return screenToFlowPosition({ x: centerX, y: centerY }); + }, [screenToFlowPosition]); + + const handleSwitchPage = useCallback( + (pageId: string) => { + navigate(`/flow/${pageId}`); + }, + [navigate] + ); + + const handleAddPage = useCallback(() => { + const newId = addPage(); + navigate(`/flow/${newId}`); + }, [addPage, navigate]); + + const handleClosePage = useCallback( + (pageId: string) => { + if (pagesLength === 1) { + alert(cannotCloseLastTabMessage); + return; + } + closePage(pageId); + }, + [cannotCloseLastTabMessage, closePage, pagesLength] + ); + + const handleRenamePage = useCallback( + (pageId: string, newName: string) => { + updatePage(pageId, { name: newName }); + }, + [updatePage] + ); + + const handleReorderPage = useCallback( + (draggedPageId: string, targetPageId: string) => { + reorderPage(draggedPageId, targetPageId); + }, + [reorderPage] + ); + + const selectAll = useCallback(() => { + setNodes((nodes) => nodes.map((node) => ({ ...node, selected: true }))); + setEdges((edges) => edges.map((edge) => ({ ...edge, selected: true }))); + }, [setEdges, setNodes]); + + const handleRestoreSnapshot = useCallback( + (snapshot: FlowSnapshot) => { + restoreSnapshot(snapshot, setNodes, setEdges); + recordHistory(); + }, + [recordHistory, restoreSnapshot, setEdges, setNodes] + ); + + const handleCommandBarApply = useCallback( + async (newNodes: FlowNode[], newEdges: FlowEdge[]) => { + const enrichedNodes = await enrichNodesWithIcons(newNodes); + recordHistory(); + startTransition(() => { + setNodes( + enrichedNodes.map((node, index) => ({ + ...node, + data: { ...node.data, freshlyAdded: true, animateDelay: Math.min(index * 20, 400) }, + })) + ); + setEdges(newEdges); + }); + setTimeout(() => fitView({ duration: 800, padding: 0.2 }), 100); + + const runId = stabilizationRunIdRef.current + 1; + stabilizationRunIdRef.current = runId; + + window.setTimeout(() => { + void (async () => { + if (stabilizationRunIdRef.current !== runId) { return; - } - closePage(pageId); - }, [cannotCloseLastTabMessage, closePage, pagesLength]); - - const handleRenamePage = useCallback((pageId: string, newName: string) => { - updatePage(pageId, { name: newName }); - }, [updatePage]); - - const handleReorderPage = useCallback((draggedPageId: string, targetPageId: string) => { - reorderPage(draggedPageId, targetPageId); - }, [reorderPage]); - - const selectAll = useCallback(() => { - setNodes((nodes) => nodes.map((node) => ({ ...node, selected: true }))); - setEdges((edges) => edges.map((edge) => ({ ...edge, selected: true }))); - }, [setEdges, setNodes]); - - const handleRestoreSnapshot = useCallback((snapshot: FlowSnapshot) => { - restoreSnapshot(snapshot, setNodes, setEdges); - recordHistory(); - }, [recordHistory, restoreSnapshot, setEdges, setNodes]); - - const handleCommandBarApply = useCallback((newNodes: FlowNode[], newEdges: FlowEdge[]) => { - recordHistory(); - startTransition(() => { - setNodes(newNodes.map((node, index) => ({ - ...node, - data: { ...node.data, freshlyAdded: true, animateDelay: Math.min(index * 20, 400) }, - }))); - setEdges(newEdges); - }); - setTimeout(() => fitView({ duration: 800, padding: 0.2 }), 100); - - const runId = stabilizationRunIdRef.current + 1; - stabilizationRunIdRef.current = runId; - - window.setTimeout(() => { - void (async () => { - if (stabilizationRunIdRef.current !== runId) { - return; - } - - const state = useFlowStore.getState(); - const measuredNodes = state.nodes; - const measuredEdges = state.edges; - const hasMeasuredDimensions = measuredNodes.some((node) => { - const measured = (node as FlowNode & { - measured?: { width?: number; height?: number }; - }).measured; - return typeof measured?.width === 'number' && typeof measured?.height === 'number'; - }); - - if (!hasMeasuredDimensions) { - return; - } - - const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId); - const { nodes: stabilizedNodes, edges: stabilizedEdges } = await composeDiagramForDisplay( - measuredNodes, - measuredEdges, - { diagramType: activeTab?.diagramType } - ); - - if (stabilizationRunIdRef.current !== runId) { - return; - } - - setNodes(stabilizedNodes); - setEdges(stabilizedEdges); - fitView({ duration: 500, padding: 0.2 }); - })(); - }, 180); - }, [fitView, recordHistory, setEdges, setNodes]); - - return { - getCenter, - handleSwitchPage, - handleAddPage, - handleClosePage, - handleRenamePage, - handleReorderPage, - selectAll, - handleRestoreSnapshot, - handleCommandBarApply, - }; + } + + const state = useFlowStore.getState(); + const measuredNodes = state.nodes; + const measuredEdges = state.edges; + const hasMeasuredDimensions = measuredNodes.some((node) => { + const measured = ( + node as FlowNode & { + measured?: { width?: number; height?: number }; + } + ).measured; + return typeof measured?.width === 'number' && typeof measured?.height === 'number'; + }); + + if (!hasMeasuredDimensions) { + return; + } + + const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId); + const { clearLayoutCache } = await import('@/services/elkLayout'); + clearLayoutCache(); + const { nodes: stabilizedNodes, edges: stabilizedEdges } = await composeDiagramForDisplay( + measuredNodes, + measuredEdges, + { diagramType: activeTab?.diagramType } + ); + + if (stabilizationRunIdRef.current !== runId) { + return; + } + + setNodes(stabilizedNodes); + setEdges(stabilizedEdges); + fitView({ duration: 500, padding: 0.2 }); + })(); + }, 180); + }, + [fitView, recordHistory, setEdges, setNodes] + ); + + return { + getCenter, + handleSwitchPage, + handleAddPage, + handleClosePage, + handleRenamePage, + handleReorderPage, + selectAll, + handleRestoreSnapshot, + handleCommandBarApply, + }; } diff --git a/src/hooks/useFlowEditorUIState.ts b/src/hooks/useFlowEditorUIState.ts index d3ff84d6..f20be1a6 100644 --- a/src/hooks/useFlowEditorUIState.ts +++ b/src/hooks/useFlowEditorUIState.ts @@ -42,7 +42,7 @@ export function useFlowEditorUIState(): UseFlowEditorUIStateResult { const [commandBarView, setCommandBarView] = useState('root'); const [editorMode, setEditorMode] = useState('canvas'); const [studioTab, setStudioTab] = useState('ai'); - const [studioCodeMode, setStudioCodeMode] = useState('openflow'); + const [studioCodeMode, setStudioCodeMode] = useState('mermaid'); const [isSelectMode, setIsSelectMode] = useState(true); const [isArchitectureRulesOpen, setIsArchitectureRulesOpen] = useState(false); diff --git a/src/lib/aiIconsPipeline.test.ts b/src/lib/aiIconsPipeline.test.ts new file mode 100644 index 00000000..6a08ad38 --- /dev/null +++ b/src/lib/aiIconsPipeline.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from 'vitest'; +import { parseOpenFlowDslV2 } from './flowmindDSLParserV2'; +import { enrichNodesWithIcons } from './nodeEnricher'; + +// These test the full pipeline: AI-generated DSL โ†’ parse โ†’ enrich โ†’ correct icons +// Simulates what happens when AI outputs DSL with archProvider/archResourceType + +describe('AI + Icons Pipeline (E2E)', () => { + it('Node.js API with PostgreSQL and Redis', async () => { + const dsl = ` + flow: Node.js Stack + direction: TB + + [system] api: Express API { archProvider: "developer", archResourceType: "others-expressjs-dark", color: "blue" } + [system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" } + [system] cache: Redis { archProvider: "developer", archResourceType: "database-redis", color: "red" } + + api ->|SQL| db + api ->|cache| cache + `; + + const parsed = parseOpenFlowDslV2(dsl); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + expect(enriched).toHaveLength(3); + + const api = enriched.find((n) => n.id === 'api'); + expect(api?.data.archIconPackId).toBe('developer-icons-v1'); + expect(api?.data.archIconShapeId).toBe('others-expressjs-dark'); + + const db = enriched.find((n) => n.id === 'db'); + expect(db?.data.archIconPackId).toBe('developer-icons-v1'); + expect(db?.data.archIconShapeId).toBe('database-postgresql'); + + const cache = enriched.find((n) => n.id === 'cache'); + expect(cache?.data.archIconPackId).toBe('developer-icons-v1'); + expect(cache?.data.archIconShapeId).toContain('redis'); + }); + + it('AWS Lambda โ†’ SQS โ†’ DynamoDB', async () => { + const dsl = ` + flow: Serverless Pipeline + direction: TB + + [architecture] lambda: Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" } + [architecture] sqs: SQS Queue { archProvider: "aws", archResourceType: "app-integration-sqs", color: "amber" } + [architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database-dynamodb", color: "emerald" } + + lambda ->|publish| sqs + sqs ->|write| dynamo + `; + + const parsed = parseOpenFlowDslV2(dsl); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + expect(enriched).toHaveLength(3); + + for (const node of enriched) { + expect(node.data.archIconPackId).toBe('aws-official-starter-v1'); + expect(node.data.archIconShapeId).toBeTruthy(); + } + }); + + it('React โ†’ Express โ†’ MongoDB โ†’ S3 (mixed stacks)', async () => { + const dsl = ` + flow: Full Stack + direction: TB + + [system] react: React App { archProvider: "developer", archResourceType: "frontend-react", color: "blue" } + [system] api: Express { archProvider: "developer", archResourceType: "others-expressjs-dark", color: "violet" } + [system] mongo: MongoDB { archProvider: "developer", archResourceType: "database-mongodb", color: "emerald" } + [architecture] s3: S3 Storage { archProvider: "aws", archResourceType: "storage-s3", color: "amber" } + + react ->|HTTP| api + api ->|query| mongo + api ->|upload| s3 + `; + + const parsed = parseOpenFlowDslV2(dsl); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + expect(enriched).toHaveLength(4); + + const react = enriched.find((n) => n.id === 'react'); + expect(react?.data.archIconPackId).toBe('developer-icons-v1'); + expect(react?.data.color).toBe('blue'); + + const s3 = enriched.find((n) => n.id === 's3'); + expect(s3?.data.archIconPackId).toBe('aws-official-starter-v1'); + }); + + it('auto-enriches nodes without explicit icons (icons: auto behavior)', async () => { + const dsl = ` + flow: Auto Icons + direction: TB + + [system] api: Express API + [system] db: PostgreSQL Database + [system] cache: Redis Cache + `; + + const parsed = parseOpenFlowDslV2(dsl); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + // Without explicit archProvider, enricher should match by label + const api = enriched.find((n) => n.id === 'api'); + expect(api?.data.archIconPackId).toBeTruthy(); + expect(api?.data.color).toBe('blue'); + + const db = enriched.find((n) => n.id === 'db'); + expect(db?.data.color).toBe('violet'); + }); + + it('enricher does not overwrite AI-set provider icons', async () => { + const dsl = ` + [architecture] lambda: My Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" } + `; + + const parsed = parseOpenFlowDslV2(dsl); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + const lambda = enriched.find((n) => n.id === 'lambda'); + expect(lambda?.data.archIconPackId).toBe('aws-official-starter-v1'); + expect(lambda?.data.archIconShapeId).toBe('compute-lambda'); + expect(lambda?.data.color).toBe('violet'); + }); + + it('enriches architecture-beta imported nodes', async () => { + const dsl = ` + flow: Architecture + direction: TB + + [architecture] server: Express.js { color: "violet" } + [architecture] db: PostgreSQL { color: "violet" } + [architecture] cache: Redis { color: "red" } + + server ->|query| db + server ->|cache| cache + `; + + const parsed = parseOpenFlowDslV2(dsl); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + const server = enriched.find((n) => n.id === 'server'); + expect(server?.data.archIconPackId).toBeTruthy(); + expect(server?.data.color).toBe('violet'); + + const db = enriched.find((n) => n.id === 'db'); + expect(db?.data.archIconPackId).toBeTruthy(); + + const cache = enriched.find((n) => n.id === 'cache'); + expect(cache?.data.archIconPackId).toBeTruthy(); + }); +}); diff --git a/src/lib/flowmindDSLParserV2.test.ts b/src/lib/flowmindDSLParserV2.test.ts index 57414fef..0eeb1407 100644 --- a/src/lib/flowmindDSLParserV2.test.ts +++ b/src/lib/flowmindDSLParserV2.test.ts @@ -1,93 +1,156 @@ - import { describe, it, expect } from 'vitest'; import { parseOpenFlowDslV2 } from './openFlowDslParserV2'; describe('OpenFlow DSL V2 Parser', () => { - it('parses basic nodes and edges', () => { - const input = ` + it('parses basic nodes and edges', () => { + const input = ` [start] Start [process] Step 1 [end] End Start -> Step 1 Step 1 -> End `; - const result = parseOpenFlowDslV2(input); - expect(result.nodes).toHaveLength(3); - expect(result.edges).toHaveLength(2); + const result = parseOpenFlowDslV2(input); + expect(result.nodes).toHaveLength(3); + expect(result.edges).toHaveLength(2); - const startNode = result.nodes.find(n => n.data.label === 'Start'); - expect(startNode).toBeDefined(); - expect(startNode?.type).toBe('start'); - }); + const startNode = result.nodes.find((n) => n.data.label === 'Start'); + expect(startNode).toBeDefined(); + expect(startNode?.type).toBe('start'); + }); - it('parses explicit IDs', () => { - const input = ` + it('parses explicit IDs', () => { + const input = ` [process] p1: Process One [process] p2: Process Two p1 -> p2 `; - const result = parseOpenFlowDslV2(input); - expect(result.nodes).toHaveLength(2); + const result = parseOpenFlowDslV2(input); + expect(result.nodes).toHaveLength(2); - const p1 = result.nodes.find(n => n.id === 'p1'); - expect(p1).toBeDefined(); - expect(p1?.data.label).toBe('Process One'); + const p1 = result.nodes.find((n) => n.id === 'p1'); + expect(p1).toBeDefined(); + expect(p1?.data.label).toBe('Process One'); - const edge = result.edges[0]; - expect(edge.source).toBe('p1'); - expect(edge.target).toBe('p2'); - }); + const edge = result.edges[0]; + expect(edge.source).toBe('p1'); + expect(edge.target).toBe('p2'); + }); - it('parses attributes', () => { - const input = ` + it('parses attributes', () => { + const input = ` [process] p1: Configured Node { color: "red", icon: "settings" } p1 -> p2 { style: "dashed", label: "async" } `; - const result = parseOpenFlowDslV2(input); - - const p1 = result.nodes.find(n => n.id === 'p1'); - expect(p1?.data.color).toBe('red'); - expect(p1?.data.icon).toBe('settings'); - - const edge = result.edges[0]; - expect(edge.data?.styleType).toBe('dashed'); // Attributes merged into edge data/attributes? Parser logic puts it in edge helper or data? - // Checking parser implementation: - // dslEdges.push({ ..., attributes }) - // finalEdges.push(createDefaultEdge(..., attributes/label?)) - // Expecting createDefaultEdge to handle it or we need to check how it's mapped. - // In parser implementation: - // createDefaultEdge(source, target, label, id) - // Wait, I missed passing attributes to createDefaultEdge in my implementation! - - // Let's check the implementation again. - }); - - it('parses quoted attribute values containing commas, colons, and escapes', () => { - const input = ` + const result = parseOpenFlowDslV2(input); + + const p1 = result.nodes.find((n) => n.id === 'p1'); + expect(p1?.data.color).toBe('red'); + expect(p1?.data.icon).toBe('settings'); + + const edge = result.edges[0]; + expect(edge.data?.styleType).toBe('dashed'); // Attributes merged into edge data/attributes? Parser logic puts it in edge helper or data? + // Checking parser implementation: + // dslEdges.push({ ..., attributes }) + // finalEdges.push(createDefaultEdge(..., attributes/label?)) + // Expecting createDefaultEdge to handle it or we need to check how it's mapped. + // In parser implementation: + // createDefaultEdge(source, target, label, id) + // Wait, I missed passing attributes to createDefaultEdge in my implementation! + + // Let's check the implementation again. + }); + + it('parses quoted attribute values containing commas, colons, and escapes', () => { + const input = ` [process] p1: Configured Node { icon: "server, api", note: "http://svc:8080/path", enabled: true, retries: 3, quote: "say \\"hello\\"" } `; - const result = parseOpenFlowDslV2(input); - - const p1 = result.nodes.find((node) => node.id === 'p1'); - expect(p1?.data.icon).toBe('server, api'); - expect(p1?.data.note).toBe('http://svc:8080/path'); - expect(p1?.data.enabled).toBe(true); - expect(p1?.data.retries).toBe(3); - expect(p1?.data.quote).toBe('say "hello"'); - }); - - it('ignores group wrappers and keeps inner nodes flat', () => { - const input = ` + const result = parseOpenFlowDslV2(input); + + const p1 = result.nodes.find((node) => node.id === 'p1'); + expect(p1?.data.icon).toBe('server, api'); + expect(p1?.data.note).toBe('http://svc:8080/path'); + expect(p1?.data.enabled).toBe(true); + expect(p1?.data.retries).toBe(3); + expect(p1?.data.quote).toBe('say "hello"'); + }); + + it('ignores group wrappers and keeps inner nodes flat', () => { + const input = ` group "Backend" { [process] api: API [database] db: DB api -> db } `; - const result = parseOpenFlowDslV2(input); - expect(result.nodes).toHaveLength(2); + const result = parseOpenFlowDslV2(input); + expect(result.nodes).toHaveLength(2); + + const api = result.nodes.find((n) => n.id === 'api'); + expect(api?.parentId).toBeUndefined(); + }); + + it('maps archProvider/archResourceType to archIconPackId/archIconShapeId', () => { + const input = ` + [system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" } + [system] api: Express API { archProvider: "developer", archResourceType: "others-expressjs-dark" } + `; + const result = parseOpenFlowDslV2(input); + expect(result.nodes).toHaveLength(2); + + const db = result.nodes.find((n) => n.id === 'db'); + expect(db?.data.archIconPackId).toBe('developer-icons-v1'); + expect(db?.data.archIconShapeId).toBe('database-postgresql'); + expect(db?.data.assetPresentation).toBe('icon'); - const api = result.nodes.find(n => n.id === 'api'); - expect(api?.parentId).toBeUndefined(); - }); + const api = result.nodes.find((n) => n.id === 'api'); + expect(api?.data.archIconPackId).toBe('developer-icons-v1'); + expect(api?.data.archIconShapeId).toBe('others-expressjs-dark'); + }); + + it('passes provider attribute through to node data', () => { + const input = ` + [architecture] lambda: API Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" } + [architecture] rds: Database { provider: "aws", color: "violet" } + `; + const result = parseOpenFlowDslV2(input); + expect(result.nodes).toHaveLength(2); + + const lambda = result.nodes.find((n) => n.id === 'lambda'); + expect(lambda?.data.archIconPackId).toBe('aws-official-starter-v1'); + expect(lambda?.data.archIconShapeId).toBe('compute-lambda'); + + const rds = result.nodes.find((n) => n.id === 'rds'); + expect(rds?.data.provider).toBe('aws'); + }); + + it('passes icon attribute for catalog search', () => { + const input = ` + [system] cache: Redis Cache { icon: "redis", color: "red" } + `; + const result = parseOpenFlowDslV2(input); + const cache = result.nodes.find((n) => n.id === 'cache'); + expect(cache?.data.icon).toBe('redis'); + }); + + it('maps [architecture] to custom node type', () => { + const input = ` + [architecture] lambda: Lambda { archProvider: "aws", archResourceType: "compute-lambda" } + `; + const result = parseOpenFlowDslV2(input); + const lambda = result.nodes.find((n) => n.id === 'lambda'); + expect(lambda?.type).toBe('custom'); + }); + + it('accepts icons: auto header in metadata', () => { + const input = ` + flow: My Architecture + direction: TB + icons: auto + [system] api: API + `; + const result = parseOpenFlowDslV2(input); + expect(result.metadata.icons).toBe('auto'); + expect(result.nodes).toHaveLength(1); + }); }); diff --git a/src/lib/flowmindDSLParserV2.ts b/src/lib/flowmindDSLParserV2.ts index ab636984..ce680b40 100644 --- a/src/lib/flowmindDSLParserV2.ts +++ b/src/lib/flowmindDSLParserV2.ts @@ -1,30 +1,35 @@ import { setNodeParent } from './nodeParent'; import { NODE_DEFAULTS } from '../theme'; import type { FlowEdge, FlowNode, NodeData } from './types'; +import { KNOWN_PROVIDER_PACK_IDS } from '@/services/shapeLibrary/providerCatalog'; + +function resolveArchPackId(provider: string): string { + return KNOWN_PROVIDER_PACK_IDS[provider.toLowerCase()] ?? `${provider}-processed-pack-v1`; +} // --- Types --- export interface DSLNode { - id: string; - type: string; - label: string; - parentId?: string; - attributes: Record; + id: string; + type: string; + label: string; + parentId?: string; + attributes: Record; } export interface DSLEdge { - sourceId: string; - targetId: string; - label?: string; - attributes: Record; - type?: 'default' | 'step' | 'smoothstep' | 'straight'; + sourceId: string; + targetId: string; + label?: string; + attributes: Record; + type?: 'default' | 'step' | 'smoothstep' | 'straight'; } export interface DSLResult { - nodes: FlowNode[]; - edges: FlowEdge[]; - metadata: Record; - errors: string[]; + nodes: FlowNode[]; + edges: FlowEdge[]; + metadata: Record; + errors: string[]; } type DSLAttributeValue = string | number | boolean; @@ -32,373 +37,372 @@ type DSLAttributeValue = string | number | boolean; // --- Constants --- const NODE_TYPE_MAP: Record = { - start: 'start', - process: 'process', - decision: 'decision', - end: 'end', - system: 'custom', - note: 'annotation', - section: 'process', - group: 'process', - browser: 'browser', - mobile: 'mobile', - container: 'container', // New generic container + start: 'start', + process: 'process', + decision: 'decision', + end: 'end', + system: 'custom', + note: 'annotation', + section: 'process', + group: 'process', + browser: 'browser', + mobile: 'mobile', + container: 'container', + architecture: 'custom', }; // --- Helpers --- function parseAttributes(text: string): Record { - const attributes: Record = {}; - if (!text) return attributes; - - const content = text.trim(); - if (!content.startsWith('{') || !content.endsWith('}')) return attributes; - - const inner = content.slice(1, -1); - const pairs: string[] = []; - let buffer = ''; - let quote: '"' | "'" | null = null; - let escaping = false; - - for (const char of inner) { - if (escaping) { - buffer += char; - escaping = false; - continue; - } - - if (char === '\\') { - buffer += char; - escaping = true; - continue; - } - - if (quote) { - buffer += char; - if (char === quote) { - quote = null; - } - continue; - } + const attributes: Record = {}; + if (!text) return attributes; + + const content = text.trim(); + if (!content.startsWith('{') || !content.endsWith('}')) return attributes; + + const inner = content.slice(1, -1); + const pairs: string[] = []; + let buffer = ''; + let quote: '"' | "'" | null = null; + let escaping = false; + + for (const char of inner) { + if (escaping) { + buffer += char; + escaping = false; + continue; + } - if (char === '"' || char === "'") { - quote = char; - buffer += char; - continue; - } + if (char === '\\') { + buffer += char; + escaping = true; + continue; + } - if (char === ',') { - const pair = buffer.trim(); - if (pair) pairs.push(pair); - buffer = ''; - continue; - } + if (quote) { + buffer += char; + if (char === quote) { + quote = null; + } + continue; + } - buffer += char; + if (char === '"' || char === "'") { + quote = char; + buffer += char; + continue; } - const trailingPair = buffer.trim(); - if (trailingPair) { - pairs.push(trailingPair); + if (char === ',') { + const pair = buffer.trim(); + if (pair) pairs.push(pair); + buffer = ''; + continue; } - pairs.forEach((pair) => { - let colonIndex = -1; - let pairQuote: '"' | "'" | null = null; - let pairEscaping = false; - - for (let index = 0; index < pair.length; index += 1) { - const char = pair[index]; - - if (pairEscaping) { - pairEscaping = false; - continue; - } - - if (char === '\\') { - pairEscaping = true; - continue; - } - - if (pairQuote) { - if (char === pairQuote) { - pairQuote = null; - } - continue; - } - - if (char === '"' || char === "'") { - pairQuote = char; - continue; - } - - if (char === ':') { - colonIndex = index; - break; - } - } + buffer += char; + } + + const trailingPair = buffer.trim(); + if (trailingPair) { + pairs.push(trailingPair); + } + + pairs.forEach((pair) => { + let colonIndex = -1; + let pairQuote: '"' | "'" | null = null; + let pairEscaping = false; + + for (let index = 0; index < pair.length; index += 1) { + const char = pair[index]; + + if (pairEscaping) { + pairEscaping = false; + continue; + } - if (colonIndex <= 0) return; - - const key = pair.slice(0, colonIndex).trim(); - const rawValue = pair.slice(colonIndex + 1).trim(); - if (!key || !rawValue) return; - - let value: DSLAttributeValue = rawValue; - if ( - (value.startsWith('"') && value.endsWith('"')) - || (value.startsWith("'") && value.endsWith("'")) - ) { - value = value - .slice(1, -1) - .replace(/\\(["'])/g, '$1') - .replace(/\\\\/g, '\\'); - } else if (!Number.isNaN(Number(value))) { - value = Number(value); - } else if (value === 'true') { - value = true; - } else if (value === 'false') { - value = false; + if (char === '\\') { + pairEscaping = true; + continue; + } + + if (pairQuote) { + if (char === pairQuote) { + pairQuote = null; } + continue; + } + + if (char === '"' || char === "'") { + pairQuote = char; + continue; + } + + if (char === ':') { + colonIndex = index; + break; + } + } - attributes[key] = value; - }); + if (colonIndex <= 0) return; + + const key = pair.slice(0, colonIndex).trim(); + const rawValue = pair.slice(colonIndex + 1).trim(); + if (!key || !rawValue) return; + + let value: DSLAttributeValue = rawValue; + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value + .slice(1, -1) + .replace(/\\(["'])/g, '$1') + .replace(/\\\\/g, '\\'); + } else if (!Number.isNaN(Number(value))) { + value = Number(value); + } else if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } - return attributes; -}; + attributes[key] = value; + }); + + return attributes; +} // --- Parser --- export function parseOpenFlowDslV2(input: string): DSLResult { - const dslNodes: DSLNode[] = []; - const dslEdges: DSLEdge[] = []; - const metadata: Record = { direction: 'TB' }; - const errors: string[] = []; - - const lines = input.split('\n'); - const currentGroupStack: string[] = []; - - // First pass: symbols and structure - // We need map label -> ID for implicit IDs - const labelToIdMap = new Map(); - - lines.forEach((rawLine, lineIndex) => { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) return; - - // 1. Metadata: key: value - const metadataMatch = line.match(/^([a-zA-Z0-9_]+):\s*(.+)$/); - // Avoid matching "label: value" inside node defs, heuristic: no brackets, no arrow - if (metadataMatch && !line.includes('[') && !line.includes('->')) { - const key = metadataMatch[1].toLowerCase(); - let value = metadataMatch[2].trim(); - // Strip quotes if present - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - metadata[key] = value; - return; - } - - // 2. Groups Start: group "Label" { - const groupStartMatch = line.match(/^group\s+"?([^"{]+)"?\s*\{$/); - if (groupStartMatch) { - currentGroupStack.push(groupStartMatch[1]); - return; - } + const dslNodes: DSLNode[] = []; + const dslEdges: DSLEdge[] = []; + const metadata: Record = { direction: 'TB' }; + const errors: string[] = []; + + const lines = input.split('\n'); + const currentGroupStack: string[] = []; + + // First pass: symbols and structure + // We need map label -> ID for implicit IDs + const labelToIdMap = new Map(); + + lines.forEach((rawLine, lineIndex) => { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) return; + + // 1. Metadata: key: value + const metadataMatch = line.match(/^([a-zA-Z0-9_]+):\s*(.+)$/); + // Avoid matching "label: value" inside node defs, heuristic: no brackets, no arrow + if (metadataMatch && !line.includes('[') && !line.includes('->')) { + const key = metadataMatch[1].toLowerCase(); + let value = metadataMatch[2].trim(); + // Strip quotes if present + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + metadata[key] = value; + return; + } - // 3. Group End: } - if (line === '}') { - if (currentGroupStack.length > 0) { - currentGroupStack.pop(); - } else { - errors.push(`Line ${lineIndex + 1}: Unexpected '}'`); - } - return; - } + // 2. Groups Start: group "Label" { + const groupStartMatch = line.match(/^group\s+"?([^"{]+)"?\s*\{$/); + if (groupStartMatch) { + currentGroupStack.push(groupStartMatch[1]); + return; + } - // 4. Edges: A -> B { attrs } - // regex: (source) (arrow) (target) (attrs?) - const edgeMatch = line.match(/^(.+?)\s*(->|-->|\.\.>|==>)\s*(.+?)(\s*\{.*\})?$/); - if (edgeMatch) { - // Note: We intentionally catch lines starting with '[' here if they have an arrow. - // This handles cases where AI mistakenly writes "[type] Node -> Node". - - const [, sourceRaw, arrow, targetRaw, attrsRaw] = edgeMatch; - - // Helper to clean potential [type] prefixes from IDs in edges - const cleanId = (raw: string) => { - const typeMatch = raw.match(/^\[.*?\]\s*(.+)$/); - return (typeMatch ? typeMatch[1] : raw).trim(); - }; - - // Extract labels/IDs from potential piped text: Source ->|Label| Target - // Re-parsing source/target for piped labels if valid arrow syntax - // "A ->|yes| B" - const source = cleanId(sourceRaw.trim()); - let targetRawTrimmed = targetRaw.trim(); - let label = ''; - - const pipeMatch = targetRawTrimmed.match(/^\|([^|]+)\|\s*(.+)$/); - if (pipeMatch) { - label = pipeMatch[1]; - targetRawTrimmed = pipeMatch[2].trim(); - } - const target = cleanId(targetRawTrimmed); - - // Attributes - const attributes = parseAttributes(attrsRaw || ''); - - // Arrow styling - if (arrow === '-->') attributes.styleType = 'curved'; - if (arrow === '..>') attributes.styleType = 'dashed'; - if (arrow === '==>') attributes.styleType = 'thick'; - - dslEdges.push({ - sourceId: source, // Resolved later - targetId: target, // Resolved later - label, - attributes - }); - return; - } + // 3. Group End: } + if (line === '}') { + if (currentGroupStack.length > 0) { + currentGroupStack.pop(); + } else { + errors.push(`Line ${lineIndex + 1}: Unexpected '}'`); + } + return; + } - // 5. Nodes: [type] id: Label { attrs } - const nodeMatch = line.match(/^\[([a-zA-Z0-9_]+)\]\s*(?:([a-zA-Z0-9_]+):\s*)?([^{]+)(\s*\{.*\})?$/); - if (nodeMatch) { - const [, typeRaw, idRaw, labelRaw, attrsRaw] = nodeMatch; - const type = NODE_TYPE_MAP[typeRaw.toLowerCase()] || 'process'; - const label = labelRaw.trim(); - const id = idRaw ? idRaw.trim() : label; // If no explicit ID, use label (backward compact) - - const attributes = parseAttributes(attrsRaw || ''); - - const node: DSLNode = { - id, - type, - label, - attributes, - parentId: undefined - }; - - dslNodes.push(node); - labelToIdMap.set(label, id); // Map label to ID for edge resolution - labelToIdMap.set(id, id); // Map ID to ID - return; - } + // 4. Edges: A -> B { attrs } + // regex: (source) (arrow) (target) (attrs?) + const edgeMatch = line.match(/^(.+?)\s*(->|-->|\.\.>|==>)\s*(.+?)(\s*\{.*\})?$/); + if (edgeMatch) { + // Note: We intentionally catch lines starting with '[' here if they have an arrow. + // This handles cases where AI mistakenly writes "[type] Node -> Node". + + const [, sourceRaw, arrow, targetRaw, attrsRaw] = edgeMatch; + + // Helper to clean potential [type] prefixes from IDs in edges + const cleanId = (raw: string) => { + const typeMatch = raw.match(/^\[.*?\]\s*(.+)$/); + return (typeMatch ? typeMatch[1] : raw).trim(); + }; + + // Extract labels/IDs from potential piped text: Source ->|Label| Target + // Re-parsing source/target for piped labels if valid arrow syntax + // "A ->|yes| B" + const source = cleanId(sourceRaw.trim()); + let targetRawTrimmed = targetRaw.trim(); + let label = ''; + + const pipeMatch = targetRawTrimmed.match(/^\|([^|]+)\|\s*(.+)$/); + if (pipeMatch) { + label = pipeMatch[1]; + targetRawTrimmed = pipeMatch[2].trim(); + } + const target = cleanId(targetRawTrimmed); + + // Attributes + const attributes = parseAttributes(attrsRaw || ''); + + // Arrow styling + if (arrow === '-->') attributes.styleType = 'curved'; + if (arrow === '..>') attributes.styleType = 'dashed'; + if (arrow === '==>') attributes.styleType = 'thick'; + + dslEdges.push({ + sourceId: source, // Resolved later + targetId: target, // Resolved later + label, + attributes, + }); + return; + } - errors.push(`Line ${lineIndex + 1}: Unrecognized syntax "${line}"`); - }); + // 5. Nodes: [type] id: Label { attrs } + const nodeMatch = line.match( + /^\[([a-zA-Z0-9_]+)\]\s*(?:([a-zA-Z0-9_]+):\s*)?([^{]+)(\s*\{.*\})?$/ + ); + if (nodeMatch) { + const [, typeRaw, idRaw, labelRaw, attrsRaw] = nodeMatch; + const type = NODE_TYPE_MAP[typeRaw.toLowerCase()] || 'process'; + const label = labelRaw.trim(); + const id = idRaw ? idRaw.trim() : label; // If no explicit ID, use label (backward compact) + + const attributes = parseAttributes(attrsRaw || ''); + + const node: DSLNode = { + id, + type, + label, + attributes, + }; + + dslNodes.push(node); + labelToIdMap.set(label, id); // Map label to ID for edge resolution + labelToIdMap.set(id, id); // Map ID to ID + return; + } - if (currentGroupStack.length > 0) { - errors.push(`Line ${lines.length}: Unclosed group block (missing closing "}")`); + errors.push(`Line ${lineIndex + 1}: Unrecognized syntax "${line}"`); + }); + + if (currentGroupStack.length > 0) { + errors.push(`Line ${lines.length}: Unclosed group block (missing closing "}")`); + } + + // Post-processing: Resolve implicit nodes and edge IDs + const finalNodes: FlowNode[] = []; + const finalEdges: FlowEdge[] = []; + const createdNodeIds = new Set(); + + // 1. Process explicit nodes + dslNodes.forEach((n) => { + const defaultStyle = NODE_DEFAULTS[n.type] || NODE_DEFAULTS['process']; + + // Layout placeholder (will be handled by ELK layout) + let node: FlowNode = { + id: n.id, + type: n.type, + position: { x: 0, y: 0 }, + data: { + label: n.label, + shape: defaultStyle?.shape as NodeData['shape'], + color: defaultStyle?.color, + icon: defaultStyle?.icon && defaultStyle.icon !== 'none' ? defaultStyle.icon : undefined, + ...n.attributes, + ...(n.attributes.archProvider + ? { archIconPackId: resolveArchPackId(String(n.attributes.archProvider)) } + : {}), + ...(n.attributes.archResourceType + ? { archIconShapeId: String(n.attributes.archResourceType) } + : {}), + ...(n.attributes.archResourceType ? { assetPresentation: 'icon' as const } : {}), + }, + }; + if (n.parentId) { + node = setNodeParent(node, n.parentId); } + finalNodes.push(node); + createdNodeIds.add(n.id); + }); + + // 2. Process edges and create implicit nodes + dslEdges.forEach((e, i) => { + const sourceId = labelToIdMap.get(e.sourceId) || e.sourceId; + const targetId = labelToIdMap.get(e.targetId) || e.targetId; + + const ensureNode = (nodeId: string) => { + if (createdNodeIds.has(nodeId)) return; + const style = NODE_DEFAULTS['process']; + finalNodes.push({ + id: nodeId, + type: 'process', + position: { x: 0, y: 0 }, + data: { + label: nodeId, + shape: style?.shape as NodeData['shape'], + color: style?.color, + icon: style?.icon && style.icon !== 'none' ? style.icon : undefined, + }, + }); + createdNodeIds.add(nodeId); + labelToIdMap.set(nodeId, nodeId); + }; - // Post-processing: Resolve implicit nodes and edge IDs - const finalNodes: FlowNode[] = []; - const finalEdges: FlowEdge[] = []; - const createdNodeIds = new Set(); - - // 1. Process explicit nodes - dslNodes.forEach((n) => { - const defaultStyle = NODE_DEFAULTS[n.type] || NODE_DEFAULTS['process']; - - // Layout placeholder (will be handled by ELK layout) - let node: FlowNode = { - id: n.id, - type: n.type, - position: { x: 0, y: 0 }, - data: { - label: n.label, - shape: defaultStyle?.shape as NodeData['shape'], - color: defaultStyle?.color, - icon: defaultStyle?.icon && defaultStyle.icon !== 'none' ? defaultStyle.icon : undefined, - ...n.attributes - }, - }; - if (n.parentId) { - node = setNodeParent(node, n.parentId); - } - finalNodes.push(node); - createdNodeIds.add(n.id); - }); - - // 2. Process edges and create implicit nodes - dslEdges.forEach((e, i) => { - const sourceId = labelToIdMap.get(e.sourceId) || e.sourceId; - const targetId = labelToIdMap.get(e.targetId) || e.targetId; - - // If nodes parse as "A -> B" and A wasn't defined, create a default process node - const defaultProcessStyle = NODE_DEFAULTS['process']; - - if (!createdNodeIds.has(sourceId)) { - finalNodes.push({ - id: sourceId, - type: 'process', - position: { x: 0, y: 0 }, - data: { - label: sourceId, - shape: defaultProcessStyle?.shape as NodeData['shape'], - color: defaultProcessStyle?.color, - icon: defaultProcessStyle?.icon && defaultProcessStyle.icon !== 'none' ? defaultProcessStyle.icon : undefined, - } - }); - createdNodeIds.add(sourceId); - labelToIdMap.set(sourceId, sourceId); - } - if (!createdNodeIds.has(targetId)) { - finalNodes.push({ - id: targetId, - type: 'process', - position: { x: 0, y: 0 }, - data: { - label: targetId, - shape: defaultProcessStyle?.shape as NodeData['shape'], - color: defaultProcessStyle?.color, - icon: defaultProcessStyle?.icon && defaultProcessStyle.icon !== 'none' ? defaultProcessStyle.icon : undefined, - } - }); - createdNodeIds.add(targetId); - labelToIdMap.set(targetId, targetId); - } + ensureNode(sourceId); + ensureNode(targetId); - const finalEdge: FlowEdge = { - id: `edge-${i}`, // Unique ID for the edge - source: sourceId, - target: targetId, - label: e.label, - type: 'default', // Default edge type - data: { label: e.label } - }; - - // Merge attributes into edge data or style - if (Object.keys(e.attributes).length > 0) { - finalEdge.data = { ...finalEdge.data, ...e.attributes }; - - // Map 'style' attribute to styleType for convenience/tests - const styleType = e.attributes.styleType || e.attributes.style; - - // Handle specific style mappings if needed - if (styleType === 'curved') { - finalEdge.type = 'smoothstep'; - finalEdge.data.styleType = 'curved'; - } else if (styleType === 'dashed') { - finalEdge.style = { strokeDasharray: '5 5' }; - finalEdge.data.styleType = 'dashed'; - } else if (styleType === 'thick') { - finalEdge.style = { strokeWidth: 3 }; - finalEdge.data.styleType = 'thick'; - } - } - finalEdges.push(finalEdge); - }); - - return { - nodes: finalNodes, - edges: finalEdges, - metadata, - errors + const finalEdge: FlowEdge = { + id: `edge-${i}`, // Unique ID for the edge + source: sourceId, + target: targetId, + label: e.label, + type: 'default', // Default edge type + data: { label: e.label }, }; -}; + + // Merge attributes into edge data or style + if (Object.keys(e.attributes).length > 0) { + finalEdge.data = { ...finalEdge.data, ...e.attributes }; + + // Map 'style' attribute to styleType for convenience/tests + const styleType = e.attributes.styleType || e.attributes.style; + + // Handle specific style mappings if needed + if (styleType === 'curved') { + finalEdge.type = 'smoothstep'; + finalEdge.data.styleType = 'curved'; + } else if (styleType === 'dashed') { + finalEdge.style = { strokeDasharray: '5 5' }; + finalEdge.data.styleType = 'dashed'; + } else if (styleType === 'thick') { + finalEdge.style = { strokeWidth: 3 }; + finalEdge.data.styleType = 'thick'; + } + } + finalEdges.push(finalEdge); + }); + + return { + nodes: finalNodes, + edges: finalEdges, + metadata, + errors, + }; +} export const parseFlowMindDSL = parseOpenFlowDslV2; diff --git a/src/lib/iconMatcher.test.ts b/src/lib/iconMatcher.test.ts new file mode 100644 index 00000000..08466713 --- /dev/null +++ b/src/lib/iconMatcher.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest'; +import { matchIcon, getMatchableIconCount, listIconProviders, buildCatalogSummary } from './iconMatcher'; + +describe('iconMatcher', () => { + it('finds icons from the catalog', () => { + const count = getMatchableIconCount(); + expect(count).toBeGreaterThan(100); + }); + + it('lists available providers', () => { + const providers = listIconProviders(); + expect(providers).toContain('developer'); + expect(providers).toContain('aws'); + }); + + it('exact match: postgresql finds the PostgreSQL icon', () => { + const results = matchIcon('postgresql'); + expect(results.length).toBeGreaterThan(0); + expect(results[0].shapeId).toContain('postgresql'); + expect(results[0].matchType).toBe('exact'); + expect(results[0].score).toBeGreaterThan(0.9); + }); + + it('alias match: "postgres" resolves to postgresql', () => { + const results = matchIcon('postgres'); + expect(results.length).toBeGreaterThan(0); + expect(results[0].shapeId).toContain('postgresql'); + expect(results[0].matchType).toBe('alias'); + }); + + it('alias match: "k8s" resolves to kubernetes', () => { + const results = matchIcon('k8s'); + expect(results.length).toBeGreaterThan(0); + expect(results[0].shapeId).toContain('kubernetes'); + expect(results[0].matchType).toBe('alias'); + }); + + it('substring match: "redis" finds redis icons', () => { + const results = matchIcon('redis'); + expect(results.length).toBeGreaterThan(0); + expect(results[0].shapeId).toContain('redis'); + }); + + it('provider filter: "lambda" with provider "aws" finds AWS Lambda', () => { + const results = matchIcon('lambda', 'aws'); + expect(results.length).toBeGreaterThan(0); + expect(results[0].provider).toBe('aws'); + expect(results[0].shapeId).toContain('lambda'); + }); + + it('provider filter: "lambda" without filter finds any provider', () => { + const results = matchIcon('lambda'); + expect(results.length).toBeGreaterThan(0); + }); + + it('returns empty for unknown queries', () => { + const results = matchIcon('zzzznotreal99999'); + expect(results).toEqual([]); + }); + + it('returns empty for empty query', () => { + expect(matchIcon('')).toEqual([]); + expect(matchIcon(' ')).toEqual([]); + }); + + it('matchType classification works', () => { + const exact = matchIcon('docker'); + if (exact.length > 0) { + expect(['exact', 'alias', 'substring']).toContain(exact[0].matchType); + } + }); + + it('lists all expected providers', () => { + const providers = listIconProviders(); + expect(providers).toEqual(expect.arrayContaining(['aws', 'azure', 'cncf', 'developer', 'gcp'])); + expect(providers).toHaveLength(5); + }); + + it('buildCatalogSummary returns non-empty summary with provider names', () => { + const summary = buildCatalogSummary(5); + expect(summary.length).toBeGreaterThan(0); + expect(summary).toContain('aws'); + expect(summary).toContain('developer'); + }); + + it('buildCatalogSummary respects maxPerProvider limit', () => { + const small = buildCatalogSummary(2); + const large = buildCatalogSummary(50); + expect(small.length).toBeLessThan(large.length); + }); +}); diff --git a/src/lib/iconMatcher.ts b/src/lib/iconMatcher.ts new file mode 100644 index 00000000..86b7afb9 --- /dev/null +++ b/src/lib/iconMatcher.ts @@ -0,0 +1,201 @@ +import { SVG_SOURCES } from '@/services/shapeLibrary/providerCatalog'; + +export interface IconMatch { + packId: string; + shapeId: string; + label: string; + provider: string; + category: string; + score: number; + matchType: 'exact' | 'alias' | 'substring' | 'category'; +} + +const ALIASES: Record = { + postgres: 'postgresql', + pg: 'postgresql', + pgsql: 'postgresql', + mongo: 'mongodb', + mdb: 'mongodb', + es: 'elasticsearch', + elastic: 'elasticsearch', + k8s: 'kubernetes', + tf: 'terraform', + hcl: 'terraform', + golang: 'go', + js: 'javascript', + ts: 'typescript', + py: 'python', + rb: 'ruby', + njs: 'nodejs', + node: 'nodejs', + 'react.js': 'react', + 'vue.js': 'vue', + next: 'nextjs', + 'nuxt.js': 'nuxt', + mq: 'rabbitmq', + apachekafka: 'kafka', + csharp: 'c#', + dotnet: '.net', + gke: 'google-kubernetes-engine', + aks: 'azure-kubernetes-service', + eks: 'amazon-elastic-kubernetes-service', + rds: 'amazon-rds', + sqs: 'amazon-sqs', + sns: 'amazon-sns', + s3: 'amazon-s3', + cf: 'cloudflare', + kib: 'kibana', + logstash: 'elastic-logstash', + beat: 'elastic-beats', +}; + +function normalize(text: string): string { + return text + .toLowerCase() + .replace(/[\s._]+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + +function entries(): IconEntry[] { + return SVG_SOURCES.map((s) => { + const parts = s.shapeId.split('/'); + const lastPathPart = parts[parts.length - 1]; + const lastHyphenPart = lastPathPart.split('-').pop() ?? lastPathPart; + return { + packId: s.packId, + shapeId: s.shapeId, + label: s.label, + provider: s.provider, + category: s.category, + normalizedName: normalize(s.shapeId), + normalizedLastSegment: normalize(lastHyphenPart), + }; + }); +} + +interface IconEntry { + packId: string; + shapeId: string; + label: string; + provider: string; + category: string; + normalizedName: string; + normalizedLastSegment: string; +} + +let cachedEntries: IconEntry[] | null = null; +function getEntries(): IconEntry[] { + if (!cachedEntries) cachedEntries = entries(); + return cachedEntries; +} + +let cachedByNormalized: Map | null = null; +function getByNormalized(): Map { + if (!cachedByNormalized) { + cachedByNormalized = new Map(); + for (const entry of getEntries()) { + cachedByNormalized.set(entry.normalizedName, entry); + if (entry.normalizedLastSegment !== entry.normalizedName) { + cachedByNormalized.set(entry.normalizedLastSegment, entry); + } + } + } + return cachedByNormalized; +} + +export function matchIcon(query: string, providerHint?: string): IconMatch[] { + const normalizedQuery = normalize(query); + if (!normalizedQuery) return []; + + const byNormalized = getByNormalized(); + const all = getEntries(); + + // 1. Exact match on shape ID + const exact = byNormalized.get(normalizedQuery); + if (exact && (!providerHint || exact.provider === providerHint)) { + return [toMatch(exact, 0.99, 'exact')]; + } + + // 2. Alias resolution + const aliasTarget = ALIASES[normalizedQuery]; + if (aliasTarget) { + const aliasEntry = byNormalized.get(normalize(aliasTarget)); + if (aliasEntry && (!providerHint || aliasEntry.provider === providerHint)) { + return [toMatch(aliasEntry, 0.95, 'alias')]; + } + } + + // 3. Substring match (query contained in name, or name contained in query) + const substringMatches: IconMatch[] = []; + for (const entry of all) { + if (providerHint && entry.provider !== providerHint) continue; + if (entry.normalizedLastSegment.length < 3 || normalizedQuery.length < 3) continue; + if ( + entry.normalizedName.includes(normalizedQuery) || + entry.normalizedLastSegment.includes(normalizedQuery) || + normalizedQuery.includes(entry.normalizedLastSegment) + ) { + substringMatches.push(toMatch(entry, 0.85, 'substring')); + } + } + if (substringMatches.length > 0) { + substringMatches.sort((a, b) => b.score - a.score); + return substringMatches.slice(0, 5); + } + + // 4. Category match + const normalizedCategory = normalizedQuery.replace(/-/g, ''); + const categoryMatches: IconMatch[] = []; + for (const entry of all) { + if (providerHint && entry.provider !== providerHint) continue; + if (normalize(entry.category).replace(/-/g, '').includes(normalizedCategory)) { + categoryMatches.push(toMatch(entry, 0.7, 'category')); + } + } + if (categoryMatches.length > 0) { + categoryMatches.sort((a, b) => b.score - a.score); + return categoryMatches.slice(0, 5); + } + + return []; +} + +function toMatch(entry: IconEntry, score: number, matchType: IconMatch['matchType']): IconMatch { + return { + packId: entry.packId, + shapeId: entry.shapeId, + label: entry.label, + provider: entry.provider, + category: entry.category, + score, + matchType, + }; +} + +export function getMatchableIconCount(): number { + return getEntries().length; +} + +export function listIconProviders(): string[] { + return [...new Set(getEntries().map((e) => e.provider))].sort(); +} + +export function buildCatalogSummary(maxPerProvider: number = 30): string { + const byProvider = new Map(); + for (const entry of getEntries()) { + const list = byProvider.get(entry.provider) ?? []; + list.push(entry); + byProvider.set(entry.provider, list); + } + + const lines: string[] = []; + for (const [provider, icons] of byProvider) { + const categories = [...new Set(icons.map((i) => i.category))]; + const sampleNames = icons.slice(0, maxPerProvider).map((i) => i.label); + lines.push(`${provider}: ${categories.join(', ')} (examples: ${sampleNames.join(', ')})`); + } + + return lines.join('\n'); +} diff --git a/src/lib/iconResolver.test.ts b/src/lib/iconResolver.test.ts new file mode 100644 index 00000000..61d13fe5 --- /dev/null +++ b/src/lib/iconResolver.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +import { resolveIconSync, resolveLucideFallback } from './iconResolver'; + +describe('resolveIconSync', () => { + it('resolves PostgreSQL alias', () => { + const result = resolveIconSync('PostgreSQL'); + expect(result.found).toBe(true); + expect(result.iconSearch).toBe('postgresql'); + expect(result.catalog).toBe('developer'); + expect(result.confidence).toBeGreaterThan(0.9); + }); + + it('resolves shorthand aliases', () => { + expect(resolveIconSync('postgres').iconSearch).toBe('postgresql'); + expect(resolveIconSync('pg').iconSearch).toBe('postgresql'); + expect(resolveIconSync('mongo').iconSearch).toBe('mongodb'); + expect(resolveIconSync('k8s').iconSearch).toBe('kubernetes'); + }); + + it('resolves framework aliases', () => { + expect(resolveIconSync('React').catalog).toBe('developer'); + expect(resolveIconSync('Next.js').iconSearch).toBe('nextjs'); + expect(resolveIconSync('Express').iconSearch).toBe('express'); + expect(resolveIconSync('Django').iconSearch).toBe('django'); + expect(resolveIconSync('FastAPI').iconSearch).toBe('fastapi'); + }); + + it('resolves infrastructure aliases', () => { + expect(resolveIconSync('Docker').catalog).toBe('developer'); + expect(resolveIconSync('Kubernetes').catalog).toBe('cncf'); + expect(resolveIconSync('nginx').iconSearch).toBe('nginx'); + expect(resolveIconSync('RabbitMQ').iconSearch).toBe('rabbitmq'); + expect(resolveIconSync('Kafka').iconSearch).toBe('apachekafka'); + }); + + it('resolves cloud service aliases', () => { + expect(resolveIconSync('S3').catalog).toBe('aws'); + expect(resolveIconSync('Lambda').catalog).toBe('aws'); + expect(resolveIconSync('Cloud Run').catalog).toBe('gcp'); + expect(resolveIconSync('Azure Functions').catalog).toBe('azure'); + }); + + it('returns not found for unknown queries', () => { + const result = resolveIconSync('RandomThing123'); + expect(result.found).toBe(false); + expect(result.confidence).toBe(0); + }); + + it('uses category fallback when alias not found', () => { + const result = resolveIconSync('MyCustomDB', 'database'); + expect(result.found).toBe(true); + expect(result.lucideIcon).toBe('database'); + expect(result.confidence).toBe(0.5); + }); + + it('handles empty query', () => { + expect(resolveIconSync('').found).toBe(false); + expect(resolveIconSync(' ').found).toBe(false); + }); +}); + +describe('resolveLucideFallback', () => { + it('returns correct fallback icons', () => { + expect(resolveLucideFallback('database')).toBe('database'); + expect(resolveLucideFallback('cache')).toBe('hard-drive'); + expect(resolveLucideFallback('service')).toBe('server'); + expect(resolveLucideFallback('frontend')).toBe('monitor'); + expect(resolveLucideFallback('user')).toBe('user'); + expect(resolveLucideFallback('gateway')).toBe('shield'); + }); + + it('returns box for unknown categories', () => { + expect(resolveLucideFallback('unknown')).toBe('box'); + }); +}); diff --git a/src/lib/iconResolver.ts b/src/lib/iconResolver.ts new file mode 100644 index 00000000..35fb684f --- /dev/null +++ b/src/lib/iconResolver.ts @@ -0,0 +1,425 @@ +import type { DomainLibraryCategory } from '@/services/domainLibrary'; + +export interface IconResolution { + found: boolean; + archIconPackId?: string; + archIconShapeId?: string; + iconSearch?: string; + catalog?: DomainLibraryCategory; + lucideIcon?: string; + label?: string; + category?: string; + confidence: number; +} + +interface AliasEntry { + patterns: RegExp[]; + iconSearch: string; + catalog: DomainLibraryCategory; + lucideFallback: string; +} + +const ALIAS_TABLE: AliasEntry[] = [ + // Databases + { + patterns: [/^postgres(?:ql)?$/i, /^pg$/i], + iconSearch: 'postgresql', + catalog: 'developer', + lucideFallback: 'database', + }, + { patterns: [/^mysql$/i], iconSearch: 'mysql', catalog: 'developer', lucideFallback: 'database' }, + { + patterns: [/^mongo(?:db)?$/i], + iconSearch: 'mongodb', + catalog: 'developer', + lucideFallback: 'database', + }, + { + patterns: [/^redis$/i], + iconSearch: 'redis', + catalog: 'developer', + lucideFallback: 'hard-drive', + }, + { + patterns: [/^elastic(?:search)?$/i], + iconSearch: 'elasticsearch', + catalog: 'developer', + lucideFallback: 'search', + }, + { patterns: [/^dynamodb$/i], iconSearch: 'dynamodb', catalog: 'aws', lucideFallback: 'database' }, + { patterns: [/^aurora$/i], iconSearch: 'aurora', catalog: 'aws', lucideFallback: 'database' }, + { + patterns: [/^sqlite$/i], + iconSearch: 'sqlite', + catalog: 'developer', + lucideFallback: 'database', + }, + { + patterns: [/^mariadb$/i], + iconSearch: 'mariadb', + catalog: 'developer', + lucideFallback: 'database', + }, + { + patterns: [/^cassandra$/i], + iconSearch: 'cassandra', + catalog: 'developer', + lucideFallback: 'database', + }, + { patterns: [/^neo4j$/i], iconSearch: 'neo4j', catalog: 'developer', lucideFallback: 'database' }, + { + patterns: [/^supabase$/i], + iconSearch: 'supabase', + catalog: 'developer', + lucideFallback: 'database', + }, + { + patterns: [/^planetscale$/i], + iconSearch: 'planetscale', + catalog: 'developer', + lucideFallback: 'database', + }, + { patterns: [/^neon\b/i], iconSearch: 'neon', catalog: 'developer', lucideFallback: 'database' }, + + // Frameworks + { + patterns: [/^express(?:\.?js)?$/i], + iconSearch: 'express', + catalog: 'developer', + lucideFallback: 'server', + }, + { + patterns: [/^node(?:\.?js)?$/i], + iconSearch: 'nodejs', + catalog: 'developer', + lucideFallback: 'server', + }, + { + patterns: [/^react(?:\.?js)?$/i], + iconSearch: 'react', + catalog: 'developer', + lucideFallback: 'monitor', + }, + { + patterns: [/^vue(?:\.?js)?$/i], + iconSearch: 'vue', + catalog: 'developer', + lucideFallback: 'monitor', + }, + { + patterns: [/^angular$/i], + iconSearch: 'angular', + catalog: 'developer', + lucideFallback: 'monitor', + }, + { + patterns: [/^svelte$/i], + iconSearch: 'svelte', + catalog: 'developer', + lucideFallback: 'monitor', + }, + { + patterns: [/^next(?:\.?js)?$/i], + iconSearch: 'nextjs', + catalog: 'developer', + lucideFallback: 'monitor', + }, + { patterns: [/^nuxt$/i], iconSearch: 'nuxt', catalog: 'developer', lucideFallback: 'monitor' }, + { patterns: [/^django$/i], iconSearch: 'django', catalog: 'developer', lucideFallback: 'server' }, + { patterns: [/^flask$/i], iconSearch: 'flask', catalog: 'developer', lucideFallback: 'server' }, + { + patterns: [/^fastapi$/i], + iconSearch: 'fastapi', + catalog: 'developer', + lucideFallback: 'server', + }, + { + patterns: [/^spring(?:\s*boot)?$/i], + iconSearch: 'spring', + catalog: 'developer', + lucideFallback: 'server', + }, + { + patterns: [/^rails$/i, /^ruby$/i], + iconSearch: 'rails', + catalog: 'developer', + lucideFallback: 'server', + }, + { + patterns: [/^laravel$/i], + iconSearch: 'laravel', + catalog: 'developer', + lucideFallback: 'server', + }, + { + patterns: [/^nest(?:\.?js)?$/i], + iconSearch: 'nestjs', + catalog: 'developer', + lucideFallback: 'server', + }, + { patterns: [/^gin$/i], iconSearch: 'gin', catalog: 'developer', lucideFallback: 'server' }, + { + patterns: [/^go$/i, /^golang$/i], + iconSearch: 'go', + catalog: 'developer', + lucideFallback: 'server', + }, + { patterns: [/^rust$/i], iconSearch: 'rust', catalog: 'developer', lucideFallback: 'server' }, + { patterns: [/^deno$/i], iconSearch: 'deno', catalog: 'developer', lucideFallback: 'server' }, + { patterns: [/^bun$/i], iconSearch: 'bun', catalog: 'developer', lucideFallback: 'server' }, + + // Infrastructure + { + patterns: [/^docker$/i], + iconSearch: 'docker', + catalog: 'developer', + lucideFallback: 'container', + }, + { + patterns: [/^kubernetes$/i, /^k8s$/i], + iconSearch: 'kubernetes', + catalog: 'cncf', + lucideFallback: 'container', + }, + { patterns: [/^nginx$/i], iconSearch: 'nginx', catalog: 'developer', lucideFallback: 'shield' }, + { + patterns: [/^rabbitmq$/i], + iconSearch: 'rabbitmq', + catalog: 'developer', + lucideFallback: 'layers', + }, + { + patterns: [/^kafka$/i, /^apache\s*kafka$/i], + iconSearch: 'apachekafka', + catalog: 'developer', + lucideFallback: 'layers', + }, + { + patterns: [/^consul$/i], + iconSearch: 'consul', + catalog: 'developer', + lucideFallback: 'map-pin', + }, + { patterns: [/^vault$/i], iconSearch: 'vault', catalog: 'developer', lucideFallback: 'lock' }, + { + patterns: [/^terraform$/i], + iconSearch: 'terraform', + catalog: 'developer', + lucideFallback: 'layers', + }, + { + patterns: [/^ansible$/i], + iconSearch: 'ansible', + catalog: 'developer', + lucideFallback: 'settings', + }, + { + patterns: [/^prometheus$/i], + iconSearch: 'prometheus', + catalog: 'developer', + lucideFallback: 'activity', + }, + { + patterns: [/^grafana$/i], + iconSearch: 'grafana', + catalog: 'developer', + lucideFallback: 'bar-chart', + }, + { + patterns: [/^jenkins$/i], + iconSearch: 'jenkins', + catalog: 'developer', + lucideFallback: 'settings', + }, + { + patterns: [/^gitlab$/i], + iconSearch: 'gitlab', + catalog: 'developer', + lucideFallback: 'git-branch', + }, + { + patterns: [/^github$/i], + iconSearch: 'github', + catalog: 'developer', + lucideFallback: 'git-branch', + }, + { patterns: [/^helm$/i], iconSearch: 'helm', catalog: 'cncf', lucideFallback: 'package' }, + { patterns: [/^istio$/i], iconSearch: 'istio', catalog: 'cncf', lucideFallback: 'network' }, + { patterns: [/^envoy$/i], iconSearch: 'envoy', catalog: 'cncf', lucideFallback: 'network' }, + { + patterns: [/^grafana\s*tempo$/i, /^tempo$/i], + iconSearch: 'grafana-tempo', + catalog: 'developer', + lucideFallback: 'activity', + }, + + // Cloud services + { patterns: [/^s3$/i], iconSearch: 's3', catalog: 'aws', lucideFallback: 'folder' }, + { patterns: [/^lambda$/i], iconSearch: 'lambda', catalog: 'aws', lucideFallback: 'zap' }, + { patterns: [/^ec2$/i], iconSearch: 'ec2', catalog: 'aws', lucideFallback: 'server' }, + { patterns: [/^ecs$/i], iconSearch: 'ecs', catalog: 'aws', lucideFallback: 'container' }, + { patterns: [/^eks$/i], iconSearch: 'eks', catalog: 'aws', lucideFallback: 'container' }, + { patterns: [/^rds$/i], iconSearch: 'rds', catalog: 'aws', lucideFallback: 'database' }, + { + patterns: [/^api\s*gateway$/i], + iconSearch: 'api-gateway', + catalog: 'aws', + lucideFallback: 'shield', + }, + { + patterns: [/^cloudfront$/i], + iconSearch: 'cloudfront', + catalog: 'aws', + lucideFallback: 'globe', + }, + { patterns: [/^sqs$/i], iconSearch: 'sqs', catalog: 'aws', lucideFallback: 'layers' }, + { patterns: [/^sns$/i], iconSearch: 'sns', catalog: 'aws', lucideFallback: 'bell' }, + { patterns: [/^cognito$/i], iconSearch: 'cognito', catalog: 'aws', lucideFallback: 'key' }, + { + patterns: [/^cloud\s*run$/i], + iconSearch: 'cloud-run', + catalog: 'gcp', + lucideFallback: 'container', + }, + { + patterns: [/^cloud\s*functions$/i], + iconSearch: 'cloud-functions', + catalog: 'gcp', + lucideFallback: 'zap', + }, + { patterns: [/^bigquery$/i], iconSearch: 'bigquery', catalog: 'gcp', lucideFallback: 'database' }, + { + patterns: [/^azure\s*functions$/i], + iconSearch: 'azure-functions', + catalog: 'azure', + lucideFallback: 'zap', + }, + { + patterns: [/^azure\s*sql$/i], + iconSearch: 'azure-sql', + catalog: 'azure', + lucideFallback: 'database', + }, + + // Messaging / Streaming + { patterns: [/^pulsar$/i], iconSearch: 'pulsar', catalog: 'developer', lucideFallback: 'layers' }, + { patterns: [/^nats$/i], iconSearch: 'nats', catalog: 'developer', lucideFallback: 'layers' }, + { + patterns: [/^zeromq$/i, /^0mq$/i], + iconSearch: 'zeromq', + catalog: 'developer', + lucideFallback: 'layers', + }, + + // Auth + { patterns: [/^auth0$/i], iconSearch: 'auth0', catalog: 'developer', lucideFallback: 'key' }, + { + patterns: [/^keycloak$/i], + iconSearch: 'keycloak', + catalog: 'developer', + lucideFallback: 'key', + }, + { + patterns: [/^firebase$/i], + iconSearch: 'firebase', + catalog: 'developer', + lucideFallback: 'flame', + }, + { + patterns: [/^supertokens$/i, /^super\s*tokens$/i], + iconSearch: 'supertokens', + catalog: 'developer', + lucideFallback: 'key', + }, + + // Payments / SaaS + { + patterns: [/^stripe$/i], + iconSearch: 'stripe', + catalog: 'developer', + lucideFallback: 'credit-card', + }, + { patterns: [/^twilio$/i], iconSearch: 'twilio', catalog: 'developer', lucideFallback: 'phone' }, + { + patterns: [/^sendgrid$/i], + iconSearch: 'sendgrid', + catalog: 'developer', + lucideFallback: 'mail', + }, + { + patterns: [/^mailchimp$/i], + iconSearch: 'mailchimp', + catalog: 'developer', + lucideFallback: 'mail', + }, + { + patterns: [/^cloudflare$/i], + iconSearch: 'cloudflare', + catalog: 'developer', + lucideFallback: 'cloud', + }, + { + patterns: [/^vercel$/i], + iconSearch: 'vercel', + catalog: 'developer', + lucideFallback: 'triangle', + }, + { + patterns: [/^netlify$/i], + iconSearch: 'netlify', + catalog: 'developer', + lucideFallback: 'globe', + }, +]; + +const LUCIDE_FALLBACK_MAP: Record = { + database: 'database', + cache: 'hard-drive', + queue: 'layers', + service: 'server', + frontend: 'monitor', + gateway: 'shield', + auth: 'key-round', + storage: 'folder', + user: 'user', + start: 'play', + end: 'check-circle', + decision: 'help-circle', + action: 'zap', + process: 'box', +}; + +export function resolveIconSync(query: string, categoryHint?: string): IconResolution { + const trimmed = query.trim(); + if (!trimmed) { + return { found: false, confidence: 0 }; + } + + for (const entry of ALIAS_TABLE) { + if (entry.patterns.some((p) => p.test(trimmed))) { + return { + found: true, + iconSearch: entry.iconSearch, + catalog: entry.catalog, + lucideIcon: entry.lucideFallback, + label: trimmed, + confidence: 0.95, + }; + } + } + + if (categoryHint && LUCIDE_FALLBACK_MAP[categoryHint]) { + return { + found: true, + lucideIcon: LUCIDE_FALLBACK_MAP[categoryHint], + label: trimmed, + confidence: 0.5, + }; + } + + return { found: false, confidence: 0 }; +} + +export function resolveLucideFallback(category: string): string { + return LUCIDE_FALLBACK_MAP[category] ?? 'box'; +} diff --git a/src/lib/mermaidEnrichmentPipeline.test.ts b/src/lib/mermaidEnrichmentPipeline.test.ts new file mode 100644 index 00000000..e5be9d29 --- /dev/null +++ b/src/lib/mermaidEnrichmentPipeline.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it } from 'vitest'; +import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; +import { enrichNodesWithIcons } from '@/lib/nodeEnricher'; + +describe('Mermaid โ†’ Enrichment Pipeline (E2E)', () => { + it('flowchart: assigns colors and icons to all node types', async () => { + const mermaid = ` + flowchart TD + S([Start]) --> login[Login Form] + login --> valid{Credentials Valid?} + valid -->|Yes| db[(PostgreSQL)] + valid -->|No| fail((Access Denied)) + db --> redis[Redis Cache] + redis --> done((Dashboard)) + `; + + const parsed = parseMermaidByType(mermaid); + expect(parsed.error).toBeUndefined(); + expect(parsed.nodes.length).toBeGreaterThan(0); + + const enriched = await enrichNodesWithIcons(parsed.nodes); + + const startNode = enriched.find((n) => n.id === 'S'); + expect(startNode?.data.color).toBe('emerald'); + expect(startNode?.data.icon).toBe('play'); + + const endNode = enriched.find((n) => n.id === 'fail'); + expect(endNode?.data.color).toBe('red'); + expect(endNode?.data.icon).toBe('check-circle'); + + const decisionNode = enriched.find((n) => n.id === 'valid'); + expect(decisionNode?.data.color).toBe('amber'); + expect(decisionNode?.data.icon).toBe('help-circle'); + + const dbNode = enriched.find((n) => n.id === 'db'); + expect(dbNode?.data.color).toBe('violet'); + expect(dbNode?.data.icon).toBe('database'); + + const redisNode = enriched.find((n) => n.id === 'redis'); + expect(redisNode?.data.color).toBe('red'); + expect(redisNode?.data.icon).toBe('hard-drive'); + }); + + it('flowchart with subgraphs: creates section nodes with proper hierarchy', async () => { + const mermaid = ` + flowchart TD + subgraph Backend + API[Express API] + DB[(PostgreSQL)] + end + subgraph Frontend + UI[React App] + end + UI --> API + API --> DB + `; + + const parsed = parseMermaidByType(mermaid); + expect(parsed.error).toBeUndefined(); + + const enriched = await enrichNodesWithIcons(parsed.nodes); + + const sectionNodes = enriched.filter((n) => n.type === 'section'); + expect(sectionNodes).toHaveLength(2); + + const backendSection = sectionNodes.find((n) => n.data.label === 'Backend'); + expect(backendSection).toBeDefined(); + + const apiNode = enriched.find((n) => n.id === 'API'); + expect(apiNode?.parentId).toBe(backendSection?.id); + expect(apiNode?.data.color).toBe('blue'); + + const dbNode = enriched.find((n) => n.id === 'DB'); + expect(dbNode?.parentId).toBe(backendSection?.id); + expect(dbNode?.data.color).toBe('violet'); + }); + + it('sequence diagram: parses participants and messages', async () => { + const mermaid = ` + sequenceDiagram + participant Client + participant Server + participant Database + Client->>Server: HTTP Request + Server->>Database: SQL Query + Database-->>Server: Results + Server-->>Client: JSON Response + `; + + const parsed = parseMermaidByType(mermaid); + expect(parsed.error).toBeUndefined(); + expect(parsed.diagramType).toBe('sequence'); + expect(parsed.nodes.length).toBeGreaterThanOrEqual(3); + expect(parsed.edges.length).toBeGreaterThanOrEqual(4); + }); + + it('sequence diagram: handles fragments (alt/loop) and activations', async () => { + const mermaid = ` + sequenceDiagram + participant A + participant B + A->>B: Request + activate B + alt success + B-->>A: 200 OK + else failure + B-->>A: 500 Error + end + loop every minute + A->>B: Heartbeat + end + deactivate B + `; + + const parsed = parseMermaidByType(mermaid); + expect(parsed.error).toBeUndefined(); + expect(parsed.diagramType).toBe('sequence'); + expect(parsed.nodes.length).toBeGreaterThanOrEqual(2); + }); + + it('sequence diagram: handles notes over participants', async () => { + const mermaid = ` + sequenceDiagram + participant A + participant B + Note over A,B: This is a note + A->>B: Message + `; + + const parsed = parseMermaidByType(mermaid); + expect(parsed.error).toBeUndefined(); + expect(parsed.nodes.length).toBeGreaterThanOrEqual(2); + }); + + it('architecture diagram: preserves archIconPackId when set by AI', async () => { + // Simulate AI-generated nodes with provider icons already set + const aiGeneratedNodes = [ + { + id: 'api_gw', + type: 'architecture' as const, + position: { x: 0, y: 0 }, + data: { + label: 'API Gateway', + subLabel: '', + color: 'violet', + archIconPackId: 'aws-official-starter-v1', + archIconShapeId: 'api-gateway', + assetPresentation: 'icon' as const, + }, + }, + ]; + + const enriched = await enrichNodesWithIcons(aiGeneratedNodes); + + // Enricher should preserve existing archIconPackId + expect(enriched[0].data.archIconPackId).toBe('aws-official-starter-v1'); + expect(enriched[0].data.archIconShapeId).toBe('api-gateway'); + expect(enriched[0].data.color).toBe('violet'); + }); + + it('does not modify section nodes', async () => { + const mermaid = ` + flowchart TD + subgraph Group A + A[Node A] + end + `; + + const parsed = parseMermaidByType(mermaid); + const enriched = await enrichNodesWithIcons(parsed.nodes); + + const sectionNode = enriched.find((n) => n.type === 'section'); + expect(sectionNode?.data.icon).toBeUndefined(); + expect(sectionNode?.data.archIconPackId).toBeUndefined(); + }); + + it('edge labels are preserved through parse+enrich', async () => { + const mermaid = ` + flowchart TD + A[Start] -->|Yes| B[Process] + A -->|No| C[End] + `; + + const parsed = parseMermaidByType(mermaid); + expect(parsed.edges).toHaveLength(2); + expect(parsed.edges[0].label).toBe('Yes'); + expect(parsed.edges[1].label).toBe('No'); + }); +}); diff --git a/src/lib/mermaidParser.ts b/src/lib/mermaidParser.ts index 3b74e02b..ee31fcbc 100644 --- a/src/lib/mermaidParser.ts +++ b/src/lib/mermaidParser.ts @@ -9,341 +9,377 @@ import { type MermaidParseModel, } from './mermaidParserModel'; import { - ARROW_PATTERNS, - CLASS_DEF_RE, - parseEdgeLine, - parseLinkStyleLine, - parseNodeDeclaration, - parseStyleString, - SKIP_PATTERNS, - STYLE_RE, - normalizeEdgeLabels, - normalizeMultilineStrings, + ARROW_PATTERNS, + CLASS_DEF_RE, + parseEdgeLine, + parseLinkStyleLine, + parseNodeDeclaration, + parseStyleString, + SKIP_PATTERNS, + STYLE_RE, + normalizeEdgeLabels, + normalizeMultilineStrings, } from './mermaidParserHelpers'; import type { FlowEdge, FlowNode } from './types'; const NODE_TYPE_DEFAULTS: Record = { - start: 'emerald', - end: 'red', - decision: 'amber', - custom: 'violet', - process: 'slate', + start: 'emerald', + end: 'red', + decision: 'amber', + custom: 'violet', + process: 'slate', }; function getDefaultColor(type: string): string { - return NODE_TYPE_DEFAULTS[type] || 'slate'; + return NODE_TYPE_DEFAULTS[type] || 'slate'; } export interface ParseResult { - nodes: FlowNode[]; - edges: FlowEdge[]; - error?: string; - direction?: MermaidDirection; + nodes: FlowNode[]; + edges: FlowEdge[]; + error?: string; + direction?: MermaidDirection; } function preprocessMermaidInput(input: string): string[] { - const processed = normalizeEdgeLabels(normalizeMultilineStrings(input.replace(/\r\n/g, '\n'))); - return processed.split('\n'); + const processed = normalizeEdgeLabels(normalizeMultilineStrings(input.replace(/\r\n/g, '\n'))); + return processed.split('\n'); } function isSkippableLine(line: string): boolean { - return !line || SKIP_PATTERNS.some((pattern) => pattern.test(line)); + return !line || SKIP_PATTERNS.some((pattern) => pattern.test(line)); } function parseFlowchartDeclaration(line: string): MermaidDirection | null { - const flowchartMatch = line.match(/^(?:flowchart|graph)\s+(TD|TB|LR|RL|BT)/i); - if (!flowchartMatch) { - return null; - } - - return (flowchartMatch[1].toUpperCase() === 'TD' - ? 'TB' - : flowchartMatch[1].toUpperCase()) as MermaidDirection; + const flowchartMatch = line.match(/^(?:flowchart|graph)\s+(TD|TB|LR|RL|BT)/i); + if (!flowchartMatch) { + return null; + } + + return ( + flowchartMatch[1].toUpperCase() === 'TD' ? 'TB' : flowchartMatch[1].toUpperCase() + ) as MermaidDirection; } function parseStateDiagramDirection(nextLine: string | undefined): MermaidDirection { - const dirMatch = nextLine?.trim().match(/^direction\s+(LR|TB)/i); - return (dirMatch?.[1].toUpperCase() ?? 'TB') as MermaidDirection; + const dirMatch = nextLine?.trim().match(/^direction\s+(LR|TB)/i); + return (dirMatch?.[1].toUpperCase() ?? 'TB') as MermaidDirection; } function registerSectionNode( - state: ReturnType, - line: string + state: ReturnType, + line: string ): boolean { - const subgraphMatch = line.match(/^subgraph\s+(.+)$/i); - const stateGroupMatch = - line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)\s+\{/i) || - line.match(/^state\s+(\w+)\s+\{/i); + const subgraphMatch = line.match(/^subgraph\s+(.+)$/i); + const stateGroupMatch = + line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)\s+\{/i) || line.match(/^state\s+(\w+)\s+\{/i); - if (!subgraphMatch && !stateGroupMatch) { - return false; - } - void state; - return true; + if (!subgraphMatch && !stateGroupMatch) { + return false; + } + + let sectionId: string; + let sectionLabel: string; + + if (subgraphMatch) { + sectionLabel = subgraphMatch[1].trim(); + sectionId = `subgraph_${sectionLabel.replace(/[^a-zA-Z0-9_]/g, '_')}`; + } else if (stateGroupMatch) { + sectionId = stateGroupMatch[2] ?? stateGroupMatch[1]; + sectionLabel = stateGroupMatch[1] ?? stateGroupMatch[2]; + } else { + return false; + } + + let attempts = 0; + let finalId = sectionId; + while (state.nodesMap.has(finalId)) { + finalId = `${sectionId}_${++attempts}`; + } + + const parentId = state.parentStack[state.parentStack.length - 1]; + state.nodesMap.set(finalId, { + id: finalId, + label: sectionLabel, + type: 'section', + parentId, + }); + state.parentStack.push(finalId); + + return true; } function applyNodeStyleDirective( - state: ReturnType, - line: string + state: ReturnType, + line: string ): boolean { - const styleMatch = line.match(STYLE_RE); - if (!styleMatch) { - return false; - } - - const [, id, styleStr] = styleMatch; - const styles = parseStyleString(styleStr); - const node = state.nodesMap.get(id); - if (node) { - node.styles = { ...node.styles, ...styles }; - } else { - registerMermaidNode(state, id); - const registeredNode = state.nodesMap.get(id); - if (registeredNode) { - registeredNode.styles = styles; - } + const styleMatch = line.match(STYLE_RE); + if (!styleMatch) { + return false; + } + + const [, id, styleStr] = styleMatch; + const styles = parseStyleString(styleStr); + const node = state.nodesMap.get(id); + if (node) { + node.styles = { ...node.styles, ...styles }; + } else { + registerMermaidNode(state, id); + const registeredNode = state.nodesMap.get(id); + if (registeredNode) { + registeredNode.styles = styles; } + } - return true; + return true; } function parseEdgeDeclaration( - state: ReturnType, - line: string + state: ReturnType, + line: string ): boolean { - if (!ARROW_PATTERNS.some((arrow) => line.includes(arrow))) { - return false; + if (!ARROW_PATTERNS.some((arrow) => line.includes(arrow))) { + return false; + } + + const edgesFound = parseEdgeLine(line); + edgesFound.forEach((edge) => { + const type = state.diagramType === 'stateDiagram' ? 'state' : 'process'; + const sourceId = registerMermaidNode(state, edge.sourceRaw, type); + const targetId = registerMermaidNode(state, edge.targetRaw, type); + + if (sourceId && targetId) { + state.rawEdges.push({ + source: sourceId, + target: targetId, + label: edge.label, + arrowType: edge.arrowType, + }); } + }); - const edgesFound = parseEdgeLine(line); - edgesFound.forEach((edge) => { - const type = state.diagramType === 'stateDiagram' ? 'state' : 'process'; - const sourceId = registerMermaidNode(state, edge.sourceRaw, type); - const targetId = registerMermaidNode(state, edge.targetRaw, type); - - if (sourceId && targetId) { - state.rawEdges.push({ - source: sourceId, - target: targetId, - label: edge.label, - arrowType: edge.arrowType, - }); - } - }); - - return true; + return true; } function parseStateDiagramNodeDeclaration( - state: ReturnType, - line: string + state: ReturnType, + line: string ): boolean { - if (state.diagramType !== 'stateDiagram') { - return false; - } + if (state.diagramType !== 'stateDiagram') { + return false; + } - const stateDefMatch = line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)/i); - if (stateDefMatch) { - registerMermaidNode(state, stateDefMatch[2], 'state', stateDefMatch[1]); - return true; - } + const stateDefMatch = line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)/i); + if (stateDefMatch) { + registerMermaidNode(state, stateDefMatch[2], 'state', stateDefMatch[1]); + return true; + } - const stateDescMatch = line.match(/^(\w+)\s*:\s*(.+)/); - if (stateDescMatch) { - registerMermaidNode(state, stateDescMatch[1], 'state', stateDescMatch[2]); - return true; - } + const stateDescMatch = line.match(/^(\w+)\s*:\s*(.+)/); + if (stateDescMatch) { + registerMermaidNode(state, stateDescMatch[1], 'state', stateDescMatch[2]); + return true; + } - return false; + return false; } function buildMermaidParseModel(lines: string[]): MermaidParseModel { - const state = createMermaidParseState(); + const state = createMermaidParseState(); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (isSkippableLine(line)) { - continue; - } + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (isSkippableLine(line)) { + continue; + } - const flowchartDirection = parseFlowchartDeclaration(line); - if (flowchartDirection) { - state.diagramType = 'flowchart'; - state.direction = flowchartDirection; - continue; - } + const flowchartDirection = parseFlowchartDeclaration(line); + if (flowchartDirection) { + state.diagramType = 'flowchart'; + state.direction = flowchartDirection; + continue; + } - if (line.match(/^stateDiagram(?:-v2)?/i)) { - state.diagramType = 'stateDiagram'; - state.direction = parseStateDiagramDirection(lines[i + 1]); - continue; - } + if (line.match(/^stateDiagram(?:-v2)?/i)) { + state.diagramType = 'stateDiagram'; + state.direction = parseStateDiagramDirection(lines[i + 1]); + continue; + } - if (line.match(/^end\s*$/i) || line === '}') { - if (state.parentStack.length > 0) { - state.parentStack.pop(); - } - continue; - } + if (line.match(/^end\s*$/i) || line === '}') { + if (state.parentStack.length > 0) { + state.parentStack.pop(); + } + continue; + } - if (registerSectionNode(state, line)) { - continue; - } + if (registerSectionNode(state, line)) { + continue; + } - const classDefMatch = line.match(CLASS_DEF_RE); - if (classDefMatch) { - state.classDefs.set(classDefMatch[1], parseStyleString(classDefMatch[2])); - continue; - } + const classDefMatch = line.match(CLASS_DEF_RE); + if (classDefMatch) { + state.classDefs.set(classDefMatch[1], parseStyleString(classDefMatch[2])); + continue; + } - if (applyNodeStyleDirective(state, line)) { - continue; - } + if (applyNodeStyleDirective(state, line)) { + continue; + } - const linkStyleMatch = parseLinkStyleLine(line); - if (linkStyleMatch) { - linkStyleMatch.indices.forEach((index) => state.linkStyles.set(index, linkStyleMatch.style)); - continue; - } + const linkStyleMatch = parseLinkStyleLine(line); + if (linkStyleMatch) { + linkStyleMatch.indices.forEach((index) => state.linkStyles.set(index, linkStyleMatch.style)); + continue; + } - if (parseEdgeDeclaration(state, line)) { - continue; - } + if (parseEdgeDeclaration(state, line)) { + continue; + } - if (parseStateDiagramNodeDeclaration(state, line)) { - continue; - } + if (parseStateDiagramNodeDeclaration(state, line)) { + continue; + } - const standalone = parseNodeDeclaration(line); - if (standalone) { - registerMermaidNode(state, line); - } + const standalone = parseNodeDeclaration(line); + if (standalone) { + registerMermaidNode(state, line); } + } - return toMermaidParseModel(state); + return toMermaidParseModel(state); } function createFlowNodes(model: MermaidParseModel): FlowNode[] { - return Array.from(model.nodesMap.values()).map((node, index) => { - let flowNode: FlowNode = { - id: node.id, - type: node.type, - position: { x: (index % 4) * 200, y: Math.floor(index / 4) * 150 }, - data: { - label: node.label, - subLabel: '', - color: getDefaultColor(node.type), - ...(node.shape ? { shape: node.shape } : {}), - }, - }; + return Array.from(model.nodesMap.values()).map((node, index) => { + let flowNode: FlowNode = { + id: node.id, + type: node.type, + position: { x: (index % 4) * 200, y: Math.floor(index / 4) * 150 }, + data: { + label: node.label, + subLabel: '', + color: getDefaultColor(node.type), + ...(node.shape ? { shape: node.shape } : {}), + }, + ...(node.type === 'section' + ? { + style: { width: 400, height: 300 }, + } + : {}), + }; - if (node.parentId) { - flowNode = setNodeParent(flowNode, node.parentId); - } + if (node.parentId) { + flowNode = setNodeParent(flowNode, node.parentId); + } - if (node.classes) { - node.classes.forEach((cls) => { - const styles = model.classDefs.get(cls); - if (!styles) { - return; - } - if (styles.fill) { - flowNode.style = { ...flowNode.style, backgroundColor: styles.fill }; - } - if (styles.stroke) { - flowNode.style = { ...flowNode.style, borderColor: styles.stroke }; - } - if (styles.color) { - flowNode.style = { ...flowNode.style, color: styles.color }; - } - }); + if (node.classes) { + node.classes.forEach((cls) => { + const styles = model.classDefs.get(cls); + if (!styles) { + return; } - - if (node.styles) { - if (node.styles.fill) { - flowNode.style = { ...flowNode.style, backgroundColor: node.styles.fill }; - } - if (node.styles.stroke) { - flowNode.style = { ...flowNode.style, borderColor: node.styles.stroke }; - } - if (node.styles.color) { - flowNode.style = { ...flowNode.style, color: node.styles.color }; - } + if (styles.fill) { + flowNode.style = { ...flowNode.style, backgroundColor: styles.fill }; } - - if (model.diagramType === 'stateDiagram') { - if (node.type === 'start') { - flowNode.style = { - ...flowNode.style, - width: 20, - height: 20, - borderRadius: '50%', - backgroundColor: '#000', - }; - flowNode.data.label = ''; - } - if (node.type === 'state') { - flowNode.data.shape = 'rounded'; - } + if (styles.stroke) { + flowNode.style = { ...flowNode.style, borderColor: styles.stroke }; } + if (styles.color) { + flowNode.style = { ...flowNode.style, color: styles.color }; + } + }); + } + + if (node.styles) { + if (node.styles.fill) { + flowNode.style = { ...flowNode.style, backgroundColor: node.styles.fill }; + } + if (node.styles.stroke) { + flowNode.style = { ...flowNode.style, borderColor: node.styles.stroke }; + } + if (node.styles.color) { + flowNode.style = { ...flowNode.style, color: node.styles.color }; + } + } + + if (model.diagramType === 'stateDiagram') { + if (node.type === 'start') { + flowNode.style = { + ...flowNode.style, + width: 20, + height: 20, + borderRadius: '50%', + backgroundColor: '#000', + }; + flowNode.data.label = ''; + } + if (node.type === 'state') { + flowNode.data.shape = 'rounded'; + } + } - return flowNode; - }); + return flowNode; + }); } function createFlowEdges(model: MermaidParseModel): FlowEdge[] { - return model.rawEdges.map((edge, index) => { - const flowEdge = createDefaultEdge( - edge.source, - edge.target, - edge.label || undefined, - `e-mermaid-${index}` - ); - - if (edge.arrowType.includes('-.') || edge.arrowType.includes('-.-')) { - flowEdge.style = { ...flowEdge.style, strokeDasharray: '5 3' }; - } - if (edge.arrowType.includes('==')) { - flowEdge.style = { ...flowEdge.style, strokeWidth: 4 }; - } - if (edge.arrowType.startsWith('<')) { - flowEdge.markerStart = { type: MarkerType.ArrowClosed }; - } - if (!edge.arrowType.includes('>')) { - flowEdge.markerEnd = undefined; - } + return model.rawEdges.map((edge, index) => { + const flowEdge = createDefaultEdge( + edge.source, + edge.target, + edge.label || undefined, + `e-mermaid-${index}` + ); + + if (edge.arrowType.includes('-.') || edge.arrowType.includes('-.-')) { + flowEdge.style = { ...flowEdge.style, strokeDasharray: '5 3' }; + } + if (edge.arrowType.includes('==')) { + flowEdge.style = { ...flowEdge.style, strokeWidth: 4 }; + } + if (edge.arrowType.startsWith('<')) { + flowEdge.markerStart = { type: MarkerType.ArrowClosed }; + } + if (!edge.arrowType.includes('>')) { + flowEdge.markerEnd = undefined; + } - const style = model.linkStyles.get(index); - if (style) { - if (style.stroke) { - flowEdge.style = { ...flowEdge.style, stroke: style.stroke }; - } - if (style['stroke-width']) { - flowEdge.style = { - ...flowEdge.style, - strokeWidth: parseInt(style['stroke-width'], 10) || 2, - }; - } - } + const style = model.linkStyles.get(index); + if (style) { + if (style.stroke) { + flowEdge.style = { ...flowEdge.style, stroke: style.stroke }; + } + if (style['stroke-width']) { + flowEdge.style = { + ...flowEdge.style, + strokeWidth: parseInt(style['stroke-width'], 10) || 2, + }; + } + } - return flowEdge; - }); + return flowEdge; + }); } export function parseMermaid(input: string): ParseResult { - const model = buildMermaidParseModel(preprocessMermaidInput(input)); - - if (model.diagramType === 'unknown') { - return { nodes: [], edges: [], error: 'Missing chart type declaration. Start with "flowchart TD" or related.' }; - } - - if (model.nodesMap.size === 0) { - return { nodes: [], edges: [], error: 'No valid nodes found.' }; - } + const model = buildMermaidParseModel(preprocessMermaidInput(input)); + if (model.diagramType === 'unknown') { return { - nodes: createFlowNodes(model), - edges: createFlowEdges(model), - direction: model.direction, + nodes: [], + edges: [], + error: 'Missing chart type declaration. Start with "flowchart TD" or related.', }; + } + + if (model.nodesMap.size === 0) { + return { nodes: [], edges: [], error: 'No valid nodes found.' }; + } + + return { + nodes: createFlowNodes(model), + edges: createFlowEdges(model), + direction: model.direction, + }; } diff --git a/src/lib/mermaidParserHelpers.ts b/src/lib/mermaidParserHelpers.ts index 4ee7bdc6..a0a73ccd 100644 --- a/src/lib/mermaidParserHelpers.ts +++ b/src/lib/mermaidParserHelpers.ts @@ -1,23 +1,28 @@ import type { NodeData } from './types'; -export const SHAPE_OPENERS: Array<{ open: string; close: string; type: string; shape: NodeData['shape'] }> = [ - { open: '([', close: '])', type: 'start', shape: 'capsule' }, - { open: '((', close: '))', type: 'end', shape: 'circle' }, - { open: '{{', close: '}}', type: 'custom', shape: 'hexagon' }, - { open: '[(', close: ')]', type: 'process', shape: 'cylinder' }, - { open: '{', close: '}', type: 'decision', shape: 'diamond' }, - { open: '[', close: ']', type: 'process', shape: 'rounded' }, - { open: '(', close: ')', type: 'process', shape: 'rounded' }, - { open: '>', close: ']', type: 'process', shape: 'parallelogram' }, +export const SHAPE_OPENERS: Array<{ + open: string; + close: string; + type: string; + shape: NodeData['shape']; +}> = [ + { open: '([', close: '])', type: 'start', shape: 'capsule' }, + { open: '((', close: '))', type: 'end', shape: 'circle' }, + { open: '{{', close: '}}', type: 'custom', shape: 'hexagon' }, + { open: '[(', close: ')]', type: 'process', shape: 'cylinder' }, + { open: '{', close: '}', type: 'decision', shape: 'diamond' }, + { open: '[', close: ']', type: 'process', shape: 'rounded' }, + { open: '(', close: ')', type: 'process', shape: 'rounded' }, + { open: '>', close: ']', type: 'process', shape: 'parallelogram' }, ]; export const SKIP_PATTERNS = [ - /^%%/, - /^class\s/i, - /^click\s/i, - /^direction\s/i, - /^accTitle\s/i, - /^accDescr\s/i, + /^%%/, + /^class\s/i, + /^click\s/i, + /^direction\s/i, + /^accTitle\s/i, + /^accDescr\s/i, ]; const LINK_STYLE_RE = /^linkStyle\s+([\d,\s]+)\s+(.+)$/i; @@ -26,232 +31,310 @@ const STYLE_RE = /^style\s+(\w+)\s+(.+)$/i; export { CLASS_DEF_RE, STYLE_RE }; -export function parseLinkStyleLine(line: string): { indices: number[]; style: Record } | null { - const match = line.match(LINK_STYLE_RE); - if (!match) return null; +export function parseLinkStyleLine( + line: string +): { indices: number[]; style: Record } | null { + const match = line.match(LINK_STYLE_RE); + if (!match) return null; - const indices = match[1] - .split(',') - .map((s) => parseInt(s.trim(), 10)) - .filter((n) => !Number.isNaN(n)); + const indices = match[1] + .split(',') + .map((s) => parseInt(s.trim(), 10)) + .filter((n) => !Number.isNaN(n)); - const styleParts = match[2].replace(/;$/, '').split(','); - const style: Record = {}; + const styleParts = match[2].replace(/;$/, '').split(','); + const style: Record = {}; - for (const part of styleParts) { - const [key, value] = part.split(':').map((s) => s.trim()); - if (key && value) { - style[key] = value; - } + for (const part of styleParts) { + const [key, value] = part.split(':').map((s) => s.trim()); + if (key && value) { + style[key] = value; } + } - return { indices, style }; + return { indices, style }; } export function normalizeMultilineStrings(input: string): string { - let result = ''; - let inQuote = false; - - for (let i = 0; i < input.length; i++) { - const char = input[i]; - if (char === '"' && input[i - 1] !== '\\') { - inQuote = !inQuote; - } - - if (inQuote && char === '\n') { - result += '\\n'; - let nextIndex = i + 1; - while (nextIndex < input.length && (input[nextIndex] === ' ' || input[nextIndex] === '\t')) { - nextIndex++; - } - i = nextIndex - 1; - } else { - result += char; - } + let result = ''; + let inQuote = false; + + for (let i = 0; i < input.length; i++) { + const char = input[i]; + if (char === '"' && input[i - 1] !== '\\') { + inQuote = !inQuote; + } + + if (inQuote && char === '\n') { + result += '\\n'; + let nextIndex = i + 1; + while (nextIndex < input.length && (input[nextIndex] === ' ' || input[nextIndex] === '\t')) { + nextIndex++; + } + i = nextIndex - 1; + } else { + result += char; } + } - return result; + return result; } export function normalizeEdgeLabels(input: string): string { - let result = input; - result = result.replace(/==(?![>])\s*(.+?)\s*==>/g, ' ==>|$1|'); - result = result.replace(/--(?![>-])\s*(.+?)\s*-->/g, ' -->|$1|'); - result = result.replace(/-\.\s*(.+?)\s*\.->/g, ' -.->|$1|'); - result = result.replace(/--(?![>-])\s*(.+?)\s*---/g, ' ---|$1|'); - return result; + let result = input; + result = result.replace(/==(?![>])\s*(.+?)\s*==>/g, ' ==>|$1|'); + result = result.replace(/--(?![>-])\s*(.+?)\s*-->/g, ' -->|$1|'); + result = result.replace(/-\.\s*(.+?)\s*\.->/g, ' -.->|$1|'); + result = result.replace(/--(?![>-])\s*(.+?)\s*---/g, ' ---|$1|'); + return result; } export interface RawNode { - id: string; - label: string; - type: string; - shape?: NodeData['shape']; - parentId?: string; - styles?: Record; - classes?: string[]; + id: string; + label: string; + type: string; + shape?: NodeData['shape']; + parentId?: string; + styles?: Record; + classes?: string[]; +} + +const MODERN_SHAPE_MAP: Record = { + cyl: { type: 'process', shape: 'cylinder' }, + cylinder: { type: 'process', shape: 'cylinder' }, + circle: { type: 'end', shape: 'circle' }, + circle2: { type: 'end', shape: 'circle' }, + cloud: { type: 'process', shape: 'rounded' }, + diamond: { type: 'decision', shape: 'diamond' }, + hexagon: { type: 'custom', shape: 'hexagon' }, + 'lean-r': { type: 'process', shape: 'parallelogram' }, + 'lean-l': { type: 'process', shape: 'parallelogram' }, + stadium: { type: 'start', shape: 'capsule' }, + rounded: { type: 'process', shape: 'rounded' }, + rect: { type: 'process', shape: 'rounded' }, + square: { type: 'process', shape: 'rounded' }, + doublecircle: { type: 'end', shape: 'circle' }, +}; + +interface ModernShapeAnnotation { + shapeKey?: string; + labelOverride?: string; + cleanInput: string; +} + +function extractModernAnnotation(input: string): ModernShapeAnnotation { + const match = input.match(/^(\w+)@\{([^}]+)\}/); + if (!match) return { cleanInput: input }; + + const id = match[1]; + const attrs = match[2]; + const rest = input.substring(match[0].length); + + const shapeMatch = attrs.match(/\bshape:\s*(\w+)/); + const labelMatch = attrs.match(/\blabel:\s*"([^"]+)"/); + + return { + shapeKey: shapeMatch?.[1]?.toLowerCase(), + labelOverride: labelMatch?.[1], + cleanInput: `${id}${rest}`, + }; +} + +function stripMarkdown(label: string): string { + return label + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/__(.+?)__/g, '$1') + .replace(/_(.+?)_/g, '$1') + .replace(/~~(.+?)~~/g, '$1') + .replace(/`(.+?)`/g, '$1'); } function stripFaIcons(label: string): string { - const stripped = label.replace(/fa:fa-[\w-]+\s*/g, '').trim(); - if (stripped) return stripped; - const iconMatch = label.match(/fa:fa-([\w-]+)/); - return iconMatch ? iconMatch[1].replace(/-/g, ' ') : label; + const stripped = label.replace(/fa:fa-[\w-]+\s*/g, '').trim(); + if (stripped) return stripped; + const iconMatch = label.match(/fa:fa-([\w-]+)/); + return iconMatch ? iconMatch[1].replace(/-/g, ' ') : label; } function tryParseWithShape( - input: string, - shape: { open: string; close: string; type: string; shape: NodeData['shape'] } + input: string, + shape: { open: string; close: string; type: string; shape: NodeData['shape'] } ): RawNode | null { - const openIndex = input.indexOf(shape.open); - if (openIndex < 1) return null; - if (openIndex > 0 && input[openIndex - 1] === shape.open[0]) return null; - - const id = input.substring(0, openIndex).trim(); - if (!/^[a-zA-Z0-9_][\w-]*$/.test(id)) return null; - - const afterOpen = input.substring(openIndex + shape.open.length); - const closeIndex = afterOpen.lastIndexOf(shape.close); - if (closeIndex < 0) return null; - - const afterClose = afterOpen.substring(closeIndex + shape.close.length).trim(); - let classes: string[] = []; - if (afterClose.startsWith(':::')) { - classes = afterClose.substring(3).split(/,\s*/); - } else if (afterClose) { - return null; - } - - let label = afterOpen.substring(0, closeIndex).trim(); - if ((label.startsWith('"') && label.endsWith('"')) || (label.startsWith("'") && label.endsWith("'"))) { - label = label.slice(1, -1); - } - label = label.replace(/\\n/g, '\n'); - label = stripFaIcons(label); - if (!label) label = id; - - return { id, label, type: shape.type, shape: shape.shape, classes: classes.length ? classes : undefined }; + const openIndex = input.indexOf(shape.open); + if (openIndex < 1) return null; + if (openIndex > 0 && input[openIndex - 1] === shape.open[0]) return null; + + const id = input.substring(0, openIndex).trim(); + if (!/^[a-zA-Z0-9_][\w-]*$/.test(id)) return null; + + const afterOpen = input.substring(openIndex + shape.open.length); + const closeIndex = afterOpen.lastIndexOf(shape.close); + if (closeIndex < 0) return null; + + const afterClose = afterOpen.substring(closeIndex + shape.close.length).trim(); + let classes: string[] = []; + if (afterClose.startsWith(':::')) { + classes = afterClose.substring(3).split(/,\s*/); + } else if (afterClose) { + return null; + } + + let label = afterOpen.substring(0, closeIndex).trim(); + if ( + (label.startsWith('"') && label.endsWith('"')) || + (label.startsWith("'") && label.endsWith("'")) + ) { + label = label.slice(1, -1); + } + label = label.replace(/\\n/g, '\n'); + label = stripFaIcons(label); + label = stripMarkdown(label); + if (!label) label = id; + + return { + id, + label, + type: shape.type, + shape: shape.shape, + classes: classes.length ? classes : undefined, + }; } export function parseNodeDeclaration(raw: string): RawNode | null { - const trimmed = raw.trim(); - if (!trimmed) return null; - - for (const shape of SHAPE_OPENERS) { - const result = tryParseWithShape(trimmed, shape); - if (result) return result; + const trimmed = raw.trim(); + if (!trimmed) return null; + + const annotation = extractModernAnnotation(trimmed); + const input = annotation.cleanInput; + + for (const shape of SHAPE_OPENERS) { + const result = tryParseWithShape(input, shape); + if (result) { + if (annotation.shapeKey && MODERN_SHAPE_MAP[annotation.shapeKey]) { + const override = MODERN_SHAPE_MAP[annotation.shapeKey]; + result.type = override.type; + result.shape = override.shape; + } + if (annotation.labelOverride) { + result.label = annotation.labelOverride; + } + result.label = stripMarkdown(result.label); + return result; } + } - let id = trimmed; - let classes: string[] = []; - if (id.includes(':::')) { - const parts = id.split(':::'); - id = parts[0]; - classes = parts[1].split(/,\s*/); - } + let id = input; + let classes: string[] = []; + if (id.includes(':::')) { + const parts = id.split(':::'); + id = parts[0]; + classes = parts[1].split(/,\s*/); + } - if (/^[a-zA-Z0-9_][\w-]*$/.test(id)) { - return { id, label: id, type: 'process', classes: classes.length ? classes : undefined }; - } + if (/^[a-zA-Z0-9_][\w-]*$/.test(id)) { + return { id, label: id, type: 'process', classes: classes.length ? classes : undefined }; + } - return null; + return null; } export const ARROW_PATTERNS = [ - '<==>', - '<-.->', - '<-->', - '<==', - '<-.', - '<--', - '===>', - '-.->', - '--->', - '-->', - '===', - '---', - '==>', - '-.-', - '--', + '<==>', + '<-.->', + '<-->', + '<==', + '<-.', + '<--', + '===>', + '-.->', + '--->', + '-->', + '===', + '---', + '==>', + '-.-', + '--', ]; function findArrowInLine(line: string): { arrow: string; before: string; after: string } | null { - for (const arrow of ARROW_PATTERNS) { - const index = line.indexOf(arrow); - if (index >= 0) { - return { - arrow, - before: line.substring(0, index).trim(), - after: line.substring(index + arrow.length).trim(), - }; - } + for (const arrow of ARROW_PATTERNS) { + const index = line.indexOf(arrow); + if (index >= 0) { + return { + arrow, + before: line.substring(0, index).trim(), + after: line.substring(index + arrow.length).trim(), + }; } - return null; + } + return null; } export function parseEdgeLine(line: string): Array<{ - sourceRaw: string; - targetRaw: string; - label: string; - arrowType: string; + sourceRaw: string; + targetRaw: string; + label: string; + arrowType: string; }> { - const edges: Array<{ sourceRaw: string; targetRaw: string; label: string; arrowType: string }> = []; - let remaining = line; - let lastNodeRaw: string | null = null; - - while (remaining.trim()) { - const arrowMatch = findArrowInLine(remaining); - if (!arrowMatch) break; - - const { arrow, before, after } = arrowMatch; - const sourceRaw = lastNodeRaw || before; - let label = ''; - let targetAndRest = after; - - const labelMatch = targetAndRest.match(/^\|"?([^"|]*)"?\|\s*/); - if (labelMatch) { - label = labelMatch[1].trim(); - targetAndRest = targetAndRest.substring(labelMatch[0].length); - } - - const nextArrowMatch = findArrowInLine(targetAndRest); - let targetRaw: string; - - if (nextArrowMatch) { - targetRaw = nextArrowMatch.before; - remaining = targetAndRest; - } else { - targetRaw = targetAndRest; - remaining = ''; - } - - let source = sourceRaw.trim(); - let target = targetRaw.trim(); - - if (source.includes(':::')) source = source.split(':::')[0]; - if (target.includes(':::')) target = target.split(':::')[0]; - - if (source && target) { - edges.push({ sourceRaw: source, targetRaw: target, label, arrowType: arrow }); - } - - lastNodeRaw = targetRaw.trim(); - if (!nextArrowMatch) break; + const edges: Array<{ sourceRaw: string; targetRaw: string; label: string; arrowType: string }> = + []; + let remaining = line; + let lastNodeRaw: string | null = null; + + while (remaining.trim()) { + const arrowMatch = findArrowInLine(remaining); + if (!arrowMatch) break; + + const { arrow, before, after } = arrowMatch; + const sourceRaw = lastNodeRaw || before; + let label = ''; + let targetAndRest = after; + + const labelMatch = targetAndRest.match(/^\|"?([^"|]*)"?\|\s*/); + if (labelMatch) { + label = labelMatch[1].trim(); + targetAndRest = targetAndRest.substring(labelMatch[0].length); } - return edges; + const nextArrowMatch = findArrowInLine(targetAndRest); + let targetRaw: string; + + if (nextArrowMatch) { + targetRaw = nextArrowMatch.before; + remaining = targetAndRest; + } else { + targetRaw = targetAndRest; + remaining = ''; + } + + let source = sourceRaw.trim(); + let target = targetRaw.trim(); + + if (source.includes(':::')) source = source.split(':::')[0]; + if (target.includes(':::')) target = target.split(':::')[0]; + + if (source && target) { + edges.push({ sourceRaw: source, targetRaw: target, label, arrowType: arrow }); + } + + lastNodeRaw = targetRaw.trim(); + if (!nextArrowMatch) break; + } + + return edges; } export function parseStyleString(styleStr: string): Record { - const styles: Record = {}; - const parts = styleStr.split(','); - - for (const part of parts) { - const [key, value] = part.split(':').map((s) => s.trim()); - if (key && value) { - styles[key] = value.replace(/;$/, ''); - } + const styles: Record = {}; + const parts = styleStr.split(','); + + for (const part of parts) { + const [key, value] = part.split(':').map((s) => s.trim()); + if (key && value) { + styles[key] = value.replace(/;$/, ''); } + } - return styles; + return styles; } diff --git a/src/lib/nodeEnricher.test.ts b/src/lib/nodeEnricher.test.ts new file mode 100644 index 00000000..8e39467a --- /dev/null +++ b/src/lib/nodeEnricher.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from 'vitest'; +import { enrichNodesWithIcons } from './nodeEnricher'; +import type { FlowNode } from './types'; + +function makeNode(id: string, label: string, overrides?: Partial): FlowNode { + return { + id, + type: 'process', + position: { x: 0, y: 0 }, + data: { label, color: 'slate' }, + ...overrides, + } as FlowNode; +} + +describe('enrichNodesWithIcons', () => { + it('assigns color based on semantic classification', async () => { + const nodes = [ + makeNode('start', 'Start'), + makeNode('end', 'End'), + makeNode('db', 'PostgreSQL'), + makeNode('check', 'Is Valid?', { + data: { label: 'Is Valid?', color: 'slate', shape: 'diamond' }, + }), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + + expect(enriched[0].data.color).toBe('emerald'); + expect(enriched[1].data.color).toBe('red'); + expect(enriched[2].data.color).toBe('violet'); + expect(enriched[3].data.color).toBe('amber'); + }); + + it('assigns icons for known technologies', async () => { + const nodes = [ + makeNode('db', 'PostgreSQL'), + makeNode('cache', 'Redis Cache'), + makeNode('api', 'Express API'), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + + // All three should get provider icons (any catalog) + expect(enriched[0].data.archIconPackId).toBeTruthy(); + expect(enriched[0].data.archIconShapeId).toContain('postgresql'); + + expect(enriched[1].data.archIconPackId).toBeTruthy(); + expect(enriched[1].data.archIconShapeId).toContain('redis'); + + expect(enriched[2].data.archIconPackId).toBeTruthy(); + }); + + it('skips section and group nodes', async () => { + const nodes = [ + { ...makeNode('grp', 'Group'), type: 'section' as const }, + makeNode('x', 'Something'), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + + expect(enriched[0].data.color).toBe('slate'); + expect(enriched[0].data.icon).toBeUndefined(); + }); + + it('preserves existing non-slate colors', async () => { + const nodes = [ + { + id: 'a', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Start', color: 'pink' }, + }, + ]; + + const enriched = await enrichNodesWithIcons(nodes as FlowNode[]); + + expect(enriched[0].data.color).toBe('pink'); + }); + + it('preserves existing icons', async () => { + const nodes = [ + { + id: 'a', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'PostgreSQL', color: 'violet', icon: 'my-icon' }, + }, + ]; + + const enriched = await enrichNodesWithIcons(nodes as FlowNode[]); + + expect(enriched[0].data.icon).toBe('my-icon'); + }); + + it('handles empty node array', async () => { + const enriched = await enrichNodesWithIcons([]); + expect(enriched).toEqual([]); + }); + + it('preserves nodes with no changes', async () => { + const nodes = [ + { + id: 'a', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Something Random', color: 'blue', icon: 'Box' }, + }, + ]; + + const enriched = await enrichNodesWithIcons(nodes as FlowNode[]); + expect(enriched[0]).toEqual(nodes[0]); + }); + + it('classifies decision shape correctly', async () => { + const nodes = [ + makeNode('check', 'Validate?', { + data: { label: 'Validate?', color: 'slate', shape: 'diamond' }, + }), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + expect(enriched[0].data.color).toBe('amber'); + }); + + it('classifies cylinder shape as database', async () => { + const nodes = [ + makeNode('pg', 'PostgreSQL DB', { + data: { label: 'PostgreSQL DB', color: 'slate', shape: 'cylinder' }, + }), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + expect(enriched[0].data.color).toBe('violet'); + if (!enriched[0].data.archIconPackId) { + expect(enriched[0].data.icon).toBe('database'); + } + }); + + it('uses icon attribute for explicit catalog search', async () => { + const nodes = [ + makeNode('cache', 'My Cache', { + data: { label: 'My Cache', color: 'slate', icon: 'redis' }, + }), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + expect(enriched[0].data.archIconPackId).toBeTruthy(); + expect(enriched[0].data.archIconShapeId).toContain('redis'); + }); + + it('uses provider filter when set', async () => { + const nodes = [ + makeNode('db', 'Database', { + data: { label: 'Database', color: 'slate', provider: 'aws' }, + }), + ]; + + const enriched = await enrichNodesWithIcons(nodes); + if (enriched[0].data.archIconPackId) { + expect(enriched[0].data.archIconPackId).toBe('aws-official-starter-v1'); + } + }); +}); diff --git a/src/lib/nodeEnricher.ts b/src/lib/nodeEnricher.ts new file mode 100644 index 00000000..20eba472 --- /dev/null +++ b/src/lib/nodeEnricher.ts @@ -0,0 +1,125 @@ +import type { FlowNode } from '@/lib/types'; +import { classifyNode } from '@/lib/semanticClassifier'; +import { matchIcon, type IconMatch } from '@/lib/iconMatcher'; + +export function enrichNodesWithIcons(nodes: FlowNode[]): FlowNode[] { + return nodes.map(enrichSingleNode); +} + +function enrichSingleNode(node: FlowNode): FlowNode { + if (node.type === 'section' || node.type === 'group' || node.type === 'swimlane') { + return node; + } + + const label = node.data?.label ?? ''; + const nodeColor = node.data?.color; + const isDefaultColor = !nodeColor || nodeColor === 'slate' || nodeColor === 'white'; + const hasExplicitColor = !isDefaultColor; + const hasExplicitProviderIcon = Boolean(node.data?.archIconPackId); + const hasAnyIcon = Boolean(node.data?.icon) || hasExplicitProviderIcon; + + if (hasExplicitColor && hasAnyIcon) { + return node; + } + + const hint = classifyNode({ id: node.id, label, shape: node.data?.shape }); + const dataUpdates: Record = {}; + + if (!hasExplicitColor) { + applyColor(node, hint.color, dataUpdates); + } + + if (!hasExplicitProviderIcon) { + applyIcon(node, label, hint, dataUpdates); + } + + if (Object.keys(dataUpdates).length === 0) { + return node; + } + + return { + ...node, + data: { + ...node.data, + ...dataUpdates, + }, + }; +} + +function applyColor( + node: FlowNode, + classifierColor: string, + updates: Record +): void { + if (node.type === 'start') { + updates.color = 'emerald'; + } else if (node.type === 'end') { + updates.color = 'red'; + } else if (node.type === 'decision') { + updates.color = 'amber'; + } else { + updates.color = classifierColor; + } +} + +function applyIcon( + node: FlowNode, + label: string, + hint: { iconQuery: string; lucideFallback: string; category: string }, + updates: Record +): void { + const explicitIcon = node.data?.icon; + const provider = node.data?.provider; + const providerHint = typeof provider === 'string' ? provider : undefined; + + // Priority 1: Explicit icon attribute (e.g., icon: "redis") + if (explicitIcon && typeof explicitIcon === 'string' && explicitIcon !== 'none') { + const match = findBestMatch(explicitIcon, providerHint); + if (match) { + updates.archIconPackId = match.packId; + updates.archIconShapeId = match.shapeId; + updates.assetPresentation = 'icon'; + } + return; + } + + // Priority 2: Classifier icon query (e.g., label contains "PostgreSQL") + if (hint.iconQuery) { + const match = findBestMatch(hint.iconQuery, providerHint); + if (match) { + updates.archIconPackId = match.packId; + updates.archIconShapeId = match.shapeId; + updates.assetPresentation = 'icon'; + updates.icon = hint.lucideFallback; + return; + } + } + + // Priority 3: Label-based fallback (icons: auto โ€” match by node label) + // Only when node has NO icon at all + if (label && !node.data?.icon) { + const match = findBestMatch(label, providerHint); + if (match) { + updates.archIconPackId = match.packId; + updates.archIconShapeId = match.shapeId; + updates.assetPresentation = 'icon'; + } + } + + // Lucide icon fallback + if (hint.lucideFallback && hint.lucideFallback !== 'box') { + updates.icon = hint.lucideFallback; + } else if (node.type === 'start') { + updates.icon = 'play'; + } else if (node.type === 'end') { + updates.icon = 'check-circle'; + } else if (node.type === 'decision') { + updates.icon = 'help-circle'; + } +} + +function findBestMatch(query: string, providerHint?: string): IconMatch | undefined { + const matches = matchIcon(query, providerHint); + const best = matches[0]; + return best && best.score >= 0.8 ? best : undefined; +} diff --git a/src/lib/semanticClassifier.test.ts b/src/lib/semanticClassifier.test.ts new file mode 100644 index 00000000..b77d9bbb --- /dev/null +++ b/src/lib/semanticClassifier.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest'; +import { classifyNode } from './semanticClassifier'; + +describe('classifyNode', () => { + it('classifies start nodes', () => { + expect(classifyNode({ id: 'start', label: 'Start' }).category).toBe('start'); + expect(classifyNode({ id: 'begin', label: 'Begin' }).category).toBe('start'); + expect(classifyNode({ id: 'entry', label: 'Entry Point' }).category).toBe('start'); + expect(classifyNode({ id: 'x', label: 'Order Start' }).category).toBe('start'); + }); + + it('classifies end nodes', () => { + expect(classifyNode({ id: 'end', label: 'End' }).category).toBe('end'); + expect(classifyNode({ id: 'done', label: 'Done' }).category).toBe('end'); + expect(classifyNode({ id: 'finish', label: 'Complete' }).category).toBe('end'); + }); + + it('classifies decision nodes by shape', () => { + const hint = classifyNode({ id: 'check', label: 'Is Valid?', shape: 'diamond' }); + expect(hint.category).toBe('decision'); + expect(hint.color).toBe('amber'); + }); + + it('classifies database nodes', () => { + const pg = classifyNode({ id: 'db', label: 'PostgreSQL' }); + expect(pg.category).toBe('database'); + expect(pg.color).toBe('violet'); + expect(pg.iconQuery).toMatch(/postgres/i); + + const mongo = classifyNode({ id: 'db', label: 'MongoDB' }); + expect(mongo.category).toBe('database'); + expect(mongo.iconQuery).toMatch(/mongo/i); + }); + + it('classifies cylinder shape as database', () => { + const hint = classifyNode({ id: 'db', label: 'Users DB', shape: 'cylinder' }); + expect(hint.category).toBe('database'); + expect(hint.color).toBe('violet'); + }); + + it('classifies cache nodes', () => { + const hint = classifyNode({ id: 'cache', label: 'Redis Cache' }); + expect(hint.category).toBe('cache'); + expect(hint.iconQuery).toMatch(/redis/i); + }); + + it('classifies queue nodes', () => { + const hint = classifyNode({ id: 'mq', label: 'RabbitMQ' }); + expect(hint.category).toBe('queue'); + expect(hint.iconQuery).toMatch(/rabbitmq/i); + }); + + it('classifies user nodes', () => { + const hint = classifyNode({ id: 'user', label: 'User' }); + expect(hint.category).toBe('user'); + expect(hint.color).toBe('blue'); + }); + + it('classifies gateway nodes', () => { + const hint = classifyNode({ id: 'gw', label: 'API Gateway' }); + expect(hint.category).toBe('gateway'); + + const nginx = classifyNode({ id: 'proxy', label: 'Nginx' }); + expect(nginx.category).toBe('gateway'); + }); + + it('classifies frontend nodes', () => { + const hint = classifyNode({ id: 'fe', label: 'React App' }); + expect(hint.category).toBe('frontend'); + expect(hint.iconQuery).toMatch(/react/i); + }); + + it('classifies service nodes', () => { + const hint = classifyNode({ id: 'api', label: 'Express API' }); + expect(hint.category).toBe('service'); + expect(hint.iconQuery).toMatch(/express/i); + + const node = classifyNode({ id: 'be', label: 'Node.js Backend' }); + expect(node.category).toBe('service'); + }); + + it('classifies auth nodes', () => { + const hint = classifyNode({ id: 'auth', label: 'OAuth Login' }); + expect(hint.category).toBe('auth'); + }); + + it('returns process as default', () => { + const hint = classifyNode({ id: 'x', label: 'Something Random' }); + expect(hint.category).toBe('process'); + expect(hint.color).toBe('slate'); + }); +}); diff --git a/src/lib/semanticClassifier.ts b/src/lib/semanticClassifier.ts new file mode 100644 index 00000000..4d3f04eb --- /dev/null +++ b/src/lib/semanticClassifier.ts @@ -0,0 +1,274 @@ +import type { NodeColorKey } from '@/theme'; + +export type SemanticCategory = + | 'start' + | 'end' + | 'decision' + | 'database' + | 'cache' + | 'queue' + | 'service' + | 'frontend' + | 'user' + | 'action' + | 'gateway' + | 'auth' + | 'storage' + | 'process'; + +export interface SemanticHint { + category: SemanticCategory; + color: NodeColorKey; + iconQuery: string; + lucideFallback: string; +} + +interface ClassifierRule { + patterns: RegExp[]; + category: SemanticCategory; + color: NodeColorKey; + lucideFallback: string; + extractQuery?: (text: string, id: string) => string; +} + +const RULES: ClassifierRule[] = [ + { + patterns: [/\bstart\b/i, /\bbegin\b/i, /\binit\b/i, /\bentry\b/i, /\blaunch\b/i], + category: 'start', + color: 'emerald', + lucideFallback: 'play', + }, + { + patterns: [/\bend\b/i, /\bfinish\b/i, /\bdone\b/i, /\bcomplete\b/i, /\bstop\b/i, /\bexit\b/i], + category: 'end', + color: 'red', + lucideFallback: 'check-circle', + }, + { + patterns: [ + /\bdb\b/i, + /\bdatabase\b/i, + /\bsql\b/i, + /\bpostgres/i, + /\bmysql\b/i, + /\bmongo/i, + /\bdynamodb\b/i, + /\baurora\b/i, + /\bsqlite\b/i, + /\bmariadb\b/i, + /\bcockroach\b/i, + /\bsupabase\b/i, + ], + category: 'database', + color: 'violet', + lucideFallback: 'database', + extractQuery: (text) => { + const m = text.match( + /(postgres(?:ql)?|mysql|mongo(?:db)?|dynamodb|aurora|sqlite|mariadb|cockroach|supabase)/i + ); + return m ? m[1] : text.split(/\s+/)[0]; + }, + }, + { + patterns: [/\bredis\b/i, /\bmemcache/i, /\bcache\b/i, /\belasticache\b/i], + category: 'cache', + color: 'red', + lucideFallback: 'hard-drive', + extractQuery: (text) => { + const m = text.match(/(redis|memcache(?:d)?|elasticache)/i); + return m ? m[1] : 'cache'; + }, + }, + { + patterns: [ + /\bkafka\b/i, + /\brabbitmq\b/i, + /\bsqs\b/i, + /\bpulsar\b/i, + /\bnats\b/i, + /\bqueue\b/i, + /\bbus\b/i, + ], + category: 'queue', + color: 'amber', + lucideFallback: 'layers', + extractQuery: (text) => { + const m = text.match(/(kafka|rabbitmq|sqs|pulsar|nats)/i); + return m ? m[1] : 'queue'; + }, + }, + { + patterns: [ + /\buser\b/i, + /\bactor\b/i, + /\bcustomer\b/i, + /\badmin\b/i, + /\bclient\b/i, + /\bperson\b/i, + /\bviewer\b/i, + ], + category: 'user', + color: 'blue', + lucideFallback: 'user', + }, + { + patterns: [ + /\bapi[- ]?gateway\b/i, + /\bgateway\b/i, + /\bload[- ]?balancer\b/i, + /\bnginx\b/i, + /\bhaproxy\b/i, + /\balb\b/i, + /\bcloudfront\b/i, + /\bingress\b/i, + /\benvoy\b/i, + ], + category: 'gateway', + color: 'slate', + lucideFallback: 'shield', + extractQuery: (text) => { + const m = text.match(/(api[- ]?gateway|nginx|haproxy|alb|cloudfront|ingress|envoy)/i); + return m ? m[1] : 'gateway'; + }, + }, + { + patterns: [ + /\bauth\b/i, + /\blogin\b/i, + /\bsign[- ]?in\b/i, + /\boauth\b/i, + /\bjwt\b/i, + /\bsso\b/i, + /\bcognito\b/i, + /\bidentity\b/i, + ], + category: 'auth', + color: 'amber', + lucideFallback: 'key-round', + }, + { + patterns: [/\bs3\b/i, /\bblob\b/i, /\bstorage\b/i, /\buploads?\b/i, /\bcdn\b/i], + category: 'storage', + color: 'yellow', + lucideFallback: 'folder', + }, + { + patterns: [ + /\breact\b/i, + /\bvue\b/i, + /\bangular\b/i, + /\bsvelte\b/i, + /\bnext\.?js\b/i, + /\bnuxt\b/i, + /\bfrontend\b/i, + /\bui\b/i, + /\bweb\s*app\b/i, + /\bclient[- ]?app\b/i, + /\bhtml\b/i, + /\bcss\b/i, + ], + category: 'frontend', + color: 'blue', + lucideFallback: 'monitor', + extractQuery: (text) => { + const m = text.match(/(react|vue|angular|svelte|next(?:\.?js)?|nuxt)/i); + return m ? m[1] : 'frontend'; + }, + }, + { + patterns: [ + /\bexpress\b/i, + /\bnode\.?js\b/i, + /\bdjango\b/i, + /\bflask\b/i, + /\bfastapi\b/i, + /\bspring\b/i, + /\brails\b/i, + /\blaravel\b/i, + /\bgin\b/i, + /\bactix\b/i, + /\bnest\.?js\b/i, + /\bapi\b/i, + /\bservice\b/i, + /\bbackend\b/i, + /\bserver\b/i, + /\bmicroservice\b/i, + ], + category: 'service', + color: 'blue', + lucideFallback: 'server', + extractQuery: (text) => { + const m = text.match( + /(express|node\.?js|django|flask|fastapi|spring|rails|laravel|gin|actix|nest\.?js)/i + ); + return m ? m[1] : text.split(/\s+/)[0]; + }, + }, + { + patterns: [ + /\bdocker\b/i, + /\bkubernetes\b/i, + /\bk8s\b/i, + /\becs\b/i, + /\beks\b/i, + /\bcloud\s*run\b/i, + /\bcontainer\b/i, + ], + category: 'service', + color: 'blue', + lucideFallback: 'container', + extractQuery: (text) => { + const m = text.match(/(docker|kubernetes|k8s|ecs|eks|cloud\s*run)/i); + return m ? m[1] : 'container'; + }, + }, +]; + +const DEFAULT_HINT: SemanticHint = { + category: 'process', + color: 'slate', + iconQuery: '', + lucideFallback: 'box', +}; + +export function classifyNode(node: { id: string; label: string; shape?: string }): SemanticHint { + if (node.shape === 'diamond') { + return { category: 'decision', color: 'amber', iconQuery: '', lucideFallback: 'help-circle' }; + } + + if (node.shape === 'cylinder') { + const text = `${node.id} ${node.label}`; + const m = text.match(/(postgres(?:ql)?|mysql|mongo(?:db)?|redis|dynamodb|aurora)/i); + return { + category: 'database', + color: 'violet', + iconQuery: m ? m[1] : node.label, + lucideFallback: 'database', + }; + } + + const text = `${node.id} ${node.label}`; + + for (const rule of RULES) { + if (rule.patterns.some((p) => p.test(text))) { + return { + category: rule.category, + color: rule.color, + iconQuery: rule.extractQuery ? rule.extractQuery(text, node.id) : node.label, + lucideFallback: rule.lucideFallback, + }; + } + } + + return DEFAULT_HINT; +} + +export function classifyNodes( + nodes: Array<{ id: string; label: string; shape?: string }> +): Map { + const results = new Map(); + for (const node of nodes) { + results.set(node.id, classifyNode(node)); + } + return results; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index ff67ae7e..8626c8ab 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -17,7 +17,6 @@ export const DIAGRAM_TYPES = [ 'stateDiagram', 'classDiagram', 'erDiagram', - 'gitGraph', 'mindmap', 'journey', 'architecture', @@ -165,18 +164,19 @@ export interface SectionNodeData { sectionCollapsed?: boolean; } -export interface NodeData extends - NodeLabelData, - NodeIconData, - NodeVisualStyleData, - NodeCanvasMetadata, - ClassNodeData, - EntityNodeData, - JourneyNodeData, - MindmapNodeData, - ArchitectureNodeData, - SequenceNodeData, - SectionNodeData { +export interface NodeData + extends + NodeLabelData, + NodeIconData, + NodeVisualStyleData, + NodeCanvasMetadata, + ClassNodeData, + EntityNodeData, + JourneyNodeData, + MindmapNodeData, + ArchitectureNodeData, + SequenceNodeData, + SectionNodeData { [key: string]: unknown; } diff --git a/src/services/export/formatting.ts b/src/services/export/formatting.ts index 667d504b..58dc360d 100644 --- a/src/services/export/formatting.ts +++ b/src/services/export/formatting.ts @@ -1,5 +1,9 @@ export function sanitizeLabel(label: string): string { - return label.replace(/['"()]/g, '').trim() || 'Node'; + return label.replace(/"/g, "'").trim() || 'Node'; +} + +export function sanitizeEdgeLabel(label: string): string { + return label.replace(/"/g, "'").replace(/[{}]/g, '').trim(); } export function sanitizeId(id: string): string { diff --git a/src/services/export/mermaid/architectureMermaid.ts b/src/services/export/mermaid/architectureMermaid.ts index 1d9906ad..90a0d656 100644 --- a/src/services/export/mermaid/architectureMermaid.ts +++ b/src/services/export/mermaid/architectureMermaid.ts @@ -1,6 +1,6 @@ import type { FlowEdge, FlowNode } from '@/lib/types'; import { handleIdToSide as handleIdToFlowSide } from '@/lib/nodeHandles'; -import { sanitizeId, sanitizeLabel } from '../formatting'; +import { sanitizeId, sanitizeLabel, sanitizeEdgeLabel } from '../formatting'; function normalizeArchitectureDirection(direction: string | undefined): '-->' | '<--' | '<-->' { if (direction === '<--' || direction === '<-->') return direction; @@ -66,7 +66,7 @@ export function toArchitectureMermaid(nodes: FlowNode[], edges: FlowEdge[]): str | undefined; const protocol = edgeData?.archProtocol; const port = edgeData?.archPort; - const label = edge.label ? sanitizeLabel(String(edge.label)) : undefined; + const label = edge.label ? sanitizeEdgeLabel(String(edge.label)) : undefined; const sourceSide = normalizeArchitectureSide(edgeData?.archSourceSide) || handleIdToSide(edge.sourceHandle); const targetSide = diff --git a/src/services/export/mermaid/stateDiagramMermaid.ts b/src/services/export/mermaid/stateDiagramMermaid.ts index 3cc9e7e6..74bd23c6 100644 --- a/src/services/export/mermaid/stateDiagramMermaid.ts +++ b/src/services/export/mermaid/stateDiagramMermaid.ts @@ -14,19 +14,15 @@ function escapeStateLabel(label: string): string { } function isStateDiagramNodeType(type: string | undefined): boolean { - return type === 'state' || type === 'start' || type === 'process'; + return type === 'state' || type === 'start' || type === 'process' || type === 'section'; } export function looksLikeStateDiagram(nodes: FlowNode[]): boolean { if (nodes.length === 0) return false; const hasStateStartNode = nodes.some((node) => node.id.startsWith('state_start_')); const hasExplicitStateNode = nodes.some((node) => node.type === 'state'); - const hasCompositeParenting = nodes.some((node) => { - const parentId = getNodeParentId(node); - return parentId.length > 0; - }); - if (!hasStateStartNode && !hasExplicitStateNode && !hasCompositeParenting) { + if (!hasStateStartNode && !hasExplicitStateNode) { return false; } @@ -120,7 +116,8 @@ export function toStateDiagramMermaid(nodes: FlowNode[], edges: FlowEdge[]): str const sourceParentId = sourceNode ? getNodeParentId(sourceNode) : ''; const targetParentId = targetNode ? getNodeParentId(targetNode) : ''; const shouldEmitInsideParent = - (sourceParentId === node.id && (targetParentId === node.id || edge.target.startsWith('state_start_'))) || + (sourceParentId === node.id && + (targetParentId === node.id || edge.target.startsWith('state_start_'))) || (targetParentId === node.id && edge.source.startsWith('state_start_')); if (!shouldEmitInsideParent) { @@ -146,7 +143,10 @@ export function toStateDiagramMermaid(nodes: FlowNode[], edges: FlowEdge[]): str const targetNode = nodeById.get(edge.target); if (!sourceNode || !targetNode) return; - if (getNodeParentId(sourceNode) && getNodeParentId(sourceNode) === getNodeParentId(targetNode)) { + if ( + getNodeParentId(sourceNode) && + getNodeParentId(sourceNode) === getNodeParentId(targetNode) + ) { return; } diff --git a/src/services/export/mermaidBuilder.ts b/src/services/export/mermaidBuilder.ts index 300a5519..987aed5a 100644 --- a/src/services/export/mermaidBuilder.ts +++ b/src/services/export/mermaidBuilder.ts @@ -1,5 +1,5 @@ import type { FlowEdge, FlowNode } from '@/lib/types'; -import { sanitizeId, sanitizeLabel } from './formatting'; +import { sanitizeId, sanitizeLabel, sanitizeEdgeLabel } from './formatting'; import { toArchitectureMermaid } from './mermaid/architectureMermaid'; import { toMindmapMermaid } from './mermaid/mindmapMermaid'; import { toJourneyMermaid } from './mermaid/journeyMermaid'; @@ -48,53 +48,119 @@ function resolveFlowchartConnector(edge: FlowEdge): string { return body; } -function toFlowchartMermaid(nodes: FlowNode[], edges: FlowEdge[]): string { - let mermaid = 'flowchart TD\n'; - - nodes.forEach((node) => { - const label = sanitizeLabel(node.data.label); - const id = sanitizeId(node.id); - let shapeStart = '['; - let shapeEnd = ']'; - - const shape = node.data.shape || 'rounded'; - const type = node.type; - - if (shape === 'diamond') { - shapeStart = '{'; - shapeEnd = '}'; - } else if (shape === 'hexagon') { - shapeStart = '{{'; - shapeEnd = '}}'; - } else if (shape === 'cylinder') { - shapeStart = '[('; - shapeEnd = ')]'; - } else if (shape === 'ellipse') { - shapeStart = '(['; - shapeEnd = '])'; - } else if (shape === 'circle') { - shapeStart = '(('; - shapeEnd = '))'; - } else if (shape === 'parallelogram') { - shapeStart = '>'; - shapeEnd = ']'; - } else if (type === 'decision') { - shapeStart = '{'; - shapeEnd = '}'; - } else if (type === 'start' || type === 'end') { - shapeStart = '(['; - shapeEnd = '])'; +function resolveShapeBrackets( + shape: string | undefined, + type: string | undefined +): { start: string; end: string } { + switch (shape) { + case 'diamond': + return { start: '{', end: '}' }; + case 'hexagon': + return { start: '{{', end: '}}' }; + case 'cylinder': + return { start: '[(', end: ')]' }; + case 'circle': + return { start: '((', end: '))' }; + case 'ellipse': + return { start: '([', end: '])' }; + case 'capsule': + return { start: '([', end: '])' }; + case 'parallelogram': + return { start: '>', end: ']' }; + case 'rounded': + return { start: '(', end: ')' }; + default: + break; + } + + if (type === 'decision') return { start: '{', end: '}' }; + if (type === 'start' || type === 'end') return { start: '([', end: '])' }; + + return { start: '[', end: ']' }; +} + +function collectSectionTree(nodes: FlowNode[]): { + roots: FlowNode[]; + childrenByParent: Map; +} { + const childrenByParent = new Map(); + const roots: FlowNode[] = []; + + for (const node of nodes) { + const parentId = node.parentId; + if (parentId) { + const children = childrenByParent.get(parentId) ?? []; + children.push(node); + childrenByParent.set(parentId, children); + } else if (node.type !== 'section' && node.type !== 'group') { + roots.push(node); } + } - mermaid += ` ${id}${shapeStart}"${label}"${shapeEnd}\n`; - }); + return { roots, childrenByParent }; +} + +function emitFlowchartNode(node: FlowNode, indent: string): string { + const label = sanitizeLabel(node.data.label); + const id = sanitizeId(node.id); + const { start, end } = resolveShapeBrackets(node.data.shape, node.type); + return `${indent}${id}${start}"${label}"${end}\n`; +} + +function emitSectionBlock( + section: FlowNode, + children: FlowNode[], + childrenByParent: Map, + indent: string +): string { + const label = sanitizeLabel(section.data.label); + let out = `${indent}subgraph ${label}\n`; + + for (const child of children) { + if (child.type === 'section' || child.type === 'group') { + const grandChildren = childrenByParent.get(child.id) ?? []; + out += emitSectionBlock(child, grandChildren, childrenByParent, indent + ' '); + } else { + out += emitFlowchartNode(child, indent + ' '); + } + } + + out += `${indent}end\n`; + return out; +} + +function toFlowchartMermaid(nodes: FlowNode[], edges: FlowEdge[], direction?: string): string { + const dir = direction ?? 'TD'; + let mermaid = `flowchart ${dir}\n`; + + const sectionNodes = nodes.filter((n) => n.type === 'section' || n.type === 'group'); + const hasSubgraphs = sectionNodes.length > 0; + + if (hasSubgraphs) { + const { roots, childrenByParent } = collectSectionTree(nodes); + + for (const section of sectionNodes) { + if (!section.parentId) { + const children = childrenByParent.get(section.id) ?? []; + mermaid += emitSectionBlock(section, children, childrenByParent, ' '); + } + } + + for (const node of roots) { + mermaid += emitFlowchartNode(node, ' '); + } + } else { + for (const node of nodes) { + mermaid += emitFlowchartNode(node, ' '); + } + } edges.forEach((edge) => { const source = sanitizeId(edge.source); const target = sanitizeId(edge.target); const connector = resolveFlowchartConnector(edge); if (edge.label) { - const label = sanitizeLabel(edge.label as string); + const label = sanitizeEdgeLabel(edge.label as string); mermaid += ` ${source} ${connector}|"${label}"| ${target}\n`; } else { mermaid += ` ${source} ${connector} ${target}\n`; @@ -104,7 +170,7 @@ function toFlowchartMermaid(nodes: FlowNode[], edges: FlowEdge[]): string { return mermaid; } -export function toMermaid(nodes: FlowNode[], edges: FlowEdge[]): string { +export function toMermaid(nodes: FlowNode[], edges: FlowEdge[], direction?: string): string { const architectureNodeCount = nodes.filter((node) => node.type === 'architecture').length; if (nodes.length > 0 && architectureNodeCount === nodes.length) { return toArchitectureMermaid(nodes, edges); @@ -141,5 +207,5 @@ export function toMermaid(nodes: FlowNode[], edges: FlowEdge[]): string { return toStateDiagramMermaid(nodes, edges); } - return toFlowchartMermaid(nodes, edges); + return toFlowchartMermaid(nodes, edges, direction); } diff --git a/src/services/export/mermaidExportQuality.test.ts b/src/services/export/mermaidExportQuality.test.ts new file mode 100644 index 00000000..a00a477e --- /dev/null +++ b/src/services/export/mermaidExportQuality.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from 'vitest'; +import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType'; +import { enrichNodesWithIcons } from '@/lib/nodeEnricher'; +import { toMermaid } from '@/services/export/mermaidBuilder'; +import type { FlowNode, FlowEdge } from '@/lib/types'; + +describe('Mermaid Export Quality', () => { + it('exports rounded shape as (label) not [label]', async () => { + const input = `flowchart TD + A("Rounded Node") + + A --> B["Rectangle Node"]`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges); + + expect(exported).toContain('("Rounded Node")'); + }); + + it('exports start/end as stadium ([label])', async () => { + const input = `flowchart TD + S(["Start"]) + E(("End")) + + S --> E`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges); + + expect(exported).toContain('(["Start"])'); + expect(exported).toContain('(("End"))'); + }); + + it('exports subgraph blocks', async () => { + const input = `flowchart TD + subgraph Frontend + UI["React App"] + end + subgraph Backend + API["Express API"] + DB[("PostgreSQL")] + end + UI --> API + API --> DB`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges); + + expect(exported).toContain('subgraph Frontend'); + expect(exported).toContain('subgraph Backend'); + expect(exported).toContain('React App'); + expect(exported).toContain('Express API'); + expect(exported).toContain('end'); + }); + + it('preserves direction when passed', async () => { + const input = `flowchart LR + A["Left"] --> B["Right"]`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges, 'LR'); + + expect(exported).toContain('flowchart LR'); + }); + + it('defaults to TD when no direction specified', async () => { + const input = `flowchart TD + A["A"] --> B["B"]`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges); + + expect(exported).toContain('flowchart TD'); + }); + + it('exports all shape types correctly', async () => { + const nodes = [ + { + id: 'a', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Rounded', shape: 'rounded', color: 'slate' }, + }, + { + id: 'b', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Rect', shape: undefined, color: 'slate' }, + }, + { + id: 'c', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Diamond', shape: 'diamond', color: 'slate' }, + }, + { + id: 'd', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Cylinder', shape: 'cylinder', color: 'slate' }, + }, + { + id: 'e', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Circle', shape: 'circle', color: 'slate' }, + }, + { + id: 'f', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Capsule', shape: 'capsule', color: 'slate' }, + }, + { + id: 'g', + type: 'process', + position: { x: 0, y: 0 }, + data: { label: 'Hexagon', shape: 'hexagon', color: 'slate' }, + }, + ]; + + const exported = toMermaid(nodes as unknown as FlowNode[], [] as unknown as FlowEdge[]); + + expect(exported).toContain('("Rounded")'); + expect(exported).toContain('["Rect"]'); + expect(exported).toContain('{"Diamond"}'); + expect(exported).toContain('[("Cylinder")]'); + expect(exported).toContain('(("Circle"))'); + expect(exported).toContain('(["Capsule"])'); + expect(exported).toContain('{{"Hexagon"}}'); + }); + + it('roundtrips basic flowchart with shapes', async () => { + const input = `flowchart TD + S(["Start"]) + P["Process"] + D{"Decision"} + E(("End")) + + S --> P + P --> D + D -->|"Yes"| E`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges); + + expect(exported).toContain('flowchart'); + expect(exported).toContain('Start'); + expect(exported).toContain('Process'); + expect(exported).toContain('Decision'); + expect(exported).toContain('End'); + expect(exported).toContain('Yes'); + }); + + it('preserves parens and apostrophes in labels', async () => { + const input = `flowchart TD + A["Parse (tokens)"] --> B["O'Brien"] + B --> C["Say \\"hello\\""]`; + + const parsed = parseMermaidByType(input); + const enriched = await enrichNodesWithIcons(parsed.nodes); + const exported = toMermaid(enriched, parsed.edges); + + expect(exported).toContain('Parse (tokens)'); + expect(exported).toContain("O'Brien"); + }); + + it('handles empty diagram', () => { + const exported = toMermaid([], []); + expect(exported).toContain('flowchart'); + }); +}); diff --git a/src/services/export/plantumlBuilder.ts b/src/services/export/plantumlBuilder.ts index 9e636ec4..cb5b33b0 100644 --- a/src/services/export/plantumlBuilder.ts +++ b/src/services/export/plantumlBuilder.ts @@ -1,5 +1,5 @@ import type { FlowEdge, FlowNode } from '@/lib/types'; -import { sanitizeId, sanitizeLabel } from './formatting'; +import { sanitizeId, sanitizeLabel, sanitizeEdgeLabel } from './formatting'; export function toPlantUML(nodes: FlowNode[], edges: FlowEdge[]): string { let plantuml = '@startuml\n\n'; @@ -28,7 +28,7 @@ export function toPlantUML(nodes: FlowNode[], edges: FlowEdge[]): string { edges.forEach((edge) => { const source = sanitizeId(edge.source); const target = sanitizeId(edge.target); - const label = edge.label ? ` : ${sanitizeLabel(edge.label as string)}` : ''; + const label = edge.label ? ` : ${sanitizeEdgeLabel(edge.label as string)}` : ''; plantuml += `${source} --> ${target}${label}\n`; }); diff --git a/src/services/exportService.ts b/src/services/exportService.ts index cd261588..c80a023b 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -2,8 +2,8 @@ import type { FlowEdge, FlowNode } from '@/lib/types'; import { toMermaid as toMermaidBuilder } from './export/mermaidBuilder'; import { toPlantUML as toPlantUMLBuilder } from './export/plantumlBuilder'; -export function toMermaid(nodes: FlowNode[], edges: FlowEdge[]): string { - return toMermaidBuilder(nodes, edges); +export function toMermaid(nodes: FlowNode[], edges: FlowEdge[], direction?: string): string { + return toMermaidBuilder(nodes, edges, direction); } export function toPlantUML(nodes: FlowNode[], edges: FlowEdge[]): string { diff --git a/src/services/flowchartRoundTrip.test.ts b/src/services/flowchartRoundTrip.test.ts index d0b6fe80..92f00673 100644 --- a/src/services/flowchartRoundTrip.test.ts +++ b/src/services/flowchartRoundTrip.test.ts @@ -22,8 +22,8 @@ describe('flowchart round-trip', () => { expect(first.edges[2].markerStart).toBeDefined(); expect(first.edges[2].markerEnd).toBeUndefined(); - const exported = toMermaid(first.nodes, first.edges); - expect(exported.startsWith('flowchart TD')).toBe(true); + const exported = toMermaid(first.nodes, first.edges, first.direction); + expect(exported.startsWith('flowchart TB')).toBe(true); expect(exported).toContain('A -.->|"warmup"| B'); expect(exported).toContain('B ==> C'); expect(exported).toContain('C <-- D'); @@ -62,4 +62,20 @@ describe('flowchart round-trip', () => { expect(second.edges[0].markerStart).toBeDefined(); expect(second.edges[0].markerEnd).toBeDefined(); }); + + it('preserves direction through parse/export/parse', () => { + const source = ` + flowchart LR + A["Left"] --> B["Right"] + `; + + const first = parseMermaidByType(source); + expect(first.direction).toBe('LR'); + + const exported = toMermaid(first.nodes, first.edges, first.direction); + expect(exported).toContain('flowchart LR'); + + const second = parseMermaidByType(exported); + expect(second.direction).toBe('LR'); + }); }); diff --git a/src/services/flowpilot/assetGrounding.ts b/src/services/flowpilot/assetGrounding.ts index 98877e7f..5f1d0f5e 100644 --- a/src/services/flowpilot/assetGrounding.ts +++ b/src/services/flowpilot/assetGrounding.ts @@ -12,25 +12,155 @@ const ALL_GROUNDING_CATEGORIES: DomainLibraryCategory[] = [ ]; const SERVICE_ALIASES: Array<{ query: string; categories?: DomainLibraryCategory[] }> = [ + // AWS Services { query: 'API Gateway', categories: ['aws'] }, { query: 'Lambda', categories: ['aws'] }, { query: 'S3', categories: ['aws'] }, { query: 'RDS', categories: ['aws'] }, { query: 'ElastiCache', categories: ['aws'] }, { query: 'Cognito', categories: ['aws'] }, + { query: 'DynamoDB', categories: ['aws'] }, + { query: 'Aurora', categories: ['aws'] }, + { query: 'EC2', categories: ['aws'] }, + { query: 'ECS', categories: ['aws'] }, + { query: 'EKS', categories: ['aws'] }, + { query: 'SQS', categories: ['aws'] }, + { query: 'SNS', categories: ['aws'] }, + { query: 'CloudFront', categories: ['aws'] }, + { query: 'ALB', categories: ['aws'] }, + { query: 'EventBridge', categories: ['aws'] }, + { query: 'Step Functions', categories: ['aws'] }, + { query: 'CloudWatch', categories: ['aws'] }, + { query: 'Secrets Manager', categories: ['aws'] }, + { query: 'Kinesis', categories: ['aws'] }, + { query: 'Redshift', categories: ['aws'] }, + { query: 'Glue', categories: ['aws'] }, + { query: 'SageMaker', categories: ['aws'] }, + + // Azure Services { query: 'Azure Functions', categories: ['azure'] }, { query: 'Azure SQL', categories: ['azure'] }, { query: 'Storage Account', categories: ['azure'] }, { query: 'API Management', categories: ['azure'] }, + { query: 'Service Bus', categories: ['azure'] }, + { query: 'Event Hubs', categories: ['azure'] }, + { query: 'Cosmos DB', categories: ['azure'] }, + { query: 'Front Door', categories: ['azure'] }, + { query: 'Key Vault', categories: ['azure'] }, + { query: 'Azure Monitor', categories: ['azure'] }, + { query: 'Azure Kubernetes', categories: ['azure'] }, + { query: 'App Service', categories: ['azure'] }, + { query: 'Azure Cache', categories: ['azure'] }, + + // GCP Services { query: 'Cloud Run', categories: ['gcp'] }, { query: 'Cloud SQL', categories: ['gcp'] }, { query: 'Cloud Storage', categories: ['gcp'] }, + { query: 'Cloud Functions', categories: ['gcp'] }, + { query: 'BigQuery', categories: ['gcp'] }, + { query: 'Pub/Sub', categories: ['gcp'] }, + { query: 'Cloud CDN', categories: ['gcp'] }, + { query: 'Firestore', categories: ['gcp'] }, + { query: 'Cloud Build', categories: ['gcp'] }, + { query: 'Vertex AI', categories: ['gcp'] }, + { query: 'Memorystore', categories: ['gcp'] }, + { query: 'GKE', categories: ['gcp'] }, + { query: 'Cloud Armor', categories: ['gcp'] }, + + // CNCF / Kubernetes { query: 'Kubernetes', categories: ['cncf'] }, { query: 'Ingress', categories: ['cncf'] }, - { query: 'Redis' }, - { query: 'Postgres' }, + { query: 'Envoy', categories: ['cncf'] }, + { query: 'Istio', categories: ['cncf'] }, + { query: 'Helm', categories: ['cncf'] }, + { query: 'Prometheus', categories: ['cncf'] }, + { query: 'Containerd', categories: ['cncf'] }, + { query: 'Fluentd', categories: ['cncf'] }, + { query: 'CoreDNS', categories: ['cncf'] }, + { query: 'etcd', categories: ['cncf'] }, + { query: 'Argo', categories: ['cncf'] }, + { query: 'Linkerd', categories: ['cncf'] }, + + // Databases (developer catalog) + { query: 'PostgreSQL', categories: ['developer'] }, + { query: 'Postgres', categories: ['developer'] }, + { query: 'MySQL', categories: ['developer'] }, + { query: 'MongoDB', categories: ['developer'] }, + { query: 'Redis', categories: ['developer'] }, + { query: 'Elasticsearch', categories: ['developer'] }, + { query: 'SQLite', categories: ['developer'] }, + { query: 'MariaDB', categories: ['developer'] }, + { query: 'Cassandra', categories: ['developer'] }, + { query: 'Neo4j', categories: ['developer'] }, + { query: 'Supabase', categories: ['developer'] }, + { query: 'PlanetScale', categories: ['developer'] }, + + // Frameworks & Runtimes + { query: 'Express', categories: ['developer'] }, + { query: 'Node.js', categories: ['developer'] }, + { query: 'React', categories: ['developer'] }, + { query: 'Vue', categories: ['developer'] }, + { query: 'Angular', categories: ['developer'] }, + { query: 'Svelte', categories: ['developer'] }, + { query: 'Next.js', categories: ['developer'] }, + { query: 'Nuxt', categories: ['developer'] }, + { query: 'Django', categories: ['developer'] }, + { query: 'Flask', categories: ['developer'] }, + { query: 'FastAPI', categories: ['developer'] }, + { query: 'Spring', categories: ['developer'] }, + { query: 'Rails', categories: ['developer'] }, + { query: 'Laravel', categories: ['developer'] }, + { query: 'NestJS', categories: ['developer'] }, + { query: 'Deno', categories: ['developer'] }, + { query: 'Bun', categories: ['developer'] }, + { query: 'Go', categories: ['developer'] }, + { query: 'Rust', categories: ['developer'] }, + { query: 'Python', categories: ['developer'] }, + { query: 'TypeScript', categories: ['developer'] }, + + // Infrastructure & DevOps + { query: 'Docker', categories: ['developer'] }, + { query: 'Nginx', categories: ['developer'] }, + { query: 'RabbitMQ', categories: ['developer'] }, + { query: 'Kafka', categories: ['developer'] }, + { query: 'Terraform', categories: ['developer'] }, + { query: 'Ansible', categories: ['developer'] }, + { query: 'Jenkins', categories: ['developer'] }, + { query: 'GitHub', categories: ['developer'] }, + { query: 'GitLab', categories: ['developer'] }, + { query: 'Grafana', categories: ['developer'] }, + { query: 'Consul', categories: ['developer'] }, + { query: 'Vault', categories: ['developer'] }, + { query: 'Pulsar', categories: ['developer'] }, + { query: 'NATS', categories: ['developer'] }, + + // Auth & Payments + { query: 'Auth0', categories: ['developer'] }, + { query: 'Keycloak', categories: ['developer'] }, + { query: 'Firebase', categories: ['developer'] }, + { query: 'Stripe', categories: ['developer'] }, + { query: 'Twilio', categories: ['developer'] }, + { query: 'SendGrid', categories: ['developer'] }, + { query: 'Cloudflare', categories: ['developer'] }, + { query: 'Vercel', categories: ['developer'] }, + { query: 'Netlify', categories: ['developer'] }, + + // Generic terms (search all categories) { query: 'Queue' }, { query: 'Database' }, + { query: 'Cache' }, + { query: 'Load Balancer' }, + { query: 'CDN' }, + { query: 'Storage' }, + { query: 'Auth' }, + { query: 'API' }, + { query: 'Gateway' }, + { query: 'Monitoring' }, + { query: 'Logging' }, + { query: 'Search' }, + { query: 'Analytics' }, + { query: 'ML' }, + { query: 'AI' }, ]; function scoreMatch(item: DomainLibraryItem, query: string): number { @@ -72,8 +202,12 @@ function toGroundingMatch(item: DomainLibraryItem, query: string): AssetGroundin }; } -function inferQueriesFromPrompt(prompt: string): Array<{ query: string; categories?: DomainLibraryCategory[] }> { - const matches = SERVICE_ALIASES.filter((entry) => prompt.toLowerCase().includes(entry.query.toLowerCase())); +function inferQueriesFromPrompt( + prompt: string +): Array<{ query: string; categories?: DomainLibraryCategory[] }> { + const matches = SERVICE_ALIASES.filter((entry) => + prompt.toLowerCase().includes(entry.query.toLowerCase()) + ); if (matches.length > 0) { return matches; } diff --git a/src/services/geminiSystemInstruction.ts b/src/services/geminiSystemInstruction.ts index 39df5e56..4157265b 100644 --- a/src/services/geminiSystemInstruction.ts +++ b/src/services/geminiSystemInstruction.ts @@ -1,10 +1,12 @@ +import { buildCatalogSummary } from '@/lib/iconMatcher'; + const EDIT_MODE_PREAMBLE = ` ## EDIT MODE โ€” MODIFYING AN EXISTING DIAGRAM A CURRENT DIAGRAM block will be provided in OpenFlow DSL. You MUST: 1. Output the COMPLETE updated diagram in OpenFlow DSL โ€” not just the changed parts -2. Preserve every node that should remain โ€” copy its id, type, label, icon, color, and all attributes EXACTLY as they appear in CURRENT DIAGRAM -3. Use the EXACT same node id for every unchanged node (e.g. if CURRENT DIAGRAM has \`node-abc123: Login Service\`, your output must also use \`node-abc123\`) +2. Preserve every node that should remain โ€” copy its id, type, label, and all attributes EXACTLY as they appear in CURRENT DIAGRAM +3. Use the EXACT same node id for every unchanged node 4. Only change what the user explicitly requested 5. New nodes should have short descriptive IDs (e.g. \`redis_cache\`, \`auth_v2\`) 6. Do NOT re-layout or restructure nodes not affected by the change @@ -17,193 +19,93 @@ A CURRENT DIAGRAM block will be provided in OpenFlow DSL. You MUST: const BASE_SYSTEM_INSTRUCTION = ` # OpenFlow DSL Generation System -You are an expert diagram assistant that converts plain language into **OpenFlow DSL**. - -Your job: -- Read any description of a process, system, or flow โ€” casual or technical. -- Use conversation history for context and refinements. -- If an image is provided, convert the diagram/sketch into OpenFlow DSL. -- Infer obvious missing steps. -- Always output **only valid OpenFlow DSL** โ€” no prose, no explanations, no markdown wrappers. +You convert plain language into **OpenFlow DSL** diagrams. Output ONLY valid OpenFlow DSL โ€” no prose, no markdown wrappers. --- -## Structure Rules - -1. Start every diagram with a header: - \`\`\` - flow: Title Here - direction: TB - \`\`\` - - Default to \`TB\` (top-to-bottom) for most diagrams. - - Use \`LR\` (left-to-right) for pipelines, timelines, stages, workflows, or CI/CD. - -2. Define all **Nodes first**, then all **Edges**. Never mix them. - - INVALID: \`[start] A -> [end] B\` - - VALID: define nodes, then \`A -> B\` +## Structure -3. Node ID rules: - - Short labels โ†’ use label as ID: \`[process] Login { icon: "LogIn" }\` - - Long labels โ†’ use ID prefix: \`[process] login_step: User enters credentials { icon: "LogIn" }\` +1. Header: \`flow: Title\` + \`direction: TB\` (default) or \`LR\` (pipelines, CI/CD). +2. Define ALL nodes first, then ALL edges. +3. Node IDs: simple labels can be the ID. Long labels need a prefix: \`[process] login_step: User enters credentials\` --- ## Node Types -| Type | When to use | +| Type | Use for | |---|---| | \`[start]\` | Entry point | -| \`[end]\` | Terminal state (success or failure) | -| \`[process]\` | Any action, step, or task | +| \`[end]\` | Terminal state | +| \`[process]\` | Action, step, task | | \`[decision]\` | Branch / conditional | -| \`[system]\` | Application-level backend service, internal API, business logic component | -| \`[architecture]\` | Cloud or infrastructure resource such as AWS, Azure, GCP, Kubernetes, network, or security components | -| \`[browser]\` | Web page / frontend screen | +| \`[system]\` | Backend service, internal API, business logic | +| \`[architecture]\` | Cloud/infra resource (AWS, Azure, GCP, K8s) | +| \`[browser]\` | Web page / frontend | | \`[mobile]\` | Mobile screen | | \`[note]\` | Callout / annotation | --- -## Edge Styles โ€” use these semantically +## Edges -| Syntax | Style | When to use | -|---|---|---| -| \`->\` | Normal arrow | Default connection | -| \`->|label|\` | Labeled arrow | Decision branches โ€” ALWAYS label Yes/No, Pass/Fail etc. | -| \`==>\` | **Thick** | Primary happy path / critical route | -| \`-->\` | Curved | Soft / secondary flow | -| \`..>\` | Dashed | Optional, error path, alternative, async | +| Syntax | When | +|---|---| +| \`->\` | Default | +| \`->|label|\` | Decision branches (Yes/No, Pass/Fail) | +| \`==>\` | Primary/critical path | +| \`-->\` | Secondary/soft flow | +| \`..\` | Async, error, optional | --- -## Node Attributes โ€” ALWAYS add \`icon\` and \`color\` to every non-start/end node +## Attributes + +Syntax: \`[type] id: Label { icon: "IconName", color: "color", subLabel: "subtitle" }\` -Syntax: \`[type] id: Label { icon: "IconName", color: "color", subLabel: "optional subtitle" }\` +For \`[architecture]\` nodes: \`[architecture] id: Label { archProvider: "aws", archResourceType: "lambda", color: "violet" }\` -For \`[architecture]\` nodes use: -\`[architecture] id: Label { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" }\` +Colors: \`blue\` (frontend), \`violet\` (backend), \`emerald\` (data), \`amber\` (decisions/queues), \`red\` (errors/end), \`slate\` (generic), \`pink\` (third-party), \`yellow\` (cache). -- Required attributes for \`[architecture]\`: \`archProvider\`, \`archResourceType\` -- Optional attributes for \`[architecture]\`: \`archIconPackId\`, \`archIconShapeId\`, \`color\`, \`subLabel\` -- Prefer \`[architecture]\` over \`[system]\` for cloud services, infrastructure, managed data stores, queues, gateways, network, and security resources -- Prefer \`[system]\` for application services, internal APIs, controllers, workers, and business logic that belong to the product itself +Icons are optional โ€” the system auto-assigns them. For known technologies, use \`archProvider\` and \`archResourceType\` to specify the icon directly: -6. **subLabel** โ€” add a short subtitle for context on complex nodes: - \`\`\` - [process] auth: Authenticate { icon: "Lock", color: "blue", subLabel: "OAuth 2.0 + JWT" } - [system] api: Payment API { icon: "CreditCard", color: "violet", subLabel: "Stripe v3" } - \`\`\` +\`[system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" }\` -7. **Annotations** โ€” use \`[note]\` to add callouts for constraints, caveats, or SLAs. Connect with a dashed edge \`..>\`: - \`\`\` - [note] sla: 99.9% Uptime required { color: "slate" } - api ..> sla - \`\`\` +Available icon catalog: +${buildCatalogSummary(15)} -8. **No container nodes** โ€” do not use \`[section]\` nodes or \`group {}\` blocks. Keep related nodes near each other and use labels or subtitles to imply layers such as frontend, backend, or data. +Use exact shape IDs from the catalog when possible (e.g. \`database-postgresql\`, \`queue-rabbitmq\`). If unsure, omit \`archResourceType\` and the system will match by label. --- -9. **Curated icon list** โ€” pick the MOST semantically appropriate icon from this list: - - Actions: \`Play\`, \`Pause\`, \`Stop\`, \`Check\`, \`X\`, \`Plus\`, \`Trash2\`, \`Edit3\`, \`Send\`, \`Upload\`, \`Download\`, \`Search\`, \`Filter\`, \`RefreshCw\`, \`LogIn\`, \`LogOut\` - - Data & Dev: \`Database\`, \`Server\`, \`Code2\`, \`Terminal\`, \`GitBranch\`, \`Zap\`, \`Settings\`, \`Key\`, \`Lock\`, \`Unlock\`, \`ShieldCheck\`, \`AlertTriangle\` - - People: \`User\`, \`Users\`, \`UserCheck\`, \`UserPlus\`, \`Bell\`, \`Mail\`, \`Phone\`, \`MessageSquare\`, \`Contact\` - - Commerce: \`ShoppingCart\`, \`CreditCard\`, \`Package\`, \`Store\`, \`Tag\`, \`Receipt\`, \`Truck\` - - Content: \`File\`, \`FileText\`, \`Folder\`, \`Image\`, \`Link\`, \`Globe\`, \`Rss\` - - Infrastructure: \`Cloud\`, \`Wifi\`, \`Smartphone\`, \`Monitor\`, \`HardDrive\`, \`Cpu\` - -10. **Cloud provider icons** โ€” when rendering infrastructure, use \`[architecture]\` nodes and these provider values: - - AWS: \`archProvider: "aws"\`, prefer \`archIconPackId: "aws-official-starter-v1"\` - Common services: EC2, S3, RDS, Lambda, DynamoDB, API Gateway, CloudFront, SQS, SNS, ECS, EKS, ElastiCache, Cognito, IAM - - Azure: \`archProvider: "azure"\`, prefer \`archIconPackId: "azure-official-icons-v20"\` - Common services: VM, Functions, Storage Account, Azure SQL, API Management, Front Door - - GCP: \`archProvider: "gcp"\` - Common services: Compute Engine, Cloud Functions, Cloud Storage, Cloud SQL, Load Balancer, Cloud Run - - Kubernetes / CNCF: \`archProvider: "cncf"\` - Common resources: Cluster, Node, Pod, Service, Ingress, ConfigMap - - Network: \`archProvider: "network"\` - Common resource types: \`load_balancer\`, \`router\`, \`switch\`, \`cdn\`, \`dns\`, \`service\` - - Security: \`archProvider: "security"\` - Common resource types: \`firewall\`, \`service\`, \`dns\` - -11. **Color semantics** โ€” use colors deliberately, not randomly: - - \`blue\` โ†’ frontend, user-facing, presentation layer - - \`violet\` โ†’ backend services, APIs, internal systems - - \`emerald\` โ†’ data stores, persistence, successful outcomes - - \`amber\` โ†’ queues, async workers, warning states, decisions - - \`red\` โ†’ security boundaries, firewalls, error, end, fail, danger, cancel - - \`slate\` โ†’ generic fallback, unknown services, neutral groups - - \`pink\` โ†’ third-party or external services - - \`yellow\` โ†’ cache, fast path, in-memory systems - -12. **Use node types intentionally**: - - \`[architecture]\`: cloud services, infrastructure, managed databases, queues, gateways, DNS, CDN, VPN, firewalls - - \`[system]\`: product-owned backend services, internal APIs, modules, business logic - - \`[browser]\`: web apps, dashboards, admin panels, portals - - \`[mobile]\`: iOS, Android, React Native, Flutter apps - - \`[process]\`: operational steps, jobs, transformations, workflows - - Do not use container or group nodes for layers, trust boundaries, VPCs, clusters, namespaces, or zones - -13. Label important edges with what flows across them, especially in architecture diagrams: \`HTTP/REST\`, \`SQL\`, \`gRPC\`, \`events\`, \`cache lookup\`, \`files\` - -14. Use comments \`#\` only when they add clarity. - -15. Do NOT explain the output. Do NOT add prose. Only output DSL. - -15b. **Diagram density** โ€” aim for the right density: - - Flowcharts: 6โ€“15 nodes is ideal. More than 20 = simplify the diagram. - - Architecture diagrams: 8โ€“20 nodes, with layers implied by labels, subtitles, and placement instead of containers. - - Sequence/journey: 4โ€“10 steps in the happy path. - - If a request is simple, keep the diagram simple. Do not pad with unnecessary detail. - -15c. **Layout quality rules**: - - Happy path flows TOP โ†’ BOTTOM (TB) or LEFT โ†’ RIGHT (LR) in a straight line, with alternatives branching off the sides. - - Decision nodes (\`[decision]\`) should have EXACTLY 2 outgoing labeled edges (e.g. \`->|Yes|\` and \`->|No|\`). - - Avoid more than 3 incoming edges on any single node โ€” use a \`[process]\` aggregator if needed. - - Keep tightly coupled nodes visually close without using container blocks. - - Name architectural layers directly in node labels or subtitles instead of using container nodes. - - Use \`==>\` (thick) for the critical path, \`->\` for normal flow, \`..>\` for async/optional, \`-->\` for soft/secondary. - -15d. **Self-describing diagrams** โ€” every diagram should be readable without a legend: - - Include \`subLabel\` on complex nodes to explain protocols, versions, or constraints. - - Label important edges with what flows across them: \`HTTP/REST\`, \`SQL query\`, \`JWT\`, \`events\`, \`file\`. - - Use \`[note]\` nodes for critical constraints, SLAs, or caveats โ€” connect with \`..>\`. - -16. **Node IDs**: - - If the label is simple (e.g., "Login"), you can use it as the ID: \`[process] Login { icon: "LogIn" }\`. - - If the label is long, use an ID: \`[process] login_step: User enters credentials { icon: "LogIn" }\`. - -17. **Iterative editing โ€” preserve existing IDs**: - - When a CURRENT CONTENT block is provided, it includes each node's exact \`id\` (e.g. \`"id": "node-abc123"\`). - - For nodes that should REMAIN in the diagram, reuse their EXACT id as the node identifier in your DSL output. - - Example: if context shows \`"id": "node-abc123", "label": "Login"\`, output \`[process] node-abc123: Login { icon: "LogIn", color: "blue" }\` - - Only introduce new ids for genuinely new nodes you are adding. - - Omit nodes that should be removed โ€” do not output them at all. - - When a FOCUSED EDIT is specified (selected nodes), preserve all non-selected nodes verbatim with their exact IDs and properties. +## Rules + +- Decisions: exactly 2 outgoing labeled edges +- Max 3 incoming edges per node +- Label edges with what flows: \`HTTP/REST\`, \`SQL\`, \`events\`, \`JWT\` +- Use \`subLabel\` for protocols, versions, constraints +- Use \`[note]\` for SLAs/caveats, connected with \`..\` +- 6โ€“15 nodes for flowcharts, 8โ€“20 for architecture +- Do NOT use container/group nodes +- When editing, preserve existing node IDs exactly --- ## Examples -### User Authentication +### Authentication Flow \`\`\` flow: User Authentication direction: TB [start] Start -[process] login: Login Form { icon: "LogIn", color: "blue", subLabel: "Email + password" } -[decision] valid: Credentials valid? { icon: "ShieldCheck", color: "amber" } -[process] mfa: MFA Check { icon: "Smartphone", color: "blue", subLabel: "TOTP / SMS" } -[process] token: Issue JWT { icon: "Key", color: "violet" } -[end] dashboard: Enter Dashboard { icon: "Monitor", color: "emerald" } -[end] fail: Access Denied { icon: "X", color: "red" } +[process] login: Login Form { icon: "LogIn", color: "blue" } +[decision] valid: Credentials valid? { color: "amber" } +[process] mfa: MFA Check { icon: "Smartphone", color: "blue" } +[system] token: Issue JWT { icon: "Key", color: "violet" } +[end] dashboard: Enter Dashboard { color: "emerald" } +[end] fail: Access Denied { color: "red" } Start ==> login login -> valid @@ -213,80 +115,40 @@ mfa ==> token token ==> dashboard \`\`\` -### E-Commerce Checkout +### AWS Serverless Architecture \`\`\` -flow: Checkout Flow +flow: Serverless API direction: TB -[start] Start -[process] cart: Review Cart { icon: "ShoppingCart", color: "blue" } -[process] address: Shipping Address { icon: "Truck", color: "blue" } -[process] payment: Payment Details { icon: "CreditCard", color: "blue", subLabel: "Stripe v3" } -[decision] fraud: Fraud check { icon: "ShieldCheck", color: "amber" } -[system] fulfil: Fulfilment Service { icon: "Package", color: "violet" } -[process] notify: Send Confirmation { icon: "Mail", color: "emerald", subLabel: "Email + SMS" } -[end] done: Order Complete { icon: "Check", color: "emerald" } -[end] declined: Payment Declined { icon: "AlertTriangle", color: "red" } - -Start ==> cart -cart ==> address -address ==> payment -payment -> fraud -fraud ->|Pass| fulfil -fraud ->|Fail| declined -fulfil ==> notify -notify ==> done -\`\`\` - -### CI/CD Pipeline +[architecture] cf: CloudFront { archProvider: "aws", archResourceType: "networking-cloudfront", color: "blue" } +[architecture] apigw: API Gateway { archProvider: "aws", archResourceType: "app-integration-api-gateway", color: "violet" } +[architecture] lambda: API Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" } +[architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database-dynamodb", color: "emerald" } +[architecture] cache: ElastiCache { archProvider: "aws", archResourceType: "database-elasticache", color: "yellow" } -\`\`\` -flow: CI/CD Pipeline -direction: LR - -[start] Push -[process] build: Build { icon: "Code2", color: "blue", subLabel: "npm run build" } -[process] test: Run Tests { icon: "Check", color: "blue", subLabel: "Jest + Playwright" } -[decision] pass: All tests pass? { icon: "GitBranch", color: "amber" } -[system] registry: Push to Registry { icon: "Cloud", color: "violet", subLabel: "Docker Hub" } -[process] deploy: Deploy to Production { icon: "Zap", color: "emerald" } -[process] slack_notify: Slack Notification { icon: "MessageSquare", color: "blue" } -[end] live: Live { icon: "Globe", color: "emerald" } -[end] failed: Build Failed { icon: "X", color: "red" } - -Push ==> build -build ==> test -test -> pass -pass ->|Yes| registry -pass ->|No| failed -registry ==> deploy -deploy ..> slack_notify -slack_notify ==> live +cf ->|HTTPS| apigw +apigw ->|HTTP/REST| lambda +lambda ->|query| dynamo +lambda ->|cache lookup| cache \`\`\` -### Architecture Diagram +### Full-Stack with Developer Icons \`\`\` -flow: Serverless API - AWS +flow: E-Commerce Stack direction: TB -[architecture] cf: CloudFront { archProvider: "aws", archResourceType: "cdn", archIconPackId: "aws-official-starter-v1", color: "blue" } -[architecture] apigw: API Gateway { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet", subLabel: "Edge Layer" } -[architecture] auth_fn: Auth Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" } -[architecture] api_fn: API Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet", subLabel: "Compute Layer" } -[architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database", archIconPackId: "aws-official-starter-v1", color: "emerald" } -[architecture] cache: ElastiCache { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "yellow" } -[architecture] s3: S3 Storage { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "emerald", subLabel: "Data Layer" } -[architecture] cognito: Cognito { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "amber" } +[system] react: React App { archProvider: "developer", archResourceType: "frontend-react", color: "blue" } +[system] api: Express API { archProvider: "developer", archResourceType: "others-expressjs-dark", color: "violet" } +[system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" } +[system] cache: Redis { archProvider: "developer", archResourceType: "database-redis", color: "red" } +[system] mq: RabbitMQ { archProvider: "developer", archResourceType: "queue-rabbitmq", color: "amber" } -cf ->|HTTPS| apigw -apigw ->|auth request| auth_fn -apigw ->|HTTP/REST| api_fn -auth_fn ->|identity| cognito -api_fn ->|query| dynamo -api_fn ->|cache lookup| cache -api_fn ->|store files| s3 +react ->|HTTP/REST| api +api ->|SQL| db +api ->|cache lookup| cache +api ->|publish| mq \`\`\` `; diff --git a/src/services/mermaid/detectDiagramType.test.ts b/src/services/mermaid/detectDiagramType.test.ts index 5afed97a..1e7333df 100644 --- a/src/services/mermaid/detectDiagramType.test.ts +++ b/src/services/mermaid/detectDiagramType.test.ts @@ -14,10 +14,10 @@ describe('detectMermaidDiagramType', () => { it('detects target q2 families', () => { expect(detectMermaidDiagramType('classDiagram\nA <|-- B')).toBe('classDiagram'); expect(detectMermaidDiagramType('erDiagram\nA ||--o{ B : has')).toBe('erDiagram'); - expect(detectMermaidDiagramType('gitGraph\ncommit')).toBe('gitGraph'); expect(detectMermaidDiagramType('mindmap\nroot')).toBe('mindmap'); expect(detectMermaidDiagramType('journey\ntitle Onboarding')).toBe('journey'); expect(detectMermaidDiagramType('architecture-beta\nservice api')).toBe('architecture'); + expect(detectMermaidDiagramType('sequenceDiagram\nparticipant A')).toBe('sequence'); }); it('skips empty and comment lines', () => { @@ -35,4 +35,3 @@ A --> B expect(detectMermaidDiagramType('')).toBeNull(); }); }); - diff --git a/src/services/mermaid/detectDiagramType.ts b/src/services/mermaid/detectDiagramType.ts index b7d5a7b2..0be69d7f 100644 --- a/src/services/mermaid/detectDiagramType.ts +++ b/src/services/mermaid/detectDiagramType.ts @@ -15,14 +15,13 @@ export function detectMermaidDiagramType(input: string): DiagramType | null { if (/^stateDiagram(?:-v2)?\b/i.test(line)) return 'stateDiagram'; if (/^classDiagram\b/i.test(line)) return 'classDiagram'; if (/^erDiagram\b/i.test(line)) return 'erDiagram'; - if (/^gitGraph\b/i.test(line)) return 'gitGraph'; if (/^mindmap\b/i.test(line)) return 'mindmap'; if (/^journey\b/i.test(line)) return 'journey'; if (/^architecture(?:-beta)?\b/i.test(line)) return 'architecture'; + if (/^sequenceDiagram\b/i.test(line)) return 'sequence'; return null; } return null; } - diff --git a/src/services/mermaid/parseMermaidByType.test.ts b/src/services/mermaid/parseMermaidByType.test.ts index 01ce8d4b..23b34b38 100644 --- a/src/services/mermaid/parseMermaidByType.test.ts +++ b/src/services/mermaid/parseMermaidByType.test.ts @@ -67,8 +67,14 @@ describe('parseMermaidByType', () => { expect(result.diagramType).toBe('classDiagram'); expect(result.error).toBeUndefined(); expect(result.nodes.length).toBeGreaterThan(0); - expect(result.diagnostics?.some((message) => message.includes('Invalid class declaration at line'))).toBe(true); - expect(result.diagnostics?.some((message) => message.includes('Invalid class relation syntax at line'))).toBe(true); + expect( + result.diagnostics?.some((message) => message.includes('Invalid class declaration at line')) + ).toBe(true); + expect( + result.diagnostics?.some((message) => + message.includes('Invalid class relation syntax at line') + ) + ).toBe(true); }); it('parses erDiagram through plugin dispatcher', () => { @@ -103,8 +109,14 @@ describe('parseMermaidByType', () => { expect(result.diagramType).toBe('erDiagram'); expect(result.error).toBeUndefined(); expect(result.nodes.length).toBeGreaterThan(0); - expect(result.diagnostics?.some((message) => message.includes('Invalid entity declaration at line'))).toBe(true); - expect(result.diagnostics?.some((message) => message.includes('Invalid erDiagram relation syntax at line'))).toBe(true); + expect( + result.diagnostics?.some((message) => message.includes('Invalid entity declaration at line')) + ).toBe(true); + expect( + result.diagnostics?.some((message) => + message.includes('Invalid erDiagram relation syntax at line') + ) + ).toBe(true); }); it('parses mindmap through plugin dispatcher', () => { @@ -148,8 +160,14 @@ describe('parseMermaidByType', () => { expect(result.diagramType).toBe('journey'); expect(result.error).toBeUndefined(); expect(result.nodes.length).toBeGreaterThan(0); - expect(result.diagnostics?.some((message) => message.includes('Invalid journey section syntax at line'))).toBe(true); - expect(result.diagnostics?.some((message) => message.includes('Invalid journey step syntax at line'))).toBe(true); + expect( + result.diagnostics?.some((message) => + message.includes('Invalid journey section syntax at line') + ) + ).toBe(true); + expect( + result.diagnostics?.some((message) => message.includes('Invalid journey step syntax at line')) + ).toBe(true); }); it('returns mindmap diagnostics for malformed indentation/wrapper lines', () => { @@ -164,8 +182,29 @@ describe('parseMermaidByType', () => { expect(result.diagramType).toBe('mindmap'); expect(result.error).toBeUndefined(); expect(result.nodes.length).toBeGreaterThan(0); - expect(result.diagnostics?.some((message) => message.includes('Mindmap indentation jump at line'))).toBe(true); - expect(result.diagnostics?.some((message) => message.includes('Malformed mindmap wrapper syntax at line'))).toBe(true); + expect( + result.diagnostics?.some((message) => message.includes('Mindmap indentation jump at line')) + ).toBe(true); + expect( + result.diagnostics?.some((message) => + message.includes('Malformed mindmap wrapper syntax at line') + ) + ).toBe(true); + }); + + it('parses sequenceDiagram through plugin dispatcher', () => { + const result = parseMermaidByType(` + sequenceDiagram + participant Alice + participant Bob + Alice->>Bob: Hello + Bob-->>Alice: Hi + `); + + expect(result.diagramType).toBe('sequence'); + expect(result.error).toBeUndefined(); + expect(result.nodes.length).toBeGreaterThan(0); + expect(result.edges.length).toBeGreaterThan(0); }); it('parses architecture through plugin dispatcher', () => { @@ -187,28 +226,32 @@ describe('parseMermaidByType', () => { }); it('rejects architecture recovery diagnostics in strict mode', () => { - const result = parseMermaidByType(` + const result = parseMermaidByType( + ` architecture-beta service api(server)[API] api --> cache - `, { architectureStrictMode: true }); + `, + { architectureStrictMode: true } + ); expect(result.diagramType).toBe('architecture'); expect(result.error).toContain('strict mode rejected'); expect(result.nodes).toHaveLength(0); expect(result.edges).toHaveLength(0); - expect(result.diagnostics?.some((d) => d.includes('Recovered implicit service node "cache"'))).toBe(true); + expect( + result.diagnostics?.some((d) => d.includes('Recovered implicit service node "cache"')) + ).toBe(true); }); - it('returns explicit unsupported error for non-supported families', () => { + it('returns missing-header error for unsupported diagram types like gitGraph', () => { const result = parseMermaidByType(` gitGraph commit id: "A" commit id: "B" `); - expect(result.diagramType).toBe('gitGraph'); - expect(result.error).toContain('not supported yet in editable mode'); + expect(result.error).toContain('Missing chart type declaration'); expect(result.nodes).toHaveLength(0); expect(result.edges).toHaveLength(0); }); diff --git a/src/services/mermaid/parseMermaidByType.ts b/src/services/mermaid/parseMermaidByType.ts index 3ece8892..787af22a 100644 --- a/src/services/mermaid/parseMermaidByType.ts +++ b/src/services/mermaid/parseMermaidByType.ts @@ -13,19 +13,31 @@ export interface ParseMermaidByTypeOptions { architectureStrictMode?: boolean; } -const SUPPORTED_MERMAID_FAMILIES: DiagramType[] = ['flowchart', 'stateDiagram', 'classDiagram', 'erDiagram', 'mindmap', 'journey', 'architecture']; +const SUPPORTED_MERMAID_FAMILIES: DiagramType[] = [ + 'flowchart', + 'stateDiagram', + 'classDiagram', + 'erDiagram', + 'mindmap', + 'journey', + 'architecture', + 'sequence', +]; function getUnsupportedTypeError(diagramType: DiagramType): string { - return `Mermaid "${diagramType}" is not supported yet in editable mode. Supported families: flowchart, stateDiagram, classDiagram, erDiagram, mindmap, journey, architecture.`; + return `Mermaid "${diagramType}" is not supported yet in editable mode. Supported families: flowchart, stateDiagram, classDiagram, erDiagram, mindmap, journey, architecture, sequence.`; } -function applyArchitectureStrictMode(result: MermaidDispatchParseResult): MermaidDispatchParseResult { +function applyArchitectureStrictMode( + result: MermaidDispatchParseResult +): MermaidDispatchParseResult { const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : []; - const strictViolations = diagnostics.filter((message) => ( - message.startsWith('Invalid architecture ') - || message.startsWith('Duplicate architecture node id') - || message.startsWith('Recovered implicit service node') - )); + const strictViolations = diagnostics.filter( + (message) => + message.startsWith('Invalid architecture ') || + message.startsWith('Duplicate architecture node id') || + message.startsWith('Recovered implicit service node') + ); if (strictViolations.length === 0) { return result; @@ -70,7 +82,8 @@ export function parseMermaidByType( return { nodes: [], edges: [], - error: 'Missing chart type declaration. Start with "flowchart TD", "stateDiagram-v2", or another Mermaid diagram type header.', + error: + 'Missing chart type declaration. Start with "flowchart TD", "stateDiagram-v2", or another Mermaid diagram type header.', }; } diff --git a/src/services/mermaidParser.test.ts b/src/services/mermaidParser.test.ts index 1ed0ebbb..28010048 100644 --- a/src/services/mermaidParser.test.ts +++ b/src/services/mermaidParser.test.ts @@ -2,195 +2,223 @@ import { describe, it, expect } from 'vitest'; import { parseMermaid } from '@/lib/mermaidParser'; describe('mermaidParser', () => { - it('should parse a basic flowchart with TD direction', () => { - const input = ` + it('should parse a basic flowchart with TD direction', () => { + const input = ` flowchart TD A[Start] --> B[End] `; - const result = parseMermaid(input); - expect(result.nodes).toHaveLength(2); - expect(result.edges).toHaveLength(1); - expect(result.nodes[0].data.label).toBe('Start'); - expect(result.nodes[1].data.label).toBe('End'); - expect(result.edges[0].source).toBe('A'); - expect(result.edges[0].target).toBe('B'); - }); - - it('should handle different node types based on shapes', () => { - const input = ` + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(2); + expect(result.edges).toHaveLength(1); + expect(result.nodes[0].data.label).toBe('Start'); + expect(result.nodes[1].data.label).toBe('End'); + expect(result.edges[0].source).toBe('A'); + expect(result.edges[0].target).toBe('B'); + }); + + it('should handle different node types based on shapes', () => { + const input = ` flowchart TD S([Start Node]) P[Process Node] D{Decision Node} E((End Node)) `; - const result = parseMermaid(input); - expect(result.nodes.find(n => n.id === 'S')?.type).toBe('start'); - expect(result.nodes.find(n => n.id === 'P')?.type).toBe('process'); - expect(result.nodes.find(n => n.id === 'D')?.type).toBe('decision'); - expect(result.nodes.find(n => n.id === 'E')?.type).toBe('end'); - }); - - it('should parse edges with labels', () => { - const input = ` + const result = parseMermaid(input); + expect(result.nodes.find((n) => n.id === 'S')?.type).toBe('start'); + expect(result.nodes.find((n) => n.id === 'P')?.type).toBe('process'); + expect(result.nodes.find((n) => n.id === 'D')?.type).toBe('decision'); + expect(result.nodes.find((n) => n.id === 'E')?.type).toBe('end'); + }); + + it('should parse edges with labels', () => { + const input = ` flowchart TD A --> |Yes| B A --> |No| C `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(2); - expect(result.edges[0].label).toBe('Yes'); - expect(result.edges[1].label).toBe('No'); - }); - - it('should handle LR direction', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(2); + expect(result.edges[0].label).toBe('Yes'); + expect(result.edges[1].label).toBe('No'); + }); + + it('should handle LR direction', () => { + const input = ` flowchart LR A --> B `; - const result = parseMermaid(input); - expect(result.direction).toBe('LR'); - }); - - it('should return error if no flowchart declaration is found', () => { - const input = `A --> B`; - const result = parseMermaid(input); - expect(result.error).toBeDefined(); - expect(result.nodes).toHaveLength(0); - }); - - it('should handle inline node declarations in edges', () => { - const input = ` + const result = parseMermaid(input); + expect(result.direction).toBe('LR'); + }); + + it('should return error if no flowchart declaration is found', () => { + const input = `A --> B`; + const result = parseMermaid(input); + expect(result.error).toBeDefined(); + expect(result.nodes).toHaveLength(0); + }); + + it('should handle inline node declarations in edges', () => { + const input = ` flowchart TD A[Node A] --> B((Node B)) `; - const result = parseMermaid(input); - expect(result.nodes).toHaveLength(2); - expect(result.nodes.find(n => n.id === 'A')?.data.label).toBe('Node A'); - expect(result.nodes.find(n => n.id === 'B')?.type).toBe('end'); - }); + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(2); + expect(result.nodes.find((n) => n.id === 'A')?.data.label).toBe('Node A'); + expect(result.nodes.find((n) => n.id === 'B')?.type).toBe('end'); + }); - // --- NEW TESTS --- + // --- NEW TESTS --- - it('should support "graph TD" keyword (not just flowchart)', () => { - const input = ` + it('should support "graph TD" keyword (not just flowchart)', () => { + const input = ` graph TD A[Start] --> B[End] `; - const result = parseMermaid(input); - expect(result.nodes).toHaveLength(2); - expect(result.edges).toHaveLength(1); - expect(result.direction).toBe('TB'); - }); - - it('should strip fa: icon prefixes from labels', () => { - const input = ` + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(2); + expect(result.edges).toHaveLength(1); + expect(result.direction).toBe('TB'); + }); + + it('should strip fa: icon prefixes from labels', () => { + const input = ` graph TD Bat(fa:fa-car-battery Batteries) --> ShutOff[Shut Off] `; - const result = parseMermaid(input); - expect(result.nodes.find(n => n.id === 'Bat')?.data.label).toBe('Batteries'); - expect(result.nodes.find(n => n.id === 'ShutOff')?.data.label).toBe('Shut Off'); - }); + const result = parseMermaid(input); + expect(result.nodes.find((n) => n.id === 'Bat')?.data.label).toBe('Batteries'); + expect(result.nodes.find((n) => n.id === 'ShutOff')?.data.label).toBe('Shut Off'); + }); - it('should handle chained edges: A --> B --> C', () => { - const input = ` + it('should handle modern @{shape: name} syntax', () => { + const input = ` + flowchart TD + A@{shape: cyl}[(Database)] + B@{shape: diamond}{Is Valid?} + C@{shape: stadium}[Start] + `; + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(3); + expect(result.nodes.find((n) => n.id === 'A')?.type).toBe('process'); + expect(result.nodes.find((n) => n.id === 'A')?.data.shape).toBe('cylinder'); + expect(result.nodes.find((n) => n.id === 'B')?.type).toBe('decision'); + expect(result.nodes.find((n) => n.id === 'B')?.data.shape).toBe('diamond'); + expect(result.nodes.find((n) => n.id === 'C')?.type).toBe('start'); + }); + + it('should strip markdown from labels', () => { + const input = ` + flowchart TD + A[**Bold** text] --> B[*Italic* label] + `; + const result = parseMermaid(input); + expect(result.nodes.find((n) => n.id === 'A')?.data.label).toBe('Bold text'); + expect(result.nodes.find((n) => n.id === 'B')?.data.label).toBe('Italic label'); + }); + + it('should handle chained edges: A --> B --> C', () => { + const input = ` flowchart TD A --> B --> C `; - const result = parseMermaid(input); - expect(result.nodes).toHaveLength(3); - expect(result.edges).toHaveLength(2); - expect(result.edges[0].source).toBe('A'); - expect(result.edges[0].target).toBe('B'); - expect(result.edges[1].source).toBe('B'); - expect(result.edges[1].target).toBe('C'); - }); - - it('should handle chained edges with labels', () => { - const input = ` + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(3); + expect(result.edges).toHaveLength(2); + expect(result.edges[0].source).toBe('A'); + expect(result.edges[0].target).toBe('B'); + expect(result.edges[1].source).toBe('B'); + expect(result.edges[1].target).toBe('C'); + }); + + it('should handle chained edges with labels', () => { + const input = ` flowchart TD Fuse -->|1.5a| Switch -->|1.5a| Wifi `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(2); - expect(result.edges[0].source).toBe('Fuse'); - expect(result.edges[0].target).toBe('Switch'); - expect(result.edges[0].label).toBe('1.5a'); - expect(result.edges[1].source).toBe('Switch'); - expect(result.edges[1].target).toBe('Wifi'); - expect(result.edges[1].label).toBe('1.5a'); - }); - - it('ignores subgraph wrappers instead of creating container nodes', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(2); + expect(result.edges[0].source).toBe('Fuse'); + expect(result.edges[0].target).toBe('Switch'); + expect(result.edges[0].label).toBe('1.5a'); + expect(result.edges[1].source).toBe('Switch'); + expect(result.edges[1].target).toBe('Wifi'); + expect(result.edges[1].label).toBe('1.5a'); + }); + + it('creates section nodes for subgraph wrappers and sets parentId on children', () => { + const input = ` flowchart TD subgraph Services API[API] + DB[(Database)] end `; - const result = parseMermaid(input); - const apiNode = result.nodes.find((node) => node.id === 'API'); - expect(result.nodes).toHaveLength(1); - expect(apiNode?.parentId).toBeUndefined(); - }); - - it('should handle duplicate edges between same pair', () => { - const input = ` + const result = parseMermaid(input); + expect(result.nodes.length).toBeGreaterThanOrEqual(3); + const sectionNode = result.nodes.find((node) => node.type === 'section'); + expect(sectionNode).toBeDefined(); + expect(sectionNode?.data.label).toBe('Services'); + const apiNode = result.nodes.find((node) => node.id === 'API'); + expect(apiNode?.parentId).toBe(sectionNode?.id); + const dbNode = result.nodes.find((node) => node.id === 'DB'); + expect(dbNode?.parentId).toBe(sectionNode?.id); + }); + + it('should handle duplicate edges between same pair', () => { + const input = ` flowchart TD Fuse -->|10a| Cig1[Cigarette Lighter] Fuse -->|10a| Cig1 `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(2); - expect(result.edges[0].source).toBe('Fuse'); - expect(result.edges[1].source).toBe('Fuse'); - }); + const result = parseMermaid(input); + expect(result.edges).toHaveLength(2); + expect(result.edges[0].source).toBe('Fuse'); + expect(result.edges[1].source).toBe('Fuse'); + }); - it('should return direction in ParseResult', () => { - const lr = parseMermaid('flowchart LR\n A --> B'); - expect(lr.direction).toBe('LR'); + it('should return direction in ParseResult', () => { + const lr = parseMermaid('flowchart LR\n A --> B'); + expect(lr.direction).toBe('LR'); - const rl = parseMermaid('graph RL\n A --> B'); - expect(rl.direction).toBe('RL'); + const rl = parseMermaid('graph RL\n A --> B'); + expect(rl.direction).toBe('RL'); - const bt = parseMermaid('flowchart BT\n A --> B'); - expect(bt.direction).toBe('BT'); - }); + const bt = parseMermaid('flowchart BT\n A --> B'); + expect(bt.direction).toBe('BT'); + }); - it('should skip linkStyle, classDef, style directives gracefully', () => { - const input = ` + it('should skip linkStyle, classDef, style directives gracefully', () => { + const input = ` graph TD A --> B linkStyle 0 stroke-width:2px,fill:none,stroke:red; classDef default fill:#f9f style A fill:#bbf `; - const result = parseMermaid(input); - expect(result.nodes).toHaveLength(2); - expect(result.edges).toHaveLength(1); - expect(result.error).toBeUndefined(); - }); - - it('should parse linkStyle and apply stroke color to edges', () => { - const input = ` + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(2); + expect(result.edges).toHaveLength(1); + expect(result.error).toBeUndefined(); + }); + + it('should parse linkStyle and apply stroke color to edges', () => { + const input = ` graph TD A --> B B --> C linkStyle 0 stroke-width:2px,fill:none,stroke:red; linkStyle 1 stroke-width:2px,fill:none,stroke:green; `; - const result = parseMermaid(input); - expect(result.edges[0].style).toEqual( - expect.objectContaining({ stroke: 'red' }) - ); - expect(result.edges[1].style).toEqual( - expect.objectContaining({ stroke: 'green' }) - ); - }); - - it('should handle the full battery diagram', () => { - const input = `graph TD + const result = parseMermaid(input); + expect(result.edges[0].style).toEqual(expect.objectContaining({ stroke: 'red' })); + expect(result.edges[1].style).toEqual(expect.objectContaining({ stroke: 'green' })); + }); + + it('should handle the full battery diagram', () => { + const input = `graph TD Bat(fa:fa-car-battery Batteries) -->|150a 50mm| ShutOff Bat -->|150a 50mm| Shunt @@ -236,125 +264,117 @@ describe('mermaidParser', () => { linkStyle 18 stroke-width:2px,fill:none,stroke:green; linkStyle 19 stroke-width:2px,fill:none,stroke:green;`; - const result = parseMermaid(input); - - // Should have no errors - expect(result.error).toBeUndefined(); - - // Direction should be TB - expect(result.direction).toBe('TB'); - - // Should find all unique nodes - const nodeIds = result.nodes.map(n => n.id); - expect(nodeIds).toContain('Bat'); - expect(nodeIds).toContain('ShutOff'); - expect(nodeIds).toContain('Shunt'); - expect(nodeIds).toContain('BusPos'); - expect(nodeIds).toContain('BusNeg'); - expect(nodeIds).toContain('Fuse'); - expect(nodeIds).toContain('Old'); - expect(nodeIds).toContain('USB'); - expect(nodeIds).toContain('Switch'); - expect(nodeIds).toContain('Wifi'); - expect(nodeIds).toContain('Cig1'); - expect(nodeIds).toContain('Cig2'); - expect(nodeIds).toContain('Solar'); - expect(nodeIds).toContain('SolarCont'); - - // Check labels - expect(result.nodes.find(n => n.id === 'Bat')?.data.label).toBe('Batteries'); - expect(result.nodes.find(n => n.id === 'ShutOff')?.data.label).toBe('Shut Off'); - expect(result.nodes.find(n => n.id === 'BusPos')?.data.label).toBe('Bus Bar +'); - expect(result.nodes.find(n => n.id === 'USB')?.data.label).toBe('USB-C'); - - // Check that Old is a decision node (diamond shape) - expect(result.nodes.find(n => n.id === 'Old')?.type).toBe('decision'); - - // Should have many edges (20 in the original) - expect(result.edges.length).toBeGreaterThanOrEqual(18); - - // Check edge labels - const batToShutoff = result.edges.find(e => e.source === 'Bat' && e.target === 'ShutOff'); - expect(batToShutoff?.label).toBe('150a 50mm'); - - // Check linkStyle applied colors - expect(result.edges[0].style).toEqual( - expect.objectContaining({ stroke: 'red' }) - ); - }); - - it('should handle dotted arrow -.-> ', () => { - const input = ` + const result = parseMermaid(input); + + // Should have no errors + expect(result.error).toBeUndefined(); + + // Direction should be TB + expect(result.direction).toBe('TB'); + + // Should find all unique nodes + const nodeIds = result.nodes.map((n) => n.id); + expect(nodeIds).toContain('Bat'); + expect(nodeIds).toContain('ShutOff'); + expect(nodeIds).toContain('Shunt'); + expect(nodeIds).toContain('BusPos'); + expect(nodeIds).toContain('BusNeg'); + expect(nodeIds).toContain('Fuse'); + expect(nodeIds).toContain('Old'); + expect(nodeIds).toContain('USB'); + expect(nodeIds).toContain('Switch'); + expect(nodeIds).toContain('Wifi'); + expect(nodeIds).toContain('Cig1'); + expect(nodeIds).toContain('Cig2'); + expect(nodeIds).toContain('Solar'); + expect(nodeIds).toContain('SolarCont'); + + // Check labels + expect(result.nodes.find((n) => n.id === 'Bat')?.data.label).toBe('Batteries'); + expect(result.nodes.find((n) => n.id === 'ShutOff')?.data.label).toBe('Shut Off'); + expect(result.nodes.find((n) => n.id === 'BusPos')?.data.label).toBe('Bus Bar +'); + expect(result.nodes.find((n) => n.id === 'USB')?.data.label).toBe('USB-C'); + + // Check that Old is a decision node (diamond shape) + expect(result.nodes.find((n) => n.id === 'Old')?.type).toBe('decision'); + + // Should have many edges (20 in the original) + expect(result.edges.length).toBeGreaterThanOrEqual(18); + + // Check edge labels + const batToShutoff = result.edges.find((e) => e.source === 'Bat' && e.target === 'ShutOff'); + expect(batToShutoff?.label).toBe('150a 50mm'); + + // Check linkStyle applied colors + expect(result.edges[0].style).toEqual(expect.objectContaining({ stroke: 'red' })); + }); + + it('should handle dotted arrow -.-> ', () => { + const input = ` flowchart TD A -.-> B `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(1); - expect(result.edges[0].style).toEqual( - expect.objectContaining({ strokeDasharray: '5 3' }) - ); - }); - - it('should handle thick arrow ==>', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(1); + expect(result.edges[0].style).toEqual(expect.objectContaining({ strokeDasharray: '5 3' })); + }); + + it('should handle thick arrow ==>', () => { + const input = ` flowchart TD A ==> B `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(1); - expect(result.edges[0].style).toEqual( - expect.objectContaining({ strokeWidth: 4 }) - ); - }); - - it('should handle thick arrow ==> with inline label', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(1); + expect(result.edges[0].style).toEqual(expect.objectContaining({ strokeWidth: 4 })); + }); + + it('should handle thick arrow ==> with inline label', () => { + const input = ` flowchart TD A == Yes ==> B `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(1); - expect(result.edges[0].style).toEqual( - expect.objectContaining({ strokeWidth: 4 }) - ); - expect(result.edges[0].label).toBe('Yes'); - }); - - it('should handle reverse arrow <-- with markerStart', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(1); + expect(result.edges[0].style).toEqual(expect.objectContaining({ strokeWidth: 4 })); + expect(result.edges[0].label).toBe('Yes'); + }); + + it('should handle reverse arrow <-- with markerStart', () => { + const input = ` flowchart TD A <-- B `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(1); - expect(result.edges[0].markerStart).toBeDefined(); - expect(result.edges[0].markerEnd).toBeUndefined(); - }); - - it('should handle bidirectional arrow <--> with both markers', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(1); + expect(result.edges[0].markerStart).toBeDefined(); + expect(result.edges[0].markerEnd).toBeUndefined(); + }); + + it('should handle bidirectional arrow <--> with both markers', () => { + const input = ` flowchart TD A <--> B `; - const result = parseMermaid(input); - expect(result.edges).toHaveLength(1); - expect(result.edges[0].markerStart).toBeDefined(); - expect(result.edges[0].markerEnd).toBeDefined(); - }); - - it('should handle multiline quoted strings', () => { - const input = ` + const result = parseMermaid(input); + expect(result.edges).toHaveLength(1); + expect(result.edges[0].markerStart).toBeDefined(); + expect(result.edges[0].markerEnd).toBeDefined(); + }); + + it('should handle multiline quoted strings', () => { + const input = ` graph TD A["Line 1 Line 2"] `; - const result = parseMermaid(input); - expect(result.nodes).toHaveLength(1); - expect(result.nodes[0].data.label).toBe('Line 1\nLine 2'); - }); + const result = parseMermaid(input); + expect(result.nodes).toHaveLength(1); + expect(result.nodes[0].data.label).toBe('Line 1\nLine 2'); + }); - it('should handle the Service Learning example', () => { - const input = `graph TB + it('should handle the Service Learning example', () => { + const input = `graph TB A("Do you think online service learning is right for you?") B("Do you have time to design @@ -375,26 +395,26 @@ D--No-->E E--Yes-->F E--No-->C`; - const result = parseMermaid(input); + const result = parseMermaid(input); - // Should parse 6 nodes - expect(result.nodes).toHaveLength(6); - // ID A should have multiline label - const nodeA = result.nodes.find(n => n.id === 'A'); - expect(nodeA).toBeDefined(); - expect(nodeA?.data.label).toContain('online service\nlearning'); + // Should parse 6 nodes + expect(result.nodes).toHaveLength(6); + // ID A should have multiline label + const nodeA = result.nodes.find((n) => n.id === 'A'); + expect(nodeA).toBeDefined(); + expect(nodeA?.data.label).toContain('online service\nlearning'); - // Should parse 8 edges - expect(result.edges).toHaveLength(8); + // Should parse 8 edges + expect(result.edges).toHaveLength(8); - // Check specific edges - const startYes = result.edges.find(e => e.source === 'A' && e.target === 'B'); - expect(startYes).toBeDefined(); - expect(startYes?.label).toBe('Yes'); - expect(startYes?.style?.strokeWidth).toBe(4); // ==> is thick + // Check specific edges + const startYes = result.edges.find((e) => e.source === 'A' && e.target === 'B'); + expect(startYes).toBeDefined(); + expect(startYes?.label).toBe('Yes'); + expect(startYes?.style?.strokeWidth).toBe(4); // ==> is thick - const startNo = result.edges.find(e => e.source === 'A' && e.target === 'C'); - expect(startNo).toBeDefined(); - expect(startNo?.label).toBe('No'); - }); + const startNo = result.edges.find((e) => e.source === 'A' && e.target === 'C'); + expect(startNo).toBeDefined(); + expect(startNo?.label).toBe('No'); + }); }); diff --git a/src/services/openFlowRoundTripGoldenFixtures.ts b/src/services/openFlowRoundTripGoldenFixtures.ts index b07ad8cd..ffbe9ee8 100644 --- a/src/services/openFlowRoundTripGoldenFixtures.ts +++ b/src/services/openFlowRoundTripGoldenFixtures.ts @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react'; import type { Edge, Node } from '@/lib/reactflowCompat'; export interface OpenFlowRoundTripGoldenFixture { @@ -39,6 +40,49 @@ function createEdge(id: string, source: string, target: string, label?: string): } as Edge; } +function createArchNode( + id: string, + label: string, + archIconPackId: string, + archIconShapeId: string, + color: string +): Node { + return { + id, + type: 'custom', + position: { x: 0, y: 0 }, + data: { + label, + color, + archIconPackId, + archIconShapeId, + }, + } as Node; +} + +function createEdgeWithStyle( + id: string, + source: string, + target: string, + label?: string, + style?: { type?: string; strokeDasharray?: string; strokeWidth?: number } +): Edge { + const edge: Record = { + id, + source, + target, + label, + }; + if (style?.type) edge.type = style.type; + if (style?.strokeDasharray || style?.strokeWidth) { + edge.style = { + ...(style.strokeDasharray ? { strokeDasharray: style.strokeDasharray } : {}), + ...(style.strokeWidth ? { strokeWidth: style.strokeWidth } : {}), + } as CSSProperties; + } + return edge as Edge; +} + export const OPENFLOW_ROUND_TRIP_GOLDEN_FIXTURES: OpenFlowRoundTripGoldenFixture[] = [ { name: 'simple-linear', @@ -74,4 +118,28 @@ export const OPENFLOW_ROUND_TRIP_GOLDEN_FIXTURES: OpenFlowRoundTripGoldenFixture createEdge('e1', 'n1', 'n3', 'ok'), ], }, + { + name: 'arch-icons', + nodes: [ + createArchNode('lambda', 'Lambda', 'aws-official-starter-v1', 'compute-lambda', 'violet'), + createArchNode('sqs', 'SQS Queue', 'aws-official-starter-v1', 'app-integration-sqs', 'amber'), + createArchNode('dynamo', 'DynamoDB', 'aws-official-starter-v1', 'database-dynamodb', 'emerald'), + ], + edges: [ + createEdge('e1', 'lambda', 'sqs', 'publish'), + createEdge('e2', 'sqs', 'dynamo', 'write'), + ], + }, + { + name: 'edge-styles', + nodes: [ + createNode('n1', 'Source', 'process'), + createNode('n2', 'Dashed Target', 'process'), + createNode('n3', 'Curved Target', 'process'), + ], + edges: [ + createEdgeWithStyle('e1', 'n1', 'n2', undefined, { strokeDasharray: '5 5' }), + createEdgeWithStyle('e2', 'n1', 'n3', 'flow', { type: 'smoothstep' }), + ], + }, ]; diff --git a/src/services/shapeLibrary/providerCatalog.ts b/src/services/shapeLibrary/providerCatalog.ts index 3816bc5d..cac13b95 100644 --- a/src/services/shapeLibrary/providerCatalog.ts +++ b/src/services/shapeLibrary/providerCatalog.ts @@ -1,221 +1,247 @@ import type { DomainLibraryCategory, DomainLibraryItem } from '@/services/domainLibrary'; export interface ProviderShapePreview { - packId: string; - shapeId: string; - label: string; - category: string; - previewUrl: string; + packId: string; + shapeId: string; + label: string; + category: string; + previewUrl: string; } interface SvgSource { - provider: string; - packId: string; - shapeId: string; - label: string; - category: string; - previewLoader: () => Promise; + provider: string; + packId: string; + shapeId: string; + label: string; + category: string; + previewLoader: () => Promise; } const svgModules = import.meta.glob('../../../assets/third-party-icons/*/processed/**/*.svg', { - query: '?url', - import: 'default', + query: '?url', + import: 'default', }) as Record Promise>; const providerCatalogPromiseCache = new Map>(); const shapePreviewCache = new Map(); const shapePreviewPromiseCache = new Map>(); -const KNOWN_PROVIDER_PACK_IDS: Partial> = { - aws: 'aws-official-starter-v1', - azure: 'azure-official-icons-v20', - cncf: 'cncf-artwork-icons-v1', - developer: 'developer-icons-v1', +export const KNOWN_PROVIDER_PACK_IDS: Record = { + aws: 'aws-official-starter-v1', + azure: 'azure-official-icons-v20', + gcp: 'gcp-official-icons-v1', + cncf: 'cncf-artwork-icons-v1', + developer: 'developer-icons-v1', }; function normalizeProviderPathSegment(value: string): string { - return value.trim().toLowerCase(); + return value.trim().toLowerCase(); } function slugify(value: string): string { - return value - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); } function inferLabelFromId(id: string): string { - return id - .split('-') - .filter(Boolean) - .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) - .join(' '); + return id + .split('-') + .filter(Boolean) + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(' '); } function getPackIdForProvider(provider: string): string { - return KNOWN_PROVIDER_PACK_IDS[provider] ?? `${provider}-processed-pack-v1`; + return KNOWN_PROVIDER_PACK_IDS[provider] ?? `${provider}-processed-pack-v1`; } -function parseSvgSource(modulePath: string, previewLoader: () => Promise): SvgSource | null { - const normalized = modulePath.replaceAll('\\', '/'); - const match = normalized.match(/assets\/third-party-icons\/([^/]+)\/processed\/(.+)\.svg$/); +function getProviderColor(provider: string): string { + if (provider === 'aws') { + return 'amber'; + } - if (!match) { - return null; - } + if (provider === 'azure') { + return 'blue'; + } - const provider = normalizeProviderPathSegment(match[1]); - const relativePath = match[2]; - const pathParts = relativePath.split('/'); - const category = pathParts.length > 1 ? inferLabelFromId(slugify(pathParts[0])) : 'Misc'; - const shapeId = slugify(relativePath.replaceAll('/', '-')); + if (provider === 'gcp') { + return 'emerald'; + } - return { - provider, - packId: getPackIdForProvider(provider), - shapeId, - label: inferLabelFromId(shapeId), - category, - previewLoader, - }; + if (provider === 'cncf') { + return 'cyan'; + } + + return 'slate'; } -const SVG_SOURCES: SvgSource[] = Object.entries(svgModules) - .map(([modulePath, previewLoader]) => parseSvgSource(modulePath, previewLoader)) - .filter((value): value is SvgSource => value !== null); - -function createProviderItem( - provider: DomainLibraryCategory, - source: SvgSource, -): DomainLibraryItem { - return { - id: `${source.packId}:${source.shapeId}`, - category: provider, - label: source.label, - description: `${provider.toUpperCase()} ${source.category}`, - icon: 'Box', - color: provider === 'aws' - ? 'amber' - : provider === 'azure' - ? 'blue' - : provider === 'gcp' - ? 'emerald' - : provider === 'cncf' - ? 'cyan' - : 'slate', - nodeType: 'custom', - assetPresentation: 'icon', - providerShapeCategory: source.category, - archIconPackId: source.packId, - archIconShapeId: source.shapeId, - }; +function parseSvgSource( + modulePath: string, + previewLoader: () => Promise +): SvgSource | null { + const normalized = modulePath.replaceAll('\\', '/'); + const match = normalized.match(/assets\/third-party-icons\/([^/]+)\/processed\/(.+)\.svg$/); + + if (!match) { + return null; + } + + const provider = normalizeProviderPathSegment(match[1]); + const relativePath = match[2]; + const pathParts = relativePath.split('/'); + const category = pathParts.length > 1 ? inferLabelFromId(slugify(pathParts[0])) : 'Misc'; + const shapeId = slugify(relativePath.replaceAll('/', '-')); + + return { + provider, + packId: getPackIdForProvider(provider), + shapeId, + label: inferLabelFromId(shapeId), + category, + previewLoader, + }; +} + +export const SVG_SOURCES: SvgSource[] = Object.entries(svgModules) + .map(([modulePath, previewLoader]) => parseSvgSource(modulePath, previewLoader)) + .filter((value): value is SvgSource => value !== null); + +function createProviderItem(provider: DomainLibraryCategory, source: SvgSource): DomainLibraryItem { + return { + id: `${source.packId}:${source.shapeId}`, + category: provider, + label: source.label, + description: `${provider.toUpperCase()} ${source.category}`, + icon: 'Box', + color: getProviderColor(provider), + nodeType: 'custom', + assetPresentation: 'icon', + providerShapeCategory: source.category, + archIconPackId: source.packId, + archIconShapeId: source.shapeId, + }; } export function listProviderCatalogProviders(): string[] { - return Array.from(new Set(SVG_SOURCES.map((source) => source.provider))).sort((left, right) => left.localeCompare(right)); + return Array.from(new Set(SVG_SOURCES.map((source) => source.provider))).sort((left, right) => + left.localeCompare(right) + ); } export function getProviderCatalogCount(provider: DomainLibraryCategory): number { - const normalizedProvider = normalizeProviderPathSegment(provider); - return SVG_SOURCES.filter((source) => source.provider === normalizedProvider).length; + const normalizedProvider = normalizeProviderPathSegment(provider); + return SVG_SOURCES.filter((source) => source.provider === normalizedProvider).length; } -export async function loadProviderCatalog(provider: DomainLibraryCategory): Promise { - const normalizedProvider = normalizeProviderPathSegment(provider); - const existingPromise = providerCatalogPromiseCache.get(normalizedProvider); - if (existingPromise) { - return existingPromise; - } - - const catalogPromise = (async () => { - return SVG_SOURCES - .filter((source) => source.provider === normalizedProvider) - .map((source) => createProviderItem(provider, source)) - .sort((left, right) => ( - left.providerShapeCategory === right.providerShapeCategory - ? left.label.localeCompare(right.label) - : (left.providerShapeCategory || '').localeCompare(right.providerShapeCategory || '') - )); - })(); - - providerCatalogPromiseCache.set(normalizedProvider, catalogPromise); - return catalogPromise; +export async function loadProviderCatalog( + provider: DomainLibraryCategory +): Promise { + const normalizedProvider = normalizeProviderPathSegment(provider); + const existingPromise = providerCatalogPromiseCache.get(normalizedProvider); + if (existingPromise) { + return existingPromise; + } + + const catalogPromise = (async () => { + return SVG_SOURCES.filter((source) => source.provider === normalizedProvider) + .map((source) => createProviderItem(provider, source)) + .sort((left, right) => + left.providerShapeCategory === right.providerShapeCategory + ? left.label.localeCompare(right.label) + : (left.providerShapeCategory || '').localeCompare(right.providerShapeCategory || '') + ); + })(); + + providerCatalogPromiseCache.set(normalizedProvider, catalogPromise); + return catalogPromise; } interface LoadProviderCatalogSuggestionsOptions { - category?: string; - excludeShapeId?: string; - limit?: number; - query?: string; + category?: string; + excludeShapeId?: string; + limit?: number; + query?: string; } export async function loadProviderCatalogSuggestions( - provider: DomainLibraryCategory, - options: LoadProviderCatalogSuggestionsOptions = {}, + provider: DomainLibraryCategory, + options: LoadProviderCatalogSuggestionsOptions = {} ): Promise { - const items = await loadProviderCatalog(provider); - const normalizedQuery = options.query?.trim().toLowerCase() ?? ''; - const filtered = items.filter((item) => { - if (options.excludeShapeId && item.archIconShapeId === options.excludeShapeId) { - return false; - } - if (options.category && item.providerShapeCategory !== options.category) { - return false; - } - if (!normalizedQuery) { - return true; - } - return item.label.toLowerCase().includes(normalizedQuery) - || item.description.toLowerCase().includes(normalizedQuery) - || (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery); - }); - - const pool = filtered.length > 0 || !options.category - ? filtered - : items.filter((item) => ( - (!options.excludeShapeId || item.archIconShapeId !== options.excludeShapeId) - && (!normalizedQuery - || item.label.toLowerCase().includes(normalizedQuery) - || item.description.toLowerCase().includes(normalizedQuery) - || (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery)) - )); - - return pool.slice(0, options.limit ?? 8); -} - -export async function loadProviderShapePreview(packId: string, shapeId: string): Promise { - const cacheKey = `${packId}:${shapeId}`; - const cachedPreview = shapePreviewCache.get(cacheKey); - if (cachedPreview) { - return cachedPreview; + const items = await loadProviderCatalog(provider); + const normalizedQuery = options.query?.trim().toLowerCase() ?? ''; + const filtered = items.filter((item) => { + if (options.excludeShapeId && item.archIconShapeId === options.excludeShapeId) { + return false; } - const cachedPromise = shapePreviewPromiseCache.get(cacheKey); - if (cachedPromise) { - return cachedPromise; + if (options.category && item.providerShapeCategory !== options.category) { + return false; } - - const source = SVG_SOURCES.find((candidate) => candidate.packId === packId && candidate.shapeId === shapeId); - if (!source) { - return null; + if (!normalizedQuery) { + return true; } + return ( + item.label.toLowerCase().includes(normalizedQuery) || + item.description.toLowerCase().includes(normalizedQuery) || + (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery) + ); + }); + + const pool = + filtered.length > 0 || !options.category + ? filtered + : items.filter( + (item) => + (!options.excludeShapeId || item.archIconShapeId !== options.excludeShapeId) && + (!normalizedQuery || + item.label.toLowerCase().includes(normalizedQuery) || + item.description.toLowerCase().includes(normalizedQuery) || + (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery)) + ); + + return pool.slice(0, options.limit ?? 8); +} + +export async function loadProviderShapePreview( + packId: string, + shapeId: string +): Promise { + const cacheKey = `${packId}:${shapeId}`; + const cachedPreview = shapePreviewCache.get(cacheKey); + if (cachedPreview) { + return cachedPreview; + } + const cachedPromise = shapePreviewPromiseCache.get(cacheKey); + if (cachedPromise) { + return cachedPromise; + } + + const source = SVG_SOURCES.find( + (candidate) => candidate.packId === packId && candidate.shapeId === shapeId + ); + if (!source) { + return null; + } + + const previewPromise = source + .previewLoader() + .then((previewUrl) => { + const preview = { + packId, + shapeId, + label: source.label, + category: source.category, + previewUrl, + }; + shapePreviewCache.set(cacheKey, preview); + shapePreviewPromiseCache.delete(cacheKey); + return preview; + }) + .catch((error) => { + shapePreviewPromiseCache.delete(cacheKey); + throw error; + }); - const previewPromise = source.previewLoader() - .then((previewUrl) => { - const preview = { - packId, - shapeId, - label: source.label, - category: source.category, - previewUrl, - }; - shapePreviewCache.set(cacheKey, preview); - shapePreviewPromiseCache.delete(cacheKey); - return preview; - }) - .catch((error) => { - shapePreviewPromiseCache.delete(cacheKey); - throw error; - }); - - shapePreviewPromiseCache.set(cacheKey, previewPromise); - return previewPromise; + shapePreviewPromiseCache.set(cacheKey, previewPromise); + return previewPromise; } diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index a440610e..b87453cc 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.test.tsx","./src/app.tsx","./src/constants.test.ts","./src/constants.ts","./src/index.tsx","./src/store.test.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/app/routestate.test.ts","./src/app/routestate.ts","./src/components/annotationnode.tsx","./src/components/architecturerulespanel.tsx","./src/components/cinematicexportoverlay.tsx","./src/components/commandbar.test.tsx","./src/components/commandbar.tsx","./src/components/connectmenu.test.tsx","./src/components/connectmenu.tsx","./src/components/connectmenusections.tsx","./src/components/contextmenu.test.tsx","./src/components/contextmenu.tsx","./src/components/customconnectionline.test.tsx","./src/components/customconnectionline.tsx","./src/components/customedge.tsx","./src/components/customnode.handleinteraction.test.tsx","./src/components/customnode.tsx","./src/components/customnodecontent.tsx","./src/components/designsystem.integration.test.tsx","./src/components/diagramviewer.tsx","./src/components/errorboundary.tsx","./src/components/exportmenu.tsx","./src/components/exportmenupanel.test.tsx","./src/components/exportmenupanel.tsx","./src/components/flowcanvas.tsx","./src/components/floweditor.tsx","./src/components/floweditoremptystate.tsx","./src/components/floweditorlayoutoverlay.tsx","./src/components/floweditorpanels.test.tsx","./src/components/floweditorpanels.tsx","./src/components/flowtabs.test.tsx","./src/components/flowtabs.tsx","./src/components/groupnode.tsx","./src/components/homepage.integration.test.tsx","./src/components/homepage.tsx","./src/components/iconassetnodebody.tsx","./src/components/iconmap.test.tsx","./src/components/iconmap.ts","./src/components/imagenode.tsx","./src/components/importrecoverydialog.test.tsx","./src/components/importrecoverydialog.tsx","./src/components/inlinetexteditsurface.test.tsx","./src/components/inlinetexteditsurface.tsx","./src/components/keyboardshortcutsmodal.tsx","./src/components/languageselector.tsx","./src/components/markdownrenderer.tsx","./src/components/memoizedmarkdown.tsx","./src/components/navigationcontrols.tsx","./src/components/nodebadges.tsx","./src/components/nodechrome.tsx","./src/components/nodequickcreatebuttons.tsx","./src/components/nodeshapesvg.tsx","./src/components/nodetransformcontrols.tsx","./src/components/playbackcontrols.tsx","./src/components/propertiespanel.tsx","./src/components/rightrail.tsx","./src/components/sectionnode.tsx","./src/components/shareembedmodal.tsx","./src/components/sharemodal.test.tsx","./src/components/sharemodal.tsx","./src/components/sidebarshell.tsx","./src/components/snapshotspanel.test.tsx","./src/components/snapshotspanel.tsx","./src/components/studioaipanel.test.tsx","./src/components/studioaipanel.tsx","./src/components/studioaipanelsections.tsx","./src/components/studiocodepanel.test.tsx","./src/components/studiocodepanel.tsx","./src/components/studiopanel.test.tsx","./src/components/studiopanel.tsx","./src/components/studioplaybackpanel.test.tsx","./src/components/studioplaybackpanel.tsx","./src/components/swimlanenode.tsx","./src/components/textnode.tsx","./src/components/themetoggle.tsx","./src/components/toolbar.tsx","./src/components/tooltip.tsx","./src/components/topnav.tsx","./src/components/welcomemodal.tsx","./src/components/annotationtheme.ts","./src/components/container-nodes.handleinteraction.test.tsx","./src/components/editorsurfacetiers.ts","./src/components/handleinteraction.test.ts","./src/components/handleinteraction.ts","./src/components/handleinteractionusage.test.ts","./src/components/lightweight-nodes.handleinteraction.test.tsx","./src/components/markdownsyntax.ts","./src/components/nodehelpers.test.ts","./src/components/nodehelpers.ts","./src/components/sharemodalcontent.ts","./src/components/studioaicopy.ts","./src/components/studioaipanelexamples.ts","./src/components/transformdiagnostics.test.ts","./src/components/transformdiagnostics.ts","./src/components/useactivenodeselection.ts","./src/components/useexportmenu.test.tsx","./src/components/useexportmenu.ts","./src/components/settingsmodal/aisettings.tsx","./src/components/settingsmodal/canvassettings.tsx","./src/components/settingsmodal/generalsettings.test.tsx","./src/components/settingsmodal/generalsettings.tsx","./src/components/settingsmodal/settingsmodal.test.tsx","./src/components/settingsmodal/settingsmodal.tsx","./src/components/settingsmodal/shortcutssettings.tsx","./src/components/settingsmodal/ai/advancedendpointsection.tsx","./src/components/settingsmodal/ai/customheaderseditor.tsx","./src/components/settingsmodal/ai/privacysection.tsx","./src/components/settingsmodal/ai/providericon.tsx","./src/components/settingsmodal/ai/providersection.tsx","./src/components/settingsmodal/ai/step.tsx","./src/components/add-items/additemregistry.tsx","./src/components/app/docssiteredirect.test.tsx","./src/components/app/docssiteredirect.tsx","./src/components/app/mobileworkspacegate.test.tsx","./src/components/app/mobileworkspacegate.tsx","./src/components/app/routeloadingfallback.test.tsx","./src/components/app/routeloadingfallback.tsx","./src/components/app/mobileworkspacegatecopy.ts","./src/components/app/routeloadingcopy.ts","./src/components/architecture-lint/lintrulespanel.tsx","./src/components/architecture-lint/ruleform.tsx","./src/components/architecture-lint/visualeditor.tsx","./src/components/command-bar/assetsview.test.tsx","./src/components/command-bar/assetsview.tsx","./src/components/command-bar/codebaseimportpanels.tsx","./src/components/command-bar/codebaseimportsection.tsx","./src/components/command-bar/designsystemeditor.tsx","./src/components/command-bar/designsystemview.tsx","./src/components/command-bar/diagramminipreview.tsx","./src/components/command-bar/figmaimportpanel.tsx","./src/components/command-bar/importsurfaceprimitives.tsx","./src/components/command-bar/importview.tsx","./src/components/command-bar/importviewpanels.tsx","./src/components/command-bar/layersview.tsx","./src/components/command-bar/layoutview.tsx","./src/components/command-bar/pagesview.tsx","./src/components/command-bar/rootview.test.tsx","./src/components/command-bar/rootview.tsx","./src/components/command-bar/searchview.tsx","./src/components/command-bar/templatesview.test.tsx","./src/components/command-bar/templatesview.tsx","./src/components/command-bar/viewheader.tsx","./src/components/command-bar/visualsview.tsx","./src/components/command-bar/applycodechanges.ts","./src/components/command-bar/assetsviewconstants.ts","./src/components/command-bar/importdetection.test.ts","./src/components/command-bar/importdetection.ts","./src/components/command-bar/importnativeparsers.ts","./src/components/command-bar/importviewmodel.test.ts","./src/components/command-bar/importviewmodel.ts","./src/components/command-bar/mermaidimportparser.ts","./src/components/command-bar/searchquery.test.ts","./src/components/command-bar/searchquery.ts","./src/components/command-bar/types.ts","./src/components/command-bar/useaiviewstate.test.tsx","./src/components/command-bar/useaiviewstate.ts","./src/components/command-bar/usecloudassetcatalog.ts","./src/components/command-bar/usecommandbarcommands.test.tsx","./src/components/command-bar/usecommandbarcommands.tsx","./src/components/custom-edge/customedgewrapper.tsx","./src/components/custom-edge/edgemarkerdefs.tsx","./src/components/custom-edge/sequencemessageedge.test.tsx","./src/components/custom-edge/sequencemessageedge.tsx","./src/components/custom-edge/animatededgepresentation.test.ts","./src/components/custom-edge/animatededgepresentation.ts","./src/components/custom-edge/classrelationsemantics.test.ts","./src/components/custom-edge/classrelationsemantics.ts","./src/components/custom-edge/edgerendermode.ts","./src/components/custom-edge/pathutils.test.ts","./src/components/custom-edge/pathutils.ts","./src/components/custom-edge/pathutilsgeometry.ts","./src/components/custom-edge/pathutilssiblingrouting.ts","./src/components/custom-edge/pathutilstypes.ts","./src/components/custom-edge/relationroutingsemantics.test.ts","./src/components/custom-edge/relationroutingsemantics.ts","./src/components/custom-edge/standardedgemarkers.test.ts","./src/components/custom-edge/standardedgemarkers.ts","./src/components/custom-nodes/architecturenode.handleinteraction.test.tsx","./src/components/custom-nodes/architecturenode.tsx","./src/components/custom-nodes/browsernode.tsx","./src/components/custom-nodes/classentitynode.handleinteraction.test.tsx","./src/components/custom-nodes/classnode.tsx","./src/components/custom-nodes/entitynode.tsx","./src/components/custom-nodes/journeynode.tsx","./src/components/custom-nodes/mindmapnode.tsx","./src/components/custom-nodes/mobilenode.tsx","./src/components/custom-nodes/sequencenotenode.tsx","./src/components/custom-nodes/sequenceparticipantnode.tsx","./src/components/custom-nodes/structurednodehandles.tsx","./src/components/custom-nodes/visualnodes.handleinteraction.test.tsx","./src/components/custom-nodes/browservariantrenderer.tsx","./src/components/custom-nodes/mobilevariantrenderer.tsx","./src/components/custom-nodes/structuredlistnavigation.test.ts","./src/components/custom-nodes/structuredlistnavigation.ts","./src/components/custom-nodes/variantconstants.ts","./src/components/diagram-diff/diffmodebanner.tsx","./src/components/flow-canvas/flowcanvasalignmentguidesoverlay.tsx","./src/components/flow-canvas/flowcanvasoverlays.tsx","./src/components/flow-canvas/flowcanvassurface.test.tsx","./src/components/flow-canvas/flowcanvassurface.tsx","./src/components/flow-canvas/streamingoverlay.tsx","./src/components/flow-canvas/alignmentguides.test.ts","./src/components/flow-canvas/alignmentguides.ts","./src/components/flow-canvas/flowcanvasreactflowcontracts.ts","./src/components/flow-canvas/flowcanvastypes.test.ts","./src/components/flow-canvas/flowcanvastypes.tsx","./src/components/flow-canvas/largegraphsafetymode.test.ts","./src/components/flow-canvas/largegraphsafetymode.ts","./src/components/flow-canvas/nodechromecoverage.test.ts","./src/components/flow-canvas/pastehelpers.test.ts","./src/components/flow-canvas/pastehelpers.ts","./src/components/flow-canvas/useflowcanvasalignmentguides.ts","./src/components/flow-canvas/useflowcanvasconnectionstate.ts","./src/components/flow-canvas/useflowcanvascontextactions.ts","./src/components/flow-canvas/useflowcanvasdragdrop.ts","./src/components/flow-canvas/useflowcanvasinteractionlod.ts","./src/components/flow-canvas/useflowcanvasmenus.ts","./src/components/flow-canvas/useflowcanvasmenusandactions.test.tsx","./src/components/flow-canvas/useflowcanvasmenusandactions.ts","./src/components/flow-canvas/useflowcanvasnodedraghandlers.ts","./src/components/flow-canvas/useflowcanvaspaste.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.test.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.ts","./src/components/flow-canvas/useflowcanvasselectiontools.test.tsx","./src/components/flow-canvas/useflowcanvasselectiontools.ts","./src/components/flow-canvas/useflowcanvasviewstate.test.ts","./src/components/flow-canvas/useflowcanvasviewstate.ts","./src/components/flow-canvas/useflowcanvaszoomlod.ts","./src/components/flow-editor/collaborationpresenceoverlay.test.tsx","./src/components/flow-editor/collaborationpresenceoverlay.tsx","./src/components/flow-editor/floweditorchrome.tsx","./src/components/flow-editor/buildfloweditorcontrollerparams.ts","./src/components/flow-editor/buildfloweditorscreencontrollerparams.ts","./src/components/flow-editor/chromeproptypes.ts","./src/components/flow-editor/floweditorchromeprops.ts","./src/components/flow-editor/infradslapply.test.ts","./src/components/flow-editor/infradslapply.ts","./src/components/flow-editor/panelprops.test.ts","./src/components/flow-editor/panelprops.ts","./src/components/flow-editor/shouldexitstudioonselection.test.ts","./src/components/flow-editor/shouldexitstudioonselection.ts","./src/components/flow-editor/usecollaborationnodepositions.ts","./src/components/flow-editor/usefloweditorcontroller.ts","./src/components/flow-editor/usefloweditorinteractionbindings.ts","./src/components/flow-editor/usefloweditorpanelactions.ts","./src/components/flow-editor/usefloweditorpanelprops.ts","./src/components/flow-editor/usefloweditorruntime.ts","./src/components/flow-editor/usefloweditorscreenbehavior.ts","./src/components/flow-editor/usefloweditorscreenmodel.ts","./src/components/flow-editor/usefloweditorscreenstate.ts","./src/components/flow-editor/usefloweditorshellcontroller.test.tsx","./src/components/flow-editor/usefloweditorshellcontroller.ts","./src/components/flow-editor/usefloweditorstudiocontroller.test.tsx","./src/components/flow-editor/usefloweditorstudiocontroller.ts","./src/components/flow-editor/useinfradslapply.ts","./src/components/home/githubcard.tsx","./src/components/home/homedashboard.tsx","./src/components/home/homeflowdialogs.tsx","./src/components/home/homesettingsview.tsx","./src/components/home/homesidebar.tsx","./src/components/home/hometemplatesview.tsx","./src/components/home/sidebarfooter.tsx","./src/components/home/welcomemodalstate.ts","./src/components/icons/assetsicon.tsx","./src/components/icons/openflowlogo.tsx","./src/components/infra-sync/infrasyncpanel.test.tsx","./src/components/infra-sync/infrasyncpanel.tsx","./src/components/journey/journeyscorecontrol.test.tsx","./src/components/journey/journeyscorecontrol.tsx","./src/components/properties/bulknodeproperties.test.tsx","./src/components/properties/bulknodeproperties.tsx","./src/components/properties/bulknodepropertiesfamilysections.tsx","./src/components/properties/bulknodepropertiessections.tsx","./src/components/properties/bulknodepropertiesutilitysections.tsx","./src/components/properties/colorpicker.test.tsx","./src/components/properties/colorpicker.tsx","./src/components/properties/customcolorpopover.tsx","./src/components/properties/diagramnodepropertiesrouter.test.ts","./src/components/properties/diagramnodepropertiesrouter.tsx","./src/components/properties/edgeproperties.tsx","./src/components/properties/iconpicker.tsx","./src/components/properties/icontilepickerprimitives.tsx","./src/components/properties/imageupload.tsx","./src/components/properties/inspectorprimitives.tsx","./src/components/properties/nodeactionbuttons.tsx","./src/components/properties/nodecontentsection.tsx","./src/components/properties/nodeimagesettingssection.tsx","./src/components/properties/nodeproperties.test.tsx","./src/components/properties/nodeproperties.tsx","./src/components/properties/nodewireframevariantsection.tsx","./src/components/properties/propertysliderrow.tsx","./src/components/properties/segmentedchoice.tsx","./src/components/properties/shapeselector.tsx","./src/components/properties/swatchpicker.tsx","./src/components/properties/bulknodepropertiesmodel.test.ts","./src/components/properties/bulknodepropertiesmodel.ts","./src/components/properties/colorpickerutils.ts","./src/components/properties/propertyinputbehavior.test.ts","./src/components/properties/propertyinputbehavior.ts","./src/components/properties/sectionactionbuilder.ts","./src/components/properties/shared.ts","./src/components/properties/wireframevariants.ts","./src/components/properties/edge/architectureedgesemanticssection.tsx","./src/components/properties/edge/edgecolorsection.test.tsx","./src/components/properties/edge/edgecolorsection.tsx","./src/components/properties/edge/edgeconditionsection.tsx","./src/components/properties/edge/edgelabelsection.tsx","./src/components/properties/edge/edgerelationsection.test.tsx","./src/components/properties/edge/edgerelationsection.tsx","./src/components/properties/edge/edgeroutesection.test.tsx","./src/components/properties/edge/edgeroutesection.tsx","./src/components/properties/edge/edgestylesection.tsx","./src/components/properties/edge/sequencemessagesection.tsx","./src/components/properties/edge/architecturesemantics.test.ts","./src/components/properties/edge/architecturesemantics.ts","./src/components/properties/edge/edgecolorutils.ts","./src/components/properties/edge/edgelabelmodel.test.ts","./src/components/properties/edge/edgelabelmodel.ts","./src/components/properties/edge/errelationoptions.ts","./src/components/properties/families/architecturenodeproperties.test.tsx","./src/components/properties/families/architecturenodeproperties.tsx","./src/components/properties/families/architecturenodesection.tsx","./src/components/properties/families/classdiagramnodeproperties.tsx","./src/components/properties/families/classmemberlisteditor.tsx","./src/components/properties/families/classnodesection.tsx","./src/components/properties/families/erdiagramnodeproperties.tsx","./src/components/properties/families/entityfieldlisteditor.tsx","./src/components/properties/families/entitynodesection.tsx","./src/components/properties/families/journeynodeproperties.tsx","./src/components/properties/families/journeynodesection.tsx","./src/components/properties/families/mindmapnodeproperties.test.tsx","./src/components/properties/families/mindmapnodeproperties.tsx","./src/components/properties/families/sequencenodeproperties.tsx","./src/components/properties/families/sequencenodesection.tsx","./src/components/properties/families/specializednodecolorsections.test.tsx","./src/components/properties/families/structuredtextlisteditor.tsx","./src/components/properties/families/architectureoptions.ts","./src/components/studio-code-panel/usestudiocodepanelcontroller.test.tsx","./src/components/studio-code-panel/usestudiocodepanelcontroller.ts","./src/components/templates/templatepresentation.tsx","./src/components/toolbar/toolbaraddmenu.test.tsx","./src/components/toolbar/toolbaraddmenu.tsx","./src/components/toolbar/toolbaraddmenupanel.tsx","./src/components/toolbar/toolbarhistorycontrols.tsx","./src/components/toolbar/toolbarmodecontrols.tsx","./src/components/toolbar/toolbarbuttonstyles.ts","./src/components/top-nav/savestatusindicator.tsx","./src/components/top-nav/topnavactions.tsx","./src/components/top-nav/topnavbrand.tsx","./src/components/top-nav/topnavmenu.test.tsx","./src/components/top-nav/topnavmenu.tsx","./src/components/top-nav/topnavmenupanel.tsx","./src/components/top-nav/usetopnavstate.ts","./src/components/ui/button.tsx","./src/components/ui/collapsiblesection.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/searchfield.tsx","./src/components/ui/segmentedtabs.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/sidebaritem.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toastcontext.tsx","./src/components/ui/editorfieldstyles.ts","./src/config/aiproviders.test.ts","./src/config/aiproviders.ts","./src/config/rolloutflags.ts","./src/context/architecturelintcontext.tsx","./src/context/cinematicexportcontext.tsx","./src/context/diagramdiffcontext.tsx","./src/context/themecontext.tsx","./src/diagram-types/bootstrap.test.ts","./src/diagram-types/bootstrap.ts","./src/diagram-types/builtinplugins.ts","./src/diagram-types/builtinpropertypanels.ts","./src/diagram-types/registerbuiltinplugins.test.ts","./src/diagram-types/registerbuiltinplugins.ts","./src/diagram-types/registerbuiltinpropertypanels.test.ts","./src/diagram-types/registerbuiltinpropertypanels.ts","./src/diagram-types/architecture/fuzzcorpus.test.ts","./src/diagram-types/architecture/plugin.test.ts","./src/diagram-types/architecture/plugin.ts","./src/diagram-types/classdiagram/fuzzcorpus.test.ts","./src/diagram-types/classdiagram/plugin.test.ts","./src/diagram-types/classdiagram/plugin.ts","./src/diagram-types/core/contracts.ts","./src/diagram-types/core/index.ts","./src/diagram-types/core/propertypanels.test.ts","./src/diagram-types/core/propertypanels.ts","./src/diagram-types/core/registry.ts","./src/diagram-types/erdiagram/fuzzcorpus.test.ts","./src/diagram-types/erdiagram/plugin.test.ts","./src/diagram-types/erdiagram/plugin.ts","./src/diagram-types/flowchart/plugin.ts","./src/diagram-types/journey/fuzzcorpus.test.ts","./src/diagram-types/journey/plugin.test.ts","./src/diagram-types/journey/plugin.ts","./src/diagram-types/mindmap/fuzzcorpus.test.ts","./src/diagram-types/mindmap/plugin.test.ts","./src/diagram-types/mindmap/plugin.ts","./src/diagram-types/sequence/plugin.test.ts","./src/diagram-types/sequence/plugin.ts","./src/diagram-types/statediagram/plugin.test.ts","./src/diagram-types/statediagram/plugin.ts","./src/docs/docsroutes.ts","./src/docs/publicdocscatalog.js","./src/hooks/edgeconnectinteractions.test.ts","./src/hooks/edgeconnectinteractions.ts","./src/hooks/flowexportviewport.test.ts","./src/hooks/flowexportviewport.ts","./src/hooks/mindmaptopicactionrequest.ts","./src/hooks/nodelabeleditrequest.test.ts","./src/hooks/nodelabeleditrequest.ts","./src/hooks/nodequickcreaterequest.test.ts","./src/hooks/nodequickcreaterequest.ts","./src/hooks/snapshotpolicy.test.ts","./src/hooks/snapshotpolicy.ts","./src/hooks/useaigeneration.ts","./src/hooks/useanalyticspreference.ts","./src/hooks/useanimatededgeperformancewarning.test.ts","./src/hooks/useanimatededgeperformancewarning.ts","./src/hooks/useassetcatalog.ts","./src/hooks/usecinematicexport.ts","./src/hooks/useclipboardoperations.ts","./src/hooks/usedesignsystem.ts","./src/hooks/useedgeinteractions.ts","./src/hooks/useedgeoperations.ts","./src/hooks/usefloweditoractions.test.ts","./src/hooks/usefloweditoractions.ts","./src/hooks/usefloweditorcallbacks.ts","./src/hooks/usefloweditorcollaboration.test.ts","./src/hooks/usefloweditorcollaboration.ts","./src/hooks/usefloweditoruistate.ts","./src/hooks/useflowexport.ts","./src/hooks/useflowhistory.test.ts","./src/hooks/useflowhistory.ts","./src/hooks/useflowoperations.ts","./src/hooks/usegithubstars.ts","./src/hooks/useinfrasync.ts","./src/hooks/useinlinenodetextedit.test.ts","./src/hooks/useinlinenodetextedit.ts","./src/hooks/usekeyboardshortcuts.test.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/uselayoutoperations.ts","./src/hooks/usemarkdowneditor.ts","./src/hooks/usemenukeyboardnavigation.ts","./src/hooks/usemodifierkeys.test.ts","./src/hooks/usemodifierkeys.ts","./src/hooks/usenodeoperations.ts","./src/hooks/useplayback.ts","./src/hooks/useprovidershapepreview.ts","./src/hooks/userecentimports.ts","./src/hooks/useshiftheld.ts","./src/hooks/usesnapshots.ts","./src/hooks/usestaticexport.ts","./src/hooks/usestoragepressureguard.ts","./src/hooks/usestructuredlisteditor.ts","./src/hooks/usestyleclipboard.test.ts","./src/hooks/usestyleclipboard.ts","./src/hooks/ai-generation/chathistorystorage.test.ts","./src/hooks/ai-generation/chathistorystorage.ts","./src/hooks/ai-generation/codetoarchitecture.test.ts","./src/hooks/ai-generation/codetoarchitecture.ts","./src/hooks/ai-generation/codebaseanalyzer.test.ts","./src/hooks/ai-generation/codebaseanalyzer.ts","./src/hooks/ai-generation/codebaseparser.test.ts","./src/hooks/ai-generation/codebaseparser.ts","./src/hooks/ai-generation/codebasetonativediagram.test.ts","./src/hooks/ai-generation/codebasetonativediagram.ts","./src/hooks/ai-generation/graphcomposer.test.ts","./src/hooks/ai-generation/graphcomposer.ts","./src/hooks/ai-generation/nodeactionprompts.test.ts","./src/hooks/ai-generation/nodeactionprompts.ts","./src/hooks/ai-generation/openapiparser.test.ts","./src/hooks/ai-generation/openapiparser.ts","./src/hooks/ai-generation/openapitosequence.ts","./src/hooks/ai-generation/positionpreservingapply.test.ts","./src/hooks/ai-generation/positionpreservingapply.ts","./src/hooks/ai-generation/readiness.test.ts","./src/hooks/ai-generation/readiness.ts","./src/hooks/ai-generation/requestlifecycle.test.ts","./src/hooks/ai-generation/requestlifecycle.ts","./src/hooks/ai-generation/sqlparser.test.ts","./src/hooks/ai-generation/sqlparser.ts","./src/hooks/ai-generation/sqltoerd.ts","./src/hooks/ai-generation/streamingparser.ts","./src/hooks/ai-generation/streamingstore.ts","./src/hooks/ai-generation/terraformtocloud.ts","./src/hooks/edge-operations/utils.test.ts","./src/hooks/edge-operations/utils.ts","./src/hooks/flow-editor-actions/exporthandlers.test.ts","./src/hooks/flow-editor-actions/exporthandlers.ts","./src/hooks/flow-editor-actions/helpers.ts","./src/hooks/flow-editor-actions/layouthandlers.test.ts","./src/hooks/flow-editor-actions/layouthandlers.ts","./src/hooks/flow-export/diagramdocumenttransfer.test.ts","./src/hooks/flow-export/diagramdocumenttransfer.ts","./src/hooks/flow-export/exportcapture.test.ts","./src/hooks/flow-export/exportcapture.ts","./src/hooks/flow-operations/useflowcoreactions.ts","./src/hooks/node-operations/createconnectedsibling.ts","./src/hooks/node-operations/dragstopreconcilepolicy.test.ts","./src/hooks/node-operations/dragstopreconcilepolicy.ts","./src/hooks/node-operations/nodefactories.ts","./src/hooks/node-operations/routingduringdrag.test.ts","./src/hooks/node-operations/routingduringdrag.ts","./src/hooks/node-operations/sectionbounds.ts","./src/hooks/node-operations/sectionhittesting.ts","./src/hooks/node-operations/sectionoperations.ts","./src/hooks/node-operations/usearchitecturenodeoperations.test.ts","./src/hooks/node-operations/usearchitecturenodeoperations.ts","./src/hooks/node-operations/usemindmapnodeoperations.test.ts","./src/hooks/node-operations/usemindmapnodeoperations.ts","./src/hooks/node-operations/usenodedragoperations.test.ts","./src/hooks/node-operations/usenodedragoperations.ts","./src/hooks/node-operations/usenodeoperationadders.ts","./src/hooks/node-operations/utils.test.ts","./src/hooks/node-operations/utils.ts","./src/i18n/config.test.ts","./src/i18n/config.ts","./src/i18n/strictmodelocalecoverage.test.ts","./src/i18n/usedlocalecoverage.test.ts","./src/lib/architecturetemplatedata.ts","./src/lib/architecturetemplates.test.ts","./src/lib/architecturetemplates.ts","./src/lib/brand.ts","./src/lib/classmembers.ts","./src/lib/colorutils.ts","./src/lib/connectcreationpolicy.test.ts","./src/lib/connectcreationpolicy.ts","./src/lib/date.ts","./src/lib/designtokens.ts","./src/lib/devperformance.ts","./src/lib/entityfields.ts","./src/lib/ertoclassconversion.test.ts","./src/lib/ertoclassconversion.ts","./src/lib/exportfilename.test.ts","./src/lib/exportfilename.ts","./src/lib/flowminddslparserv2.test.ts","./src/lib/flowminddslparserv2.ts","./src/lib/fuzzymatch.ts","./src/lib/genericshapepolicy.ts","./src/lib/id.ts","./src/lib/index.ts","./src/lib/legacybranding.ts","./src/lib/logger.ts","./src/lib/mermaidparser.ts","./src/lib/mermaidparserhelpers.ts","./src/lib/mermaidparsermodel.ts","./src/lib/mindmaplayout.test.ts","./src/lib/mindmaplayout.ts","./src/lib/mindmaplayoutengine.ts","./src/lib/mindmaptree.ts","./src/lib/nodebulkediting.test.ts","./src/lib/nodebulkediting.ts","./src/lib/nodehandles.ts","./src/lib/nodeparent.ts","./src/lib/nodestyledata.test.ts","./src/lib/nodestyledata.ts","./src/lib/openflowdslparser.ts","./src/lib/openflowdslparserv2.ts","./src/lib/reactflowcompat.ts","./src/lib/reconnectedge.test.ts","./src/lib/reconnectedge.ts","./src/lib/relationsemantics.test.ts","./src/lib/relationsemantics.ts","./src/lib/releasestaleelkroutes.test.ts","./src/lib/releasestaleelkroutes.ts","./src/lib/result.ts","./src/lib/storagepressure.test.ts","./src/lib/storagepressure.ts","./src/lib/types.ts","./src/lib/xsssafety.test.tsx","./src/services/aligndistribute.ts","./src/services/aiservice.test.ts","./src/services/aiservice.ts","./src/services/aiserviceschemas.ts","./src/services/animatedexport.test.ts","./src/services/animatedexport.ts","./src/services/architectureroundtrip.test.ts","./src/services/assetcatalog.test.ts","./src/services/assetcatalog.ts","./src/services/assetpresentation.ts","./src/services/canonicalserialization.test.ts","./src/services/canonicalserialization.ts","./src/services/composediagramfordisplay.test.ts","./src/services/composediagramfordisplay.ts","./src/services/diagramdocument.test.ts","./src/services/diagramdocument.ts","./src/services/diagramdocumentschemas.ts","./src/services/domainlibrary.test.ts","./src/services/domainlibrary.ts","./src/services/elklayout.test.ts","./src/services/elklayout.ts","./src/services/exportservice.test.ts","./src/services/exportservice.ts","./src/services/figmaexportservice.ts","./src/services/flowchartroundtrip.test.ts","./src/services/geminiservice.ts","./src/services/geminisysteminstruction.ts","./src/services/gifencoder.test.ts","./src/services/gifencoder.ts","./src/services/githubfetcher.test.ts","./src/services/githubfetcher.ts","./src/services/iconassetcatalog.ts","./src/services/importfidelity.test.ts","./src/services/importfidelity.ts","./src/services/mermaidparser.test.ts","./src/services/openflowdslexporter.test.ts","./src/services/openflowdslexporter.ts","./src/services/openflowdslparser.test.ts","./src/services/openflowroundtripgolden.test.ts","./src/services/openflowroundtripgoldenfixtures.ts","./src/services/operationfeedback.ts","./src/services/remainingfamiliesroundtrip.test.ts","./src/services/smartedgerouting.test.ts","./src/services/smartedgerouting.ts","./src/services/statediagramroundtrip.test.ts","./src/services/templates.selector.test.ts","./src/services/templates.ts","./src/services/zipextractor.ts","./src/services/ai/contextserializer.test.ts","./src/services/ai/contextserializer.ts","./src/services/analytics/analytics.ts","./src/services/analytics/analyticssettings.test.ts","./src/services/analytics/analyticssettings.ts","./src/services/analytics/surfaceanalyticsclient.ts","./src/services/architecturelint/defaultrules.ts","./src/services/architecturelint/ruleengine.test.ts","./src/services/architecturelint/ruleengine.ts","./src/services/architecturelint/rulelibrary.ts","./src/services/architecturelint/types.ts","./src/services/architecturelint/workspacerules.ts","./src/services/collaboration/bootstrap.test.ts","./src/services/collaboration/bootstrap.ts","./src/services/collaboration/canvasdiff.test.ts","./src/services/collaboration/canvasdiff.ts","./src/services/collaboration/comments.test.ts","./src/services/collaboration/comments.ts","./src/services/collaboration/contracts.test.ts","./src/services/collaboration/contracts.ts","./src/services/collaboration/hookutils.ts","./src/services/collaboration/operationlog.test.ts","./src/services/collaboration/operationlog.ts","./src/services/collaboration/presenceviewmodel.test.ts","./src/services/collaboration/presenceviewmodel.ts","./src/services/collaboration/reducer.test.ts","./src/services/collaboration/reducer.ts","./src/services/collaboration/roomconfig.ts","./src/services/collaboration/roomlink.test.ts","./src/services/collaboration/roomlink.ts","./src/services/collaboration/runtimecontroller.test.ts","./src/services/collaboration/runtimecontroller.ts","./src/services/collaboration/runtimehookutils.test.ts","./src/services/collaboration/runtimehookutils.ts","./src/services/collaboration/schemas.ts","./src/services/collaboration/session.test.ts","./src/services/collaboration/session.ts","./src/services/collaboration/storebridge.test.ts","./src/services/collaboration/storebridge.ts","./src/services/collaboration/transport.test.ts","./src/services/collaboration/transport.ts","./src/services/collaboration/transportfactory.test.ts","./src/services/collaboration/transportfactory.ts","./src/services/collaboration/types.ts","./src/services/collaboration/versioning.test.ts","./src/services/collaboration/versioning.ts","./src/services/collaboration/yjspeertransport.test.ts","./src/services/collaboration/yjspeertransport.ts","./src/services/diagramdiff/diffengine.ts","./src/services/elk-layout/boundaryfanout.ts","./src/services/elk-layout/determinism.ts","./src/services/elk-layout/options.ts","./src/services/elk-layout/types.ts","./src/services/export/cinematicbuildplan.test.ts","./src/services/export/cinematicbuildplan.ts","./src/services/export/cinematicexport.ts","./src/services/export/cinematicexporttheme.test.ts","./src/services/export/cinematicexporttheme.ts","./src/services/export/cinematicrenderstate.test.ts","./src/services/export/cinematicrenderstate.ts","./src/services/export/formatting.ts","./src/services/export/mermaidbuilder.test.ts","./src/services/export/mermaidbuilder.ts","./src/services/export/pdfdocument.test.ts","./src/services/export/pdfdocument.ts","./src/services/export/plantumlbuilder.ts","./src/services/export/mermaid/architecturemermaid.ts","./src/services/export/mermaid/classdiagrammermaid.ts","./src/services/export/mermaid/erdiagrammermaid.ts","./src/services/export/mermaid/journeymermaid.ts","./src/services/export/mermaid/mindmapmermaid.ts","./src/services/export/mermaid/sequencemermaid.ts","./src/services/export/mermaid/statediagrammermaid.ts","./src/services/figma/edgehelpers.ts","./src/services/figma/iconhelpers.ts","./src/services/figma/nodelayers.ts","./src/services/figma/themehelpers.ts","./src/services/figmaimport/figmaapiclient.ts","./src/services/flowpilot/assetgrounding.test.ts","./src/services/flowpilot/assetgrounding.ts","./src/services/flowpilot/prompting.ts","./src/services/flowpilot/responsepolicy.test.ts","./src/services/flowpilot/responsepolicy.ts","./src/services/flowpilot/skills.ts","./src/services/flowpilot/thread.ts","./src/services/flowpilot/types.ts","./src/services/infrasync/dockercomposeparser.test.ts","./src/services/infrasync/dockercomposeparser.ts","./src/services/infrasync/infratodsl.test.ts","./src/services/infrasync/infratodsl.ts","./src/services/infrasync/kubernetesparser.test.ts","./src/services/infrasync/kubernetesparser.ts","./src/services/infrasync/terraformstateparser.test.ts","./src/services/infrasync/terraformstateparser.ts","./src/services/infrasync/types.ts","./src/services/mermaid/detectdiagramtype.test.ts","./src/services/mermaid/detectdiagramtype.ts","./src/services/mermaid/diagnosticformatting.test.ts","./src/services/mermaid/diagnosticformatting.ts","./src/services/mermaid/parsemermaidbytype.test.ts","./src/services/mermaid/parsemermaidbytype.ts","./src/services/mermaid/strictmodediagnosticspresentation.test.ts","./src/services/mermaid/strictmodediagnosticspresentation.ts","./src/services/mermaid/strictmodeguidance.test.ts","./src/services/mermaid/strictmodeguidance.ts","./src/services/mermaid/strictmodeuxregression.test.ts","./src/services/offline/registerappshellserviceworker.test.ts","./src/services/offline/registerappshellserviceworker.ts","./src/services/onboarding/config.ts","./src/services/onboarding/eventschemas.ts","./src/services/onboarding/events.test.ts","./src/services/onboarding/events.ts","./src/services/playback/contracts.test.ts","./src/services/playback/contracts.ts","./src/services/playback/model.test.ts","./src/services/playback/model.ts","./src/services/playback/studio.test.ts","./src/services/playback/studio.ts","./src/services/sequence/sequencemessage.test.ts","./src/services/sequence/sequencemessage.ts","./src/services/shapelibrary/bootstrap.test.ts","./src/services/shapelibrary/bootstrap.ts","./src/services/shapelibrary/ingestionpipeline.test.ts","./src/services/shapelibrary/ingestionpipeline.ts","./src/services/shapelibrary/manifestvalidation.test.ts","./src/services/shapelibrary/manifestvalidation.ts","./src/services/shapelibrary/providercatalog.test.ts","./src/services/shapelibrary/providercatalog.ts","./src/services/shapelibrary/registry.test.ts","./src/services/shapelibrary/registry.ts","./src/services/shapelibrary/starterpacks.ts","./src/services/shapelibrary/types.ts","./src/services/storage/fallbackstorage.ts","./src/services/storage/flowdocumentmodel.test.ts","./src/services/storage/flowdocumentmodel.ts","./src/services/storage/flowpersiststorage.test.ts","./src/services/storage/flowpersiststorage.ts","./src/services/storage/indexeddbhelpers.ts","./src/services/storage/indexeddbschema.test.ts","./src/services/storage/indexeddbschema.ts","./src/services/storage/indexeddbstatestorage.test.ts","./src/services/storage/indexeddbstatestorage.ts","./src/services/storage/localfirstrepository.ts","./src/services/storage/localfirstruntime.ts","./src/services/storage/persisteddocumentadapters.test.ts","./src/services/storage/persisteddocumentadapters.ts","./src/services/storage/persistencetypes.ts","./src/services/storage/snapshotstorage.test.ts","./src/services/storage/snapshotstorage.ts","./src/services/storage/storageruntime.test.ts","./src/services/storage/storageruntime.ts","./src/services/storage/storageschemas.test.ts","./src/services/storage/storageschemas.ts","./src/services/storage/storagetelemetry.test.ts","./src/services/storage/storagetelemetry.ts","./src/services/storage/storagetelemetrysink.test.ts","./src/services/storage/storagetelemetrysink.ts","./src/services/storage/uilocalstorage.test.ts","./src/services/storage/uilocalstorage.ts","./src/services/templatelibrary/registry.test.ts","./src/services/templatelibrary/registry.ts","./src/services/templatelibrary/startertemplates.assets.test.ts","./src/services/templatelibrary/startertemplates.test.ts","./src/services/templatelibrary/startertemplates.ts","./src/services/templatelibrary/templatefactories.ts","./src/services/templatelibrary/types.ts","./src/store/actionfactory.ts","./src/store/aisettings.test.ts","./src/store/aisettings.ts","./src/store/aisettingspersistence.ts","./src/store/aisettingsschemas.test.ts","./src/store/aisettingsschemas.ts","./src/store/canvashooks.ts","./src/store/createflowstore.test.ts","./src/store/createflowstore.ts","./src/store/createflowstorepersistoptions.ts","./src/store/createflowstorestate.ts","./src/store/defaults.ts","./src/store/designsystemhooks.ts","./src/store/documenthooks.ts","./src/store/documentstatesync.test.ts","./src/store/documentstatesync.ts","./src/store/editorpagehooks.ts","./src/store/historyhooks.ts","./src/store/historystate.ts","./src/store/persistence.test.ts","./src/store/persistence.ts","./src/store/persistenceschemas.ts","./src/store/selectionhooks.ts","./src/store/selectors.test.ts","./src/store/selectors.ts","./src/store/tabhooks.ts","./src/store/types.ts","./src/store/viewhooks.ts","./src/store/workspacedocumentmodel.test.ts","./src/store/workspacedocumentmodel.ts","./src/store/actions/createaiandselectionactions.ts","./src/store/actions/createcanvasactions.ts","./src/store/actions/createdesignsystemactions.ts","./src/store/actions/createhistoryactions.test.ts","./src/store/actions/createhistoryactions.ts","./src/store/actions/createlayeractions.ts","./src/store/actions/createtabactions.ts","./src/store/actions/createviewactions.ts","./src/store/actions/createworkspacedocumentactions.ts","./src/store/actions/synctabnodesedges.ts","./src/store/slices/createcanvaseditorslice.ts","./src/store/slices/createexperienceslice.ts","./src/store/slices/createworkspaceslice.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.test.tsx","./src/app.tsx","./src/constants.test.ts","./src/constants.ts","./src/index.tsx","./src/store.test.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/app/routestate.test.ts","./src/app/routestate.ts","./src/components/annotationnode.tsx","./src/components/architecturerulespanel.tsx","./src/components/cinematicexportoverlay.tsx","./src/components/commandbar.test.tsx","./src/components/commandbar.tsx","./src/components/connectmenu.test.tsx","./src/components/connectmenu.tsx","./src/components/connectmenusections.tsx","./src/components/contextmenu.test.tsx","./src/components/contextmenu.tsx","./src/components/customconnectionline.test.tsx","./src/components/customconnectionline.tsx","./src/components/customedge.tsx","./src/components/customnode.handleinteraction.test.tsx","./src/components/customnode.tsx","./src/components/customnodecontent.tsx","./src/components/designsystem.integration.test.tsx","./src/components/diagramviewer.tsx","./src/components/errorboundary.tsx","./src/components/exportmenu.tsx","./src/components/exportmenupanel.test.tsx","./src/components/exportmenupanel.tsx","./src/components/flowcanvas.tsx","./src/components/floweditor.tsx","./src/components/floweditoremptystate.tsx","./src/components/floweditorlayoutoverlay.tsx","./src/components/floweditorpanels.test.tsx","./src/components/floweditorpanels.tsx","./src/components/flowtabs.test.tsx","./src/components/flowtabs.tsx","./src/components/groupnode.tsx","./src/components/homepage.integration.test.tsx","./src/components/homepage.tsx","./src/components/iconassetnodebody.tsx","./src/components/iconmap.test.tsx","./src/components/iconmap.ts","./src/components/imagenode.tsx","./src/components/importrecoverydialog.test.tsx","./src/components/importrecoverydialog.tsx","./src/components/inlinetexteditsurface.test.tsx","./src/components/inlinetexteditsurface.tsx","./src/components/keyboardshortcutsmodal.tsx","./src/components/languageselector.tsx","./src/components/markdownrenderer.tsx","./src/components/memoizedmarkdown.tsx","./src/components/navigationcontrols.tsx","./src/components/nodebadges.tsx","./src/components/nodechrome.tsx","./src/components/nodequickcreatebuttons.tsx","./src/components/nodeshapesvg.tsx","./src/components/nodetransformcontrols.tsx","./src/components/playbackcontrols.tsx","./src/components/propertiespanel.tsx","./src/components/rightrail.tsx","./src/components/sectionnode.tsx","./src/components/shareembedmodal.tsx","./src/components/sharemodal.test.tsx","./src/components/sharemodal.tsx","./src/components/sidebarshell.tsx","./src/components/snapshotspanel.test.tsx","./src/components/snapshotspanel.tsx","./src/components/studioaipanel.test.tsx","./src/components/studioaipanel.tsx","./src/components/studioaipanelsections.tsx","./src/components/studiocodepanel.test.tsx","./src/components/studiocodepanel.tsx","./src/components/studiopanel.test.tsx","./src/components/studiopanel.tsx","./src/components/studioplaybackpanel.test.tsx","./src/components/studioplaybackpanel.tsx","./src/components/swimlanenode.tsx","./src/components/textnode.tsx","./src/components/themetoggle.tsx","./src/components/toolbar.tsx","./src/components/tooltip.tsx","./src/components/topnav.tsx","./src/components/welcomemodal.tsx","./src/components/annotationtheme.ts","./src/components/container-nodes.handleinteraction.test.tsx","./src/components/editorsurfacetiers.ts","./src/components/handleinteraction.test.ts","./src/components/handleinteraction.ts","./src/components/handleinteractionusage.test.ts","./src/components/lightweight-nodes.handleinteraction.test.tsx","./src/components/markdownsyntax.ts","./src/components/nodehelpers.test.ts","./src/components/nodehelpers.ts","./src/components/sharemodalcontent.ts","./src/components/studioaicopy.ts","./src/components/studioaipanelexamples.ts","./src/components/transformdiagnostics.test.ts","./src/components/transformdiagnostics.ts","./src/components/useactivenodeselection.ts","./src/components/useexportmenu.test.tsx","./src/components/useexportmenu.ts","./src/components/settingsmodal/aisettings.tsx","./src/components/settingsmodal/canvassettings.tsx","./src/components/settingsmodal/generalsettings.test.tsx","./src/components/settingsmodal/generalsettings.tsx","./src/components/settingsmodal/settingsmodal.test.tsx","./src/components/settingsmodal/settingsmodal.tsx","./src/components/settingsmodal/shortcutssettings.tsx","./src/components/settingsmodal/ai/advancedendpointsection.tsx","./src/components/settingsmodal/ai/customheaderseditor.tsx","./src/components/settingsmodal/ai/privacysection.tsx","./src/components/settingsmodal/ai/providericon.tsx","./src/components/settingsmodal/ai/providersection.tsx","./src/components/settingsmodal/ai/step.tsx","./src/components/add-items/additemregistry.tsx","./src/components/app/docssiteredirect.test.tsx","./src/components/app/docssiteredirect.tsx","./src/components/app/mobileworkspacegate.test.tsx","./src/components/app/mobileworkspacegate.tsx","./src/components/app/routeloadingfallback.test.tsx","./src/components/app/routeloadingfallback.tsx","./src/components/app/mobileworkspacegatecopy.ts","./src/components/app/routeloadingcopy.ts","./src/components/architecture-lint/lintrulespanel.tsx","./src/components/architecture-lint/ruleform.tsx","./src/components/architecture-lint/visualeditor.tsx","./src/components/command-bar/assetsview.test.tsx","./src/components/command-bar/assetsview.tsx","./src/components/command-bar/codebaseimportpanels.tsx","./src/components/command-bar/codebaseimportsection.tsx","./src/components/command-bar/designsystemeditor.tsx","./src/components/command-bar/designsystemview.tsx","./src/components/command-bar/diagramminipreview.tsx","./src/components/command-bar/figmaimportpanel.tsx","./src/components/command-bar/importsurfaceprimitives.tsx","./src/components/command-bar/importview.tsx","./src/components/command-bar/importviewpanels.tsx","./src/components/command-bar/layersview.tsx","./src/components/command-bar/layoutview.tsx","./src/components/command-bar/pagesview.tsx","./src/components/command-bar/rootview.test.tsx","./src/components/command-bar/rootview.tsx","./src/components/command-bar/searchview.tsx","./src/components/command-bar/templatesview.test.tsx","./src/components/command-bar/templatesview.tsx","./src/components/command-bar/viewheader.tsx","./src/components/command-bar/visualsview.tsx","./src/components/command-bar/applycodechanges.ts","./src/components/command-bar/assetsviewconstants.ts","./src/components/command-bar/importdetection.test.ts","./src/components/command-bar/importdetection.ts","./src/components/command-bar/importnativeparsers.ts","./src/components/command-bar/importviewmodel.test.ts","./src/components/command-bar/importviewmodel.ts","./src/components/command-bar/mermaidimportparser.ts","./src/components/command-bar/searchquery.test.ts","./src/components/command-bar/searchquery.ts","./src/components/command-bar/types.ts","./src/components/command-bar/useaiviewstate.test.tsx","./src/components/command-bar/useaiviewstate.ts","./src/components/command-bar/usecloudassetcatalog.ts","./src/components/command-bar/usecommandbarcommands.test.tsx","./src/components/command-bar/usecommandbarcommands.tsx","./src/components/custom-edge/customedgewrapper.tsx","./src/components/custom-edge/edgemarkerdefs.tsx","./src/components/custom-edge/sequencemessageedge.test.tsx","./src/components/custom-edge/sequencemessageedge.tsx","./src/components/custom-edge/animatededgepresentation.test.ts","./src/components/custom-edge/animatededgepresentation.ts","./src/components/custom-edge/classrelationsemantics.test.ts","./src/components/custom-edge/classrelationsemantics.ts","./src/components/custom-edge/edgerendermode.ts","./src/components/custom-edge/pathutils.test.ts","./src/components/custom-edge/pathutils.ts","./src/components/custom-edge/pathutilsgeometry.ts","./src/components/custom-edge/pathutilssiblingrouting.ts","./src/components/custom-edge/pathutilstypes.ts","./src/components/custom-edge/relationroutingsemantics.test.ts","./src/components/custom-edge/relationroutingsemantics.ts","./src/components/custom-edge/standardedgemarkers.test.ts","./src/components/custom-edge/standardedgemarkers.ts","./src/components/custom-nodes/architecturenode.handleinteraction.test.tsx","./src/components/custom-nodes/architecturenode.tsx","./src/components/custom-nodes/browsernode.tsx","./src/components/custom-nodes/classentitynode.handleinteraction.test.tsx","./src/components/custom-nodes/classnode.tsx","./src/components/custom-nodes/entitynode.tsx","./src/components/custom-nodes/journeynode.tsx","./src/components/custom-nodes/mindmapnode.tsx","./src/components/custom-nodes/mobilenode.tsx","./src/components/custom-nodes/sequencenotenode.tsx","./src/components/custom-nodes/sequenceparticipantnode.tsx","./src/components/custom-nodes/structurednodehandles.tsx","./src/components/custom-nodes/visualnodes.handleinteraction.test.tsx","./src/components/custom-nodes/browservariantrenderer.tsx","./src/components/custom-nodes/mobilevariantrenderer.tsx","./src/components/custom-nodes/structuredlistnavigation.test.ts","./src/components/custom-nodes/structuredlistnavigation.ts","./src/components/custom-nodes/variantconstants.ts","./src/components/diagram-diff/diffmodebanner.tsx","./src/components/flow-canvas/flowcanvasalignmentguidesoverlay.tsx","./src/components/flow-canvas/flowcanvasoverlays.tsx","./src/components/flow-canvas/flowcanvassurface.test.tsx","./src/components/flow-canvas/flowcanvassurface.tsx","./src/components/flow-canvas/streamingoverlay.tsx","./src/components/flow-canvas/alignmentguides.test.ts","./src/components/flow-canvas/alignmentguides.ts","./src/components/flow-canvas/flowcanvasreactflowcontracts.ts","./src/components/flow-canvas/flowcanvastypes.test.ts","./src/components/flow-canvas/flowcanvastypes.tsx","./src/components/flow-canvas/largegraphsafetymode.test.ts","./src/components/flow-canvas/largegraphsafetymode.ts","./src/components/flow-canvas/nodechromecoverage.test.ts","./src/components/flow-canvas/pastehelpers.test.ts","./src/components/flow-canvas/pastehelpers.ts","./src/components/flow-canvas/useflowcanvasalignmentguides.ts","./src/components/flow-canvas/useflowcanvasconnectionstate.ts","./src/components/flow-canvas/useflowcanvascontextactions.ts","./src/components/flow-canvas/useflowcanvasdragdrop.ts","./src/components/flow-canvas/useflowcanvasinteractionlod.ts","./src/components/flow-canvas/useflowcanvasmenus.ts","./src/components/flow-canvas/useflowcanvasmenusandactions.test.tsx","./src/components/flow-canvas/useflowcanvasmenusandactions.ts","./src/components/flow-canvas/useflowcanvasnodedraghandlers.ts","./src/components/flow-canvas/useflowcanvaspaste.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.test.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.ts","./src/components/flow-canvas/useflowcanvasselectiontools.test.tsx","./src/components/flow-canvas/useflowcanvasselectiontools.ts","./src/components/flow-canvas/useflowcanvasviewstate.test.ts","./src/components/flow-canvas/useflowcanvasviewstate.ts","./src/components/flow-canvas/useflowcanvaszoomlod.ts","./src/components/flow-editor/collaborationpresenceoverlay.test.tsx","./src/components/flow-editor/collaborationpresenceoverlay.tsx","./src/components/flow-editor/floweditorchrome.tsx","./src/components/flow-editor/buildfloweditorcontrollerparams.ts","./src/components/flow-editor/buildfloweditorscreencontrollerparams.ts","./src/components/flow-editor/chromeproptypes.ts","./src/components/flow-editor/floweditorchromeprops.ts","./src/components/flow-editor/infradslapply.test.ts","./src/components/flow-editor/infradslapply.ts","./src/components/flow-editor/panelprops.test.ts","./src/components/flow-editor/panelprops.ts","./src/components/flow-editor/shouldexitstudioonselection.test.ts","./src/components/flow-editor/shouldexitstudioonselection.ts","./src/components/flow-editor/usecollaborationnodepositions.ts","./src/components/flow-editor/usefloweditorcontroller.ts","./src/components/flow-editor/usefloweditorinteractionbindings.ts","./src/components/flow-editor/usefloweditorpanelactions.ts","./src/components/flow-editor/usefloweditorpanelprops.ts","./src/components/flow-editor/usefloweditorruntime.ts","./src/components/flow-editor/usefloweditorscreenbehavior.ts","./src/components/flow-editor/usefloweditorscreenmodel.ts","./src/components/flow-editor/usefloweditorscreenstate.ts","./src/components/flow-editor/usefloweditorshellcontroller.test.tsx","./src/components/flow-editor/usefloweditorshellcontroller.ts","./src/components/flow-editor/usefloweditorstudiocontroller.test.tsx","./src/components/flow-editor/usefloweditorstudiocontroller.ts","./src/components/flow-editor/useinfradslapply.ts","./src/components/home/githubcard.tsx","./src/components/home/homedashboard.tsx","./src/components/home/homeflowdialogs.tsx","./src/components/home/homesettingsview.tsx","./src/components/home/homesidebar.tsx","./src/components/home/hometemplatesview.tsx","./src/components/home/sidebarfooter.tsx","./src/components/home/welcomemodalstate.ts","./src/components/icons/assetsicon.tsx","./src/components/icons/openflowlogo.tsx","./src/components/infra-sync/infrasyncpanel.test.tsx","./src/components/infra-sync/infrasyncpanel.tsx","./src/components/journey/journeyscorecontrol.test.tsx","./src/components/journey/journeyscorecontrol.tsx","./src/components/properties/bulknodeproperties.test.tsx","./src/components/properties/bulknodeproperties.tsx","./src/components/properties/bulknodepropertiesfamilysections.tsx","./src/components/properties/bulknodepropertiessections.tsx","./src/components/properties/bulknodepropertiesutilitysections.tsx","./src/components/properties/colorpicker.test.tsx","./src/components/properties/colorpicker.tsx","./src/components/properties/customcolorpopover.tsx","./src/components/properties/diagramnodepropertiesrouter.test.ts","./src/components/properties/diagramnodepropertiesrouter.tsx","./src/components/properties/edgeproperties.tsx","./src/components/properties/iconpicker.tsx","./src/components/properties/icontilepickerprimitives.tsx","./src/components/properties/imageupload.tsx","./src/components/properties/inspectorprimitives.tsx","./src/components/properties/nodeactionbuttons.tsx","./src/components/properties/nodecontentsection.tsx","./src/components/properties/nodeimagesettingssection.tsx","./src/components/properties/nodeproperties.test.tsx","./src/components/properties/nodeproperties.tsx","./src/components/properties/nodewireframevariantsection.tsx","./src/components/properties/propertysliderrow.tsx","./src/components/properties/segmentedchoice.tsx","./src/components/properties/shapeselector.tsx","./src/components/properties/swatchpicker.tsx","./src/components/properties/bulknodepropertiesmodel.test.ts","./src/components/properties/bulknodepropertiesmodel.ts","./src/components/properties/colorpickerutils.ts","./src/components/properties/propertyinputbehavior.test.ts","./src/components/properties/propertyinputbehavior.ts","./src/components/properties/sectionactionbuilder.ts","./src/components/properties/shared.ts","./src/components/properties/wireframevariants.ts","./src/components/properties/edge/architectureedgesemanticssection.tsx","./src/components/properties/edge/edgecolorsection.test.tsx","./src/components/properties/edge/edgecolorsection.tsx","./src/components/properties/edge/edgeconditionsection.tsx","./src/components/properties/edge/edgelabelsection.tsx","./src/components/properties/edge/edgerelationsection.test.tsx","./src/components/properties/edge/edgerelationsection.tsx","./src/components/properties/edge/edgeroutesection.test.tsx","./src/components/properties/edge/edgeroutesection.tsx","./src/components/properties/edge/edgestylesection.tsx","./src/components/properties/edge/sequencemessagesection.tsx","./src/components/properties/edge/architecturesemantics.test.ts","./src/components/properties/edge/architecturesemantics.ts","./src/components/properties/edge/edgecolorutils.ts","./src/components/properties/edge/edgelabelmodel.test.ts","./src/components/properties/edge/edgelabelmodel.ts","./src/components/properties/edge/errelationoptions.ts","./src/components/properties/families/architecturenodeproperties.test.tsx","./src/components/properties/families/architecturenodeproperties.tsx","./src/components/properties/families/architecturenodesection.tsx","./src/components/properties/families/classdiagramnodeproperties.tsx","./src/components/properties/families/classmemberlisteditor.tsx","./src/components/properties/families/classnodesection.tsx","./src/components/properties/families/erdiagramnodeproperties.tsx","./src/components/properties/families/entityfieldlisteditor.tsx","./src/components/properties/families/entitynodesection.tsx","./src/components/properties/families/journeynodeproperties.tsx","./src/components/properties/families/journeynodesection.tsx","./src/components/properties/families/mindmapnodeproperties.test.tsx","./src/components/properties/families/mindmapnodeproperties.tsx","./src/components/properties/families/sequencenodeproperties.tsx","./src/components/properties/families/sequencenodesection.tsx","./src/components/properties/families/specializednodecolorsections.test.tsx","./src/components/properties/families/structuredtextlisteditor.tsx","./src/components/properties/families/architectureoptions.ts","./src/components/studio-code-panel/usestudiocodepanelcontroller.test.tsx","./src/components/studio-code-panel/usestudiocodepanelcontroller.ts","./src/components/templates/templatepresentation.tsx","./src/components/toolbar/toolbaraddmenu.test.tsx","./src/components/toolbar/toolbaraddmenu.tsx","./src/components/toolbar/toolbaraddmenupanel.tsx","./src/components/toolbar/toolbarhistorycontrols.tsx","./src/components/toolbar/toolbarmodecontrols.tsx","./src/components/toolbar/toolbarbuttonstyles.ts","./src/components/top-nav/savestatusindicator.tsx","./src/components/top-nav/topnavactions.tsx","./src/components/top-nav/topnavbrand.tsx","./src/components/top-nav/topnavmenu.test.tsx","./src/components/top-nav/topnavmenu.tsx","./src/components/top-nav/topnavmenupanel.tsx","./src/components/top-nav/usetopnavstate.ts","./src/components/ui/button.tsx","./src/components/ui/collapsiblesection.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/searchfield.tsx","./src/components/ui/segmentedtabs.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/sidebaritem.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toastcontext.tsx","./src/components/ui/editorfieldstyles.ts","./src/config/aiproviders.test.ts","./src/config/aiproviders.ts","./src/config/rolloutflags.ts","./src/context/architecturelintcontext.tsx","./src/context/cinematicexportcontext.tsx","./src/context/diagramdiffcontext.tsx","./src/context/themecontext.tsx","./src/diagram-types/bootstrap.test.ts","./src/diagram-types/bootstrap.ts","./src/diagram-types/builtinplugins.ts","./src/diagram-types/builtinpropertypanels.ts","./src/diagram-types/registerbuiltinplugins.test.ts","./src/diagram-types/registerbuiltinplugins.ts","./src/diagram-types/registerbuiltinpropertypanels.test.ts","./src/diagram-types/registerbuiltinpropertypanels.ts","./src/diagram-types/architecture/fuzzcorpus.test.ts","./src/diagram-types/architecture/plugin.test.ts","./src/diagram-types/architecture/plugin.ts","./src/diagram-types/classdiagram/fuzzcorpus.test.ts","./src/diagram-types/classdiagram/plugin.test.ts","./src/diagram-types/classdiagram/plugin.ts","./src/diagram-types/core/contracts.ts","./src/diagram-types/core/index.ts","./src/diagram-types/core/propertypanels.test.ts","./src/diagram-types/core/propertypanels.ts","./src/diagram-types/core/registry.ts","./src/diagram-types/erdiagram/fuzzcorpus.test.ts","./src/diagram-types/erdiagram/plugin.test.ts","./src/diagram-types/erdiagram/plugin.ts","./src/diagram-types/flowchart/plugin.ts","./src/diagram-types/journey/fuzzcorpus.test.ts","./src/diagram-types/journey/plugin.test.ts","./src/diagram-types/journey/plugin.ts","./src/diagram-types/mindmap/fuzzcorpus.test.ts","./src/diagram-types/mindmap/plugin.test.ts","./src/diagram-types/mindmap/plugin.ts","./src/diagram-types/sequence/plugin.test.ts","./src/diagram-types/sequence/plugin.ts","./src/diagram-types/statediagram/plugin.test.ts","./src/diagram-types/statediagram/plugin.ts","./src/docs/docsroutes.ts","./src/docs/publicdocscatalog.js","./src/hooks/edgeconnectinteractions.test.ts","./src/hooks/edgeconnectinteractions.ts","./src/hooks/flowexportviewport.test.ts","./src/hooks/flowexportviewport.ts","./src/hooks/mindmaptopicactionrequest.ts","./src/hooks/nodelabeleditrequest.test.ts","./src/hooks/nodelabeleditrequest.ts","./src/hooks/nodequickcreaterequest.test.ts","./src/hooks/nodequickcreaterequest.ts","./src/hooks/snapshotpolicy.test.ts","./src/hooks/snapshotpolicy.ts","./src/hooks/useaigeneration.ts","./src/hooks/useanalyticspreference.ts","./src/hooks/useanimatededgeperformancewarning.test.ts","./src/hooks/useanimatededgeperformancewarning.ts","./src/hooks/useassetcatalog.ts","./src/hooks/usecinematicexport.ts","./src/hooks/useclipboardoperations.ts","./src/hooks/usedesignsystem.ts","./src/hooks/useedgeinteractions.ts","./src/hooks/useedgeoperations.ts","./src/hooks/usefloweditoractions.test.ts","./src/hooks/usefloweditoractions.ts","./src/hooks/usefloweditorcallbacks.ts","./src/hooks/usefloweditorcollaboration.test.ts","./src/hooks/usefloweditorcollaboration.ts","./src/hooks/usefloweditoruistate.ts","./src/hooks/useflowexport.ts","./src/hooks/useflowhistory.test.ts","./src/hooks/useflowhistory.ts","./src/hooks/useflowoperations.ts","./src/hooks/usegithubstars.ts","./src/hooks/useinfrasync.ts","./src/hooks/useinlinenodetextedit.test.ts","./src/hooks/useinlinenodetextedit.ts","./src/hooks/usekeyboardshortcuts.test.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/uselayoutoperations.ts","./src/hooks/usemarkdowneditor.ts","./src/hooks/usemenukeyboardnavigation.ts","./src/hooks/usemodifierkeys.test.ts","./src/hooks/usemodifierkeys.ts","./src/hooks/usenodeoperations.ts","./src/hooks/useplayback.ts","./src/hooks/useprovidershapepreview.ts","./src/hooks/userecentimports.ts","./src/hooks/useshiftheld.ts","./src/hooks/usesnapshots.ts","./src/hooks/usestaticexport.ts","./src/hooks/usestoragepressureguard.ts","./src/hooks/usestructuredlisteditor.ts","./src/hooks/usestyleclipboard.test.ts","./src/hooks/usestyleclipboard.ts","./src/hooks/ai-generation/chathistorystorage.test.ts","./src/hooks/ai-generation/chathistorystorage.ts","./src/hooks/ai-generation/codetoarchitecture.test.ts","./src/hooks/ai-generation/codetoarchitecture.ts","./src/hooks/ai-generation/codebaseanalyzer.test.ts","./src/hooks/ai-generation/codebaseanalyzer.ts","./src/hooks/ai-generation/codebaseparser.test.ts","./src/hooks/ai-generation/codebaseparser.ts","./src/hooks/ai-generation/codebasetonativediagram.test.ts","./src/hooks/ai-generation/codebasetonativediagram.ts","./src/hooks/ai-generation/graphcomposer.test.ts","./src/hooks/ai-generation/graphcomposer.ts","./src/hooks/ai-generation/nodeactionprompts.test.ts","./src/hooks/ai-generation/nodeactionprompts.ts","./src/hooks/ai-generation/openapiparser.test.ts","./src/hooks/ai-generation/openapiparser.ts","./src/hooks/ai-generation/openapitosequence.ts","./src/hooks/ai-generation/positionpreservingapply.test.ts","./src/hooks/ai-generation/positionpreservingapply.ts","./src/hooks/ai-generation/readiness.test.ts","./src/hooks/ai-generation/readiness.ts","./src/hooks/ai-generation/requestlifecycle.test.ts","./src/hooks/ai-generation/requestlifecycle.ts","./src/hooks/ai-generation/sqlparser.test.ts","./src/hooks/ai-generation/sqlparser.ts","./src/hooks/ai-generation/sqltoerd.ts","./src/hooks/ai-generation/streamingparser.ts","./src/hooks/ai-generation/streamingstore.ts","./src/hooks/ai-generation/terraformtocloud.ts","./src/hooks/edge-operations/utils.test.ts","./src/hooks/edge-operations/utils.ts","./src/hooks/flow-editor-actions/exporthandlers.test.ts","./src/hooks/flow-editor-actions/exporthandlers.ts","./src/hooks/flow-editor-actions/helpers.ts","./src/hooks/flow-editor-actions/layouthandlers.test.ts","./src/hooks/flow-editor-actions/layouthandlers.ts","./src/hooks/flow-export/diagramdocumenttransfer.test.ts","./src/hooks/flow-export/diagramdocumenttransfer.ts","./src/hooks/flow-export/exportcapture.test.ts","./src/hooks/flow-export/exportcapture.ts","./src/hooks/flow-operations/useflowcoreactions.ts","./src/hooks/node-operations/createconnectedsibling.ts","./src/hooks/node-operations/dragstopreconcilepolicy.test.ts","./src/hooks/node-operations/dragstopreconcilepolicy.ts","./src/hooks/node-operations/nodefactories.ts","./src/hooks/node-operations/routingduringdrag.test.ts","./src/hooks/node-operations/routingduringdrag.ts","./src/hooks/node-operations/sectionbounds.ts","./src/hooks/node-operations/sectionhittesting.ts","./src/hooks/node-operations/sectionoperations.ts","./src/hooks/node-operations/usearchitecturenodeoperations.test.ts","./src/hooks/node-operations/usearchitecturenodeoperations.ts","./src/hooks/node-operations/usemindmapnodeoperations.test.ts","./src/hooks/node-operations/usemindmapnodeoperations.ts","./src/hooks/node-operations/usenodedragoperations.test.ts","./src/hooks/node-operations/usenodedragoperations.ts","./src/hooks/node-operations/usenodeoperationadders.ts","./src/hooks/node-operations/utils.test.ts","./src/hooks/node-operations/utils.ts","./src/i18n/config.test.ts","./src/i18n/config.ts","./src/i18n/strictmodelocalecoverage.test.ts","./src/i18n/usedlocalecoverage.test.ts","./src/lib/aiiconspipeline.test.ts","./src/lib/architecturetemplatedata.ts","./src/lib/architecturetemplates.test.ts","./src/lib/architecturetemplates.ts","./src/lib/brand.ts","./src/lib/classmembers.ts","./src/lib/colorutils.ts","./src/lib/connectcreationpolicy.test.ts","./src/lib/connectcreationpolicy.ts","./src/lib/date.ts","./src/lib/designtokens.ts","./src/lib/devperformance.ts","./src/lib/entityfields.ts","./src/lib/ertoclassconversion.test.ts","./src/lib/ertoclassconversion.ts","./src/lib/exportfilename.test.ts","./src/lib/exportfilename.ts","./src/lib/flowminddslparserv2.test.ts","./src/lib/flowminddslparserv2.ts","./src/lib/fuzzymatch.ts","./src/lib/genericshapepolicy.ts","./src/lib/iconmatcher.test.ts","./src/lib/iconmatcher.ts","./src/lib/iconresolver.test.ts","./src/lib/iconresolver.ts","./src/lib/id.ts","./src/lib/index.ts","./src/lib/legacybranding.ts","./src/lib/logger.ts","./src/lib/mermaidenrichmentpipeline.test.ts","./src/lib/mermaidparser.ts","./src/lib/mermaidparserhelpers.ts","./src/lib/mermaidparsermodel.ts","./src/lib/mindmaplayout.test.ts","./src/lib/mindmaplayout.ts","./src/lib/mindmaplayoutengine.ts","./src/lib/mindmaptree.ts","./src/lib/nodebulkediting.test.ts","./src/lib/nodebulkediting.ts","./src/lib/nodeenricher.test.ts","./src/lib/nodeenricher.ts","./src/lib/nodehandles.ts","./src/lib/nodeparent.ts","./src/lib/nodestyledata.test.ts","./src/lib/nodestyledata.ts","./src/lib/openflowdslparser.ts","./src/lib/openflowdslparserv2.ts","./src/lib/reactflowcompat.ts","./src/lib/reconnectedge.test.ts","./src/lib/reconnectedge.ts","./src/lib/relationsemantics.test.ts","./src/lib/relationsemantics.ts","./src/lib/releasestaleelkroutes.test.ts","./src/lib/releasestaleelkroutes.ts","./src/lib/result.ts","./src/lib/semanticclassifier.test.ts","./src/lib/semanticclassifier.ts","./src/lib/storagepressure.test.ts","./src/lib/storagepressure.ts","./src/lib/types.ts","./src/lib/xsssafety.test.tsx","./src/services/aligndistribute.ts","./src/services/aiservice.test.ts","./src/services/aiservice.ts","./src/services/aiserviceschemas.ts","./src/services/animatedexport.test.ts","./src/services/animatedexport.ts","./src/services/architectureroundtrip.test.ts","./src/services/assetcatalog.test.ts","./src/services/assetcatalog.ts","./src/services/assetpresentation.ts","./src/services/canonicalserialization.test.ts","./src/services/canonicalserialization.ts","./src/services/composediagramfordisplay.test.ts","./src/services/composediagramfordisplay.ts","./src/services/diagramdocument.test.ts","./src/services/diagramdocument.ts","./src/services/diagramdocumentschemas.ts","./src/services/domainlibrary.test.ts","./src/services/domainlibrary.ts","./src/services/elklayout.test.ts","./src/services/elklayout.ts","./src/services/exportservice.test.ts","./src/services/exportservice.ts","./src/services/figmaexportservice.ts","./src/services/flowchartroundtrip.test.ts","./src/services/geminiservice.ts","./src/services/geminisysteminstruction.ts","./src/services/gifencoder.test.ts","./src/services/gifencoder.ts","./src/services/githubfetcher.test.ts","./src/services/githubfetcher.ts","./src/services/iconassetcatalog.ts","./src/services/importfidelity.test.ts","./src/services/importfidelity.ts","./src/services/mermaidparser.test.ts","./src/services/openflowdslexporter.test.ts","./src/services/openflowdslexporter.ts","./src/services/openflowdslparser.test.ts","./src/services/openflowroundtripgolden.test.ts","./src/services/openflowroundtripgoldenfixtures.ts","./src/services/operationfeedback.ts","./src/services/remainingfamiliesroundtrip.test.ts","./src/services/smartedgerouting.test.ts","./src/services/smartedgerouting.ts","./src/services/statediagramroundtrip.test.ts","./src/services/templates.selector.test.ts","./src/services/templates.ts","./src/services/zipextractor.ts","./src/services/ai/contextserializer.test.ts","./src/services/ai/contextserializer.ts","./src/services/analytics/analytics.ts","./src/services/analytics/analyticssettings.test.ts","./src/services/analytics/analyticssettings.ts","./src/services/analytics/surfaceanalyticsclient.ts","./src/services/architecturelint/defaultrules.ts","./src/services/architecturelint/ruleengine.test.ts","./src/services/architecturelint/ruleengine.ts","./src/services/architecturelint/rulelibrary.ts","./src/services/architecturelint/types.ts","./src/services/architecturelint/workspacerules.ts","./src/services/collaboration/bootstrap.test.ts","./src/services/collaboration/bootstrap.ts","./src/services/collaboration/canvasdiff.test.ts","./src/services/collaboration/canvasdiff.ts","./src/services/collaboration/comments.test.ts","./src/services/collaboration/comments.ts","./src/services/collaboration/contracts.test.ts","./src/services/collaboration/contracts.ts","./src/services/collaboration/hookutils.ts","./src/services/collaboration/operationlog.test.ts","./src/services/collaboration/operationlog.ts","./src/services/collaboration/presenceviewmodel.test.ts","./src/services/collaboration/presenceviewmodel.ts","./src/services/collaboration/reducer.test.ts","./src/services/collaboration/reducer.ts","./src/services/collaboration/roomconfig.ts","./src/services/collaboration/roomlink.test.ts","./src/services/collaboration/roomlink.ts","./src/services/collaboration/runtimecontroller.test.ts","./src/services/collaboration/runtimecontroller.ts","./src/services/collaboration/runtimehookutils.test.ts","./src/services/collaboration/runtimehookutils.ts","./src/services/collaboration/schemas.ts","./src/services/collaboration/session.test.ts","./src/services/collaboration/session.ts","./src/services/collaboration/storebridge.test.ts","./src/services/collaboration/storebridge.ts","./src/services/collaboration/transport.test.ts","./src/services/collaboration/transport.ts","./src/services/collaboration/transportfactory.test.ts","./src/services/collaboration/transportfactory.ts","./src/services/collaboration/types.ts","./src/services/collaboration/versioning.test.ts","./src/services/collaboration/versioning.ts","./src/services/collaboration/yjspeertransport.test.ts","./src/services/collaboration/yjspeertransport.ts","./src/services/diagramdiff/diffengine.ts","./src/services/elk-layout/boundaryfanout.ts","./src/services/elk-layout/determinism.ts","./src/services/elk-layout/options.ts","./src/services/elk-layout/types.ts","./src/services/export/cinematicbuildplan.test.ts","./src/services/export/cinematicbuildplan.ts","./src/services/export/cinematicexport.ts","./src/services/export/cinematicexporttheme.test.ts","./src/services/export/cinematicexporttheme.ts","./src/services/export/cinematicrenderstate.test.ts","./src/services/export/cinematicrenderstate.ts","./src/services/export/formatting.ts","./src/services/export/mermaidbuilder.test.ts","./src/services/export/mermaidbuilder.ts","./src/services/export/mermaidexportquality.test.ts","./src/services/export/pdfdocument.test.ts","./src/services/export/pdfdocument.ts","./src/services/export/plantumlbuilder.ts","./src/services/export/mermaid/architecturemermaid.ts","./src/services/export/mermaid/classdiagrammermaid.ts","./src/services/export/mermaid/erdiagrammermaid.ts","./src/services/export/mermaid/journeymermaid.ts","./src/services/export/mermaid/mindmapmermaid.ts","./src/services/export/mermaid/sequencemermaid.ts","./src/services/export/mermaid/statediagrammermaid.ts","./src/services/figma/edgehelpers.ts","./src/services/figma/iconhelpers.ts","./src/services/figma/nodelayers.ts","./src/services/figma/themehelpers.ts","./src/services/figmaimport/figmaapiclient.ts","./src/services/flowpilot/assetgrounding.test.ts","./src/services/flowpilot/assetgrounding.ts","./src/services/flowpilot/prompting.ts","./src/services/flowpilot/responsepolicy.test.ts","./src/services/flowpilot/responsepolicy.ts","./src/services/flowpilot/skills.ts","./src/services/flowpilot/thread.ts","./src/services/flowpilot/types.ts","./src/services/infrasync/dockercomposeparser.test.ts","./src/services/infrasync/dockercomposeparser.ts","./src/services/infrasync/infratodsl.test.ts","./src/services/infrasync/infratodsl.ts","./src/services/infrasync/kubernetesparser.test.ts","./src/services/infrasync/kubernetesparser.ts","./src/services/infrasync/terraformstateparser.test.ts","./src/services/infrasync/terraformstateparser.ts","./src/services/infrasync/types.ts","./src/services/mermaid/detectdiagramtype.test.ts","./src/services/mermaid/detectdiagramtype.ts","./src/services/mermaid/diagnosticformatting.test.ts","./src/services/mermaid/diagnosticformatting.ts","./src/services/mermaid/parsemermaidbytype.test.ts","./src/services/mermaid/parsemermaidbytype.ts","./src/services/mermaid/strictmodediagnosticspresentation.test.ts","./src/services/mermaid/strictmodediagnosticspresentation.ts","./src/services/mermaid/strictmodeguidance.test.ts","./src/services/mermaid/strictmodeguidance.ts","./src/services/mermaid/strictmodeuxregression.test.ts","./src/services/offline/registerappshellserviceworker.test.ts","./src/services/offline/registerappshellserviceworker.ts","./src/services/onboarding/config.ts","./src/services/onboarding/eventschemas.ts","./src/services/onboarding/events.test.ts","./src/services/onboarding/events.ts","./src/services/playback/contracts.test.ts","./src/services/playback/contracts.ts","./src/services/playback/model.test.ts","./src/services/playback/model.ts","./src/services/playback/studio.test.ts","./src/services/playback/studio.ts","./src/services/sequence/sequencemessage.test.ts","./src/services/sequence/sequencemessage.ts","./src/services/shapelibrary/bootstrap.test.ts","./src/services/shapelibrary/bootstrap.ts","./src/services/shapelibrary/ingestionpipeline.test.ts","./src/services/shapelibrary/ingestionpipeline.ts","./src/services/shapelibrary/manifestvalidation.test.ts","./src/services/shapelibrary/manifestvalidation.ts","./src/services/shapelibrary/providercatalog.test.ts","./src/services/shapelibrary/providercatalog.ts","./src/services/shapelibrary/registry.test.ts","./src/services/shapelibrary/registry.ts","./src/services/shapelibrary/starterpacks.ts","./src/services/shapelibrary/types.ts","./src/services/storage/fallbackstorage.ts","./src/services/storage/flowdocumentmodel.test.ts","./src/services/storage/flowdocumentmodel.ts","./src/services/storage/flowpersiststorage.test.ts","./src/services/storage/flowpersiststorage.ts","./src/services/storage/indexeddbhelpers.ts","./src/services/storage/indexeddbschema.test.ts","./src/services/storage/indexeddbschema.ts","./src/services/storage/indexeddbstatestorage.test.ts","./src/services/storage/indexeddbstatestorage.ts","./src/services/storage/localfirstrepository.ts","./src/services/storage/localfirstruntime.ts","./src/services/storage/persisteddocumentadapters.test.ts","./src/services/storage/persisteddocumentadapters.ts","./src/services/storage/persistencetypes.ts","./src/services/storage/snapshotstorage.test.ts","./src/services/storage/snapshotstorage.ts","./src/services/storage/storageruntime.test.ts","./src/services/storage/storageruntime.ts","./src/services/storage/storageschemas.test.ts","./src/services/storage/storageschemas.ts","./src/services/storage/storagetelemetry.test.ts","./src/services/storage/storagetelemetry.ts","./src/services/storage/storagetelemetrysink.test.ts","./src/services/storage/storagetelemetrysink.ts","./src/services/storage/uilocalstorage.test.ts","./src/services/storage/uilocalstorage.ts","./src/services/templatelibrary/registry.test.ts","./src/services/templatelibrary/registry.ts","./src/services/templatelibrary/startertemplates.assets.test.ts","./src/services/templatelibrary/startertemplates.test.ts","./src/services/templatelibrary/startertemplates.ts","./src/services/templatelibrary/templatefactories.ts","./src/services/templatelibrary/types.ts","./src/store/actionfactory.ts","./src/store/aisettings.test.ts","./src/store/aisettings.ts","./src/store/aisettingspersistence.ts","./src/store/aisettingsschemas.test.ts","./src/store/aisettingsschemas.ts","./src/store/canvashooks.ts","./src/store/createflowstore.test.ts","./src/store/createflowstore.ts","./src/store/createflowstorepersistoptions.ts","./src/store/createflowstorestate.ts","./src/store/defaults.ts","./src/store/designsystemhooks.ts","./src/store/documenthooks.ts","./src/store/documentstatesync.test.ts","./src/store/documentstatesync.ts","./src/store/editorpagehooks.ts","./src/store/historyhooks.ts","./src/store/historystate.ts","./src/store/persistence.test.ts","./src/store/persistence.ts","./src/store/persistenceschemas.ts","./src/store/selectionhooks.ts","./src/store/selectors.test.ts","./src/store/selectors.ts","./src/store/tabhooks.ts","./src/store/types.ts","./src/store/viewhooks.ts","./src/store/workspacedocumentmodel.test.ts","./src/store/workspacedocumentmodel.ts","./src/store/actions/createaiandselectionactions.ts","./src/store/actions/createcanvasactions.ts","./src/store/actions/createdesignsystemactions.ts","./src/store/actions/createhistoryactions.test.ts","./src/store/actions/createhistoryactions.ts","./src/store/actions/createlayeractions.ts","./src/store/actions/createtabactions.ts","./src/store/actions/createviewactions.ts","./src/store/actions/createworkspacedocumentactions.ts","./src/store/actions/synctabnodesedges.ts","./src/store/slices/createcanvaseditorslice.ts","./src/store/slices/createexperienceslice.ts","./src/store/slices/createworkspaceslice.ts"],"version":"5.8.3"} \ No newline at end of file
๐Ÿง‘โ€๐Ÿ’ป Code โ†’ Diagram
SQL ยท Terraform ยท K8s
OpenAPI ยท Source code
โœจ Mermaid โ†’ Icons
Paste Mermaid ยท 1,100+ icons
auto-assigned ยท beautiful
๐Ÿค– AI Generation
9 providers ยท BYOK
Direct-to-canvas output
`{}` Diagram as Code
Bidirectional live sync
Git-friendly DSL
๐Ÿงฉ Asset Libraries
Developer ยท AWS ยท Azure
GCP ยท CNCF ยท Icons