diff --git a/echo/.gitignore b/echo/.gitignore index 1bf72605..aed7e6a5 100644 --- a/echo/.gitignore +++ b/echo/.gitignore @@ -30,4 +30,6 @@ __queuestorage__echo/server/dembrane/workspace_script.py tools/translator echo-gitops/ -cypress/reports/ \ No newline at end of file +cypress/reports/ + +cookies.txt \ No newline at end of file diff --git a/echo/.vscode/sessions.json b/echo/.vscode/sessions.json index 7a0e3b91..c330c33e 100644 --- a/echo/.vscode/sessions.json +++ b/echo/.vscode/sessions.json @@ -1,68 +1,68 @@ { "$schema": "https://cdn.statically.io/gh/nguyenngoclongdev/cdn/main/schema/v11/terminal-keeper.json", - "theme": "tribe", "active": "default", "keepExistingTerminals": false, "sessions": { "default": [ { "autoExecuteCommands": true, - "name": "server", - "icon": "server", "commands": [ "cd server", "./run.sh" - ] + ], + "icon": "server", + "name": "server" }, [ { "autoExecuteCommands": true, - "name": "workers", - "icon": "gear", "commands": [ "cd server", "./run-worker.sh" - ] + ], + "icon": "gear", + "name": "workers" }, { "autoExecuteCommands": true, - "name": "workers-cpu", - "icon": "gear", "commands": [ "cd server", "./run-worker-cpu.sh" - ] + ], + "icon": "gear", + "name": "workers-cpu" }, { "autoExecuteCommands": true, - "name": "scheduler", - "icon": "clock", "commands": [ "cd server", "./run-scheduler.sh" - ] + ], + "icon": "clock", + "name": "scheduler" } ], [ { "autoExecuteCommands": true, - "name": "admin-dashboard", - "icon": "browser", "commands": [ "cd frontend", "pnpm run dev" - ] + ], + "icon": "browser", + "name": "admin-dashboard" }, { "autoExecuteCommands": true, - "name": "participant-portal", - "icon": "browser", "commands": [ "cd frontend", "pnpm run participant:dev" - ] + ], + "icon": "browser", + "name": "participant-portal" } ] ] - } + }, + "theme": "tribe" } \ No newline at end of file diff --git a/echo/.vscode/settings.json b/echo/.vscode/settings.json index 91779e2f..2f840a33 100644 --- a/echo/.vscode/settings.json +++ b/echo/.vscode/settings.json @@ -1,41 +1,42 @@ { - "files.eol": "\n", - "debug.internalConsoleOptions": "neverOpen", - // py - "cursorpyright.analysis.typeCheckingMode": "off", - "python.languageServer": "None", - "python.defaultInterpreterPath": "./server/.venv/bin/python", - "ruff.lint.enable": true, - "ruff.configuration": "./server/pyproject.toml", - "mypy.runUsingActiveInterpreter": true, - "mypy.targets": ["./server/dembrane"], - "mypy.configFile": "./server/pyproject.toml", - "[python]": { - "editor.defaultFormatter": "charliermarsh.ruff", - "editor.useTabStops": true, - "editor.tabSize": 4, - "editor.formatOnSave": true - }, - "python.testing.pytestArgs": ["server"], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.testing.autoTestDiscoverOnSaveEnabled": true, - // ts - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - "editor.useTabStops": true - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.useTabStops": true, - "editor.formatOnSave": true - }, - "biome.enabled": true, - "biome.lsp.bin": "frontend/node_modules/.bin/biome", - "biome.configurationPath": "frontend/biome.json", - "editor.codeActionsOnSave": { - "source.fixAll.biome": "always", - "source.action.organizeImports.biome": "always" - } + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.tabSize": 4, + "editor.useTabStops": true + }, + // ts + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.useTabStops": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.useTabStops": true + }, + "biome.configurationPath": "frontend/biome.json", + "biome.enabled": true, + "biome.lsp.bin": "frontend/node_modules/.bin/biome", + // py + "cursorpyright.analysis.typeCheckingMode": "off", + "debug.internalConsoleOptions": "neverOpen", + "editor.codeActionsOnSave": { + "source.action.organizeImports.biome": "always", + "source.fixAll.biome": "always" + }, + "files.eol": "\n", + "mypy.configFile": "./server/pyproject.toml", + "mypy.runUsingActiveInterpreter": true, + "mypy.targets": ["./server/dembrane"], + "python.defaultInterpreterPath": "./server/.venv/bin/python", + "python.languageServer": "None", + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.pytestArgs": ["server"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "ruff.configuration": "./server/pyproject.toml", + "ruff.lint.enable": true, + "terminal.integrated.enableVisualBell": true } diff --git a/echo/AGENTS.md b/echo/AGENTS.md index 050c5064..479df426 100644 --- a/echo/AGENTS.md +++ b/echo/AGENTS.md @@ -158,6 +158,8 @@ bash echo/server/scripts/agentic/latest_runs.sh --chat-id --limit 1 | `frontend/src/config.ts` | Frontend feature flags | | `server/dembrane/settings.py` | Backend configuration | | `docs/frontend_translations.md` | Translation workflow | +| `docs/branching_and_releases.md` | Branching strategy, release process, hotfixes | +| `docs/database_migrations.md` | Directus data migration steps | ## Code Style @@ -165,28 +167,85 @@ bash echo/server/scripts/agentic/latest_runs.sh --chat-id --limit 1 - Backend: Python 3.11+, FastAPI, Pydantic - Use existing patterns in the codebase as reference -## Dev Notes +## Branching Strategy & Deployment -### Recent Changes (testing branch) -- Copy guide enforcement: "context limit" → "selection too large" -- Translations updated for all 6 languages -- Suggestions use faster model (`TEXT_FAST` instead of `MULTI_MODAL_PRO`) -- Stream status shows inline under "Thinking..." instead of toast -- Webhooks (conversation-level notifications) +See [docs/branching_and_releases.md](docs/branching_and_releases.md) for the full guide including hotfix process, release checklist, and ASCII diagrams. -### Tech Debt / Known Issues +Quick reference: +- **Feature flow**: branch off `main` → (optional) merge to `testing` → PR to `main` → auto-deploys to Echo Next +- **Releases**: tagged from `main` every ~2 weeks → auto-deploys to production +- **Hotfixes**: branch off release tag → fix → new release → cherry-pick into main +- **Project management**: Linear (`ECHO-xxx` tickets, two-week cycles) +- **GitOps**: `dembrane/echo-gitops` (Terraform + Helm + Argo CD) + +## Architecture Notes + +### High-Level Stack + +``` +Frontend (React/Vite/Mantine) → Backend API (FastAPI) → Directus (headless CMS/DB) + ↕ ↕ + Dramatiq Workers PostgreSQL + (gevent + standard) + ↕ + Redis (pub/sub, task broker, caching) + ↕ + Agent Service (LangGraph, port 8001) +``` + +- **Directus** is the data layer — all collections (projects, conversations, reports, etc.) live there +- **FastAPI** handles API routes, SSE streaming, and orchestration +- **Dramatiq** handles background work: transcription, summarization, report generation +- **Redis** is used for task brokering, pub/sub (SSE progress), and caching +- **LiteLLM** routes all LLM calls with automatic failover between deployments + +### Report Generation Pipeline + +Report generation runs **synchronously** in Dramatiq network-queue workers (no asyncio — this was a deliberate choice after recurring event-loop corruption bugs): + +1. Fetch conversations for the project +2. Fan-out summarization of individual conversations via `dramatiq.group()` +3. Poll Redis for group completion +4. Refetch conversations with summaries +5. Fetch full transcripts via `gevent.pool.Pool` (concurrent I/O) +6. Build prompt with token budget management +7. Call LLM via `router_completion()` (sync litellm, uses `MULTI_MODAL_PRO`) + +Key files: +- `server/dembrane/report_generation.py` — main pipeline +- `server/dembrane/report_events.py` — Redis pub/sub for real-time SSE progress +- `server/prompt_templates/system_report.{lang}.jinja` — per-language prompt templates (written IN the target language) + +### BFF Pattern + +Backend For Frontend endpoints under `/bff/` aggregate data the frontend needs in a single call. This is the preferred pattern over having the frontend make multiple Directus SDK calls directly. + +Example: `/bff/projects/home` bundles pinned projects, paginated project list, search results, and admin info into one response. + +### Transcription Pipeline + +Two-step process: +1. **AssemblyAI** (`universal-3-pro`) for raw speech-to-text — supports en, es, pt, fr, de, it. Dutch ("nl") requires `universal-2` fallback. +2. **Gemini correction** — fixes transcripts, normalizes hotwords, PII redaction, adds recording feedback + +Production uses webhook mode (`ASSEMBLYAI_WEBHOOK_URL`); polling is only a fallback path. + +### Agent Service + +The `agent/` directory contains the agentic chat service (LangGraph-based). It runs as a separate FastAPI service on port 8001. Agentic chat streams via `POST /api/agentic/runs/{run_id}/stream` — no Dramatiq dispatch. See `agent/README.md`. + +## Tech Debt / Known Issues - Some mypy errors in `llm_router.py` and `settings.py` (pre-existing, non-blocking) -## Deployment Process +## Deployment Checklist -### Merging to Main (for echo-next environment) +### Before Merging to Main -1. **Compare branches**: `git log main..testing --oneline` -2. **Check for new env vars**: Look for new `Field()` definitions in `settings.py` and new exports in `config.ts` -3. **Update deployment env vars** if needed (see checklist below) -4. **Push Directus schema** if there were database changes -5. **Create PR**: `testing` → `main` -6. **Deploy** after merge +1. **Check for new env vars**: Look for new `Field()` definitions in `settings.py` and new exports in `config.ts` +2. **Update deployment env vars** if needed (see checklist below) +3. **Push Directus schema** if there were database changes (see `docs/database_migrations.md`) +4. **Create PR** from feature branch to `main` +5. After merge → auto-deploys to Echo Next ### Environment Variables Checklist @@ -225,4 +284,4 @@ LLM__MULTI_MODAL_FAST_2__GCP_SA_JSON=${GCP_SA_JSON} LLM__MULTI_MODAL_FAST_2__VERTEX_LOCATION=europe-west1 ``` -Model groups: `TEXT_FAST`, `MULTI_MODAL_PRO`, `MULTI_MODAL_FAST` +Model groups: `MULTI_MODAL_PRO` (Gemini 2.5 Pro — chat, reports, transcript correction), `MULTI_MODAL_FAST` (Gemini 2.5 Flash — suggestions, verification, lightweight tasks), `TEXT_FAST` (Azure GPT-4.1 — being deprecated) diff --git a/echo/cypress/cypress/downloads/merged-c73ccb37-8d3b-42e0-a51e-3edf8e20469b-eaa71516-0fcf-49e5-b9a7-3d45f4b5c4a6.mp3 b/echo/cypress/cypress/downloads/merged-c73ccb37-8d3b-42e0-a51e-3edf8e20469b-eaa71516-0fcf-49e5-b9a7-3d45f4b5c4a6.mp3 deleted file mode 100644 index 0723154d..00000000 Binary files a/echo/cypress/cypress/downloads/merged-c73ccb37-8d3b-42e0-a51e-3edf8e20469b-eaa71516-0fcf-49e5-b9a7-3d45f4b5c4a6.mp3 and /dev/null differ diff --git a/echo/cypress/cypress/downloads/transcript-1771916490192 b/echo/cypress/cypress/downloads/transcript-1771916490192 deleted file mode 100644 index 462f87a0..00000000 --- a/echo/cypress/cypress/downloads/transcript-1771916490192 +++ /dev/null @@ -1 +0,0 @@ -Hey, everybody. My name is Chris Nash. It's a new year, which means it's time for a new podcast. Now, unlike all of your other favorite podcasts, this one will only take one minute of your time. Every time, all the time. Sometimes it'll be just me. Other times it'll be just you. Seriously, submit stuff, your opinions, videos. I want to know what you have to say. I want to share what you have to say. Other times, I'll be joined by excellent guests like Cameron Hart of the Tournamental Podcast. Hey, Nash, that one minute podcast idea. Don't do it. It's a terrible idea. Swell. Some episodes will be really funny. Other episodes will be really serious. But I guarantee that every episode will be the best minute of your day. Warning. Depending on your preferences, one minute podcast may be the worst minute of your day. \ No newline at end of file diff --git a/echo/cypress/cypress/screenshots/04-create-edit-delete-project.cy.js/Project Create, Edit, and Delete Flow -- should create a project, edit its name and portal settings, verify changes, and delete it (failed).png b/echo/cypress/cypress/screenshots/04-create-edit-delete-project.cy.js/Project Create, Edit, and Delete Flow -- should create a project, edit its name and portal settings, verify changes, and delete it (failed).png deleted file mode 100644 index d6c065b8..00000000 Binary files a/echo/cypress/cypress/screenshots/04-create-edit-delete-project.cy.js/Project Create, Edit, and Delete Flow -- should create a project, edit its name and portal settings, verify changes, and delete it (failed).png and /dev/null differ diff --git a/echo/cypress/cypress/screenshots/30-report-lifecycle.cy.js/Report Lifecycle Flow -- creates a project and generates a report draft (failed).png b/echo/cypress/cypress/screenshots/30-report-lifecycle.cy.js/Report Lifecycle Flow -- creates a project and generates a report draft (failed).png deleted file mode 100644 index 9f51dd2e..00000000 Binary files a/echo/cypress/cypress/screenshots/30-report-lifecycle.cy.js/Report Lifecycle Flow -- creates a project and generates a report draft (failed).png and /dev/null differ diff --git a/echo/cypress/support/functions/report/index.js b/echo/cypress/support/functions/report/index.js index eabdc10e..094da3a6 100644 --- a/echo/cypress/support/functions/report/index.js +++ b/echo/cypress/support/functions/report/index.js @@ -43,15 +43,23 @@ export const generateReport = (langCode = 'en') => { // ============= Report Actions ============= /** - * Clicks the share button (mobile) + * Opens the report actions menu (three-dot menu) + */ +const openReportActionsMenu = () => { + cy.get('[data-testid="report-actions-menu"]').should('be.visible').click(); +}; + +/** + * Clicks the share button (in actions menu dropdown) */ export const shareReport = () => { cy.log('Sharing Report'); + openReportActionsMenu(); cy.get('[data-testid="report-share-button"]').should('be.visible').click(); }; /** - * Copies the report link + * Copies the report link (inline icon button, only when published) */ export const copyReportLink = () => { cy.log('Copying Report Link'); @@ -59,10 +67,11 @@ export const copyReportLink = () => { }; /** - * Prints the report + * Prints the report (in actions menu dropdown, only when published) */ export const printReport = () => { cy.log('Printing Report'); + openReportActionsMenu(); cy.get('[data-testid="report-print-button"]').should('be.visible').click(); }; diff --git a/echo/directus/sync/collections/folders.json b/echo/directus/sync/collections/folders.json index de17c519..2032803c 100644 --- a/echo/directus/sync/collections/folders.json +++ b/echo/directus/sync/collections/folders.json @@ -8,5 +8,10 @@ "name": "Public", "parent": null, "_syncId": "74232676-80e7-4f8c-8012-c0d59e6d0a24" + }, + { + "name": "avatars", + "parent": null, + "_syncId": "da1c3f3e-4398-4dda-950e-9123c0873fbb" } ] diff --git a/echo/directus/sync/collections/permissions.json b/echo/directus/sync/collections/permissions.json index 33dee44d..73946008 100644 --- a/echo/directus/sync/collections/permissions.json +++ b/echo/directus/sync/collections/permissions.json @@ -914,6 +914,13 @@ } } }, + { + "folder": { + "name": { + "_contains": "avatars" + } + } + }, { "folder": { "name": { diff --git a/echo/directus/sync/snapshot/collections/prompt_template.json b/echo/directus/sync/snapshot/collections/prompt_template.json new file mode 100644 index 00000000..d9eb5314 --- /dev/null +++ b/echo/directus/sync/snapshot/collections/prompt_template.json @@ -0,0 +1,28 @@ +{ + "collection": "prompt_template", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "prompt_template", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": null, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "prompt_template" + } +} diff --git a/echo/directus/sync/snapshot/collections/prompt_template_preference.json b/echo/directus/sync/snapshot/collections/prompt_template_preference.json new file mode 100644 index 00000000..e3034b06 --- /dev/null +++ b/echo/directus/sync/snapshot/collections/prompt_template_preference.json @@ -0,0 +1,28 @@ +{ + "collection": "prompt_template_preference", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "prompt_template_preference", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": null, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "prompt_template_preference" + } +} diff --git a/echo/directus/sync/snapshot/collections/prompt_template_rating.json b/echo/directus/sync/snapshot/collections/prompt_template_rating.json new file mode 100644 index 00000000..f70534d8 --- /dev/null +++ b/echo/directus/sync/snapshot/collections/prompt_template_rating.json @@ -0,0 +1,28 @@ +{ + "collection": "prompt_template_rating", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "prompt_template_rating", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": null, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "prompt_template_rating" + } +} diff --git a/echo/directus/sync/snapshot/fields/directus_users/hide_ai_suggestions.json b/echo/directus/sync/snapshot/fields/directus_users/hide_ai_suggestions.json new file mode 100644 index 00000000..d9732e7b --- /dev/null +++ b/echo/directus/sync/snapshot/fields/directus_users/hide_ai_suggestions.json @@ -0,0 +1,46 @@ +{ + "collection": "directus_users", + "field": "hide_ai_suggestions", + "type": "boolean", + "meta": { + "collection": "directus_users", + "conditions": null, + "display": null, + "display_options": null, + "field": "hide_ai_suggestions", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 8, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "hide_ai_suggestions", + "table": "directus_users", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/pin_order.json b/echo/directus/sync/snapshot/fields/project/pin_order.json new file mode 100644 index 00000000..7d6fbff3 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/pin_order.json @@ -0,0 +1,44 @@ +{ + "collection": "project", + "field": "pin_order", + "type": "integer", + "meta": { + "collection": "project", + "conditions": null, + "display": null, + "display_options": null, + "field": "pin_order", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 36, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "pin_order", + "table": "project", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": 32, + "numeric_scale": 0, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_report/error_message.json b/echo/directus/sync/snapshot/fields/project_report/error_message.json new file mode 100644 index 00000000..b961596d --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_report/error_message.json @@ -0,0 +1,44 @@ +{ + "collection": "project_report", + "field": "error_message", + "type": "text", + "meta": { + "collection": "project_report", + "conditions": null, + "display": null, + "display_options": null, + "field": "error_message", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 15, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "error_message", + "table": "project_report", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_report/scheduled_at.json b/echo/directus/sync/snapshot/fields/project_report/scheduled_at.json new file mode 100644 index 00000000..c237433e --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_report/scheduled_at.json @@ -0,0 +1,46 @@ +{ + "collection": "project_report", + "field": "scheduled_at", + "type": "timestamp", + "meta": { + "collection": "project_report", + "conditions": null, + "display": null, + "display_options": null, + "field": "scheduled_at", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": { + "includeSeconds": true + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 14, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "scheduled_at", + "table": "project_report", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_report/status.json b/echo/directus/sync/snapshot/fields/project_report/status.json index 595a1cfd..51096b21 100644 --- a/echo/directus/sync/snapshot/fields/project_report/status.json +++ b/echo/directus/sync/snapshot/fields/project_report/status.json @@ -53,6 +53,18 @@ "color": "var(--theme--primary)", "text": "$t:published", "value": "published" + }, + { + "text": "draft", + "value": "draft" + }, + { + "text": "cancelled", + "value": "cancelled" + }, + { + "text": "scheduled", + "value": "scheduled" } ] }, diff --git a/echo/directus/sync/snapshot/fields/project_report/user_instructions.json b/echo/directus/sync/snapshot/fields/project_report/user_instructions.json new file mode 100644 index 00000000..d377f6d2 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_report/user_instructions.json @@ -0,0 +1,44 @@ +{ + "collection": "project_report", + "field": "user_instructions", + "type": "text", + "meta": { + "collection": "project_report", + "conditions": null, + "display": null, + "display_options": null, + "field": "user_instructions", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 13, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "user_instructions", + "table": "project_report", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/content.json b/echo/directus/sync/snapshot/fields/prompt_template/content.json new file mode 100644 index 00000000..8494cd8d --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/content.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "content", + "type": "text", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "content", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": true, + "searchable": true, + "sort": 6, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "content", + "table": "prompt_template", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/date_created.json b/echo/directus/sync/snapshot/fields/prompt_template/date_created.json new file mode 100644 index 00000000..51785417 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/date_created.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template", + "field": "date_created", + "type": "timestamp", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "date_created", + "group": null, + "hidden": true, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 3, + "special": [ + "date-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "date_created", + "table": "prompt_template", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/date_updated.json b/echo/directus/sync/snapshot/fields/prompt_template/date_updated.json new file mode 100644 index 00000000..937e5877 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/date_updated.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template", + "field": "date_updated", + "type": "timestamp", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "date_updated", + "group": null, + "hidden": true, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 4, + "special": [ + "date-updated" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "date_updated", + "table": "prompt_template", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/description.json b/echo/directus/sync/snapshot/fields/prompt_template/description.json new file mode 100644 index 00000000..cf239761 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/description.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "description", + "type": "text", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "description", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 10, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "description", + "table": "prompt_template", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/icon.json b/echo/directus/sync/snapshot/fields/prompt_template/icon.json new file mode 100644 index 00000000..ed766fa0 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/icon.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "icon", + "type": "string", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "icon", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 7, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "icon", + "table": "prompt_template", + "data_type": "character varying", + "default_value": null, + "max_length": 50, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/id.json b/echo/directus/sync/snapshot/fields/prompt_template/id.json new file mode 100644 index 00000000..453fb88e --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/id.json @@ -0,0 +1,46 @@ +{ + "collection": "prompt_template", + "field": "id", + "type": "uuid", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": true, + "interface": "input", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 1, + "special": [ + "uuid" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "prompt_template", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": true, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/is_anonymous.json b/echo/directus/sync/snapshot/fields/prompt_template/is_anonymous.json new file mode 100644 index 00000000..e07a694f --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/is_anonymous.json @@ -0,0 +1,46 @@ +{ + "collection": "prompt_template", + "field": "is_anonymous", + "type": "boolean", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "is_anonymous", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 13, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "is_anonymous", + "table": "prompt_template", + "data_type": "boolean", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/is_public.json b/echo/directus/sync/snapshot/fields/prompt_template/is_public.json new file mode 100644 index 00000000..8c77842e --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/is_public.json @@ -0,0 +1,46 @@ +{ + "collection": "prompt_template", + "field": "is_public", + "type": "boolean", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "is_public", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 9, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "is_public", + "table": "prompt_template", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/language.json b/echo/directus/sync/snapshot/fields/prompt_template/language.json new file mode 100644 index 00000000..39ee0464 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/language.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "language", + "type": "string", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "language", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 12, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "language", + "table": "prompt_template", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/sort.json b/echo/directus/sync/snapshot/fields/prompt_template/sort.json new file mode 100644 index 00000000..619f9bc8 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/sort.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "sort", + "type": "integer", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "sort", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 8, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "sort", + "table": "prompt_template", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": 32, + "numeric_scale": 0, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/tags.json b/echo/directus/sync/snapshot/fields/prompt_template/tags.json new file mode 100644 index 00000000..2a2d5061 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/tags.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "tags", + "type": "text", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "tags", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 11, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "tags", + "table": "prompt_template", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/title.json b/echo/directus/sync/snapshot/fields/prompt_template/title.json new file mode 100644 index 00000000..6d2017dd --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/title.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template", + "field": "title", + "type": "string", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": null, + "display_options": null, + "field": "title", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": true, + "searchable": true, + "sort": 5, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "title", + "table": "prompt_template", + "data_type": "character varying", + "default_value": null, + "max_length": 200, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template/user_created.json b/echo/directus/sync/snapshot/fields/prompt_template/user_created.json new file mode 100644 index 00000000..b3e68dfa --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template/user_created.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template", + "field": "user_created", + "type": "uuid", + "meta": { + "collection": "prompt_template", + "conditions": null, + "display": "user", + "display_options": null, + "field": "user_created", + "group": null, + "hidden": true, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "template": "{{avatar}} {{first_name}} {{last_name}}" + }, + "readonly": true, + "required": false, + "searchable": true, + "sort": 2, + "special": [ + "user-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "user_created", + "table": "prompt_template", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "directus_users", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/date_created.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/date_created.json new file mode 100644 index 00000000..cbb38b73 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/date_created.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_preference", + "field": "date_created", + "type": "timestamp", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "date_created", + "group": null, + "hidden": true, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 3, + "special": [ + "date-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "date_created", + "table": "prompt_template_preference", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/id.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/id.json new file mode 100644 index 00000000..dd78fcc3 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/id.json @@ -0,0 +1,46 @@ +{ + "collection": "prompt_template_preference", + "field": "id", + "type": "uuid", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": true, + "interface": "input", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 1, + "special": [ + "uuid" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "prompt_template_preference", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": true, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/prompt_template_id.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/prompt_template_id.json new file mode 100644 index 00000000..461d1c05 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/prompt_template_id.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_preference", + "field": "prompt_template_id", + "type": "uuid", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": null, + "display_options": null, + "field": "prompt_template_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "template": "{{title}}" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 6, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "prompt_template_id", + "table": "prompt_template_preference", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "prompt_template", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/sort.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/sort.json new file mode 100644 index 00000000..25ebccbb --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/sort.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template_preference", + "field": "sort", + "type": "integer", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": null, + "display_options": null, + "field": "sort", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 7, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "sort", + "table": "prompt_template_preference", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": 32, + "numeric_scale": 0, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/static_template_id.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/static_template_id.json new file mode 100644 index 00000000..37a971b8 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/static_template_id.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template_preference", + "field": "static_template_id", + "type": "string", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": null, + "display_options": null, + "field": "static_template_id", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 5, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "static_template_id", + "table": "prompt_template_preference", + "data_type": "character varying", + "default_value": null, + "max_length": 100, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/template_type.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/template_type.json new file mode 100644 index 00000000..fae89fca --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/template_type.json @@ -0,0 +1,44 @@ +{ + "collection": "prompt_template_preference", + "field": "template_type", + "type": "string", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": null, + "display_options": null, + "field": "template_type", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": true, + "searchable": true, + "sort": 4, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "template_type", + "table": "prompt_template_preference", + "data_type": "character varying", + "default_value": null, + "max_length": 100, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_preference/user_created.json b/echo/directus/sync/snapshot/fields/prompt_template_preference/user_created.json new file mode 100644 index 00000000..4df92e93 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_preference/user_created.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_preference", + "field": "user_created", + "type": "uuid", + "meta": { + "collection": "prompt_template_preference", + "conditions": null, + "display": "user", + "display_options": null, + "field": "user_created", + "group": null, + "hidden": true, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "template": "{{avatar}} {{first_name}} {{last_name}}" + }, + "readonly": true, + "required": false, + "searchable": true, + "sort": 2, + "special": [ + "user-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "user_created", + "table": "prompt_template_preference", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "directus_users", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/chat_message_id.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/chat_message_id.json new file mode 100644 index 00000000..3db86d4c --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/chat_message_id.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_rating", + "field": "chat_message_id", + "type": "uuid", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": null, + "display_options": null, + "field": "chat_message_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 8, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "chat_message_id", + "table": "prompt_template_rating", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "project_chat_message", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/date_created.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/date_created.json new file mode 100644 index 00000000..66497b80 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/date_created.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_rating", + "field": "date_created", + "type": "timestamp", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "date_created", + "group": null, + "hidden": true, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 3, + "special": [ + "date-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "date_created", + "table": "prompt_template_rating", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/date_updated.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/date_updated.json new file mode 100644 index 00000000..0cfe9684 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/date_updated.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_rating", + "field": "date_updated", + "type": "timestamp", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "date_updated", + "group": null, + "hidden": true, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 5, + "special": [ + "date-updated" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "date_updated", + "table": "prompt_template_rating", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/id.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/id.json new file mode 100644 index 00000000..aa7bae23 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/id.json @@ -0,0 +1,46 @@ +{ + "collection": "prompt_template_rating", + "field": "id", + "type": "uuid", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": true, + "interface": "input", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 1, + "special": [ + "uuid" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "prompt_template_rating", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": true, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/prompt_template_id.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/prompt_template_id.json new file mode 100644 index 00000000..bef4fb66 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/prompt_template_id.json @@ -0,0 +1,49 @@ +{ + "collection": "prompt_template_rating", + "field": "prompt_template_id", + "type": "uuid", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": null, + "display_options": null, + "field": "prompt_template_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true, + "template": "{{title}}" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 6, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "prompt_template_id", + "table": "prompt_template_rating", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "prompt_template", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/rating.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/rating.json new file mode 100644 index 00000000..08e9008b --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/rating.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_rating", + "field": "rating", + "type": "integer", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": null, + "display_options": null, + "field": "rating", + "group": null, + "hidden": false, + "interface": "slider", + "note": null, + "options": { + "maxValue": 5, + "minValue": 1, + "stepInterval": 1 + }, + "readonly": false, + "required": true, + "searchable": true, + "sort": 7, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "rating", + "table": "prompt_template_rating", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": 32, + "numeric_scale": 0, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/user_created.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/user_created.json new file mode 100644 index 00000000..63e32188 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/user_created.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_rating", + "field": "user_created", + "type": "uuid", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": "user", + "display_options": null, + "field": "user_created", + "group": null, + "hidden": true, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "template": "{{avatar}} {{first_name}} {{last_name}}" + }, + "readonly": true, + "required": false, + "searchable": true, + "sort": 2, + "special": [ + "user-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "user_created", + "table": "prompt_template_rating", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "directus_users", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/prompt_template_rating/user_updated.json b/echo/directus/sync/snapshot/fields/prompt_template_rating/user_updated.json new file mode 100644 index 00000000..04cd40a9 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/prompt_template_rating/user_updated.json @@ -0,0 +1,48 @@ +{ + "collection": "prompt_template_rating", + "field": "user_updated", + "type": "uuid", + "meta": { + "collection": "prompt_template_rating", + "conditions": null, + "display": "user", + "display_options": null, + "field": "user_updated", + "group": null, + "hidden": true, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "template": "{{avatar}} {{first_name}} {{last_name}}" + }, + "readonly": true, + "required": false, + "searchable": true, + "sort": 4, + "special": [ + "user-updated" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "user_updated", + "table": "prompt_template_rating", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "directus_users", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template/user_created.json b/echo/directus/sync/snapshot/relations/prompt_template/user_created.json new file mode 100644 index 00000000..eaeba090 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template/user_created.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template", + "field": "user_created", + "related_collection": "directus_users", + "meta": { + "junction_field": null, + "many_collection": "prompt_template", + "many_field": "user_created", + "one_allowed_collections": null, + "one_collection": "directus_users", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template", + "column": "user_created", + "foreign_key_table": "directus_users", + "foreign_key_column": "id", + "constraint_name": "prompt_template_user_created_foreign", + "on_update": "NO ACTION", + "on_delete": "NO ACTION" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template_preference/prompt_template_id.json b/echo/directus/sync/snapshot/relations/prompt_template_preference/prompt_template_id.json new file mode 100644 index 00000000..48c00710 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template_preference/prompt_template_id.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template_preference", + "field": "prompt_template_id", + "related_collection": "prompt_template", + "meta": { + "junction_field": null, + "many_collection": "prompt_template_preference", + "many_field": "prompt_template_id", + "one_allowed_collections": null, + "one_collection": "prompt_template", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template_preference", + "column": "prompt_template_id", + "foreign_key_table": "prompt_template", + "foreign_key_column": "id", + "constraint_name": "prompt_template_preference_prompt_template_id_foreign", + "on_update": "NO ACTION", + "on_delete": "SET NULL" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template_preference/user_created.json b/echo/directus/sync/snapshot/relations/prompt_template_preference/user_created.json new file mode 100644 index 00000000..610fc944 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template_preference/user_created.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template_preference", + "field": "user_created", + "related_collection": "directus_users", + "meta": { + "junction_field": null, + "many_collection": "prompt_template_preference", + "many_field": "user_created", + "one_allowed_collections": null, + "one_collection": "directus_users", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template_preference", + "column": "user_created", + "foreign_key_table": "directus_users", + "foreign_key_column": "id", + "constraint_name": "prompt_template_preference_user_created_foreign", + "on_update": "NO ACTION", + "on_delete": "NO ACTION" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template_rating/chat_message_id.json b/echo/directus/sync/snapshot/relations/prompt_template_rating/chat_message_id.json new file mode 100644 index 00000000..c73dcdf3 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template_rating/chat_message_id.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template_rating", + "field": "chat_message_id", + "related_collection": "project_chat_message", + "meta": { + "junction_field": null, + "many_collection": "prompt_template_rating", + "many_field": "chat_message_id", + "one_allowed_collections": null, + "one_collection": "project_chat_message", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template_rating", + "column": "chat_message_id", + "foreign_key_table": "project_chat_message", + "foreign_key_column": "id", + "constraint_name": "prompt_template_rating_chat_message_id_foreign", + "on_update": "NO ACTION", + "on_delete": "SET NULL" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template_rating/prompt_template_id.json b/echo/directus/sync/snapshot/relations/prompt_template_rating/prompt_template_id.json new file mode 100644 index 00000000..0380b510 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template_rating/prompt_template_id.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template_rating", + "field": "prompt_template_id", + "related_collection": "prompt_template", + "meta": { + "junction_field": null, + "many_collection": "prompt_template_rating", + "many_field": "prompt_template_id", + "one_allowed_collections": null, + "one_collection": "prompt_template", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template_rating", + "column": "prompt_template_id", + "foreign_key_table": "prompt_template", + "foreign_key_column": "id", + "constraint_name": "prompt_template_rating_prompt_template_id_foreign", + "on_update": "NO ACTION", + "on_delete": "SET NULL" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template_rating/user_created.json b/echo/directus/sync/snapshot/relations/prompt_template_rating/user_created.json new file mode 100644 index 00000000..0f00c9c9 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template_rating/user_created.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template_rating", + "field": "user_created", + "related_collection": "directus_users", + "meta": { + "junction_field": null, + "many_collection": "prompt_template_rating", + "many_field": "user_created", + "one_allowed_collections": null, + "one_collection": "directus_users", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template_rating", + "column": "user_created", + "foreign_key_table": "directus_users", + "foreign_key_column": "id", + "constraint_name": "prompt_template_rating_user_created_foreign", + "on_update": "NO ACTION", + "on_delete": "NO ACTION" + } +} diff --git a/echo/directus/sync/snapshot/relations/prompt_template_rating/user_updated.json b/echo/directus/sync/snapshot/relations/prompt_template_rating/user_updated.json new file mode 100644 index 00000000..f71c64e9 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/prompt_template_rating/user_updated.json @@ -0,0 +1,25 @@ +{ + "collection": "prompt_template_rating", + "field": "user_updated", + "related_collection": "directus_users", + "meta": { + "junction_field": null, + "many_collection": "prompt_template_rating", + "many_field": "user_updated", + "one_allowed_collections": null, + "one_collection": "directus_users", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "prompt_template_rating", + "column": "user_updated", + "foreign_key_table": "directus_users", + "foreign_key_column": "id", + "constraint_name": "prompt_template_rating_user_updated_foreign", + "on_update": "NO ACTION", + "on_delete": "NO ACTION" + } +} diff --git a/echo/docs/branching_and_releases.md b/echo/docs/branching_and_releases.md new file mode 100644 index 00000000..03a6bad1 --- /dev/null +++ b/echo/docs/branching_and_releases.md @@ -0,0 +1,99 @@ +# Branching Strategy & Release Process + +## Environments + +| Environment | URL | Deploys from | Notes | +|---|---|---|---| +| **Testing** | dashboard.testing.dembrane.com | `testing` branch (on push) | Shared, unprotected | +| **Echo Next** | dashboard.echo-next.dembrane.com | `main` branch (on merge) | Staging / preview | +| **Production** | dashboard.dembrane.com | GitHub release tag on `main` | Every ~2 weeks | + +Each environment has dashboard, portal, and directus subpaths (e.g., `dashboard.dembrane.com`, `portal.dembrane.com`, `directus.dembrane.com`). + +## Feature Development Flow + +``` +main ──────────────────●──────────────────●──── (auto-deploys to Echo Next) + \ ↗ PR | + feat/ECHO-123 ──→ (optional) | + \ | + testing ────→ dashboard.testing.dembrane.com +``` + +1. **Branch off `main`** — name your branch `feat/ECHO-xxx-description` or similar +2. **Develop** on the feature branch +3. **(Optional) Test on testing environment**: + - Merge your feature branch into `testing` to deploy to dashboard.testing.dembrane.com + - The `testing` branch is **unprotected** — you can push/merge directly + - **Before merging**, check that nobody else is currently using it: + ```bash + git log main..testing --oneline # any commits ahead of main? + ``` + - If there are commits ahead, check with the team before overwriting +4. **Create a PR** from your feature branch to `main` +5. **After merge** — changes auto-deploy to Echo Next +6. **After done testing** — reset `testing` back to `main`: + ```bash + git checkout testing + git reset --hard origin/main + git push --force + ``` + +## Release Process + +Releases happen every ~2 weeks, aligned with Linear two-week cycles. + +1. **Accumulate changes** on `main` throughout the cycle +2. **Pre-release checks**: + - Check for new env vars (`settings.py` fields, `config.ts` exports) + - Run Directus data migrations if needed (see [database_migrations.md](database_migrations.md)) + - Update deployment env vars in the GitOps repo if needed +3. **Tag and release** — create a GitHub release from a commit on `main` +4. The release triggers **auto-deployment to production**: + - Backend: new image tags are picked up by the GitOps repo (`dembrane/echo-gitops`, Argo CD auto-sync) + - Frontend: auto-deploys via Vercel + +## Hotfix Process + +When a critical bug is found in production and `main` has unreleased changes that shouldn't go out yet: + +``` +main ────────●────────●────── (has unreleased work) + | +v1.2.0 (tag) \ + hotfix-fix-description ──→ v1.2.1 (new release, auto-deploys) +``` + +1. **Branch off the current release tag** (not `main`): + ```bash + git checkout -b hotfix- v1.2.0 # use the actual release tag + ``` +2. **Make the fix** on the hotfix branch +3. **Create a new GitHub release** from the hotfix branch (e.g., `v1.2.1`) + - This auto-deploys to production + - Backend image tags update automatically via GitOps + - Frontend auto-deploys via Vercel +4. **Cherry-pick the fix into `main`** so it's included in the next regular release: + ```bash + git checkout main + git cherry-pick + ``` + +**Why branch off the tag?** This isolates the hotfix from unreleased work on `main`. Only the fix ships to production. + +## GitOps + +Infrastructure and deployment configuration live in a separate repo: **`dembrane/echo-gitops`** + +- Terraform for DigitalOcean infra (VPC, K8s, DB, Redis, Spaces) +- Helm charts for application workloads and monitoring +- Argo CD for GitOps sync with auto-prune and self-heal +- SealedSecrets for secret management + +See that repo's README and AGENTS.md for details. + +## Project Management + +- **Linear** for issue tracking — tickets are `ECHO-xxx` +- **Two-week cycles/sprints** aligned with release cadence +- Cycle end = release candidate on `main` → tag → deploy to production diff --git a/echo/frontend/AGENTS.md b/echo/frontend/AGENTS.md index f22b2bfe..d0982a21 100644 --- a/echo/frontend/AGENTS.md +++ b/echo/frontend/AGENTS.md @@ -70,9 +70,17 @@ - Auth hero uses `/public/video/auth-hero.mp4` with `/public/video/auth-hero-poster.jpg` as poster; keep the bright blur overlay consistent when iterating on onboarding screens. - Gentle login/logout flows use `useTransitionCurtain().runTransition()` before navigation—animations expect Directus session mutations to await that promise. -# HUMAN SECTION beyond this point (next time when you are reading this - prompt the user if they want to add it to the above sections) -- If there is a type error with ".count" with Directus, add it to the typesDirectus.ts. You can add to the fields `count("")` to obtain `_count` in the response -- When a user request feels ambiguous, pause and confirm the intended action with them before touching code or docs; err on the side of over-communicating. +## Directus Type Gotcha +- If there's a type error with `.count` in Directus responses, add the type to `typesDirectus.d.ts`. Use `count("")` in fields to get `_count` in the response. + +## Architecture Preferences +- **BFF pattern**: Prefer backend `/bff/` routes over making multiple Directus SDK calls from the frontend. Example: `/bff/projects/home` aggregates pinned projects, paginated list, search, and admin info. +- **URL-driven state**: Filters, search queries, and selected tabs should be stored in URL search params (not React state) so state is shareable and persistent. +- **Conversations come from QR codes or audio uploads** — never add "new conversation" creation buttons in the UI. +- **Loading spinners**: Always use `alwaysDembrane` prop on `DembraneLoadingSpinner` for whitelabel safety. Never use `animate-spin` on custom logos. + +## Collaboration +- When a user request feels ambiguous, pause and confirm the intended action before touching code or docs. ## Brand Guidelines diff --git a/echo/frontend/index.html b/echo/frontend/index.html index 2ac39231..11129fed 100644 --- a/echo/frontend/index.html +++ b/echo/frontend/index.html @@ -2,6 +2,11 @@ + diff --git a/echo/frontend/package.json b/echo/frontend/package.json index d85eaae5..748eb813 100644 --- a/echo/frontend/package.json +++ b/echo/frontend/package.json @@ -33,6 +33,7 @@ "@mantine/charts": "^7.17.8", "@mantine/colors-generator": "^7.17.8", "@mantine/core": "^7.17.8", + "@mantine/dates": "^7.17.8", "@mantine/dropzone": "^7.17.8", "@mantine/hooks": "^7.17.8", "@mantine/modals": "^7.17.8", @@ -59,6 +60,7 @@ "d3-selection": "^3.0.0", "d3-transition": "^3.0.1", "date-fns": "^4.1.0", + "dayjs": "^1.11.20", "diff": "^8.0.2", "js-cookie": "^3.0.5", "lodash": "^4.17.21", @@ -66,17 +68,19 @@ "match-sorter": "^8.0.3", "motion": "^11.18.2", "next-themes": "^0.4.6", - "notifications\n": "link:@mantine/notifications\n", "plausible-tracker": "^0.3.9", "re-resizable": "^6.11.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-easy-crop": "^5.5.6", + "react-grab": "^0.1.28", "react-hook-form": "^7.54.2", "react-intersection-observer": "^9.16.0", "react-markdown": "^9.1.0", "react-pdf": "^9.2.1", "react-qrcode-logo": "^3.0.0", "react-router": "^7.8.2", + "react-scan": "^0.5.3", "react-transition-group": "^4.4.5", "recharts": "^2.15.1", "rehype-stringify": "^10.0.1", diff --git a/echo/frontend/pnpm-lock.yaml b/echo/frontend/pnpm-lock.yaml index 479b6cc2..6a8658d1 100644 --- a/echo/frontend/pnpm-lock.yaml +++ b/echo/frontend/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: '@mantine/core': specifier: ^7.17.8 version: 7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/dates': + specifier: ^7.17.8 + version: 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.8(react@19.0.0))(dayjs@1.11.20)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/dropzone': specifier: ^7.17.8 version: 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.8(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -134,6 +137,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + dayjs: + specifier: ^1.11.20 + version: 1.11.20 diff: specifier: ^8.0.2 version: 8.0.2 @@ -155,11 +161,6 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - "notifications\n": - specifier: | - link:@mantine/notifications - version: | - link:@mantine/notifications plausible-tracker: specifier: ^0.3.9 version: 0.3.9 @@ -172,6 +173,12 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-easy-crop: + specifier: ^5.5.6 + version: 5.5.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-grab: + specifier: ^0.1.28 + version: 0.1.28(@types/react@19.0.12)(react@19.0.0) react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) @@ -190,6 +197,9 @@ importers: react-router: specifier: ^7.8.2 version: 7.8.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-scan: + specifier: ^0.5.3 + version: 0.5.3(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.40.1) react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -345,6 +355,10 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@antfu/ni@0.23.2': + resolution: {integrity: sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ==} + hasBin: true + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1215,9 +1229,6 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} @@ -1501,6 +1512,15 @@ packages: react: ^18.x || ^19.x react-dom: ^18.x || ^19.x + '@mantine/dates@7.17.8': + resolution: {integrity: sha512-KYog/YL83PnsMef7EZagpOFq9I2gfnK0eYSzC8YvV9Mb6t/x9InqRssGWVb0GIr+TNILpEkhKoGaSKZNy10Q1g==} + peerDependencies: + '@mantine/core': 7.17.8 + '@mantine/hooks': 7.17.8 + dayjs: '>=1.0.0' + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + '@mantine/dropzone@7.17.8': resolution: {integrity: sha512-c9WEArpP23E9tbRWqoznEY3bGPVntMuBKr3F2LQijgdpdALIzt6DYmwXu7gUajGX9Qg9NrCHenhvWLTYqKnRlA==} peerDependencies: @@ -1552,6 +1572,9 @@ packages: react: '>= 18 || >= 19' react-dom: '>= 18 || >= 19' + '@medv/finder@4.0.2': + resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==} + '@messageformat/parser@5.1.1': resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} @@ -1585,6 +1608,14 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@preact/signals-core@1.14.0': + resolution: {integrity: sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==} + + '@preact/signals@1.3.4': + resolution: {integrity: sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g==} + peerDependencies: + preact: 10.x + '@radix-ui/colors@3.0.0': resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} @@ -1962,6 +1993,10 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-grab/cli@0.1.28': + resolution: {integrity: sha512-IE4bTeH0mCq0FBRaYRUtdiIfvO7NCv14lHS4TiTY8YNG5tPYwHnLZZtniud0ElmLIWGP95Kk6E7A6J2uDmOY+Q==} + hasBin: true + '@react-hook/intersection-observer@3.1.2': resolution: {integrity: sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==} peerDependencies: @@ -2018,6 +2053,15 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.40.1': resolution: {integrity: sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==} cpu: [arm] @@ -2354,6 +2398,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@types/node@22.13.14': resolution: {integrity: sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==} @@ -2365,6 +2412,11 @@ packages: peerDependencies: '@types/react': ^19.0.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.0.12': resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} @@ -2504,6 +2556,11 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bippy@0.5.32: + resolution: {integrity: sha512-yt1mC8eReTxjfg41YBZdN4PvsDwHFWxltoiQX0Q+Htlbf41aSniopb7ECZits01HwNAvXEh69RGk/ImlswDTEw==} + peerDependencies: + react: '>=17.0.1' + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2612,6 +2669,10 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -2672,6 +2733,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2858,6 +2923,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -3032,6 +3100,12 @@ packages: estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} @@ -3149,6 +3223,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3248,6 +3326,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3314,6 +3396,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3326,6 +3412,14 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-url@1.2.4: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} @@ -3391,11 +3485,18 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsondiffpatch@0.6.0: resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3452,6 +3553,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3694,6 +3799,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -3790,6 +3899,9 @@ packages: normalize-svg-path@1.1.0: resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} + normalize-wheel@1.0.1: + resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3805,10 +3917,18 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -3977,6 +4097,9 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.0: + resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -3990,6 +4113,10 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4044,12 +4171,27 @@ packages: peerDependencies: react: '>= 16.8 || 18.0.0' + react-easy-crop@5.5.6: + resolution: {integrity: sha512-Jw3/ozs8uXj3NpL511Suc4AHY+mLRO23rUgipXvNYKqezcFSYHxe4QXibBymkOoY6oOtLVMPO2HNPRHYvMPyTw==} + peerDependencies: + react: '>=16.4.0' + react-dom: '>=16.4.0' + react-error-boundary@3.1.4: resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} engines: {node: '>=10', npm: '>=6'} peerDependencies: react: '>=16.13.1' + react-grab@0.1.28: + resolution: {integrity: sha512-u3fvu7a7ejHhuWKzf/N6sFavV04vcqwtbcqyxwNPtvd3ts9KSVerpHp1ZH0/XVKTsh3MZz1u1jyIt0KYC9L/Rg==} + hasBin: true + peerDependencies: + react: '>=17.0.0' + peerDependenciesMeta: + react: + optional: true + react-hook-form@7.54.2: resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} engines: {node: '>=18.0.0'} @@ -4152,6 +4294,13 @@ packages: react-dom: optional: true + react-scan@0.5.3: + resolution: {integrity: sha512-qde9PupmUf0L3MU1H6bjmoukZNbCXdMyTEwP4Gh8RQ4rZPd2GGNBgEKWszwLm96E8k+sGtMpc0B9P0KyFDP6Bw==} + hasBin: true + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-smooth@4.0.4: resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} peerDependencies: @@ -4247,6 +4396,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} @@ -4300,6 +4453,16 @@ packages: engines: {node: '>=10'} hasBin: true + seroval-plugins@1.5.1: + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.1: + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} + engines: {node: '>=10'} + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -4327,6 +4490,16 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.6.0: + resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + engines: {node: '>= 18'} + + solid-js@1.9.11: + resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} + sonner@1.7.4: resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} peerDependencies: @@ -4347,6 +4520,10 @@ packages: static-browser-server@1.0.3: resolution: {integrity: sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + strict-event-emitter@0.4.6: resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==} @@ -4358,6 +4535,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -4507,6 +4688,9 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -4537,6 +4721,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + unraw@3.0.0: resolution: {integrity: sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==} @@ -4685,6 +4873,9 @@ packages: webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -4768,12 +4959,14 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@antfu/ni@0.23.2': {} '@babel/code-frame@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -4791,14 +4984,14 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 + '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.0 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) '@babel/helpers': 7.27.0 '@babel/parser': 7.27.0 '@babel/template': 7.27.0 '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 convert-source-map: 2.0.0 debug: 4.4.0 gensync: 1.0.0-beta.2 @@ -4830,7 +5023,7 @@ snapshots: '@babel/generator@7.27.0': dependencies: '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 @@ -4864,7 +5057,7 @@ snapshots: '@babel/helper-module-imports@7.25.9': dependencies: '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -4879,7 +5072,7 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 '@babel/traverse': 7.27.0 transitivePeerDependencies: - supports-color @@ -4910,7 +5103,7 @@ snapshots: '@babel/helpers@7.27.0': dependencies: '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 '@babel/helpers@7.27.6': dependencies: @@ -4919,7 +5112,7 @@ snapshots: '@babel/parser@7.27.0': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 '@babel/parser@7.28.0': dependencies: @@ -4944,8 +5137,8 @@ snapshots: '@babel/template@7.27.0': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 '@babel/template@7.27.2': dependencies: @@ -4956,10 +5149,10 @@ snapshots: '@babel/traverse@7.27.0': dependencies: '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: @@ -5658,21 +5851,19 @@ snapshots: '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.4': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 '@jridgewell/trace-mapping@0.3.29': dependencies: @@ -6150,6 +6341,15 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.8(react@19.0.0))(dayjs@1.11.20)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.8(react@19.0.0) + clsx: 2.1.1 + dayjs: 1.11.20 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@mantine/dropzone@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.8(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -6255,6 +6455,8 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@medv/finder@4.0.2': {} + '@messageformat/parser@5.1.1': dependencies: moo: 0.5.2 @@ -6283,6 +6485,13 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@preact/signals-core@1.14.0': {} + + '@preact/signals@1.3.4(preact@10.29.0)': + dependencies: + '@preact/signals-core': 1.14.0 + preact: 10.29.0 + '@radix-ui/colors@3.0.0': {} '@radix-ui/number@1.1.1': {} @@ -6649,6 +6858,17 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-grab/cli@0.1.28': + dependencies: + '@antfu/ni': 0.23.2 + commander: 14.0.3 + ignore: 7.0.5 + jsonc-parser: 3.3.1 + ora: 8.2.0 + picocolors: 1.1.1 + prompts: 2.4.2 + smol-toml: 1.6.0 + '@react-hook/intersection-observer@3.1.2(react@19.0.0)': dependencies: '@react-hook/passive-layout-effect': 1.2.1(react@19.0.0) @@ -6762,6 +6982,14 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/pluginutils@5.3.0(rollup@4.40.1)': + dependencies: + '@types/estree': 1.0.7 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.40.1 + '@rollup/rollup-android-arm-eabi@4.40.1': optional: true @@ -6965,16 +7193,16 @@ snapshots: '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.1 '@types/d3-array@3.2.2': {} @@ -7042,6 +7270,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + '@types/node@22.13.14': dependencies: undici-types: 6.20.0 @@ -7052,6 +7284,10 @@ snapshots: dependencies: '@types/react': 19.0.12 + '@types/react-reconciler@0.28.9(@types/react@19.0.12)': + dependencies: + '@types/react': 19.0.12 + '@types/react@19.0.12': dependencies: csstype: 3.1.3 @@ -7187,6 +7423,13 @@ snapshots: binary-extensions@2.3.0: {} + bippy@0.5.32(@types/react@19.0.12)(react@19.0.0): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.0.12) + react: 19.0.0 + transitivePeerDependencies: + - '@types/react' + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -7312,6 +7555,10 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} cli-table@0.3.11: @@ -7364,6 +7611,8 @@ snapshots: commander@10.0.1: {} + commander@14.0.3: {} + commander@4.1.1: {} commander@7.2.0: {} @@ -7566,6 +7815,8 @@ snapshots: date-fns@4.1.0: {} + dayjs@1.11.20: {} + debug@4.4.0: dependencies: ms: 2.1.3 @@ -7798,6 +8049,12 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + event-emitter@0.3.5: dependencies: d: 1.0.2 @@ -7910,6 +8167,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8048,6 +8307,8 @@ snapshots: ieee754@1.2.1: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8113,12 +8374,18 @@ snapshots: is-interactive@1.0.0: {} + is-interactive@2.0.0: {} + is-number@7.0.0: {} is-plain-obj@4.1.0: {} is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} isexe@2.0.0: {} @@ -8174,12 +8441,16 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsondiffpatch@0.6.0: dependencies: '@types/diff-match-patch': 1.0.36 chalk: 5.4.1 diff-match-patch: 1.0.5 + kleur@3.0.3: {} + leven@3.1.0: {} lexical@0.32.1: {} @@ -8223,6 +8494,11 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-symbols@6.0.0: + dependencies: + chalk: 5.4.1 + is-unicode-supported: 1.3.0 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -8748,6 +9024,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + mimic-response@3.1.0: optional: true @@ -8827,6 +9105,8 @@ snapshots: dependencies: svg-arc-to-cubic-bezier: 3.2.0 + normalize-wheel@1.0.1: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -8840,6 +9120,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -8852,6 +9136,18 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + ora@8.2.0: + dependencies: + chalk: 5.4.1 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + os-tmpdir@1.0.2: {} outvariant@1.4.0: {} @@ -9001,6 +9297,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.0.3 @@ -9025,6 +9323,11 @@ snapshots: prismjs@1.30.0: {} + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -9082,11 +9385,29 @@ snapshots: prop-types: 15.8.1 react: 19.0.0 + react-easy-crop@5.5.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + normalize-wheel: 1.0.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + tslib: 2.8.1 + react-error-boundary@3.1.4(react@19.0.0): dependencies: '@babel/runtime': 7.27.6 react: 19.0.0 + react-grab@0.1.28(@types/react@19.0.12)(react@19.0.0): + dependencies: + '@medv/finder': 4.0.2 + '@react-grab/cli': 0.1.28 + bippy: 0.5.32(@types/react@19.0.12)(react@19.0.0) + solid-js: 1.9.11 + optionalDependencies: + react: 19.0.0 + transitivePeerDependencies: + - '@types/react' + react-hook-form@7.54.2(react@19.0.0): dependencies: react: 19.0.0 @@ -9192,6 +9513,30 @@ snapshots: optionalDependencies: react-dom: 19.0.0(react@19.0.0) + react-scan@0.5.3(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.40.1): + dependencies: + '@babel/core': 7.28.0 + '@babel/generator': 7.28.0 + '@babel/types': 7.28.1 + '@preact/signals': 1.3.4(preact@10.29.0) + '@rollup/pluginutils': 5.3.0(rollup@4.40.1) + '@types/node': 20.19.37 + bippy: 0.5.32(@types/react@19.0.12)(react@19.0.0) + commander: 14.0.3 + esbuild: 0.25.8 + estree-walker: 3.0.3 + picocolors: 1.1.1 + preact: 10.29.0 + prompts: 2.4.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + unplugin: 2.1.0 + transitivePeerDependencies: + - '@types/react' + - rollup + - supports-color + react-smooth@4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: fast-equals: 5.2.2 @@ -9322,6 +9667,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + restructure@3.0.2: {} reusify@1.1.0: {} @@ -9381,6 +9731,12 @@ snapshots: semver@7.7.2: optional: true + seroval-plugins@1.5.1(seroval@1.5.1): + dependencies: + seroval: 1.5.1 + + seroval@1.5.1: {} + set-cookie-parser@2.7.1: {} shebang-command@2.0.0: @@ -9407,6 +9763,16 @@ snapshots: dependencies: is-arrayish: 0.3.2 + sisteransi@1.0.5: {} + + smol-toml@1.6.0: {} + + solid-js@1.9.11: + dependencies: + csstype: 3.1.3 + seroval: 1.5.1 + seroval-plugins: 1.5.1(seroval@1.5.1) + sonner@1.7.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -9427,6 +9793,8 @@ snapshots: mime-db: 1.54.0 outvariant: 1.4.0 + stdin-discarder@0.2.2: {} + strict-event-emitter@0.4.6: {} string-width@4.2.3: @@ -9441,6 +9809,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -9609,6 +9983,8 @@ snapshots: undici-types@6.20.0: {} + undici-types@6.21.0: {} + unicode-properties@1.4.1: dependencies: base64-js: 1.5.1 @@ -9660,6 +10036,12 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + unplugin@2.1.0: + dependencies: + acorn: 8.14.1 + webpack-virtual-modules: 0.6.2 + optional: true + unraw@3.0.0: {} update-browserslist-db@1.1.3(browserslist@4.24.4): @@ -9789,6 +10171,9 @@ snapshots: webidl-conversions@4.0.2: {} + webpack-virtual-modules@0.6.2: + optional: true + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/echo/frontend/src/App.tsx b/echo/frontend/src/App.tsx index 3ca64694..0fb5e7ea 100644 --- a/echo/frontend/src/App.tsx +++ b/echo/frontend/src/App.tsx @@ -1,8 +1,10 @@ import "@fontsource-variable/space-grotesk"; import "@mantine/core/styles.css"; +import "@mantine/dates/styles.css"; import "@mantine/dropzone/styles.css"; import { MantineProvider } from "@mantine/core"; +import { DatesProvider } from "@mantine/dates"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useEffect } from "react"; @@ -81,13 +83,15 @@ export const App = () => { {/* */} - - - - - - - + + + + + + + + + ); diff --git a/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx b/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx index 59863527..0faa6d0d 100644 --- a/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx +++ b/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx @@ -1,7 +1,7 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { ActionIcon, Button, Group, Stack, Text } from "@mantine/core"; -import { IconX } from "@tabler/icons-react"; +import { X } from "@phosphor-icons/react"; import { testId } from "@/lib/testUtils"; import { useUnreadAnnouncements } from "./hooks"; @@ -30,7 +30,7 @@ export const AnnouncementDrawerHeader = ({ className="focus:outline-none" {...testId("announcement-close-drawer-button")} > - + diff --git a/echo/frontend/src/components/announcement/AnnouncementIcon.tsx b/echo/frontend/src/components/announcement/AnnouncementIcon.tsx index dc0a9844..409166b2 100644 --- a/echo/frontend/src/components/announcement/AnnouncementIcon.tsx +++ b/echo/frontend/src/components/announcement/AnnouncementIcon.tsx @@ -1,8 +1,7 @@ -import { ActionIcon, Box, Group, Indicator, Loader } from "@mantine/core"; +import { ActionIcon, Box, Group, Indicator, Loader, Text } from "@mantine/core"; import { FlagBannerIcon } from "@phosphor-icons/react"; import { useAnnouncementDrawer } from "@/components/announcement/hooks"; import { getTranslatedContent } from "@/components/announcement/hooks/useProcessedAnnouncements"; -import { Markdown } from "@/components/common/Markdown"; import { useLanguage } from "@/hooks/useLanguage"; import { testId } from "@/lib/testUtils"; import { useLatestAnnouncement, useUnreadAnnouncements } from "./hooks"; @@ -15,20 +14,18 @@ export const AnnouncementIcon = () => { const { data: unreadCount, isLoading: isLoadingUnread } = useUnreadAnnouncements(); - // Get latest urgent announcement message - const message = latestAnnouncement - ? getTranslatedContent(latestAnnouncement as Announcement, language).message + const title = latestAnnouncement + ? getTranslatedContent(latestAnnouncement as Announcement, language).title : ""; - // Check if the latest announcement is unread const isUnread = latestAnnouncement ? !latestAnnouncement.activity?.some( (activity: AnnouncementActivity) => activity.read === true, ) : false; - const showMessage = - isUnread && message && latestAnnouncement?.level === "info"; + const showPreview = + isUnread && title && latestAnnouncement?.level === "info"; const isLoading = isLoadingLatest || isLoadingUnread; @@ -69,13 +66,15 @@ export const AnnouncementIcon = () => { - {showMessage && ( + {showPreview && ( - + + {title} + )} diff --git a/echo/frontend/src/components/announcement/AnnouncementItem.tsx b/echo/frontend/src/components/announcement/AnnouncementItem.tsx index 1a1761d1..d838b4cb 100644 --- a/echo/frontend/src/components/announcement/AnnouncementItem.tsx +++ b/echo/frontend/src/components/announcement/AnnouncementItem.tsx @@ -9,11 +9,11 @@ import { useMantineTheme, } from "@mantine/core"; import { - IconAlertTriangle, - IconChevronDown, - IconChevronUp, - IconInfoCircle, -} from "@tabler/icons-react"; + CaretDown, + CaretUp, + Info, + WarningCircle, +} from "@phosphor-icons/react"; import { forwardRef, useEffect, useRef, useState } from "react"; import { Markdown } from "@/components/common/Markdown"; import { testId } from "@/lib/testUtils"; @@ -31,14 +31,13 @@ type Announcement = { interface AnnouncementItemProps { announcement: Announcement; - onMarkAsRead: (id: string) => void; index: number; } export const AnnouncementItem = forwardRef< HTMLDivElement, AnnouncementItemProps ->(({ announcement, onMarkAsRead, index }, ref) => { +>(({ announcement, index }, ref) => { const theme = useMantineTheme(); const [showMore, setShowMore] = useState(false); const [showReadMoreButton, setShowReadMoreButton] = useState(false); @@ -65,24 +64,24 @@ export const AnnouncementItem = forwardRef< > - { - - {announcement.level === "urgent" ? ( - - ) : ( - - )} - - } + + {announcement.level === "urgent" ? ( + + ) : ( + + )} +
- + + {announcement.title} +
@@ -90,7 +89,6 @@ export const AnnouncementItem = forwardRef< {formatDate(announcement.created_at)} - {/* this part needs a second look */} {!announcement.read && (
)} - {/* this part needs a second look */} @@ -113,8 +110,8 @@ export const AnnouncementItem = forwardRef< /> - - {showReadMoreButton && ( + {showReadMoreButton && ( + - )} - - - {!announcement.read && ( - - )} - - + + )} diff --git a/echo/frontend/src/components/announcement/Announcements.tsx b/echo/frontend/src/components/announcement/Announcements.tsx index 3a76a2c1..72eda756 100644 --- a/echo/frontend/src/components/announcement/Announcements.tsx +++ b/echo/frontend/src/components/announcement/Announcements.tsx @@ -1,9 +1,25 @@ import { Trans } from "@lingui/react/macro"; -import { Box, Center, Loader, ScrollArea, Stack, Text } from "@mantine/core"; -import { useEffect, useState } from "react"; +import { + Box, + Center, + Collapse, + Divider, + Group, + Loader, + ScrollArea, + Stack, + Text, + ThemeIcon, + UnstyledButton, +} from "@mantine/core"; +import { CaretDown, CaretUp, Sparkle } from "@phosphor-icons/react"; +import { useEffect, useRef, useState } from "react"; import { useInView } from "react-intersection-observer"; import { useAnnouncementDrawer } from "@/components/announcement/hooks"; -import { useProcessedAnnouncements } from "@/components/announcement/hooks/useProcessedAnnouncements"; +import { + useProcessedAnnouncements, + useWhatsNewProcessed, +} from "@/components/announcement/hooks/useProcessedAnnouncements"; import { useLanguage } from "@/hooks/useLanguage"; import { analytics } from "@/lib/analytics"; import { AnalyticsEvents as events } from "@/lib/analyticsEvents"; @@ -13,18 +29,20 @@ import { AnnouncementDrawerHeader } from "./AnnouncementDrawerHeader"; import { AnnouncementErrorState } from "./AnnouncementErrorState"; import { AnnouncementItem } from "./AnnouncementItem"; import { AnnouncementSkeleton } from "./AnnouncementSkeleton"; +import { WhatsNewItem } from "./WhatsNewItem"; import { useInfiniteAnnouncements, useMarkAllAsReadMutation, - useMarkAsReadMutation, + useWhatsNewAnnouncements, } from "./hooks"; export const Announcements = () => { const { isOpen, close } = useAnnouncementDrawer(); const { language } = useLanguage(); - const markAsReadMutation = useMarkAsReadMutation(); const markAllAsReadMutation = useMarkAllAsReadMutation(); const [openedOnce, setOpenedOnce] = useState(false); + const [whatsNewExpanded, setWhatsNewExpanded] = useState(false); + const autoReadTimerRef = useRef | null>(null); const { ref: loadMoreRef, inView } = useInView(); @@ -40,6 +58,23 @@ export const Announcements = () => { } }, [isOpen, openedOnce]); + // Auto-mark all as read after 1 second when drawer opens + // biome-ignore lint/correctness/useExhaustiveDependencies: only trigger on isOpen changes, mutate ref is stable + useEffect(() => { + if (isOpen) { + autoReadTimerRef.current = setTimeout(() => { + markAllAsReadMutation.mutate(); + }, 1000); + } + + return () => { + if (autoReadTimerRef.current) { + clearTimeout(autoReadTimerRef.current); + autoReadTimerRef.current = null; + } + }; + }, [isOpen]); + const { data: announcementsData, fetchNextPage, @@ -55,7 +90,11 @@ export const Announcements = () => { }, }); - // Flatten all announcements from all pages, with type safety + const { data: whatsNewData } = useWhatsNewAnnouncements({ + enabled: openedOnce, + }); + + // Flatten all announcements from all pages const allAnnouncements = announcementsData?.pages.flatMap( (page) => (page as { announcements: Announcement[] }).announcements, @@ -67,6 +106,15 @@ export const Announcements = () => { language, ); + // Only show unread announcements (read ones are hidden) + const unreadAnnouncements = processedAnnouncements.filter((a) => !a.read); + + // Process "What's new" announcements + const whatsNewAnnouncements = useWhatsNewProcessed( + whatsNewData ?? [], + language, + ); + // Load more announcements when user scrolls to bottom useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage) { @@ -74,12 +122,6 @@ export const Announcements = () => { } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); - const handleMarkAsRead = async (id: string) => { - markAsReadMutation.mutate({ - announcementId: id, - }); - }; - const handleMarkAllAsRead = async () => { markAllAsReadMutation.mutate(); }; @@ -124,7 +166,8 @@ export const Announcements = () => { /> ) : isLoading ? ( - ) : processedAnnouncements.length === 0 ? ( + ) : unreadAnnouncements.length === 0 && + whatsNewAnnouncements.length === 0 ? ( No announcements available @@ -132,24 +175,74 @@ export const Announcements = () => { ) : ( <> - {processedAnnouncements.map((announcement, index) => ( + {/* Unread announcements */} + {unreadAnnouncements.map((announcement, index) => ( ))} + {isFetchingNextPage && (
)} + + {/* Release notes under "View earlier" */} + {whatsNewAnnouncements.length > 0 && ( + <> + + setWhatsNewExpanded(!whatsNewExpanded) + } + > + + + + Release notes + + {whatsNewExpanded ? ( + + ) : ( + + )} + + + } + labelPosition="left" + /> + + + + {whatsNewAnnouncements.map((announcement) => ( + + ))} + + + + )} )} diff --git a/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx b/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx index d7ab3b27..d6c5675d 100644 --- a/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx +++ b/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx @@ -2,27 +2,23 @@ import { ActionIcon, Box, Group, + Text, ThemeIcon, - useMantineTheme, } from "@mantine/core"; -import { IconAlertTriangle, IconX } from "@tabler/icons-react"; +import { WarningCircle, X } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; import { useAnnouncementDrawer } from "@/components/announcement/hooks"; import { getTranslatedContent } from "@/components/announcement/hooks/useProcessedAnnouncements"; -import { Markdown } from "@/components/common/Markdown"; import { useLanguage } from "@/hooks/useLanguage"; import { useLatestAnnouncement, useMarkAsReadMutation } from "./hooks"; export function TopAnnouncementBar() { - const theme = useMantineTheme(); const { data: announcement, isLoading } = useLatestAnnouncement(); const markAsReadMutation = useMarkAsReadMutation(); const [isClosed, setIsClosed] = useState(false); const { open } = useAnnouncementDrawer(); const { language } = useLanguage(); - // Check if the announcement has been read by the current user - // Directus already filters activity data for the current user const isRead = announcement?.activity?.some( (activity: AnnouncementActivity) => activity.read === true, ); @@ -51,7 +47,6 @@ export function TopAnnouncementBar() { ); }, [isLoading, announcement, isClosed, isRead]); - // Only show if we have an urgent announcement, it's not closed, and it's not read if ( isLoading || !announcement || @@ -71,7 +66,6 @@ export function TopAnnouncementBar() { e.stopPropagation(); setIsClosed(true); - // Mark announcement as read if (announcement.id) { markAsReadMutation.mutate({ announcementId: announcement.id, @@ -83,10 +77,15 @@ export function TopAnnouncementBar() { open(); }; + const bgColor = + announcement.level === "urgent" + ? "rgba(255, 209, 102, 0.15)" + : "var(--mantine-color-blue-0)"; + return ( @@ -96,9 +95,11 @@ export function TopAnnouncementBar() { color={announcement.level === "urgent" ? "orange" : "blue"} radius="xl" > - + - + + {title} + - + ); diff --git a/echo/frontend/src/components/announcement/WhatsNewItem.tsx b/echo/frontend/src/components/announcement/WhatsNewItem.tsx new file mode 100644 index 00000000..a6ecf3c8 --- /dev/null +++ b/echo/frontend/src/components/announcement/WhatsNewItem.tsx @@ -0,0 +1,67 @@ +import { + Box, + Collapse, + Group, + Stack, + Text, + ThemeIcon, + UnstyledButton, +} from "@mantine/core"; +import { CaretDown, CaretRight, Sparkle } from "@phosphor-icons/react"; +import { useState } from "react"; +import { Markdown } from "@/components/common/Markdown"; +import { testId } from "@/lib/testUtils"; +import type { ProcessedAnnouncement } from "./hooks/useProcessedAnnouncements"; +import { useFormatDate } from "./utils/dateUtils"; + +interface WhatsNewItemProps { + announcement: ProcessedAnnouncement; +} + +export const WhatsNewItem = ({ announcement }: WhatsNewItemProps) => { + const [expanded, setExpanded] = useState(false); + const formatDate = useFormatDate(); + + return ( + + setExpanded(!expanded)} + w="100%" + > + + {expanded ? ( + + ) : ( + + )} + + + + + {announcement.title} + + + {formatDate(announcement.created_at)} + + + + + + + + + + + ); +}; diff --git a/echo/frontend/src/components/announcement/hooks/index.ts b/echo/frontend/src/components/announcement/hooks/index.ts index 81f31eac..d73b5826 100644 --- a/echo/frontend/src/components/announcement/hooks/index.ts +++ b/echo/frontend/src/components/announcement/hooks/index.ts @@ -1,4 +1,4 @@ -import { aggregate, createItems, type Query, readItems } from "@directus/sdk"; +import { createItems, type Query, readItems } from "@directus/sdk"; import { t } from "@lingui/core/macro"; import * as Sentry from "@sentry/react"; import { @@ -420,64 +420,108 @@ export const useUnreadAnnouncements = () => { const { data: currentUser } = useCurrentUser(); return useQuery({ - enabled: !!currentUser?.id, // Only run query if user is logged in + enabled: !!currentUser?.id, queryFn: async () => { try { - // If no user is logged in, return 0 if (!currentUser?.id) { return 0; } const unreadAnnouncements = await directus.request( - aggregate("announcement", { - aggregate: { count: "*" }, - query: { - filter: { - _or: [ - { - expires_at: { - _gte: new Date().toISOString(), + readItems("announcement", { + fields: ["id"], + filter: { + _and: [ + { + activity: { + _none: { + user_id: { + _eq: currentUser.id, + }, }, }, - { - expires_at: { - _null: true, + }, + { + _or: [ + { + expires_at: { + _gte: new Date().toISOString(), + }, }, - }, - ], - }, + { + expires_at: { + _null: true, + }, + }, + ], + }, + ], }, }), ); - const activities = await directus.request( - aggregate("announcement_activity", { - aggregate: { count: "*" }, - query: { - filter: { - _and: [ - { - user_id: { _eq: currentUser.id }, + return unreadAnnouncements.length; + } catch (error) { + Sentry.captureException(error); + console.error("Error fetching unread announcements count:", error); + throw error; + } + }, + queryKey: ["announcements", "unread", currentUser?.id], + retry: 2, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +}; + +export const useWhatsNewAnnouncements = ({ + enabled = true, +}: { + enabled?: boolean; +} = {}) => { + const { data: currentUser } = useCurrentUser(); + + return useQuery({ + enabled, + queryFn: async () => { + try { + const response: Announcement[] = await directus.request( + readItems("announcement", { + deep: { + activity: { + _filter: { + user_id: { + _eq: currentUser?.id, }, - ], + }, }, }, + fields: [ + "id", + "created_at", + "expires_at", + "level", + { + translations: ["id", "languages_code", "title", "message"], + }, + { + activity: ["id", "user_id", "announcement_activity", "read"], + }, + ], + sort: ["-created_at"], + limit: 50, }), ); - const count = - Number.parseInt(unreadAnnouncements?.[0]?.count?.toString() ?? "0") - - Number.parseInt(activities?.[0]?.count?.toString() ?? "0"); - return Math.max(0, count); + return response; } catch (error) { Sentry.captureException(error); - console.error("Error fetching unread announcements count:", error); + console.error("Error fetching what's new announcements:", error); throw error; } }, - queryKey: ["announcements", "unread", currentUser?.id], + queryKey: ["announcements", "whats-new"], retry: 2, - staleTime: 1000 * 60 * 5, // 5 minutes + staleTime: 1000 * 60 * 5, }); }; diff --git a/echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts b/echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts index 716c9c86..60708920 100644 --- a/echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts +++ b/echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts @@ -20,25 +20,73 @@ export const getTranslatedContent = ( }; }; -// @FIXME: this doesn't need to be a hook, it can be a simple function, memo for a .find is overkill +export const isWhatsNew = (announcement: Announcement): boolean => { + const enTranslation = announcement.translations?.find( + (t) => (t as AnnouncementTranslation).languages_code === "en-US", + ); + const title = + (enTranslation as AnnouncementTranslation)?.title?.toLowerCase() || ""; + return title.includes("new features") || title.startsWith("new:"); +}; + +export interface ProcessedAnnouncement { + id: string; + created_at: string | Date | null | undefined; + expires_at?: string | Date | null | undefined; + level: "info" | "urgent"; + title: string; + message: string; + read: boolean; +} + +function processAnnouncement( + announcement: Announcement, + language: string, +): ProcessedAnnouncement { + const { title, message } = getTranslatedContent(announcement, language); + return { + created_at: announcement.created_at, + expires_at: announcement.expires_at, + id: announcement.id, + level: announcement.level as "info" | "urgent", + message, + read: + (announcement.activity?.[0] as AnnouncementActivity)?.read || false, + title, + }; +} + +function sortByDateDesc(a: ProcessedAnnouncement, b: ProcessedAnnouncement) { + const dateA = a.created_at ? new Date(a.created_at).getTime() : 0; + const dateB = b.created_at ? new Date(b.created_at).getTime() : 0; + return dateB - dateA; +} + export function useProcessedAnnouncements( announcements: Announcement[], language: string, ) { return useMemo(() => { - return announcements.map((announcement) => { - const { title, message } = getTranslatedContent(announcement, language); - - return { - created_at: announcement.created_at, - expires_at: announcement.expires_at, - id: announcement.id, - level: announcement.level as "info" | "urgent", - message, - read: - (announcement.activity?.[0] as AnnouncementActivity)?.read || false, - title, - }; + const processed = announcements + .filter((a) => !isWhatsNew(a)) + .map((a) => processAnnouncement(a, language)); + + // Sort: unread first, then by date descending + return processed.sort((a, b) => { + if (a.read !== b.read) return a.read ? 1 : -1; + return sortByDateDesc(a, b); }); }, [announcements, language]); } + +export function useWhatsNewProcessed( + announcements: Announcement[], + language: string, +) { + return useMemo(() => { + return announcements + .filter((a) => isWhatsNew(a)) + .map((a) => processAnnouncement(a, language)) + .sort(sortByDateDesc); + }, [announcements, language]); +} diff --git a/echo/frontend/src/components/auth/hooks/index.ts b/echo/frontend/src/components/auth/hooks/index.ts index c15e19c4..bf6a2b4c 100644 --- a/echo/frontend/src/components/auth/hooks/index.ts +++ b/echo/frontend/src/components/auth/hooks/index.ts @@ -1,7 +1,6 @@ import { passwordRequest, passwordReset, - readUser, registerUser, registerUserVerify, } from "@directus/sdk"; @@ -9,7 +8,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; import { useLocation, useSearchParams } from "react-router"; import { toast } from "@/components/common/Toaster"; -import { ADMIN_BASE_URL } from "@/config"; +import { ADMIN_BASE_URL, API_BASE_URL } from "@/config"; import { useI18nNavigate } from "@/hooks/useI18nNavigate"; import { directus } from "@/lib/directus"; import { throwWithMessage } from "../utils/errorUtils"; @@ -21,22 +20,14 @@ export const useCurrentUser = ({ } = {}) => useQuery({ enabled, - queryFn: () => { + queryFn: async () => { try { - return directus.request( - readUser("me", { - fields: [ - "id", - "first_name", - "email", - "disable_create_project", - "tfa_secret", - "whitelabel_logo", - "legal_basis", - "privacy_policy_url", - ], - }), + const response = await fetch( + `${API_BASE_URL}/user-settings/me`, + { credentials: "include" }, ); + if (!response.ok) return null; + return response.json(); } catch (_error) { return null; } diff --git a/echo/frontend/src/components/chat/ChatHistoryMessage.tsx b/echo/frontend/src/components/chat/ChatHistoryMessage.tsx index 8b74fe4a..b344a81a 100644 --- a/echo/frontend/src/components/chat/ChatHistoryMessage.tsx +++ b/echo/frontend/src/components/chat/ChatHistoryMessage.tsx @@ -1,6 +1,8 @@ +import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import { Box, Collapse, Divider, Group, Text } from "@mantine/core"; +import { ActionIcon, Box, Collapse, Divider, Group, Text, Tooltip } from "@mantine/core"; import { formatDate } from "date-fns"; +import { BookmarkSimple } from "@phosphor-icons/react"; import type React from "react"; import { useEffect, useState } from "react"; import { useParams } from "react-router"; @@ -25,12 +27,14 @@ export const ChatHistoryMessage = ({ referenceIds, setReferenceIds, chatMode, + onSaveAsTemplate, }: { message: ChatHistory[number]; section?: React.ReactNode; referenceIds?: string[]; setReferenceIds?: (ids: string[]) => void; chatMode?: ChatMode; + onSaveAsTemplate?: (content: string) => void; }) => { const [metadata, setMetadata] = useState([]); const { projectId } = useParams(); @@ -80,6 +84,18 @@ export const ChatHistoryMessage = ({ + {message.role === "user" && onSaveAsTemplate && ( + + onSaveAsTemplate(message.content)} + > + + + + )} {/* Info button for citations */} {ENABLE_CHAT_AUTO_SELECT && diff --git a/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx b/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx index 6dee98a3..7765791b 100644 --- a/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx +++ b/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx @@ -1,35 +1,39 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import { ActionIcon, Group, Paper, Stack, Text, Tooltip } from "@mantine/core"; +import { ActionIcon, Group, Paper, Text, Tooltip } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { - IconBulb, - IconDots, - IconList, - IconQuote, - IconSearch, - IconSparkles, -} from "@tabler/icons-react"; + GearSix, + type Icon, + Lightbulb, + List, + MagnifyingGlass, + Quotes, + Sparkle, +} from "@phosphor-icons/react"; +import { useEffect, useMemo } from "react"; import { analytics } from "@/lib/analytics"; import { AnalyticsEvents as events } from "@/lib/analyticsEvents"; import type { ChatMode } from "@/lib/api"; import { testId } from "@/lib/testUtils"; import { MODE_COLORS } from "./ChatModeSelector"; +import type { QuickAccessItem } from "./QuickAccessConfigurator"; import { TemplatesModal } from "./TemplatesModal"; +import { decodeTemplateKey, encodeTemplateKey } from "./templateKey"; import { agenticQuickAccessTemplates, quickAccessTemplates, Templates, } from "./templates"; -// Map icon names from API to Tabler icons -const SUGGESTION_ICONS: Record = { - lightbulb: IconBulb, - list: IconList, - quote: IconQuote, - search: IconSearch, - sparkles: IconSparkles, +// Map icon names from API to Phosphor icons +const SUGGESTION_ICONS: Record = { + lightbulb: Lightbulb, + list: List, + quote: Quotes, + search: MagnifyingGlass, + sparkles: Sparkle, }; type ChatTemplatesMenuProps = { @@ -43,53 +47,114 @@ type ChatTemplatesMenuProps = { selectedTemplateKey?: string | null; suggestions?: TSuggestion[]; chatMode?: ChatMode | null; + // User templates + userTemplates?: Array<{ + id: string; + title: string; + content: string; + icon: string | null; + }>; + onCreateUserTemplate?: (payload: { title: string; content: string }) => void; + onUpdateUserTemplate?: (payload: { + id: string; + title: string; + content: string; + }) => void; + onDeleteUserTemplate?: (id: string) => void; + isCreatingTemplate?: boolean; + isUpdatingTemplate?: boolean; + isDeletingTemplate?: boolean; + // Quick access + quickAccessItems?: QuickAccessItem[]; + onSaveQuickAccess?: (items: QuickAccessItem[]) => void; + isSavingQuickAccess?: boolean; + // AI suggestions toggle + hideAiSuggestions?: boolean; + onToggleAiSuggestions?: (hide: boolean) => void; + // Favorites + favoriteTemplateIds?: Set; + onToggleFavorite?: (promptTemplateId: string, isFavorited: boolean) => void; + // External open control + externalOpen?: boolean; + onExternalClose?: () => void; + // Save as template prefill + saveAsTemplateContent?: string | null; + onClearSaveAsTemplate?: () => void; + // Community publish context + userTemplateDetails?: Array<{ + id: string; + is_public: boolean; + star_count: number; + copied_from: string | null; + author_display_name: string | null; + }>; + defaultLanguage?: string; + userName?: string | null; }; -// Suggestion pill component with subtle styling and colored icon -const SuggestionPill = ({ - suggestion, - chatMode, +// Reusable chip for both dynamic suggestions and pinned templates +const TemplatePill = ({ + label, + icon: IconComponent, isSelected, onClick, + chatMode, + testIdSuffix, }: { - suggestion: TSuggestion; - chatMode?: ChatMode | null; + label: string; + icon?: Icon; isSelected: boolean; onClick: () => void; + chatMode?: ChatMode | null; + testIdSuffix: string; }) => { - const Icon = SUGGESTION_ICONS[suggestion.icon] || IconSparkles; const colors = chatMode ? MODE_COLORS[chatMode] : null; - const isOverview = chatMode === "overview"; const isDeepDive = chatMode === "deep_dive"; return ( - - - - - {suggestion.label} - - - + + + + {IconComponent && ( + + )} + + {label} + + + + ); }; @@ -98,11 +163,74 @@ export const ChatTemplatesMenu = ({ selectedTemplateKey, suggestions = [], chatMode, + userTemplates = [], + onCreateUserTemplate, + onUpdateUserTemplate, + onDeleteUserTemplate, + isCreatingTemplate = false, + isUpdatingTemplate = false, + isDeletingTemplate = false, + quickAccessItems = [], + onSaveQuickAccess, + isSavingQuickAccess = false, + hideAiSuggestions = false, + onToggleAiSuggestions, + favoriteTemplateIds = new Set(), + onToggleFavorite, + externalOpen = false, + onExternalClose, + userTemplateDetails = [], + defaultLanguage, + userName, + saveAsTemplateContent, + onClearSaveAsTemplate, }: ChatTemplatesMenuProps) => { const [opened, { open, close }] = useDisclosure(false); - const [animateRef] = useAutoAnimate(); - const activeQuickAccessTemplates = - chatMode === "agentic" ? agenticQuickAccessTemplates : quickAccessTemplates; + + // Handle external open + useEffect(() => { + if (externalOpen) { + open(); + } + }, [externalOpen, open]); + + const handleClose = () => { + close(); + onExternalClose?.(); + }; + + // Resolve quick-access templates from quickAccessItems (already resolved by parent) + const resolvedQuickAccessTemplates = useMemo(() => { + if (chatMode === "agentic") return agenticQuickAccessTemplates; + + if (quickAccessItems.length === 0) { + return quickAccessTemplates; + } + + const resolved: Array<{ title: string; content: string; key: string }> = []; + for (const item of quickAccessItems) { + if (item.type === "static") { + const found = Templates.find((t) => t.id === item.id); + if (found) { + resolved.push({ + content: found.content, + key: encodeTemplateKey("dembrane", found.id), + title: found.title, + }); + } + } else if (item.type === "user") { + const found = userTemplates.find((t) => t.id === item.id); + if (found) { + resolved.push({ + content: found.content, + key: encodeTemplateKey("user", found.id), + title: found.title, + }); + } + } + } + return resolved.length > 0 ? resolved : quickAccessTemplates; + }, [chatMode, quickAccessItems, userTemplates]); const handleTemplateSelect = ( template: { content: string; key: string }, @@ -121,109 +249,162 @@ export const ChatTemplatesMenu = ({ // Check if selected template is from modal (not in quick access) const isModalTemplateSelected = selectedTemplateKey && - !activeQuickAccessTemplates.some((t) => t.title === selectedTemplateKey) && + !resolvedQuickAccessTemplates.some( + (t) => ("key" in t ? t.key : t.title) === selectedTemplateKey, + ) && !suggestions.some((s) => s.label === selectedTemplateKey); - const selectedModalTemplate = isModalTemplateSelected - ? Templates.find((t) => t.title === selectedTemplateKey) - : null; + const selectedModalTemplate = (() => { + if (!isModalTemplateSelected || !selectedTemplateKey) return null; + const ref = decodeTemplateKey(selectedTemplateKey); + if (!ref) return null; + if (ref.source === "dembrane") + return Templates.find((t) => t.id === ref.id) ?? null; + if (ref.source === "user") + return userTemplates.find((t) => t.id === ref.id) ?? null; + return null; + })(); + + const [animateRef] = useAutoAnimate(); + + // Slot allocation: max 7 pills total (including "+N more" overflow pill) + const MAX_PILLS = 7; + const MAX_SUGGESTIONS = 3; + const visibleSuggestions = hideAiSuggestions + ? [] + : suggestions.slice(0, MAX_SUGGESTIONS); + const slotsForPinned = MAX_PILLS - visibleSuggestions.length; + const pinnedCount = resolvedQuickAccessTemplates.length; + // If all pinned fit, show them all. Otherwise reserve 1 slot for "+N more". + const pinnedSlots = pinnedCount <= slotsForPinned + ? pinnedCount + : slotsForPinned - 1; + const visiblePinned = resolvedQuickAccessTemplates.slice(0, Math.max(0, pinnedSlots)); + const pinnedOverflow = pinnedCount - visiblePinned.length; return ( <> - - {/* Single "Suggested" row with AI suggestions (colored) + static templates (gray) */} - - + + {/* Contextual suggestions */} + {visibleSuggestions.length > 0 && ( + Suggested: + )} + {visibleSuggestions.map((suggestion) => ( + + handleTemplateSelect( + { + content: suggestion.prompt, + key: suggestion.label, + }, + true, + ) + } + testIdSuffix={`suggestion-${suggestion.label.toLowerCase().replace(/\s+/g, "-")}`} + /> + ))} - {/* AI Suggestions - colored based on mode, animated in/out */} - {suggestions.map((suggestion, idx) => ( - - handleTemplateSelect( - { - content: suggestion.prompt, - key: suggestion.label, - }, - true, // isDynamic - track analytics for AI suggestions - ) - } - /> - ))} - - {/* Static Templates - gray (fill remaining slots up to 5 total) */} - {activeQuickAccessTemplates - .slice(0, Math.max(0, 5 - suggestions.length)) - .map((template) => { - const isSelected = selectedTemplateKey === template.title; - return ( - - handleTemplateSelect({ - content: template.content, - key: template.title, - }) - } - {...testId( - `chat-template-static-${template.title.toLowerCase().replace(/\s+/g, "-")}`, - )} - > - - {template.title} - - - ); - })} - - {/* Show selected modal template */} - {selectedModalTemplate && ( - { + const templateKey = "key" in template ? template.key : template.title; + return ( + handleTemplateSelect({ - content: selectedModalTemplate.content, - key: selectedModalTemplate.title, + content: template.content, + key: templateKey, }) } - > - - {selectedModalTemplate.title} - - - )} + testIdSuffix={`static-${templateKey.toLowerCase().replace(/\s+/g, "-")}`} + /> + ); + })} - - - - - - - + {/* Show selected modal template (not in quick access) */} + {selectedModalTemplate && ( + + handleTemplateSelect({ + content: selectedModalTemplate.content, + key: + "id" in selectedModalTemplate && + typeof selectedModalTemplate.id === "string" && + selectedModalTemplate.id.length > 10 + ? encodeTemplateKey("user", selectedModalTemplate.id) + : encodeTemplateKey("dembrane", selectedModalTemplate.title), + }) + } + testIdSuffix={`modal-${selectedModalTemplate.title.toLowerCase().replace(/\s+/g, "-")}`} + /> + )} + + {/* Overflow pill */} + {pinnedOverflow > 0 && ( + + + +{pinnedOverflow} more + + + )} + + + + + + + ); diff --git a/echo/frontend/src/components/chat/CommunityTab.tsx b/echo/frontend/src/components/chat/CommunityTab.tsx new file mode 100644 index 00000000..928805dd --- /dev/null +++ b/echo/frontend/src/components/chat/CommunityTab.tsx @@ -0,0 +1,144 @@ +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import { + Badge, + Group, + ScrollArea, + Select, + Stack, + Text, +} from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import { useState } from "react"; +import { CommunityTemplateCard } from "./CommunityTemplateCard"; +import { + useCommunityTemplates, + useCopyTemplate, + useMyCommunityStars, + useToggleStar, +} from "./hooks/useCommunityTemplates"; + +const ALLOWED_TAGS = [ + "Workshop", + "Interview", + "Focus Group", + "Meeting", + "Research", + "Community", + "Education", + "Analysis", +]; + +const getSortOptions = () => [ + { value: "newest", label: t`Newest` }, + { value: "most_starred", label: t`Most popular` }, + { value: "most_used", label: t`Most used` }, +]; + +type CommunityTabProps = { + searchQuery: string; +}; + +export const CommunityTab = ({ searchQuery }: CommunityTabProps) => { + const [selectedTag, setSelectedTag] = useState(null); + const [sortBy, setSortBy] = useState("newest"); + const [expandedId, setExpandedId] = useState(null); + const sortOptions = getSortOptions(); + + const [debouncedSearch] = useDebouncedValue(searchQuery, 300); + + const communityQuery = useCommunityTemplates({ + search: debouncedSearch || undefined, + tag: selectedTag ?? undefined, + sort: sortBy as "newest" | "most_starred" | "most_used", + }); + + const starsQuery = useMyCommunityStars(); + const toggleStarMutation = useToggleStar(); + const copyMutation = useCopyTemplate(); + + const starredIds = starsQuery.data ?? new Set(); + const templates = communityQuery.data ?? []; + + return ( + + {/* Filters row */} + + + + setSelectedTag(null)} + > + All + + {ALLOWED_TAGS.map((tag) => ( + + setSelectedTag(selectedTag === tag ? null : tag) + } + > + {tag} + + ))} + + +