Compare commits
41 Commits
92648472d7
...
feat/proje
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a8acd08c0 | ||
|
|
82a7a8dc27 | ||
|
|
5a90dbc832 | ||
|
|
f72aaa8424 | ||
|
|
fa09ed2156 | ||
|
|
1341fb3144 | ||
|
|
3705316a25 | ||
|
|
72d7cc2f6e | ||
|
|
e1d15b3edd | ||
|
|
faea5f0448 | ||
|
|
c68e23b713 | ||
|
|
aba0f38816 | ||
|
|
8dffbc714c | ||
|
|
0abed9563b | ||
|
|
361f89a29d | ||
|
|
74e2152596 | ||
|
|
649e4f00a5 | ||
|
|
1c8c7e2ddc | ||
|
|
b111c76661 | ||
|
|
804a0a5af3 | ||
|
|
314d5656ae | ||
|
|
e073f4f774 | ||
|
|
20240c5fea | ||
|
|
310410350f | ||
|
|
5274f014b9 | ||
|
|
ba9c9a4702 | ||
|
|
064142d386 | ||
|
|
0c3dfb3564 | ||
|
|
c16e68f0d0 | ||
|
|
75f6b0dca5 | ||
|
|
27dbfdfa8d | ||
|
|
6a352075ec | ||
|
|
d80d4d6b9e | ||
|
|
b2989e53eb | ||
|
|
0ac2ce924d | ||
|
|
3538050e75 | ||
|
|
2ee3bb37db | ||
|
|
25a5a6672e | ||
|
|
8ce3ade8ce | ||
|
|
54eb863c52 | ||
|
|
bc2c76d2bb |
@@ -1,48 +1,60 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
Guide Claude Code when work in repo.
|
||||||
|
|
||||||
## Keeping This File Up to Date
|
## Keeping This File Up to Date
|
||||||
|
|
||||||
Update this file whenever a lesson is learned during development. Specifically, update CLAUDE.md when:
|
Update when lesson learned. Update when:
|
||||||
|
|
||||||
- A non-obvious architectural decision is made or discovered
|
- Non-obvious arch decision made or found
|
||||||
- A gotcha, footgun, or surprising behavior is encountered (and the fix/workaround)
|
- Gotcha, footgun, surprising behavior hit (+ fix/workaround)
|
||||||
- A new command, workflow, or tool is added to the project
|
- New command, workflow, tool added
|
||||||
- A convention is established that isn't obvious from reading the code
|
- Convention set that not obvious from code
|
||||||
- An integration detail is clarified (e.g., how the IPC protocol actually behaves, edge cases in the agent tool call cycle)
|
- Integration detail clarified (IPC protocol behavior, agent tool call edge cases)
|
||||||
|
|
||||||
Do **not** add things already derivable from reading the code, generic best practices, or ephemeral task notes — only durable, reusable knowledge.
|
Do **not** add derivable-from-code things, generic best practices, or ephemeral task notes — durable knowledge only.
|
||||||
|
|
||||||
|
## graphify
|
||||||
|
|
||||||
|
This project has a graphify knowledge graph at graphify-out/.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
|
||||||
|
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
|
||||||
|
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
|
||||||
|
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
|
||||||
|
|
||||||
## Repository Layout
|
## Repository Layout
|
||||||
|
|
||||||
This is a **monorepo with git submodules**. Each submodule is an independent repo with its own `.claude/CLAUDE.md` for detailed guidance.
|
**Monorepo with git submodules.** Each submodule independent repo with own `.claude/CLAUDE.md`.
|
||||||
|
|
||||||
| Directory | What | Submodule |
|
| Directory | What | Submodule |
|
||||||
|-----------|------|-----------|
|
|-----------|------|-----------|
|
||||||
| **`adiuvAI/`** | Electron desktop app (TypeScript/React) | `git.muticolturano.com/adiuvAI/adiuvAI` |
|
| **`adiuvAI/`** | Electron desktop app (TypeScript/React) | `git.muticolturano.com/adiuvAI/adiuvAI` |
|
||||||
| **`api/`** | FastAPI backend (Python) | `git.muticolturano.com/adiuvAI/api` |
|
| **`api/`** | FastAPI backend (Python) | `git.muticolturano.com/adiuvAI/api` |
|
||||||
| **`website/`** | Landing page (single `index.html`) | `git.muticolturano.com/adiuvAI/website` |
|
|
||||||
| **`docs/`** | Planning docs & working memory (not a submodule) | -- |
|
| **`docs/`** | Planning docs & working memory (not a submodule) | -- |
|
||||||
|
|
||||||
After cloning, run `git submodule update --init --recursive` to populate submodule contents.
|
After clone, run `git submodule update --init --recursive` to populate submodules.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## adiuvAI (Electron App)
|
## adiuvAI (Electron App)
|
||||||
|
|
||||||
> **Detailed docs**: `adiuvAI/.claude/CLAUDE.md` covers commands, architecture, AI subsystem, design context, and conventions in depth.
|
> **Detailed docs**: `adiuvAI/.claude/CLAUDE.md` — commands, architecture, AI subsystem, design context, conventions.
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd adiuvAI
|
cd adiuvAI
|
||||||
npm run start # Start dev server (Electron + Vite)
|
npm run start # Dev (Electron + Vite)
|
||||||
npm run lint # ESLint
|
npm run lint # ESLint
|
||||||
npm run knip # Dead code analysis
|
npm run knip # Dead code analysis
|
||||||
npm run make # Build installers (Windows/Linux/macOS)
|
npm run make # Build installers (Win/Linux/macOS)
|
||||||
npm run package # Package without creating installers
|
npm run package # Package without installers
|
||||||
npx drizzle-kit generate # Generate migration from schema changes
|
npm run dev:web # Standalone web SPA dev
|
||||||
|
npm run build:web # Build standalone SPA → dist-web/
|
||||||
|
npm run preview:web # Preview built web SPA
|
||||||
|
npx drizzle-kit generate # Generate migration from schema
|
||||||
npx drizzle-kit push # Push schema directly (dev only)
|
npx drizzle-kit push # Push schema directly (dev only)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,34 +68,87 @@ Renderer (React 19 + TanStack Router)
|
|||||||
Preload (contextBridge: window.electronTRPC + window.electronAI)
|
Preload (contextBridge: window.electronTRPC + window.electronAI)
|
||||||
↓ IPC channels
|
↓ IPC channels
|
||||||
Main Process (Node.js)
|
Main Process (Node.js)
|
||||||
├── tRPC router (all CRUD + AI procedures)
|
├── tRPC router (CRUD + AI proxy procedures)
|
||||||
├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
|
├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
|
||||||
├── LanceDB (vector embeddings, 1536-dim text-embedding-3-small)
|
└── Backend delegation layer (orchestrator.ts forwards to FastAPI WS)
|
||||||
└── LangGraph orchestrator (3 specialist agents, pluggable LLM providers)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**This is a local-first app.** All user data (tasks, notes, projects) lives in local SQLite. The AI system (LangGraph + LangChain) runs entirely in the Electron main process with pluggable providers (OpenAI, Anthropic, GitHub Copilot).
|
**Local-first storage, cloud AI.** All user data (clients, projects, tasks, notes, timelines) in local SQLite. AI lives entirely on the FastAPI backend — Electron orchestrator is a thin delegation shell that forwards to `/api/v1/device` WS and dispatches v3 typed stream frames + tool-call ↔ DrizzleExecutor round-trips back to renderer.
|
||||||
|
|
||||||
**IPC channels**:
|
**IPC channels**:
|
||||||
- `'trpc'` — bidirectional tRPC request/response (all CRUD)
|
- `'trpc'` — bidirectional tRPC request/response (all CRUD + auth + agent + memory proxy)
|
||||||
- `'ai:stream'` — one-way token streaming from main → renderer
|
- `'ai:stream'` — one-way v3 stream frames main → renderer
|
||||||
- `'ai:action'` — AI side-effects (e.g., task auto-created by agent)
|
- `'ai:action'` — AI side-effects (e.g. agent auto-creates task)
|
||||||
|
|
||||||
**Key source paths**:
|
**Main process layout (`src/main/`)**:
|
||||||
- `src/main/ipc.ts` — Custom tRPC↔IPC bridge
|
- `index.ts` — Window creation, app lifecycle, protocol handler
|
||||||
- `src/main/router/index.ts` — All tRPC routers (~600 LOC)
|
- `ipc.ts` — Custom tRPC↔IPC bridge
|
||||||
- `src/main/ai/orchestrator.ts` — LangGraph intent routing + 3 agents (~991 LOC)
|
- `store.ts` — electron-store for `FormatPrefs` + `uiLanguage`; exports `getUiLanguage()`
|
||||||
- `src/main/db/schema.ts` — 6 tables (clients, projects, tasks, checkpoints, notes, taskComments)
|
- `router/index.ts` — All tRPC sub-routers (~1627 LOC)
|
||||||
- `src/renderer/routes/` — File-based routing (TanStack Router auto-generates `routeTree.gen.ts`)
|
- `db/schema.ts` — 10 tables: clients, projects, tasks, timelineEvents, timelineEventDependencies, notes, noteEdits, taskComments, agentRuns, agentRunActions
|
||||||
- `src/renderer/components/ui/` — shadcn/ui primitives (new-york theme, neutral colors)
|
- `db/index.ts` — Drizzle + better-sqlite3 (WAL), singleton `getDb()`, `initDb()` migrations
|
||||||
|
- `db/notes-backfill.ts` — Startup backfill: generates `aiSummary` for notes with null summary
|
||||||
|
- `ai/orchestrator.ts` — Thin backend-delegation layer (~304 LOC). Connectivity/auth guard → `BackendClient.sendHomeRequest()` / `sendFloatingRequest()` → forwards v3 stream frames to renderer. Also schedules daily-brief regeneration.
|
||||||
|
- `ai/token.ts` — Two-tier token storage (safeStorage + electron-store fallback)
|
||||||
|
- `agents/agent-scheduler.ts` — Local agent scheduling (filesystem agents)
|
||||||
|
- `api/backend-client.ts` — WS client to FastAPI: handles tool-call round-trips, v3 stream frame dispatch, journey + agent proxies
|
||||||
|
- `api/drizzle-executor.ts` — Executes backend-issued tool calls against local SQLite. Wraps results through `formatRow()`/`formatRows()` using user FormatPrefs
|
||||||
|
- `auth/auth-manager.ts` — Login, register, logout, OAuth flow (singleton)
|
||||||
|
- `auth/backup-key.ts` — Device-specific AES-256 backup key (safeStorage, not password-derived)
|
||||||
|
- `auth/locale-defaults.ts` — Detects timezone, date/time format, language from OS locale
|
||||||
|
|
||||||
|
**tRPC routers** (in `appRouter`): `health`, `settings`, `clients`, `projects`, `tasks`, `timelineEvents`, `timelineEventDependencies`, `notes`, `noteEdits`, `taskComments`, `ai`, `auth`, `agent` (with `local` / `cloud` / `journey` sub-routers), `memory`.
|
||||||
|
|
||||||
|
**Renderer** (`src/renderer/`): file-based routing via TanStack Router (`routeTree.gen.ts` auto-generated). shadcn/ui new-york theme, neutral colors. Path alias `@/*` → `src/renderer/*`. Notes editor: Milkdown (`@milkdown/crepe`).
|
||||||
|
|
||||||
**Non-obvious details**:
|
**Non-obvious details**:
|
||||||
- `electron-trpc` is NOT used — custom IPC bridge in `ipc.ts` + `ipcLink.ts` because electron-trpc bundles tRPC v10 internals
|
- `electron-trpc` NOT used — custom IPC bridge (`ipc.ts` + `lib/ipcLink.ts`) because electron-trpc bundles tRPC v10 internals
|
||||||
- Vite configs use `.mts` extension to avoid ESM/CJS conflicts with electron-forge
|
- Vite configs use `.mts` extension to avoid ESM/CJS conflicts with electron-forge
|
||||||
- `forge.config.ts` has complex cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3 and LanceDB)
|
- `forge.config.ts` has cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3)
|
||||||
- DB has no foreign key constraints — cascade deletes are implemented in tRPC procedures
|
- DB has no foreign key constraints — cascade deletes in tRPC procedures
|
||||||
- Timestamps are milliseconds (JavaScript `Date.getTime()`), not ISO strings
|
- Timestamps are milliseconds (`Date.getTime()`), not ISO strings
|
||||||
- Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)
|
- Notes use `aiSummary` (≤250 char, backend `gpt-4o-mini` via `POST /api/v1/agents/notes/summarize`) for AI navigation — LanceDB fully removed
|
||||||
|
- AI note edits go through `noteEdits` HITL table (`type: append|insert|replace`, `status: pending|approved|rejected`); backend tool `propose_note_edit` → drizzle-executor inserts row; user approves/rejects in UI; auto-reject on missing anchor
|
||||||
|
- `checkpoints` table replaced by `timelineEvents` + `timelineEventDependencies` (events are typed `milestone|checkpoint|activity`, with optional dep edges)
|
||||||
|
- `agentRuns` + `agentRunActions` populated by backend-client on tool_call/run_complete frames; UI reads via `agent.runs` / `agent.runActions`
|
||||||
|
|
||||||
|
**Settings Page (shared Electron + Web)**:
|
||||||
|
- Settings page runs in **both** Electron and standalone web SPA. Same React components — no duplication.
|
||||||
|
- **Platform Adapter**: `PlatformProvider` context (`src/renderer/lib/platform.tsx`) exposes `isElectron`/`isWeb`/`hasLocalAgents`/`hasFileDialog`. Components use `usePlatform()` to gate Electron-only features.
|
||||||
|
- **Web build**: `vite.web.config.mts` → `dist-web/`. Entry: `web.html` → `src/renderer/web-main.tsx` (uses `httpBatchLink` via `lib/httpLink.ts` instead of `ipcLink`).
|
||||||
|
- **Electron-only gating**: Device ID card and local agent filesystem gated behind `platform.isElectron`. On web: visible but disabled, not hidden.
|
||||||
|
- **Gotcha**: Do NOT add Electron-specific settings (server URL, native file pickers) without wrapping in `platform.isElectron`. Same component tree renders on web.
|
||||||
|
|
||||||
|
**Onboarding Wizard**:
|
||||||
|
- First-run wizard collects 5 fields: `job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`. Plus `user_name` from `name`+`surname`.
|
||||||
|
- All fields stored as encrypted core memory (backend `MemoryMiddleware`), not local electron-store.
|
||||||
|
- `onboarding_completed_at` on `users` table (nullable TIMESTAMPTZ) gates flow — `null` = show wizard, non-null = skip.
|
||||||
|
- `AppShell.tsx` gates: if `profile.onboardingCompletedAt == null` → render `<OnboardingFlow>` instead of app.
|
||||||
|
- `auth.status` tRPC procedure auto-seeds `language` and `user_name` into MemoryCore if missing (fire-and-forget `.catch(() => {})`).
|
||||||
|
- Format prefs (timezone, dateFormat, timeFormat) stored in electron-store (`FormatPrefs`), not core memory — device-specific.
|
||||||
|
- `drizzle-executor.ts` wraps all query results through `formatRow()`/`formatRows()` using user FormatPrefs.
|
||||||
|
- Settings > Profile allows post-onboarding edit of all fields + format prefs.
|
||||||
|
- **Gotcha — shadcn Button `outline` variant in dark mode**: variant defines `dark:bg-input/30 dark:border-input dark:hover:bg-input/50` — overrides custom `className` background. Fix: switch between `variant="default"` and `variant="outline"` instead of className overrides.
|
||||||
|
- **Gotcha — locale codes vs human names**: `app.getLocale()` and `navigator.language` return codes like `en-US`. Use `Intl.DisplayNames(undefined, { type: 'language' })` to convert to "English". Must do in both main process (`locale-defaults.ts`) and renderer (`OnboardingFlow.tsx`).
|
||||||
|
|
||||||
|
**i18n (Internationalization)**:
|
||||||
|
- `i18next` + `react-i18next` with bundled JSON translations (no lazy loading).
|
||||||
|
- Config in `src/renderer/i18n.ts`. 5 languages: EN, IT, ES, FR, DE. `SUPPORTED_LANGUAGES` exported for UI selectors.
|
||||||
|
- Translation files: `src/renderer/locales/{en,it,es,fr,de}/translation.json`. Namespaces: `nav`, `auth`, `tasks`, `settings`, `common`, `errors`, `home`, `timeline`, `projects`, `agents`.
|
||||||
|
- **`common.*` namespace** holds shared labels (`save`, `cancel`, `delete`, `edit`, `add`, `rename`, `saving`, `deleting`, `creating`, `renameDescription`, `deleteTitle`). Check `common.*` before adding new key.
|
||||||
|
- Pluralization uses i18next `_one`/`_other` suffixes.
|
||||||
|
- `LanguageSync` component in `src/renderer/index.tsx` reads persisted `uiLanguage` from electron-store via tRPC on startup, syncs to i18next.
|
||||||
|
- Language selector in `GeneralSection.tsx` (Settings > General). On change: (1) calls `i18n.changeLanguage()`, (2) persists to electron-store via `setUiLanguage` mutation, (3) writes to backend core memory so AI responds in same language.
|
||||||
|
- `getUiLanguage()` exported from `src/main/store.ts`.
|
||||||
|
- Static data arrays needing translation use `labelKey` pattern: store translation key, call `t(labelKey)` at render. Used in `NAV_ITEMS`, `COLUMNS`, `SECTIONS`, `SUGGESTION_CHIPS`.
|
||||||
|
- When adding new translated text: add key to **all 5** JSON files. Keep `common.*` consistent across all languages.
|
||||||
|
|
||||||
|
**Google OAuth (adiuvAI side)**:
|
||||||
|
- `adiuvai://` NOT accepted by Google as redirect URI — Google only accepts `http://localhost` or `https://`. API backend exposes `GET /auth/oauth/google/web-callback` which receives Google redirect and bounces to `adiuvai://oauth/callback?...`. Redirect URI in Google Cloud Console points to backend, not Electron app.
|
||||||
|
- `app.requestSingleInstanceLock()` required for `second-instance` event on Windows/Linux. If returns `false`, call `app.quit()` immediately.
|
||||||
|
- In dev (`process.defaultApp === true`), `setAsDefaultProtocolClient('adiuvai')` must include `[path.resolve(process.argv[1])]` as third arg so OS protocol registration includes entry script.
|
||||||
|
- `loginWithOAuth` uses `fetch()` directly (not `this.get()`) — authorize endpoint is public, `get()` throws when not authenticated.
|
||||||
|
- Backup key in `backup-key.ts` stored in `encryptedTokens` under key `backup_key`, reusing `getToken/setToken` from `token.ts`. Device-bound, never password-derived — social-login users can use backup features.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -106,14 +171,14 @@ alembic upgrade head
|
|||||||
# Testing
|
# Testing
|
||||||
pytest # all tests
|
pytest # all tests
|
||||||
pytest -v # verbose
|
pytest -v # verbose
|
||||||
pytest tests/test_agents.py # single file
|
pytest tests/test_deep_agent.py # single file
|
||||||
pytest tests/test_agents.py -k test_name # single test
|
pytest tests/test_deep_agent.py -k test_name # single test
|
||||||
|
|
||||||
# Linting/formatting
|
# Linting/formatting
|
||||||
ruff check .
|
ruff check .
|
||||||
ruff format .
|
ruff format .
|
||||||
|
|
||||||
# Docker (full stack: app + postgres + minio + qdrant)
|
# Docker (full stack)
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -121,85 +186,123 @@ docker compose up --build
|
|||||||
|
|
||||||
```
|
```
|
||||||
FastAPI app (app/main.py)
|
FastAPI app (app/main.py)
|
||||||
├── Middleware: TierRateLimiter → Sanitizer → CORS
|
├── Lifespan: APScheduler crons (memory hourly + audit weekly) when SCHEDULER_ENABLED
|
||||||
├── HTTP Routes (app/api/routes/)
|
├── Middleware: TierRateLimit → Sanitizer → CORS
|
||||||
│ ├── auth.py — register, login, token refresh (bcrypt + HS256 JWT)
|
├── HTTP Routes (app/api/routes/) — all under /api/v1
|
||||||
│ ├── chat.py — POST /chat, WS /chat/stream
|
│ ├── auth.py — register, login, refresh, profile, OAuth, onboarding, password
|
||||||
│ ├── plans.py — execution plan playbooks
|
│ ├── chat.py — POST /chat, /chat/brief, /chat/embed
|
||||||
│ ├── storage.py — E2E-encrypted cloud storage (S3)
|
│ ├── agents.py — catalog, can-create, trigger, notes/summarize
|
||||||
│ ├── backup.py — encrypted backup upload/download
|
│ ├── agent_setup.py — guided agent setup (journey)
|
||||||
│ ├── vectors.py — encrypted vector upsert/search (Pinecone/Qdrant)
|
│ ├── billing.py — Stripe checkout, webhook, subscription, invoices
|
||||||
│ ├── plugins.py — plugin marketplace (Power+ tier)
|
│ ├── device_ws.py — WS /device (unified streaming endpoint: home, floating, brief, journey)
|
||||||
│ └── billing.py — Stripe subscriptions
|
│ └── memory.py — core / relational / forget-all
|
||||||
├── Agent System (app/agents/)
|
├── Agent System (app/agents/)
|
||||||
│ ├── task_agent.py — 8 tools
|
│ ├── task_agent.py
|
||||||
│ ├── project_agent.py — 6 tools
|
│ ├── project_agent.py
|
||||||
│ ├── checkpoint_agent.py — 4 tools
|
│ ├── note_agent.py
|
||||||
│ └── note_agent.py — 5 tools
|
│ ├── timeline_agent.py
|
||||||
├── Orchestration (app/core/)
|
│ └── filesystem_agent.py
|
||||||
│ ├── orchestrator.py — intent classification + agent routing
|
├── Core (app/core/)
|
||||||
|
│ ├── deep_agent.py — main agent runner (run_home / run_floating / run_brief / run_journey)
|
||||||
|
│ ├── brief_agent.py — daily brief generation
|
||||||
|
│ ├── agent_runner.py — local + cloud agent run executor
|
||||||
|
│ ├── agent_session_buffer.py — per-session conversation buffer
|
||||||
│ ├── agent_registry.py — decorator-based agent registry
|
│ ├── agent_registry.py — decorator-based agent registry
|
||||||
│ ├── execution_plan.py — server-side prompt templates + plan builder
|
│ ├── llm.py — LiteLLM factory (multi-provider)
|
||||||
│ ├── llm.py — LiteLLM factory (100+ providers)
|
│ ├── memory_middleware.py — encrypted core memory read/write
|
||||||
│ └── memory_middleware.py
|
│ ├── memory_extraction.py — LLM extraction from conversation tail
|
||||||
├── Billing (app/billing/)
|
│ ├── memory_maintenance.py — drain queue, contradiction audit, proactive mining
|
||||||
│ ├── tier_manager.py — feature matrix (Free/Pro/Power/Team)
|
│ ├── note_summarizer.py — gpt-4o-mini summary for notes
|
||||||
│ └── stripe_service.py — Stripe checkout + webhooks
|
│ ├── output_formatter.py — render agent output to user-facing markdown
|
||||||
├── Storage (app/storage/) — S3 blob store, vector store, encryption
|
│ ├── embeddings.py
|
||||||
└── Marketplace (app/marketplace/) — plugin catalog, review, revenue sharing
|
│ ├── device_manager.py — device registration / WS session tracking
|
||||||
|
│ ├── ws_context.py — per-WS user context plumbing
|
||||||
|
│ ├── langfuse_client.py — Langfuse prompt + tracing client
|
||||||
|
│ └── preprocessors/ — input preprocessors (e.g. email_html.py)
|
||||||
|
├── Auth (app/auth/oauth_providers.py) — GoogleOAuthProvider (httpx + manual PKCE)
|
||||||
|
├── Billing (app/billing/) — tier_manager + stripe_service
|
||||||
|
├── Integrations (app/integrations/) — gmail.py, ms_graph.py
|
||||||
|
└── Models (app/models.py) — SQLAlchemy 2.0 ORM
|
||||||
```
|
```
|
||||||
|
|
||||||
**LLM routing**: `gpt-4o-mini` classifies intent → routes to domain agent → agent uses `gpt-4o` with tools → tool calls describe client-side operations (JSON) → Electron executes locally and returns results.
|
**HTTP route prefix**: every router included with `prefix="/api/v1"`. So `/api/v1/auth/...`, `/api/v1/chat`, `/api/v1/agents/...`, `/api/v1/memory/...`, `/api/v1/device` (WS).
|
||||||
|
|
||||||
**Zero-trust data model**: The backend never stores or decrypts user content. PostgreSQL holds only auth, billing, plugin metadata, and storage record pointers. All user data is E2E-encrypted before leaving the Electron client.
|
**ORM models** (`app/models.py`): `User`, `RefreshToken`, `OAuthAccount`, `Subscription`, `LocalAgentConfig`, `CloudAgentConfig`, `AgentRunLog`, `MemoryCore`, `MemoryAssociative`, `MemoryEpisodic`, `MemoryProactive`, `ExtractionQueue`, `MemoryRelation`, `Plugin`. PostgreSQL (asyncpg + SQLAlchemy 2.0 async). Alembic migrations in `alembic/versions/`.
|
||||||
|
|
||||||
**Key config**: `app/config/settings.py` — all env vars via Pydantic Settings. Copy `.env.example` to `.env` for local dev. Stripe and S3 gracefully stub when keys aren't configured.
|
**Lifespan crons** (only if `settings.SCHEDULER_ENABLED`):
|
||||||
|
- `_memory_cron_tick` — hourly: drains Free-tier extraction queue + mines proactive patterns for Power+ users
|
||||||
|
- `_memory_audit_cron_tick` — weekly: contradiction scan + label canonicalization for all users (Phase 7)
|
||||||
|
|
||||||
**Database**: PostgreSQL with async SQLAlchemy 2.0 + asyncpg. 9 ORM models in `app/models.py`. Alembic migrations in `alembic/versions/`.
|
**LLM routing**: backend agents own all intelligence. Tool calls describe client-side ops (JSON) → Electron `drizzle-executor` runs them against local SQLite → result returned to backend over WS. Tool loop cap inside agent runner prevents runaway iteration.
|
||||||
|
|
||||||
**Testing**: pytest + pytest-asyncio. Fixtures in `tests/conftest.py` create in-memory SQLite + moto-mocked S3. Test users seeded per tier (free/pro/power/team).
|
**Zero-trust data model**: backend never stores raw user content. PostgreSQL holds auth, billing, plugin metadata, encrypted memory (Core/Associative/Episodic/Proactive/Relational), agent configs, run logs.
|
||||||
|
|
||||||
|
**Config**: `app/config/settings.py` — all env vars via Pydantic Settings. Copy `.env.example` to `.env` for local dev.
|
||||||
|
|
||||||
|
**Testing**: pytest + pytest-asyncio. Fixtures in `tests/conftest.py`. Active suites: agent runner, auth, brief/deep agents, device WS, integrations, journey, memory (audit/extraction/middleware/models/proactive/relations), middleware, output formatter, preprocessors, schemas, ws_unified.
|
||||||
|
|
||||||
### Non-obvious details
|
### Non-obvious details
|
||||||
|
|
||||||
- **Tier from DB, not JWT**: `get_current_user` decodes JWT but fetches authoritative tier from `subscriptions` table — tier changes take effect immediately without re-login
|
- **Tier from DB, not JWT**: `get_current_user` decodes JWT but fetches authoritative tier from `subscriptions` — tier changes take effect immediately, no re-login needed
|
||||||
- **Refresh tokens hashed**: Plaintext returned to client, stored as SHA-256 in DB — server can never retrieve the plaintext (intentional)
|
- **Refresh tokens hashed**: plaintext returned to client, stored as SHA-256 in DB — server can never retrieve plaintext (intentional)
|
||||||
- **WebSocket auth via query param**: `?token=<jwt>` instead of Bearer header (WebSocket handshake limitation)
|
- **WebSocket auth via query param**: `?token=<jwt>` instead of Bearer header (WebSocket handshake limitation)
|
||||||
- **Prompt IP protection**: `PromptTemplateRegistry` keeps prompts server-side; clients receive opaque `template_id`. `SanitizerMiddleware` strips leaked fragments from responses
|
- **Unified device WS**: `/api/v1/device` is the single bidirectional channel. Handles home requests, floating requests, daily briefs, journeys, heartbeats. Tool calls round-trip through the same socket
|
||||||
- **Agents don't execute operations**: Tools return JSON describing client-side ops — the Electron client executes against local SQLite
|
- **Prompt IP protection**: prompts kept server-side via Langfuse (`langfuse_client`). `SanitizerMiddleware` strips leaked fragments from responses
|
||||||
- **Alembic async/sync split**: App uses `postgresql+asyncpg`, Alembic CLI needs `postgresql+psycopg2` — `env.py` handles the URL conversion
|
- **Agents don't execute operations**: tools return JSON describing client-side ops — Electron client executes against local SQLite
|
||||||
- **Tool loop cap**: Agent `_tool_loop` stops after 5 iterations to prevent infinite loops
|
- **Alembic async/sync split**: app uses `postgresql+asyncpg`, Alembic CLI needs `postgresql+psycopg2` — `env.py` handles URL conversion
|
||||||
- **Route order matters**: `/backup/history` must be declared before `/backup/{backup_id}` to avoid path param shadowing
|
|
||||||
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
|
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
|
||||||
- **Vector search on encrypted data is not semantic**: Backend derives deterministic 32-dim floats from blob SHA-256 for storage/search — a known trade-off
|
- **Run-disconnect tracking**: `_mark_runs_disconnected` flips active runs when WS drops so client can resume cleanly
|
||||||
|
|
||||||
|
**Onboarding (API side)**:
|
||||||
|
- `PUT /auth/me/memory` — updates core memory k/v pairs, optionally marks onboarding complete (`mark_onboarded: true` sets `users.onboarding_completed_at`).
|
||||||
|
- `POST /auth/me/onboarding/reset` — nullifies `onboarding_completed_at` so wizard re-runs.
|
||||||
|
- `POST /auth/onboarding/normalize` — LLM-normalizes free-text onboarding inputs via `gpt-4o-mini`; returns inputs unchanged on error.
|
||||||
|
- `get_current_user()` in `auth.py` middleware decrypts core memory blocks, includes in `UserProfile.memory` dict.
|
||||||
|
- `users.onboarding_completed_at` — nullable TIMESTAMPTZ, returned as epoch ms (int) in UserProfile schema.
|
||||||
|
|
||||||
|
**i18n (API side)**:
|
||||||
|
- `_language_instruction()` in `app/core/deep_agent.py` reads user's `language` from `MemoryCore`, appends system prompt directive ("Always respond in {language}") to all `run_*` functions.
|
||||||
|
- Electron client writes chosen language to backend core memory on change — API picks up on next agent call.
|
||||||
|
|
||||||
|
**Google OAuth (api side)**:
|
||||||
|
- OAuth routes in `app/api/routes/auth.py`: `GET /auth/oauth/{provider}/authorize`, `POST /auth/oauth/{provider}/callback`, `GET /auth/oauth/{provider}/web-callback` (bounces to deep link, excluded from OpenAPI schema).
|
||||||
|
- Provider abstraction in `app/auth/oauth_providers.py` — `GoogleOAuthProvider` uses `httpx` directly (no `authlib`). PKCE S256 implemented manually via `generate_pkce_pair()`.
|
||||||
|
- `_pending_states` dict in `routes/auth.py` is **in-memory** — works for single-process dev, doesn't survive restarts, doesn't scale to multiple workers. Replace with Redis in production.
|
||||||
|
- `users.password_hash` is **nullable** — social-only users have `password_hash=None`. `await db.flush()` required before creating linked `OAuthAccount` to populate `new_user.id` before commit.
|
||||||
|
- `OAUTH_REDIRECT_URI` must point to **API backend** (e.g. `https://api.adiuvai.com/...`).
|
||||||
|
- **Unverified email + existing account = 409**: if `email_verified=False` and email already registered, callback returns 409. Without this guard, branch 3 would INSERT duplicate email and crash with DB constraint violation (500).
|
||||||
|
- **Testing OAuth routes**: mock `GoogleOAuthProvider.exchange_code` and `get_userinfo` with `patch.object(..., new=AsyncMock(...))` — works because FastAPI instantiates new provider per request. Use `monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...)` to simulate configured credentials without restart.
|
||||||
|
|
||||||
### Tier System
|
### Tier System
|
||||||
|
|
||||||
| Feature | Free | Pro | Power | Team |
|
Source of truth: `app/billing/tier_manager.py` (`FEATURES` + `RATE_LIMITS` dicts).
|
||||||
|---------|------|-----|-------|------|
|
|
||||||
| Rate limit | 20/min | 60/min | 120/min | 200/min |
|
|
||||||
| Agents | 3 | unlimited | unlimited | unlimited |
|
|
||||||
| Cloud storage | 0 GB | 5 GB | 25 GB | unlimited |
|
|
||||||
| Plugin marketplace | no | no | yes | yes |
|
|
||||||
|
|
||||||
Enforced in `app/api/middleware/rate_limit.py` (sliding window) and `app/billing/tier_manager.py` (feature checks + quota enforcement).
|
| Feature | Free | Pro | Power | Team |
|
||||||
|
|---------------------|--------|-----------|-----------|-----------|
|
||||||
|
| Rate limit | 20/min | 60/min | 120/min | 200/min |
|
||||||
|
| Providers | 1 | unlimited | unlimited | unlimited |
|
||||||
|
| Relational memory | no | yes | yes | yes |
|
||||||
|
| Proactive mining | no | no | yes | yes |
|
||||||
|
|
||||||
|
`tier_manager.get_tier()` falls back to `'power'` in dev (`settings.ENV == 'dev'`) when no subscription found, else `'free'`. Enforced in `app/api/middleware/rate_limit.py` (sliding window) and `tier_manager.check_feature()` calls scattered through agent + memory paths.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cross-Project Integration
|
## Cross-Project Integration
|
||||||
|
|
||||||
The Electron app and FastAPI backend communicate via **WebSocket** (`/chat/stream`):
|
Electron app and FastAPI backend communicate via **WebSocket** (`/api/v1/device`):
|
||||||
|
|
||||||
1. Electron connects with `?token=<jwt>` query param
|
1. Electron connects with `?token=<jwt>` query param
|
||||||
2. Client sends `ChatRequest` JSON frame
|
2. Client sends typed request frames (home / floating / brief / journey_start / journey_message)
|
||||||
3. Server streams text chunks, then a final frame: `{"done": true, "response": "...", "actions": []}`
|
3. Server streams v3 typed frames (text deltas, tool_call, run_complete, error)
|
||||||
4. Server sends `tool_call` frames → Electron executes against local SQLite → returns `tool_result`
|
4. Tool call frames → Electron `drizzle-executor` runs against local SQLite → returns `tool_result` over same socket
|
||||||
5. Server pings every 30 seconds to keep connection alive
|
5. Heartbeat loop keeps connection alive; backend marks runs disconnected on drop
|
||||||
|
|
||||||
The Electron app also has a **fully local AI path** (LangGraph orchestrator in main process) that doesn't require the backend — this is the primary path for desktop use.
|
There is no fully-local AI fallback — the Electron orchestrator is a thin delegation shell that requires connectivity + auth. If offline or logged out, `checkConnectivity()` short-circuits with a user-facing error.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MCP Servers
|
## MCP Servers
|
||||||
|
|
||||||
- **Langfuse Docs** (`https://langfuse.com/api/mcp`) — configured at workspace level for prompt management documentation
|
- **Langfuse Docs** (`https://langfuse.com/api/mcp`) — workspace-level, prompt management docs
|
||||||
- **shadcn** (`npx shadcn@latest mcp`) — configured in `adiuvAI/` for UI component generation
|
- **shadcn** (`npx shadcn@latest mcp`) — configured in `adiuvAI/` for UI component generation
|
||||||
|
|||||||
269
.claude/CLAUDE.original.md
Normal file
269
.claude/CLAUDE.original.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Keeping This File Up to Date
|
||||||
|
|
||||||
|
Update this file whenever a lesson is learned during development. Specifically, update CLAUDE.md when:
|
||||||
|
|
||||||
|
- A non-obvious architectural decision is made or discovered
|
||||||
|
- A gotcha, footgun, or surprising behavior is encountered (and the fix/workaround)
|
||||||
|
- A new command, workflow, or tool is added to the project
|
||||||
|
- A convention is established that isn't obvious from reading the code
|
||||||
|
- An integration detail is clarified (e.g., how the IPC protocol actually behaves, edge cases in the agent tool call cycle)
|
||||||
|
|
||||||
|
Do **not** add things already derivable from reading the code, generic best practices, or ephemeral task notes — only durable, reusable knowledge.
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
This is a **monorepo with git submodules**. Each submodule is an independent repo with its own `.claude/CLAUDE.md` for detailed guidance.
|
||||||
|
|
||||||
|
| Directory | What | Submodule |
|
||||||
|
|-----------|------|-----------|
|
||||||
|
| **`adiuvAI/`** | Electron desktop app (TypeScript/React) | `git.muticolturano.com/adiuvAI/adiuvAI` |
|
||||||
|
| **`api/`** | FastAPI backend (Python) | `git.muticolturano.com/adiuvAI/api` |
|
||||||
|
| **`website/`** | Landing page (single `index.html`) | `git.muticolturano.com/adiuvAI/website` |
|
||||||
|
| **`docs/`** | Planning docs & working memory (not a submodule) | -- |
|
||||||
|
|
||||||
|
After cloning, run `git submodule update --init --recursive` to populate submodule contents.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## adiuvAI (Electron App)
|
||||||
|
|
||||||
|
> **Detailed docs**: `adiuvAI/.claude/CLAUDE.md` covers commands, architecture, AI subsystem, design context, and conventions in depth.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI
|
||||||
|
npm run start # Start dev server (Electron + Vite)
|
||||||
|
npm run lint # ESLint
|
||||||
|
npm run knip # Dead code analysis
|
||||||
|
npm run make # Build installers (Windows/Linux/macOS)
|
||||||
|
npm run package # Package without creating installers
|
||||||
|
npx drizzle-kit generate # Generate migration from schema changes
|
||||||
|
npx drizzle-kit push # Push schema directly (dev only)
|
||||||
|
```
|
||||||
|
|
||||||
|
No test suite currently.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Renderer (React 19 + TanStack Router)
|
||||||
|
↓ custom ipcLink (NOT electron-trpc — incompatible with tRPC v11)
|
||||||
|
Preload (contextBridge: window.electronTRPC + window.electronAI)
|
||||||
|
↓ IPC channels
|
||||||
|
Main Process (Node.js)
|
||||||
|
├── tRPC router (all CRUD + AI procedures)
|
||||||
|
├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
|
||||||
|
├── LanceDB (vector embeddings, 1536-dim text-embedding-3-small)
|
||||||
|
└── LangGraph orchestrator (3 specialist agents, pluggable LLM providers)
|
||||||
|
```
|
||||||
|
|
||||||
|
**This is a local-first app.** All user data (tasks, notes, projects) lives in local SQLite. The AI system (LangGraph + LangChain) runs entirely in the Electron main process with pluggable providers (OpenAI, Anthropic, GitHub Copilot).
|
||||||
|
|
||||||
|
**IPC channels**:
|
||||||
|
- `'trpc'` — bidirectional tRPC request/response (all CRUD)
|
||||||
|
- `'ai:stream'` — one-way token streaming from main → renderer
|
||||||
|
- `'ai:action'` — AI side-effects (e.g., task auto-created by agent)
|
||||||
|
|
||||||
|
**Key source paths**:
|
||||||
|
- `src/main/ipc.ts` — Custom tRPC↔IPC bridge
|
||||||
|
- `src/main/router/index.ts` — All tRPC routers (~600 LOC)
|
||||||
|
- `src/main/ai/orchestrator.ts` — LangGraph intent routing + 3 agents (~991 LOC)
|
||||||
|
- `src/main/db/schema.ts` — 6 tables (clients, projects, tasks, checkpoints, notes, taskComments)
|
||||||
|
- `src/renderer/routes/` — File-based routing (TanStack Router auto-generates `routeTree.gen.ts`)
|
||||||
|
- `src/renderer/components/ui/` — shadcn/ui primitives (new-york theme, neutral colors)
|
||||||
|
- `src/main/auth/auth-manager.ts` — Login, register, logout, OAuth flow (singleton)
|
||||||
|
- `src/main/auth/backup-key.ts` — Device-specific AES-256 backup key (safeStorage, not password-derived)
|
||||||
|
- `src/main/ai/token.ts` — Two-tier token storage: safeStorage + electron-store fallback
|
||||||
|
- `src/main/auth/locale-defaults.ts` — Detects timezone, date/time format, language from OS locale
|
||||||
|
- `src/main/api/format-row.ts` — Formats timestamp columns in query results using user's FormatPrefs
|
||||||
|
|
||||||
|
**Non-obvious details**:
|
||||||
|
- `electron-trpc` is NOT used — custom IPC bridge in `ipc.ts` + `ipcLink.ts` because electron-trpc bundles tRPC v10 internals
|
||||||
|
- Vite configs use `.mts` extension to avoid ESM/CJS conflicts with electron-forge
|
||||||
|
- `forge.config.ts` has complex cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3 and LanceDB)
|
||||||
|
- DB has no foreign key constraints — cascade deletes are implemented in tRPC procedures
|
||||||
|
- Timestamps are milliseconds (JavaScript `Date.getTime()`), not ISO strings
|
||||||
|
- Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)
|
||||||
|
|
||||||
|
**Settings Page (shared between Electron and Web)**:
|
||||||
|
- The Settings page is designed to run in **both** the Electron app and a standalone web SPA (future landing-page user portal). The same React components are used — no duplication.
|
||||||
|
- **Platform Adapter pattern**: `PlatformProvider` context (`src/renderer/lib/platform.tsx`) exposes `isElectron`/`isWeb`/`hasLocalAgents`/`hasFileDialog` flags. Components use `usePlatform()` to conditionally render Electron-only features (device ID, local agent filesystem) or disable them on web.
|
||||||
|
- **6 sections**: Profile, AI Preferences, Account, Billing, Appearance, Agents. Sidebar nav with icons in `types.ts` (`SECTIONS` array).
|
||||||
|
- **Web build**: `vite.web.config.mts` builds a standalone SPA to `dist-web/`. Entry: `web.html` → `src/renderer/web-main.tsx` (uses `httpBatchLink` via `src/renderer/lib/httpLink.ts` instead of `ipcLink`). Scripts: `npm run dev:web`, `npm run build:web`, `npm run preview:web`.
|
||||||
|
- **Electron-only gating**: Device ID card and local agent filesystem features are gated behind `platform.isElectron`. On web, local agents are visible but disabled (not hidden).
|
||||||
|
- **Gotcha**: Do NOT add Electron-specific settings (e.g. server URL, native file pickers) without wrapping in `platform.isElectron`. The same component tree renders on web.
|
||||||
|
|
||||||
|
**Onboarding Wizard**:
|
||||||
|
- First-run wizard collects 5 fields: `job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`. Plus `user_name` derived from profile `name`+`surname`.
|
||||||
|
- All fields stored as encrypted core memory (backend `MemoryMiddleware`), not local electron-store.
|
||||||
|
- `onboarding_completed_at` on the `users` table (nullable TIMESTAMPTZ) gates the flow — `null` = show wizard, non-null = skip.
|
||||||
|
- `AppShell.tsx` gates: if `profile.onboardingCompletedAt == null` → render `<OnboardingFlow>` instead of the app.
|
||||||
|
- `auth.status` tRPC procedure auto-seeds `language` and `user_name` into MemoryCore if missing (fire-and-forget `.catch(() => {})`).
|
||||||
|
- Format prefs (timezone, dateFormat, timeFormat) are stored in electron-store (`FormatPrefs`), not core memory — they're device-specific.
|
||||||
|
- `drizzle-executor.ts` wraps all query results through `formatRow()`/`formatRows()` using the user's FormatPrefs.
|
||||||
|
- Settings > Profile section allows post-onboarding editing of all fields + format prefs.
|
||||||
|
- **Gotcha — shadcn Button `outline` variant in dark mode**: The variant defines `dark:bg-input/30 dark:border-input dark:hover:bg-input/50` which overrides any custom `className` background. Fix: switch between `variant="default"` and `variant="outline"` instead of adding className overrides.
|
||||||
|
- **Gotcha — locale codes vs human names**: `app.getLocale()` and `navigator.language` return codes like `en-US`. Use `Intl.DisplayNames(undefined, { type: 'language' })` to convert to "English". This must be done in both the main process (`locale-defaults.ts`) and renderer (`OnboardingFlow.tsx`).
|
||||||
|
|
||||||
|
**i18n (Internationalization)**:
|
||||||
|
- Uses `i18next` + `react-i18next` with bundled JSON translations (no lazy loading).
|
||||||
|
- Config in `src/renderer/i18n.ts`. 5 languages: EN, IT, ES, FR, DE. `SUPPORTED_LANGUAGES` array exported for UI selectors.
|
||||||
|
- Translation files: `src/renderer/locales/{en,it,es,fr,de}/translation.json`. Namespaces: `nav`, `auth`, `tasks`, `settings`, `common`, `errors`, `home`, `timeline`, `projects`, `agents`.
|
||||||
|
- **`common.*` namespace** holds shared labels (`save`, `cancel`, `delete`, `edit`, `add`, `rename`, `saving`, `deleting`, `creating`, `renameDescription`, `deleteTitle`). Before adding a new key, check if `common.*` already has it.
|
||||||
|
- Pluralization uses i18next `_one`/`_other` suffixes (e.g. `tasksDueToday_one`, `tasksDueToday_other`).
|
||||||
|
- `LanguageSync` component in `src/renderer/index.tsx` reads persisted `uiLanguage` from electron-store via tRPC on startup and syncs to i18next.
|
||||||
|
- Language selector lives in `GeneralSection.tsx` (Settings > General). On change it: (1) calls `i18n.changeLanguage()`, (2) persists to electron-store via `setUiLanguage` mutation, (3) writes to backend core memory so AI responds in the same language.
|
||||||
|
- `getUiLanguage()` exported from `src/main/store.ts` — used by `orchestrator.ts` to append language hint to daily brief prompt.
|
||||||
|
- Static data arrays that need translation use `labelKey` pattern (not `label`): store a translation key, call `t(labelKey)` at render time. Used in `NAV_ITEMS`, `COLUMNS`, `SECTIONS`, `SUGGESTION_CHIPS`.
|
||||||
|
- When adding new translated text: add the key to **all 5** JSON files. Keep `common.*` keys consistent across all languages.
|
||||||
|
|
||||||
|
**Google OAuth (adiuvAI side)**:
|
||||||
|
- `adiuvai://` is NOT accepted by Google as a redirect URI — Google only accepts `http://localhost` or `https://`. The API backend exposes `GET /auth/oauth/google/web-callback` which receives the Google redirect and immediately bounces to `adiuvai://oauth/callback?...`. The redirect URI registered in Google Cloud Console points to the backend, not the Electron app.
|
||||||
|
- `app.requestSingleInstanceLock()` is required for the `second-instance` event to fire on Windows/Linux. If it returns `false`, call `app.quit()` immediately (another instance is already running).
|
||||||
|
- In dev (`process.defaultApp === true`), `setAsDefaultProtocolClient('adiuvai')` must include `[path.resolve(process.argv[1])]` as the third argument so the OS protocol registration includes the entry script.
|
||||||
|
- `loginWithOAuth` uses `fetch()` directly (not `this.get()`) because the authorize endpoint is public — `get()` throws when not authenticated.
|
||||||
|
- The backup key in `backup-key.ts` is stored in `encryptedTokens` under the key `backup_key`, reusing `getToken/setToken` from `token.ts`. It is device-bound and never password-derived, so social-login users can use backup features without issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## api (FastAPI Backend)
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd api
|
||||||
|
|
||||||
|
# Development
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
# Production
|
||||||
|
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --timeout 120
|
||||||
|
|
||||||
|
# Database migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
pytest # all tests
|
||||||
|
pytest -v # verbose
|
||||||
|
pytest tests/test_agents.py # single file
|
||||||
|
pytest tests/test_agents.py -k test_name # single test
|
||||||
|
|
||||||
|
# Linting/formatting
|
||||||
|
ruff check .
|
||||||
|
ruff format .
|
||||||
|
|
||||||
|
# Docker (full stack: app + postgres + minio + qdrant)
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
FastAPI app (app/main.py)
|
||||||
|
├── Middleware: TierRateLimiter → Sanitizer → CORS
|
||||||
|
├── HTTP Routes (app/api/routes/)
|
||||||
|
│ ├── auth.py — register, login, token refresh (bcrypt + HS256 JWT)
|
||||||
|
│ ├── chat.py — POST /chat, WS /chat/stream
|
||||||
|
│ ├── plans.py — execution plan playbooks
|
||||||
|
│ ├── storage.py — E2E-encrypted cloud storage (S3)
|
||||||
|
│ ├── backup.py — encrypted backup upload/download
|
||||||
|
│ ├── vectors.py — encrypted vector upsert/search (Pinecone/Qdrant)
|
||||||
|
│ ├── plugins.py — plugin marketplace (Power+ tier)
|
||||||
|
│ └── billing.py — Stripe subscriptions
|
||||||
|
├── Agent System (app/agents/)
|
||||||
|
│ ├── task_agent.py — 8 tools
|
||||||
|
│ ├── project_agent.py — 6 tools
|
||||||
|
│ ├── checkpoint_agent.py — 4 tools
|
||||||
|
│ └── note_agent.py — 5 tools
|
||||||
|
├── Orchestration (app/core/)
|
||||||
|
│ ├── orchestrator.py — intent classification + agent routing
|
||||||
|
│ ├── agent_registry.py — decorator-based agent registry
|
||||||
|
│ ├── execution_plan.py — server-side prompt templates + plan builder
|
||||||
|
│ ├── llm.py — LiteLLM factory (100+ providers)
|
||||||
|
│ └── memory_middleware.py
|
||||||
|
├── Billing (app/billing/)
|
||||||
|
│ ├── tier_manager.py — feature matrix (Free/Pro/Power/Team)
|
||||||
|
│ └── stripe_service.py — Stripe checkout + webhooks
|
||||||
|
├── Storage (app/storage/) — S3 blob store, vector store, encryption
|
||||||
|
└── Marketplace (app/marketplace/) — plugin catalog, review, revenue sharing
|
||||||
|
```
|
||||||
|
|
||||||
|
**LLM routing**: `gpt-4o-mini` classifies intent → routes to domain agent → agent uses `gpt-4o` with tools → tool calls describe client-side operations (JSON) → Electron executes locally and returns results.
|
||||||
|
|
||||||
|
**Zero-trust data model**: The backend never stores or decrypts user content. PostgreSQL holds only auth, billing, plugin metadata, and storage record pointers. All user data is E2E-encrypted before leaving the Electron client.
|
||||||
|
|
||||||
|
**Key config**: `app/config/settings.py` — all env vars via Pydantic Settings. Copy `.env.example` to `.env` for local dev. Stripe and S3 gracefully stub when keys aren't configured.
|
||||||
|
|
||||||
|
**Database**: PostgreSQL with async SQLAlchemy 2.0 + asyncpg. 9 ORM models in `app/models.py`. Alembic migrations in `alembic/versions/`.
|
||||||
|
|
||||||
|
**Testing**: pytest + pytest-asyncio. Fixtures in `tests/conftest.py` create in-memory SQLite + moto-mocked S3. Test users seeded per tier (free/pro/power/team).
|
||||||
|
|
||||||
|
### Non-obvious details
|
||||||
|
|
||||||
|
- **Tier from DB, not JWT**: `get_current_user` decodes JWT but fetches authoritative tier from `subscriptions` table — tier changes take effect immediately without re-login
|
||||||
|
- **Refresh tokens hashed**: Plaintext returned to client, stored as SHA-256 in DB — server can never retrieve the plaintext (intentional)
|
||||||
|
- **WebSocket auth via query param**: `?token=<jwt>` instead of Bearer header (WebSocket handshake limitation)
|
||||||
|
- **Prompt IP protection**: `PromptTemplateRegistry` keeps prompts server-side; clients receive opaque `template_id`. `SanitizerMiddleware` strips leaked fragments from responses
|
||||||
|
- **Agents don't execute operations**: Tools return JSON describing client-side ops — the Electron client executes against local SQLite
|
||||||
|
- **Alembic async/sync split**: App uses `postgresql+asyncpg`, Alembic CLI needs `postgresql+psycopg2` — `env.py` handles the URL conversion
|
||||||
|
- **Tool loop cap**: Agent `_tool_loop` stops after 5 iterations to prevent infinite loops
|
||||||
|
- **Route order matters**: `/backup/history` must be declared before `/backup/{backup_id}` to avoid path param shadowing
|
||||||
|
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
|
||||||
|
- **Vector search on encrypted data is not semantic**: Backend derives deterministic 32-dim floats from blob SHA-256 for storage/search — a known trade-off
|
||||||
|
|
||||||
|
**Onboarding (API side)**:
|
||||||
|
- `PUT /auth/me/memory` — updates core memory k/v pairs and optionally marks onboarding complete (`mark_onboarded: true` sets `users.onboarding_completed_at`).
|
||||||
|
- `POST /auth/me/onboarding/reset` — nullifies `onboarding_completed_at` so the wizard re-runs.
|
||||||
|
- `POST /auth/onboarding/normalize` — LLM-normalizes free-text onboarding inputs via `gpt-4o-mini`; returns inputs unchanged on error.
|
||||||
|
- `get_current_user()` in `auth.py` middleware now decrypts core memory blocks and includes them in `UserProfile.memory` dict.
|
||||||
|
- `users.onboarding_completed_at` is a nullable TIMESTAMPTZ column — returned as epoch ms (int) in UserProfile schema.
|
||||||
|
|
||||||
|
**i18n (API side)**:
|
||||||
|
- `_language_instruction()` in `app/core/deep_agent.py` reads the user's `language` from `MemoryCore` and appends a system prompt directive ("Always respond in {language}") to all 4 `run_*` functions.
|
||||||
|
- The Electron client writes the user's chosen language to backend core memory on language change, so the API picks it up on the next agent call.
|
||||||
|
|
||||||
|
**Google OAuth (api side)**:
|
||||||
|
- OAuth routes live in `app/api/routes/auth.py`: `GET /auth/oauth/{provider}/authorize`, `POST /auth/oauth/{provider}/callback`, `GET /auth/oauth/{provider}/web-callback` (bounces to deep link, excluded from OpenAPI schema).
|
||||||
|
- Provider abstraction in `app/auth/oauth_providers.py` — `GoogleOAuthProvider` uses `httpx` directly (no `authlib`). PKCE S256 is implemented manually via `generate_pkce_pair()`.
|
||||||
|
- `_pending_states` dict in `routes/auth.py` is **in-memory** — works for single-process dev but does not survive restarts and does not scale to multiple workers. Replace with Redis in production.
|
||||||
|
- `users.password_hash` is **nullable** — social-only users have `password_hash=None`. `await db.flush()` is required before creating a linked `OAuthAccount` to populate `new_user.id` before commit.
|
||||||
|
- `OAUTH_REDIRECT_URI` must point to the **API backend** (e.g. `https://api.adiuvai.com/...`), not the website domain. `adiuvai.com` is a static site with no server-side routing.
|
||||||
|
- **Unverified email + existing account = 409**: if `email_verified=False` and the email is already registered, the callback returns 409. Without this guard, branch 3 would attempt to INSERT a duplicate email and crash with a DB constraint violation (500).
|
||||||
|
- **Testing OAuth routes**: mock `GoogleOAuthProvider.exchange_code` and `get_userinfo` with `patch.object(..., new=AsyncMock(...))` — works because FastAPI instantiates a new provider per request. Use `monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...)` to simulate configured credentials without restarting the app.
|
||||||
|
|
||||||
|
### Tier System
|
||||||
|
|
||||||
|
| Feature | Free | Pro | Power | Team |
|
||||||
|
|---------|------|-----|-------|------|
|
||||||
|
| Rate limit | 20/min | 60/min | 120/min | 200/min |
|
||||||
|
| Agents | 3 | unlimited | unlimited | unlimited |
|
||||||
|
| Cloud storage | 0 GB | 5 GB | 25 GB | unlimited |
|
||||||
|
| Plugin marketplace | no | no | yes | yes |
|
||||||
|
|
||||||
|
Enforced in `app/api/middleware/rate_limit.py` (sliding window) and `app/billing/tier_manager.py` (feature checks + quota enforcement).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Project Integration
|
||||||
|
|
||||||
|
The Electron app and FastAPI backend communicate via **WebSocket** (`/chat/stream`):
|
||||||
|
|
||||||
|
1. Electron connects with `?token=<jwt>` query param
|
||||||
|
2. Client sends `ChatRequest` JSON frame
|
||||||
|
3. Server streams text chunks, then a final frame: `{"done": true, "response": "...", "actions": []}`
|
||||||
|
4. Server sends `tool_call` frames → Electron executes against local SQLite → returns `tool_result`
|
||||||
|
5. Server pings every 30 seconds to keep connection alive
|
||||||
|
|
||||||
|
The Electron app also has a **fully local AI path** (LangGraph orchestrator in main process) that doesn't require the backend — this is the primary path for desktop use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Servers
|
||||||
|
|
||||||
|
- **Langfuse Docs** (`https://langfuse.com/api/mcp`) — configured at workspace level for prompt management documentation
|
||||||
|
- **shadcn** (`npx shadcn@latest mcp`) — configured in `adiuvAI/` for UI component generation
|
||||||
@@ -1 +1,21 @@
|
|||||||
{}
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": []
|
||||||
|
},
|
||||||
|
"enabledPlugins": {
|
||||||
|
"caveman@caveman": true
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.\"}}' || true ;; esac"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"enableAllProjectMcpServers": true
|
|
||||||
}
|
|
||||||
@@ -173,8 +173,9 @@ npx shadcn@latest docs button dialog select
|
|||||||
6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project.
|
6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project.
|
||||||
7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on.
|
7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on.
|
||||||
8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user.
|
8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user.
|
||||||
9. **Switching presets** — Ask the user first: **overwrite**, **merge**, or **skip**?
|
9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?
|
||||||
- **Overwrite**: `npx shadcn@latest apply --preset <code>`. Overwrites detected components, fonts, and CSS variables.
|
- **Overwrite**: `npx shadcn@latest apply --preset <code>`. Overwrites detected components, fonts, and CSS variables.
|
||||||
|
- **Partial**: `npx shadcn@latest apply --preset <code> --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms.
|
||||||
- **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually.
|
- **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually.
|
||||||
- **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
|
- **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
|
||||||
- **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
|
- **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
|
||||||
@@ -209,6 +210,9 @@ npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (ba
|
|||||||
# Apply a preset to an existing project.
|
# Apply a preset to an existing project.
|
||||||
npx shadcn@latest apply --preset a2r6bw
|
npx shadcn@latest apply --preset a2r6bw
|
||||||
npx shadcn@latest apply a2r6bw
|
npx shadcn@latest apply a2r6bw
|
||||||
|
npx shadcn@latest apply --preset a2r6bw --only theme
|
||||||
|
npx shadcn@latest apply --preset a2r6bw --only font
|
||||||
|
npx shadcn@latest apply --preset a2r6bw --only theme,font
|
||||||
|
|
||||||
# Add components.
|
# Add components.
|
||||||
npx shadcn@latest add button card dialog
|
npx shadcn@latest add button card dialog
|
||||||
|
|||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
skills/
|
||||||
|
unused_skills/
|
||||||
|
.vscode/mcp.json
|
||||||
|
.claude/skills/brand-guidelines/*
|
||||||
|
.claude/skills/frontend-design/*
|
||||||
|
.claude/skills/remotion-best-practices/*
|
||||||
|
.mcp.json
|
||||||
|
docs/node_modules
|
||||||
|
docs/package.json
|
||||||
|
docs/package-lock.json
|
||||||
|
tmp/
|
||||||
|
.superpowers/
|
||||||
|
graphify-out/cache/
|
||||||
|
graphify-out/manifest.json
|
||||||
|
graphify-out/cost.json
|
||||||
|
.claude/settings.local.json
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
|||||||
[submodule "website"]
|
[submodule "website"]
|
||||||
path = website
|
path = website
|
||||||
url = https://git.muticolturano.com/adiuvAI/website.git
|
url = https://git.muticolturano.com/adiuvAI/website.git
|
||||||
|
[submodule "waitlist"]
|
||||||
|
path = waitlist
|
||||||
|
url = https://git.muticolturano.com/adiuvAI/waitlist.git
|
||||||
|
|||||||
22
.mcp.json
22
.mcp.json
@@ -3,6 +3,28 @@
|
|||||||
"langfuse-docs": {
|
"langfuse-docs": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"url": "https://langfuse.com/api/mcp"
|
"url": "https://langfuse.com/api/mcp"
|
||||||
|
},
|
||||||
|
"langfuse": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://langfuse.muticolturano.com/api/public/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Basic cGstbGYtMGU2MmE5ZWItMDk3OC00ZTJlLWIzYWQtYmIzNjE5NDcwMWI4OnNrLWxmLTI4NmMxNjVmLTFjODQtNGEzNi1iMGIwLWNmZTViNjgwODk3ZA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"DATABASE_URI",
|
||||||
|
"crystaldba/postgres-mcp",
|
||||||
|
"--access-mode=restricted"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URI": "postgresql://postgres:XVTsmNqsMJX5Cd%2FNrAG4%2F4KFoaVDEy2CXsFMDqi8m58%3D@10.0.0.123:5432/adiuvai"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
.vscode/mcp.json
vendored
9
.vscode/mcp.json
vendored
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"servers": {
|
|
||||||
"langfuse-docs": {
|
|
||||||
"url": "https://langfuse.com/api/mcp",
|
|
||||||
"type": "http"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inputs": []
|
|
||||||
}
|
|
||||||
9
CLAUDE.md
Normal file
9
CLAUDE.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## graphify
|
||||||
|
|
||||||
|
This project has a graphify knowledge graph at graphify-out/.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
|
||||||
|
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
|
||||||
|
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
|
||||||
|
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
|
||||||
2
adiuvAI
2
adiuvAI
Submodule adiuvAI updated: 20bc28e59b...81fe6d29e2
2
api
2
api
Submodule api updated: 90500a3462...cc0e258e8c
309
docs/2026-05-08-task-ux-evolution-design.md
Normal file
309
docs/2026-05-08-task-ux-evolution-design.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Task UX Evolution — Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-08
|
||||||
|
**Scope:** adiuvAI desktop app (renderer + main process)
|
||||||
|
**Status:** Approved by user, ready for implementation plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Evolve the task management UX:
|
||||||
|
|
||||||
|
1. Replace the list-of-cards view with a paginated **shadcn Table**, keep the card grid as an alternative view, share pagination across both.
|
||||||
|
2. Replace `TaskDetailDialog` with a **right-side `Sheet`** (sticky header + scrolling body + sticky composer), add **attachments** support.
|
||||||
|
3. Redesign the create dialog as a **quick-capture form** with pill-style property controls. Edit dialog reuses the same shell.
|
||||||
|
4. Apply the same task list view inside the **project detail page**, scoped to that project.
|
||||||
|
|
||||||
|
Single spec, single implementation plan. All four subsystems ship together.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- AI-driven estimate generation (column added now, populated by a future agent).
|
||||||
|
- Comment attachments (composer has no attach icon).
|
||||||
|
- Per-column header sorting in the table (existing `Order by` Select stays).
|
||||||
|
- Reporter / Tags fields (image reference includes them but spec excludes).
|
||||||
|
|
||||||
|
## 1. Architecture & shared state
|
||||||
|
|
||||||
|
A new `TaskListView` component owns the task list rendering for both the Tasks page and the Project detail page. It encapsulates the toolbar, the table or grid body, and the pager. The page consuming it passes a task array plus an optional `hideProjectColumn` flag.
|
||||||
|
|
||||||
|
**Persisted state (`localStorage`):**
|
||||||
|
- `tasksViewMode`: `'list' | 'grid'` (already exists, kept).
|
||||||
|
- `tasksPageSize`: `10 | 25 | 50 | 100` — default `25`.
|
||||||
|
|
||||||
|
Page index is component-local and resets per route entry. It also resets when the search, status filter, or `Order by` changes.
|
||||||
|
|
||||||
|
**Pagination scope:** Tasks page and Project page each maintain their own page state. Toggling list ↔ grid within a page preserves the current page.
|
||||||
|
|
||||||
|
**Why client-side slicing:** task list is already fully loaded via `trpc.tasks.list`. No backend pagination required at this scale.
|
||||||
|
|
||||||
|
## 2. Database schema
|
||||||
|
|
||||||
|
Two changes to `src/main/db/schema.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// tasks: add column
|
||||||
|
estimate: integer('estimate'), // minutes, nullable
|
||||||
|
|
||||||
|
// new table
|
||||||
|
export const taskAttachments = sqliteTable('task_attachments', {
|
||||||
|
id: text('id').primaryKey().$defaultFn(() => randomUUID()),
|
||||||
|
taskId: text('task_id').notNull(),
|
||||||
|
filename: text('filename').notNull(),
|
||||||
|
mimeType: text('mime_type'),
|
||||||
|
sizeBytes: integer('size_bytes').notNull(),
|
||||||
|
storedPath: text('stored_path').notNull(), // relative to userData/attachments
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Migration generated with `drizzle-kit generate`. Per project convention, no foreign key constraint — cascade is handled in the `tasks.delete` tRPC procedure (delete attachment files + rows before deleting the task).
|
||||||
|
|
||||||
|
## 3. Attachments — file storage and IPC
|
||||||
|
|
||||||
|
**Storage path:** `app.getPath('userData') / attachments / <taskId> / <uuid>-<sanitizedFilename>`.
|
||||||
|
|
||||||
|
**Sanitization:** strip path separators, control characters, leading dots; cap filename at 200 chars.
|
||||||
|
|
||||||
|
**Limits:**
|
||||||
|
- Soft cap 50 MB per file. Larger files trigger a warning toast and are not uploaded.
|
||||||
|
- No per-task total cap.
|
||||||
|
|
||||||
|
**New tRPC sub-router `taskAttachments`** (in `src/main/router/index.ts`):
|
||||||
|
|
||||||
|
| Procedure | Input | Behavior |
|
||||||
|
|---|---|---|
|
||||||
|
| `list` | `{ taskId }` | Returns attachment rows for the task. |
|
||||||
|
| `pick` | `{}` | Main: `dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] })`. Returns `Array<{ path, name, size }>`. |
|
||||||
|
| `create` | `{ taskId, sourcePath, filename, sizeBytes, mimeType? }` | Main: `fs.mkdir(userData/attachments/<taskId>, recursive)`, copy file with new uuid name, insert row. |
|
||||||
|
| `delete` | `{ id }` | Look up row, `fs.unlink(storedPath)`, delete row. |
|
||||||
|
| `open` | `{ id }` | `shell.openPath(absoluteStoredPath)`. |
|
||||||
|
|
||||||
|
**Helper module `src/main/attachments/storage.ts`:** path resolution, sanitize, copy, delete. Keeps tRPC procedures thin.
|
||||||
|
|
||||||
|
**Tasks router updates:**
|
||||||
|
- `tasks.update` accepts `estimate?: number | null`.
|
||||||
|
- `tasks.delete` enumerates `taskAttachments` for the task and deletes files + rows before deleting the task row.
|
||||||
|
|
||||||
|
## 4. Table view
|
||||||
|
|
||||||
|
**Component:** `TaskTable` using shadcn `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableCell`.
|
||||||
|
|
||||||
|
**Container styling (translucent card over gradient bg):**
|
||||||
|
```
|
||||||
|
bg-card/65 backdrop-blur-xl border border-border/50 rounded-lg shadow
|
||||||
|
```
|
||||||
|
The `--card` token is used (not a hard-coded color), so dark mode works.
|
||||||
|
|
||||||
|
**Columns:**
|
||||||
|
1. **Task** — title, single line, truncate with tooltip.
|
||||||
|
2. **Project** — `Client › Project` breadcrumb. Client text muted, project text foreground. Hidden when `hideProjectColumn` is set. Click navigates to the project page.
|
||||||
|
3. **Priority** — existing `<PriorityBadge>` component (arrow icon + colored text, no pill).
|
||||||
|
4. **Due** — `formatDueDate(t.dueDate, prefs)`. Overdue: red text. None: muted `—`.
|
||||||
|
5. **Assignee** — `<AssigneeStack>`: overlapping avatars (max 2 visible), `+N` chip if more, tooltip listing all. None: muted `—`.
|
||||||
|
|
||||||
|
**Row interaction:**
|
||||||
|
- Click row → opens `TaskDetailSheet`.
|
||||||
|
- Right-click / context menu (kept from current `TaskRow` behavior): **Edit**, **Delete**, **Change status →** submenu (To Do / In Progress / Done with checkmark on current).
|
||||||
|
|
||||||
|
**Sorting:** existing `Order by` Select in the toolbar remains the only sort control. No per-column header sort.
|
||||||
|
|
||||||
|
**Empty state:** existing `<Empty>` component spans all columns when the filtered list is empty.
|
||||||
|
|
||||||
|
## 5. Pagination
|
||||||
|
|
||||||
|
**Component:** `TaskPager` rendered in its own translucent card box below the list/grid (same style tokens as the table card, separate box).
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Showing 1–25 of 312 tasks Rows per page: [25 ▾] │
|
||||||
|
│ ‹ 1 2 3 4 5 … 13 › │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Page-number window: always include first, last, current. Up to 7 buttons total. Ellipsis when the gap is greater than 1.
|
||||||
|
- `ResizeObserver` on the pager → reduce visible buttons on narrow widths (7 → 5 → 3 → just prev/next).
|
||||||
|
- Page-size change resets `pageIndex` to 0.
|
||||||
|
- If filters trim the total below `pageIndex * pageSize`, snap `pageIndex` to the last valid page.
|
||||||
|
- Pager renders **for both list and grid views**, identically.
|
||||||
|
|
||||||
|
## 6. Detail sheet
|
||||||
|
|
||||||
|
**Replaces:** `TaskDetailDialog.tsx` → new `TaskDetailSheet.tsx` using shadcn `Sheet`, right side, width ~480 px.
|
||||||
|
|
||||||
|
**Three fixed regions:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐ ← STICKY HEADER
|
||||||
|
│ Acme › Communications │ breadcrumb (small, muted)
|
||||||
|
│ Draft Q2 investor update email │ title (18 px, semibold)
|
||||||
|
│ [↑ High priority] [● In progress] chip row
|
||||||
|
│ [⋯] │ overflow menu (Edit, Delete)
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ │ ← SCROLLING BODY
|
||||||
|
│ ┌─ Properties card ──────────┐ │
|
||||||
|
│ │ Assignee | Due │ │
|
||||||
|
│ │ Estimate | Created │ │
|
||||||
|
│ │ Files: [chip] [chip] [+Add]│ │
|
||||||
|
│ └────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Description │
|
||||||
|
│ <body text> │
|
||||||
|
│ │
|
||||||
|
│ ── separator ── │
|
||||||
|
│ │
|
||||||
|
│ Comments · 4 │
|
||||||
|
│ <comment list, no inner scroll>│
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────┤ ← STICKY COMPOSER
|
||||||
|
│ [👤] ┌──────────────┐ [↑] │
|
||||||
|
│ │ Write comment│ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Header chips:**
|
||||||
|
- Priority: existing `<PriorityBadge>` (arrow + colored text).
|
||||||
|
- Status: pill using existing `STATUS_CONFIG` colors.
|
||||||
|
- Both are clickable → popover to change.
|
||||||
|
|
||||||
|
**Properties card** (translucent inner box, 2-column grid):
|
||||||
|
- Assignee, Due, Estimate, Created — each row is small uppercase label + value.
|
||||||
|
- Estimate shows muted `—` until the AI agent ships.
|
||||||
|
- Files row spans both columns: horizontal chip strip. Each chip: `📎 filename · sizeKB ×`. `+ Add` is a dashed pill that triggers `taskAttachments.pick`.
|
||||||
|
- Click chip filename → `taskAttachments.open`.
|
||||||
|
- Click × → confirm + `taskAttachments.delete`.
|
||||||
|
|
||||||
|
**Comments:** no inner ScrollArea — they scroll with the body.
|
||||||
|
|
||||||
|
**Composer (sticky bottom):** reuses the home / AI input wrapper styling:
|
||||||
|
|
||||||
|
```
|
||||||
|
rounded-2xl bg-background/70 backdrop-blur-xl
|
||||||
|
border border-border/50 shadow-lg ring-1 ring-border/20
|
||||||
|
focus-within:shadow-xl focus-within:border-ring/50
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally reuses the existing `ChatInputBox` component via a new `'comment'` variant (auto-grow textarea + `ArrowUp` send button, draft persistence, `⌘ + Enter` submit). **No attach icon in the composer.**
|
||||||
|
|
||||||
|
**Edit / Delete:** moved into the header overflow menu. The previous footer action bar is removed.
|
||||||
|
|
||||||
|
## 7. Create / Edit dialog
|
||||||
|
|
||||||
|
**Components:**
|
||||||
|
- `TaskFormDialog` — shared shell, props: `mode: 'create' | 'edit'`, initial values, `onSubmit`.
|
||||||
|
- `NewTaskDialog` and `EditTaskDialog` become thin wrappers (different default values + mutation).
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ New task ─────────────────── ⌘+Enter to create ─┐
|
||||||
|
│ │
|
||||||
|
│ What needs to be done? ← 22 px input │
|
||||||
|
│ Add a description… ← textarea │
|
||||||
|
│ │
|
||||||
|
│ PROPERTIES │
|
||||||
|
│ [📁 Project: Acme › Communications] │
|
||||||
|
│ [↑ Priority: High] [● Status: To Do] │
|
||||||
|
│ [📅 Due: Apr 30, 2026] │
|
||||||
|
│ [+ Add assignees] ← dashed (empty) │
|
||||||
|
│ │
|
||||||
|
│ ────────────────────────────────────────────────│
|
||||||
|
│ [📎] * [Cancel] [Create task] │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`*` 📎 icon-pill is **only visible in Edit mode**. Attachments are blocked in Create mode (need a `taskId`); the user saves first, then attaches via the detail sheet or via Edit.
|
||||||
|
|
||||||
|
**Pill states:**
|
||||||
|
- Set: `bg-card/70`, solid border, label + value visible.
|
||||||
|
- Empty: dashed border, muted text.
|
||||||
|
|
||||||
|
**Pill click → field-specific shadcn `Popover`:**
|
||||||
|
- Project — Select w/ inline create flow (existing logic preserved: project + client + sub-client).
|
||||||
|
- Priority — three-option select (high / medium / low).
|
||||||
|
- Status — three-option select.
|
||||||
|
- Due — Calendar + optional hour/minute selectors.
|
||||||
|
- Assignees — existing Popover (known-assignees list + add-new).
|
||||||
|
|
||||||
|
**Keyboard:** `⌘ / Ctrl + Enter` submits. Title `autoFocus`.
|
||||||
|
|
||||||
|
## 8. Project page integration
|
||||||
|
|
||||||
|
**File:** `src/renderer/routes/projects.$projectId.tsx`.
|
||||||
|
|
||||||
|
The existing tasks tab content is replaced by `<TaskListView projectId={...} hideProjectColumn />`. The toolbar (status tabs, search, `Order by`, view toggle, **New task** button) is identical to the Tasks page. Pagination state is local to this page (separate from the Tasks page state). Clicking a row opens the same `TaskDetailSheet`.
|
||||||
|
|
||||||
|
No changes to other project tabs (overview, notes, timeline).
|
||||||
|
|
||||||
|
## 9. Files
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
```
|
||||||
|
src/renderer/components/tasks/
|
||||||
|
TaskListView.tsx — shared toolbar + table/grid + pager
|
||||||
|
TaskTable.tsx — shadcn Table renderer
|
||||||
|
TaskTableRow.tsx — single row + context menu
|
||||||
|
TaskPager.tsx — pagination card box
|
||||||
|
TaskDetailSheet.tsx — right-side Sheet replacing TaskDetailDialog
|
||||||
|
TaskFormDialog.tsx — shared shell for create/edit
|
||||||
|
TaskAttachmentChip.tsx — file chip
|
||||||
|
AssigneeStack.tsx — overlapping avatars + overflow chip
|
||||||
|
StatusBadge.tsx — status pill (existing STATUS_CONFIG colors)
|
||||||
|
|
||||||
|
src/main/
|
||||||
|
attachments/storage.ts — path resolution, sanitize, copy, delete helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
```
|
||||||
|
src/renderer/routes/tasks.tsx — render <TaskListView>
|
||||||
|
src/renderer/routes/projects.$projectId.tsx — tasks tab uses <TaskListView hideProjectColumn>
|
||||||
|
src/renderer/components/tasks/NewTaskDialog.tsx — wrapper around TaskFormDialog (mode='create')
|
||||||
|
src/renderer/components/tasks/EditTaskDialog.tsx — wrapper around TaskFormDialog (mode='edit')
|
||||||
|
src/main/db/schema.ts — tasks.estimate, taskAttachments
|
||||||
|
src/main/db/migrations/ — new generated migration
|
||||||
|
src/main/router/index.ts — taskAttachments router; tasks.update accepts estimate; tasks.delete cascades attachments
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deleted files:**
|
||||||
|
|
||||||
|
```
|
||||||
|
src/renderer/components/tasks/TaskDetailDialog.tsx — replaced by TaskDetailSheet
|
||||||
|
src/renderer/components/tasks/TaskRow.tsx — replaced by TaskTableRow (TaskCard kept for grid)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. i18n keys
|
||||||
|
|
||||||
|
New keys in the `tasks.*` namespace, added to all five language files (en, it, es, fr, de):
|
||||||
|
|
||||||
|
```
|
||||||
|
tasks.colTask, tasks.colProject, tasks.colPriority, tasks.colDue, tasks.colAssignee
|
||||||
|
tasks.rowsPerPage, tasks.showingNofM (plural-aware), tasks.noAssignees
|
||||||
|
tasks.estimate, tasks.attachments, tasks.addFile, tasks.removeFile, tasks.fileTooLarge
|
||||||
|
tasks.changeStatus, tasks.properties
|
||||||
|
tasks.confirmDeleteAttachment
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Out-of-scope follow-ups
|
||||||
|
|
||||||
|
- AI agent that generates `estimate` for a task.
|
||||||
|
- Comment attachments.
|
||||||
|
- Per-column sort in the table.
|
||||||
|
- Backend pagination (only needed if task counts grow much larger).
|
||||||
|
|
||||||
|
## 12. Implementation order (suggested)
|
||||||
|
|
||||||
|
A natural shipping order; the implementation plan can refine:
|
||||||
|
|
||||||
|
1. DB migration (estimate column + taskAttachments table).
|
||||||
|
2. Main-process attachments storage module + tRPC sub-router.
|
||||||
|
3. `TaskDetailSheet` with attachment UI (deletes the old dialog).
|
||||||
|
4. `TaskFormDialog` shared shell; rewire `NewTaskDialog` / `EditTaskDialog`.
|
||||||
|
5. `TaskListView`, `TaskTable`, `TaskPager`, `AssigneeStack`, `StatusBadge`. Wire into Tasks page.
|
||||||
|
6. Wire `TaskListView` into project detail page with `hideProjectColumn`.
|
||||||
|
7. i18n keys for all five languages.
|
||||||
BIN
docs/2026-05-08-task-ux-evolution-plan.md
Normal file
BIN
docs/2026-05-08-task-ux-evolution-plan.md
Normal file
Binary file not shown.
201
docs/2026-05-14-task-form-dialog-kbd-design.md
Normal file
201
docs/2026-05-14-task-form-dialog-kbd-design.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Task Form Dialog — keyboard + header polish — Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-14
|
||||||
|
**Scope:** adiuvAI renderer (`src/renderer/components/tasks/TaskFormDialog.tsx`) + supporting libs
|
||||||
|
**Status:** Approved by user (mockup at `docs/mockups/2026-05-14-task-form-dialog-mockup.html`), ready for implementation plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Port three UX features shipped in the timeline batch-add `AddEventDialog` (`docs/2026-05-08-task-ux-evolution-design.md` § timeline batch) into `TaskFormDialog`:
|
||||||
|
|
||||||
|
1. **Header style** — `DialogTitle` + `DialogDescription` (no separator border), matching `AddEventDialog`.
|
||||||
|
2. **Full keyboard navigation** — Tab/Shift-Tab between fields & pills, arrow keys within pills row, Enter to open focused pill, arrow keys inside list popovers + calendar, Esc to close popover.
|
||||||
|
3. **Date + time via keyboard** — replace the Calendar + 2× hour/minute `Select` triplet with a typeable `DateField` that supports an optional `HH:MM` suffix and respects `FormatPrefs.dateFormat`.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Migrating `TaskFormDialog` to a Sheet (deferred — see `docs/2026-05-08-task-ux-evolution-plan.md`).
|
||||||
|
- Touching `NewTaskDialog` / `EditTaskDialog` wrappers (no behavior change).
|
||||||
|
- Changes to other property popovers' rendering beyond keyboard handling.
|
||||||
|
- Inline project creation flow (`InlineProjectForm`) — unchanged.
|
||||||
|
|
||||||
|
## 1. Header
|
||||||
|
|
||||||
|
Replace the current minimal header:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DialogHeader className="px-5 py-3 border-b border-border/40">
|
||||||
|
<DialogTitle className="text-sm font-medium">{...}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
```
|
||||||
|
|
||||||
|
with the `AddEventDialog` style:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{mode === 'create' ? t('tasks.newTask') : t('tasks.editTask')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{mode === 'create' ? t('tasks.newTaskDescription') : t('tasks.editTaskDescription')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
```
|
||||||
|
|
||||||
|
**No border-bottom** under the header — body flows directly under it. Keep the existing `bg-card/92 backdrop-blur-xl` overlay on `DialogContent`.
|
||||||
|
|
||||||
|
New i18n keys (all 5 languages): `tasks.newTaskDescription`, `tasks.editTaskDescription`.
|
||||||
|
|
||||||
|
## 2. Keyboard navigation
|
||||||
|
|
||||||
|
### Pills row — roving focus + arrow movement
|
||||||
|
|
||||||
|
The property pills (`Project · Priority · Status · Due · Assignees`) become a roving-tabindex group:
|
||||||
|
|
||||||
|
- Only one pill at a time has `tabindex={0}`; the rest have `tabindex={-1}`. Default focused pill = first (Project).
|
||||||
|
- `Tab` / `Shift+Tab` enters/exits the group as a single stop. Inside the group, `Tab` exits forward to the footer; on entry from the footer-side, focus restores to the last-focused pill.
|
||||||
|
- `ArrowRight` / `ArrowDown` → next pill (clamped at end).
|
||||||
|
- `ArrowLeft` / `ArrowUp` → previous pill (clamped at start).
|
||||||
|
- `Home` / `End` → first / last pill.
|
||||||
|
- `Enter` or `Space` on focused pill → open its popover.
|
||||||
|
|
||||||
|
Implementation: a small hook `useRovingFocus(ref, count)` returning `(index) => { tabIndex, onKeyDown, onFocus }`. Pills consume it inside `PropertyPill` (kept presentational) via a wrapping `<button>`.
|
||||||
|
|
||||||
|
`PropertyPill` is already a `<button>`-ish trigger via `<span>`. To support real focus rings + key events, change the trigger element rendered by `PopoverTrigger asChild` from `<span>` to a `<button type="button">`. Visible focus ring matches `--ring` via `focus-visible:ring-2 ring-ring/30`.
|
||||||
|
|
||||||
|
### List popovers — Project, Priority, Status, Assignees
|
||||||
|
|
||||||
|
shadcn `Popover` does not provide list semantics. Inside each popover content:
|
||||||
|
|
||||||
|
- Items render with `role="option"` (or `menuitem`) and roving `tabIndex` (active item = `0`, rest `-1`).
|
||||||
|
- When popover opens, focus moves to the currently-selected item (or first item).
|
||||||
|
- `ArrowDown` / `ArrowUp` move the active item; `Home`/`End` jump to ends.
|
||||||
|
- `Enter` / `Space` selects the active item.
|
||||||
|
- For single-select popovers (Project, Priority, Status) selection closes the popover and returns focus to the originating pill.
|
||||||
|
- For multi-select (Assignees) selection toggles; popover stays open. `Esc` closes and returns focus to the pill.
|
||||||
|
- `Tab` inside a popover closes the popover (focus returns to pill, then the next Tab advances normally).
|
||||||
|
|
||||||
|
Implementation: a single shared hook `useListboxKeys(items, opts)` consumed by each popover content. Items are sourced from existing data (`projectsList`, `knownAssignees`, hard-coded priority/status arrays).
|
||||||
|
|
||||||
|
### Calendar — keyboard
|
||||||
|
|
||||||
|
The shadcn `Calendar` already supports arrow-key day navigation and `Enter` to select (via react-day-picker). We need only to confirm that focus lands on the calendar grid when the Due popover opens. The new `DateField` (§3) replaces the current Popover+Calendar+Selects assembly and embeds the calendar.
|
||||||
|
|
||||||
|
### Description — Enter
|
||||||
|
|
||||||
|
Keep existing behavior: `Enter` inserts a newline in the description textarea. The form-level `⌘/Ctrl+Enter` submit handler already lives on the `<form>` element and continues to work; the footer's "⌘+Enter to create" hint is removed from the UI (the shortcut still works).
|
||||||
|
|
||||||
|
## 3. Date + time via keyboard
|
||||||
|
|
||||||
|
### Strategy — extend existing `DateField`
|
||||||
|
|
||||||
|
Reuse `src/renderer/components/ui/date-field.tsx` (already typeable, format-aware via `useFormatPrefs`, with embedded Calendar). Add **optional time** support behind a new prop `withTime?: boolean`.
|
||||||
|
|
||||||
|
When `withTime` is on:
|
||||||
|
- The text input accepts either a bare date (`30/04/2026`, `Apr 30`, `+3d`, `tomorrow`, …) or date-with-time suffix (`30/04/2026 14:30`).
|
||||||
|
- The Popover content gains a small `Time` row under the Calendar — two `Select`s (hour 00–23, minute in 5-min steps) identical to the current TaskFormDialog implementation. They edit the time portion of the committed `Date`.
|
||||||
|
- Display value after commit: `<date in FormatPrefs.dateFormat> HH:MM` when time component is non-midnight, otherwise just the date.
|
||||||
|
|
||||||
|
### Parser extension (`lib/parseDate.ts`)
|
||||||
|
|
||||||
|
`parseDate(input, prefs, keywords)` adopts optional trailing time:
|
||||||
|
|
||||||
|
- Regex split: `RE_TIME = /\s+(\d{1,2}):(\d{2})\s*$/`.
|
||||||
|
- If matched, parse `HH`/`MM` (`0–23` / `0–59`), strip the suffix, parse remaining string with the existing logic, then set `hours` and `minutes` on the result.
|
||||||
|
- If time match is invalid (e.g. `25:99`), whole input is invalid.
|
||||||
|
|
||||||
|
Unit-test cases (existing tests if any get extended; otherwise small new file):
|
||||||
|
|
||||||
|
| Input | Format pref | Expected |
|
||||||
|
|---|---|---|
|
||||||
|
| `30/04/2026 14:30` | `dd/MM/yyyy` | 2026-04-30 14:30 local |
|
||||||
|
| `04/30/2026 09:00` | `MM/dd/yyyy` | 2026-04-30 09:00 |
|
||||||
|
| `2026-04-30 23:59` | `yyyy-MM-dd` | 2026-04-30 23:59 |
|
||||||
|
| `tomorrow 08:15` | any | next-day 08:15 |
|
||||||
|
| `30/04/2026 25:00` | any | invalid |
|
||||||
|
| `30/04/2026` | dd/MM | 2026-04-30 00:00 (date only, time unchanged) |
|
||||||
|
|
||||||
|
### Caller change in `TaskFormDialog`
|
||||||
|
|
||||||
|
The whole Due Popover block (Calendar + hour/minute Selects + clear button) is replaced by:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<DateField
|
||||||
|
withTime
|
||||||
|
value={values.dueDate ? new Date(values.dueDate) : undefined}
|
||||||
|
onChange={(d) => setValues((v) => ({ ...v, dueDate: d ? d.getTime() : null }))}
|
||||||
|
placeholder={t('tasks.colDue')}
|
||||||
|
aria-label={t('tasks.colDue')}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The pill itself remains for display when the field is collapsed. Two arrangements considered:
|
||||||
|
|
||||||
|
- **(A) Pill opens a popover containing the `DateField`** — keeps visual parity with the other pills. The `DateField` *inside* the popover is just an `Input` + Calendar, no nested Popover. Recommended.
|
||||||
|
- **(B) `DateField` replaces the pill inline in the row** — visually breaks the pill row.
|
||||||
|
|
||||||
|
Going with **(A)**. To avoid a nested-popover (`Popover` inside `PopoverContent`), `DateField` gains a `flat?: boolean` prop. When `flat` is set, it renders:
|
||||||
|
|
||||||
|
- the typeable `Input`,
|
||||||
|
- the `Calendar` inline (no internal `Popover` wrapper),
|
||||||
|
- the Time row (when `withTime`).
|
||||||
|
|
||||||
|
The Due pill's `PopoverContent` renders `<DateField withTime flat />`. Outside the task dialog, existing callers (e.g. `AddEventDialog`) keep using the default (non-flat) DateField with its own popover trigger.
|
||||||
|
|
||||||
|
The Due popover content:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ Due popover ───────────────────┐
|
||||||
|
│ [📅 30/04/2026 14:30 ] │ ← typeable Input (parses date + time)
|
||||||
|
│ Calendar grid (kbd nav) │
|
||||||
|
│ ── ── ── ── ── ── ── ── ── ── │
|
||||||
|
│ Time: [HH ⌄] : [MM ⌄] [Clear] │ ← shown only when withTime
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
(The mockup illustrated standalone segments; that was a sketch — the real impl reuses `DateField`'s single-input typeable parser, which is already keyboard-driven via `parseDate`.)
|
||||||
|
|
||||||
|
## 4. Files
|
||||||
|
|
||||||
|
**Modified:**
|
||||||
|
|
||||||
|
```
|
||||||
|
src/renderer/components/tasks/TaskFormDialog.tsx — new header; roving focus on pills row; replace Due popover with <DateField withTime />; drop the "⌘+Enter" hint
|
||||||
|
src/renderer/components/ui/date-field.tsx — new props withTime + flat; Time Selects; expanded onCommit/text-display logic
|
||||||
|
src/renderer/lib/parseDate.ts — accept optional trailing " HH:MM"
|
||||||
|
src/renderer/locales/{en,it,es,fr,de}/translation.json
|
||||||
|
— add tasks.newTaskDescription, tasks.editTaskDescription
|
||||||
|
```
|
||||||
|
|
||||||
|
**New (small, kept local to features):**
|
||||||
|
|
||||||
|
```
|
||||||
|
src/renderer/hooks/useRovingFocus.ts — generic roving-tabindex hook
|
||||||
|
src/renderer/hooks/useListboxKeys.ts — popover-list arrow/enter/esc handler
|
||||||
|
```
|
||||||
|
|
||||||
|
If a unit-test setup is later introduced for `parseDate`, add cases there. Not blocking.
|
||||||
|
|
||||||
|
## 5. Accessibility
|
||||||
|
|
||||||
|
- Pills row: `role="toolbar"` with `aria-label={t('tasks.properties')}`; pills are `<button>` with descriptive `aria-label` (e.g. `Project: Acme · Communications`).
|
||||||
|
- Listbox popovers: container `role="listbox"`, items `role="option"`, `aria-selected` on the chosen one. Single-select popovers also set `aria-activedescendant` on the listbox when convenient; otherwise rely on `.focus()`.
|
||||||
|
- Multi-select Assignees uses `aria-multiselectable="true"`.
|
||||||
|
- `DateField` keeps existing `aria-invalid` + `aria-describedby` semantics.
|
||||||
|
|
||||||
|
## 6. Out-of-scope follow-ups
|
||||||
|
|
||||||
|
- Project popover inline-create flow keyboard polish (currently a sub-form inside the popover — separate effort).
|
||||||
|
- `DateField` natural-language time keywords (e.g. `tomorrow 9am`) — only `HH:MM` accepted.
|
||||||
|
- Migrating `TaskFormDialog` shell to a Sheet — already deferred.
|
||||||
|
|
||||||
|
## 7. Implementation order (suggested)
|
||||||
|
|
||||||
|
1. `useRovingFocus` + `useListboxKeys` hooks (no UI changes).
|
||||||
|
2. `parseDate` time-suffix support; refresh existing parseDate tests.
|
||||||
|
3. `DateField` `withTime` prop + time Selects in Popover.
|
||||||
|
4. `TaskFormDialog`:
|
||||||
|
- Header swap (Title + Description, no border).
|
||||||
|
- Pills row wired to `useRovingFocus`; pill trigger element switched to `<button>`.
|
||||||
|
- Each list popover wired to `useListboxKeys`.
|
||||||
|
- Due popover content replaced by `<DateField withTime />`.
|
||||||
|
- Remove footer `⌘+Enter` hint.
|
||||||
|
5. i18n strings in all five languages.
|
||||||
1172
docs/2026-05-14-task-form-dialog-kbd-plan.md
Normal file
1172
docs/2026-05-14-task-form-dialog-kbd-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
546
docs/PROMPT-memory-evolution.md
Normal file
546
docs/PROMPT-memory-evolution.md
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
# RALPH LOOP PROMPT — Memory Subsystem Evolution (MemGPT + Mem0 + Mem0g-light)
|
||||||
|
|
||||||
|
> **How to run:**
|
||||||
|
> ```
|
||||||
|
> /ralph-loop "Implement the memory evolution exactly as specified in docs/PROMPT-memory-evolution.md. ALWAYS start each iteration by invoking the /caveman:caveman ultra skill at intensity 'full'. Output <promise>MEMORY EVOLUTION COMPLETE</promise> when all phases pass lint + tests." --max-iterations 40 --completion-promise "MEMORY EVOLUTION COMPLETE"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MANDATORY PER-ITERATION PREAMBLE
|
||||||
|
|
||||||
|
**Every iteration MUST begin with these two actions, in order:**
|
||||||
|
|
||||||
|
1. **Activate caveman mode.** Invoke the `caveman:caveman ultra` skill at intensity `full` before any other tool call. All prose you emit during the iteration must follow caveman rules (drop articles, fragments OK, no filler, no pleasantries). Code/commits/PRs stay normal per caveman plugin rules.
|
||||||
|
2. **Read this file in full** (`docs/PROMPT-memory-evolution.md`) to re-anchor on the plan.
|
||||||
|
|
||||||
|
If caveman already active from prior iteration, re-assert it anyway — ralph loop restarts cold each time.
|
||||||
|
|
||||||
|
After preamble:
|
||||||
|
|
||||||
|
3. Inspect repo state: check which tasks already done by reading target files / running grep.
|
||||||
|
4. Pick next incomplete task in phase order (Phase 1 → 2 → 3 → 4 → 5). No skipping, no out-of-order.
|
||||||
|
5. Implement task.
|
||||||
|
6. Run relevant lint + tests for that phase before exit.
|
||||||
|
7. When ALL phases complete AND lints + tests green → output `<promise>MEMORY EVOLUTION COMPLETE</promise>`.
|
||||||
|
|
||||||
|
**DO NOT** implement multiple phases in one iteration unless they are tiny edits in the same file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LINT + TEST COMMANDS
|
||||||
|
|
||||||
|
Run after each phase:
|
||||||
|
- Backend lint: `cd api && ruff check . --fix`
|
||||||
|
- Backend tests: `cd api && pytest -q`
|
||||||
|
- Frontend lint: `cd adiuvAI && npx eslint . --fix`
|
||||||
|
- Frontend typecheck: `cd adiuvAI && npx tsc --noEmit`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SOURCE OF TRUTH
|
||||||
|
|
||||||
|
Architectural rationale lives in [docs/memory-evolution-strategy.md](docs/memory-evolution-strategy.md). This file is the execution plan derived from it. If a conflict appears, the strategy doc wins on *why*, this doc wins on *how*.
|
||||||
|
|
||||||
|
**Zero-trust invariant:** all user-content writes/reads go through per-user Fernet in [api/app/core/memory_middleware.py](api/app/core/memory_middleware.py). Backend never stores plaintext user content. Embeddings may leak text to OpenAI — already accepted trade-off, documented in privacy policy.
|
||||||
|
|
||||||
|
**Tier gates** live in [api/app/billing/tier_manager.py](api/app/billing/tier_manager.py). New capabilities MUST be gated there, not ad-hoc in routes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WHAT THIS FEATURE DOES
|
||||||
|
|
||||||
|
Five goals from the strategy doc, executed in order:
|
||||||
|
|
||||||
|
1. **Activate real pgvector** on `associative` tier (replace keyword fallback). Pro+ only.
|
||||||
|
2. **Mem0-style Extract/Update pipeline** post-`store_episode`. Batch for Free, realtime for Pro+.
|
||||||
|
3. **`relational` tier (Mem0g-light)**: new table `memory_relations` — person/project/topic graph in Postgres.
|
||||||
|
4. **Settings > Memory UI** in Electron renderer — view/edit `core` + `relational`, GDPR forget.
|
||||||
|
5. **Proactive mining** (Power tier only, optional last): scheduled job promotes episodic patterns to `proactive`.
|
||||||
|
|
||||||
|
**Architectural anchors already in place** (do NOT re-create):
|
||||||
|
- `MemoryMiddleware.enrich_context` injects 4 tiers into orchestrator — extend, not replace.
|
||||||
|
- `MemoryAssociative.embedding` column exists (JSON fallback); swap to `pgvector.Vector(1536)` in migration.
|
||||||
|
- `get_llm("gpt-4o-mini", ...)` in [api/app/core/llm.py](api/app/core/llm.py) is canonical LLM factory.
|
||||||
|
- Tier-gating helper: `TierManager.has_feature(user, feature)` — add new feature enums.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 1 — pgvector on associative tier (Pro+ gated)
|
||||||
|
|
||||||
|
### TASK 1.1: Alembic migration — switch `memory_associative.embedding` to `vector(1536)`
|
||||||
|
|
||||||
|
**File:** `api/alembic/versions/XXX_associative_pgvector.py` (new)
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
- `CREATE EXTENSION IF NOT EXISTS vector;` (idempotent).
|
||||||
|
- `ALTER TABLE memory_associative ALTER COLUMN embedding TYPE vector(1536) USING embedding::text::vector;` — must handle existing JSON rows. If conversion risky, drop column and re-add: `DROP COLUMN embedding; ADD COLUMN embedding vector(1536);` (data loss acceptable — keyword fallback still works).
|
||||||
|
- Create IVFFlat index: `CREATE INDEX memory_associative_embedding_idx ON memory_associative USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);`
|
||||||
|
- `downgrade()` reverses: drop index, `ALTER TYPE ... TYPE jsonb`.
|
||||||
|
|
||||||
|
Revision id: increment from latest in `api/alembic/versions/`. Check `004_add_memory_tables.py` for style.
|
||||||
|
|
||||||
|
**Done signal:** Migration applies cleanly on a fresh DB: `alembic upgrade head` exits 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.2: Update `MemoryAssociative.embedding` SQLAlchemy column
|
||||||
|
|
||||||
|
**File:** [api/app/models.py](api/app/models.py)
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
```python
|
||||||
|
embedding: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from pgvector.sqlalchemy import Vector
|
||||||
|
...
|
||||||
|
embedding: Mapped[list | None] = mapped_column(Vector(1536), nullable=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `pgvector>=0.2.5` to `api/requirements.txt` (or `pyproject.toml` — check which is authoritative).
|
||||||
|
|
||||||
|
**Done signal:** `pgvector` import resolves, `pytest -q` still green on model import.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.3: Add `TierFeature.REAL_EMBEDDINGS` feature flag
|
||||||
|
|
||||||
|
**File:** `api/app/billing/tier_manager.py`
|
||||||
|
|
||||||
|
Add to the feature enum / matrix:
|
||||||
|
- `REAL_EMBEDDINGS = "real_embeddings"` → granted for `pro`, `power`, `team`. Free = False.
|
||||||
|
|
||||||
|
**Done signal:** `TierManager.has_feature(user, "real_embeddings")` returns correct bool per tier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.4: Embedding helper
|
||||||
|
|
||||||
|
**File:** `api/app/core/embeddings.py` (new)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def embed_text(text: str) -> list[float] | None:
|
||||||
|
"""Call OpenAI text-embedding-3-small. Return None on failure (caller falls back to keyword)."""
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `AsyncOpenAI` client (already a dep via LiteLLM). Truncate input to 8000 chars. On any exception log warning + return None — MUST not raise.
|
||||||
|
|
||||||
|
**Done signal:** Unit test `test_embed_text_returns_1536_floats` passes with mocked client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.5: Wire embeddings into `_load_associative` + `store_associative`
|
||||||
|
|
||||||
|
**File:** [api/app/core/memory_middleware.py](api/app/core/memory_middleware.py)
|
||||||
|
|
||||||
|
In `_load_associative`:
|
||||||
|
1. Check user tier via `TierManager.has_feature(user, "real_embeddings")`.
|
||||||
|
2. If True → `embed_text(message)` → if vector not None run:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM memory_associative
|
||||||
|
WHERE user_id = :uid
|
||||||
|
ORDER BY embedding <=> :qvec
|
||||||
|
LIMIT :k;
|
||||||
|
```
|
||||||
|
Use SQLAlchemy `embedding.cosine_distance(qvec)` (pgvector).
|
||||||
|
3. Fallback (False or None): keep current keyword-order path.
|
||||||
|
|
||||||
|
Add new `store_associative(user_id, content)` method:
|
||||||
|
- Encrypt content with user Fernet.
|
||||||
|
- If tier has real_embeddings → compute embedding, store alongside.
|
||||||
|
- Else → store with `embedding=NULL` (still useful for future upgrade).
|
||||||
|
|
||||||
|
**Done signal:** Associative search returns semantically-closer results on a pro test user, keyword-ordered for free user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.6: Phase 1 checks
|
||||||
|
|
||||||
|
- `cd api && ruff check . --fix`
|
||||||
|
- `cd api && pytest -q tests/test_memory_middleware.py` (create minimal test if absent).
|
||||||
|
- Manual smoke: spin up docker compose, insert two associative memories via pro user, query → verify cosine ordering.
|
||||||
|
|
||||||
|
**Done signal:** All three green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 2 — Mem0-style Extract/Update pipeline
|
||||||
|
|
||||||
|
### TASK 2.1: Extraction prompt + schema
|
||||||
|
|
||||||
|
**File:** `api/app/core/memory_extraction.py` (new)
|
||||||
|
|
||||||
|
Define Pydantic models:
|
||||||
|
```python
|
||||||
|
class MemoryCandidate(BaseModel):
|
||||||
|
type: Literal["fact", "preference", "relation", "routine"]
|
||||||
|
content: str # short canonical statement
|
||||||
|
target_tier: Literal["core", "associative", "relational", "proactive"]
|
||||||
|
subject: str | None = None # only for relation
|
||||||
|
predicate: str | None = None # only for relation
|
||||||
|
object: str | None = None # only for relation
|
||||||
|
confidence: float = 0.7
|
||||||
|
|
||||||
|
class ExtractionResult(BaseModel):
|
||||||
|
candidates: list[MemoryCandidate]
|
||||||
|
```
|
||||||
|
|
||||||
|
Prompt template (system): "You are a memory extractor for a personal AI secretary. Given the last turn + core memory + recent episodes, identify durable facts, preferences, routines, and person/project relations. Output JSON matching the schema. Skip small talk. Max 5 candidates per turn."
|
||||||
|
|
||||||
|
Use `gpt-4o-mini`, `temperature=0`, `response_format={"type": "json_object"}`.
|
||||||
|
|
||||||
|
**Done signal:** Calling `extract_candidates(last_turn, core, recent)` on a fixture returns a valid `ExtractionResult`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.2: Update decision (ADD / UPDATE / DELETE / NOOP)
|
||||||
|
|
||||||
|
**File:** `api/app/core/memory_extraction.py` (same file)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def decide_action(
|
||||||
|
candidate: MemoryCandidate,
|
||||||
|
existing: list[str], # plaintext neighbours (top-3 by similarity in target tier)
|
||||||
|
) -> Literal["ADD", "UPDATE", "DELETE", "NOOP"]:
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses a second `gpt-4o-mini` call with small prompt: "Given candidate and existing memories, decide ADD / UPDATE / DELETE / NOOP. Return only the verb."
|
||||||
|
|
||||||
|
Heuristic short-circuit: if `existing` empty → ADD without LLM (save cost).
|
||||||
|
|
||||||
|
**Done signal:** Unit tests for all 4 branches pass with mocked LLM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.3: Pipeline orchestrator
|
||||||
|
|
||||||
|
**File:** `api/app/core/memory_extraction.py` (same file)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def run_extraction(
|
||||||
|
db: AsyncSession,
|
||||||
|
user_id: str,
|
||||||
|
last_user_msg: str,
|
||||||
|
last_assistant_msg: str,
|
||||||
|
session_id: str | None,
|
||||||
|
) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Load small context: `core_memory` + last 5 episodes (via middleware helpers).
|
||||||
|
2. `extract_candidates(...)`.
|
||||||
|
3. For each candidate: similarity-search target tier → top-3 neighbours → `decide_action` → apply via `MemoryMiddleware.update_core` / `store_associative` / (new) `upsert_relation` / `store_proactive`.
|
||||||
|
4. Log Langfuse trace with `trace_id`.
|
||||||
|
5. MUST not raise — wrap in try/except, log warning.
|
||||||
|
|
||||||
|
**Done signal:** Calling `run_extraction` on a fake "user said my CFO is Giulia" produces a relation candidate and a core candidate, and writes them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.4: Tier-gated dispatch
|
||||||
|
|
||||||
|
**File:** [api/app/core/memory_middleware.py](api/app/core/memory_middleware.py)
|
||||||
|
|
||||||
|
After `store_episode` success, dispatch extraction:
|
||||||
|
- Pro / Power / Team → schedule realtime task (`asyncio.create_task(run_extraction(...))` — fire-and-forget, exceptions swallowed).
|
||||||
|
- Free → enqueue a daily-batch marker row (new table `extraction_queue(user_id, episode_id, created_at)`). A separate cron (Phase 5 stub OK) drains it.
|
||||||
|
|
||||||
|
Add `TierFeature.REALTIME_EXTRACTION` to tier_manager (Free=False).
|
||||||
|
|
||||||
|
**Done signal:** Pro user triggers realtime task (verified via log line); Free user gets queue row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.5: Phase 2 checks
|
||||||
|
|
||||||
|
- `cd api && ruff check . --fix`
|
||||||
|
- `cd api && pytest -q tests/test_memory_extraction.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 3 — `relational` tier (Mem0g-light)
|
||||||
|
|
||||||
|
### TASK 3.1: Alembic migration — `memory_relations` table
|
||||||
|
|
||||||
|
**File:** `api/alembic/versions/XXX_memory_relations.py` (new)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE memory_relations (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
subject_label VARCHAR(128) NOT NULL, -- canonical label (e.g. "Giulia")
|
||||||
|
subject_type VARCHAR(32) NOT NULL, -- 'person' | 'company' | 'project' | 'topic'
|
||||||
|
predicate VARCHAR(64) NOT NULL, -- 'works_at' | 'reports_to' | 'stakeholder_of' | 'last_contacted_on' | 'owes_followup' | custom
|
||||||
|
object_label VARCHAR(128) NOT NULL,
|
||||||
|
object_type VARCHAR(32) NOT NULL,
|
||||||
|
confidence FLOAT NOT NULL DEFAULT 0.7,
|
||||||
|
source_episode_id UUID NULL REFERENCES memory_episodic(id),
|
||||||
|
notes_encrypted BYTEA NULL, -- Fernet, optional per-user commentary
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_confirmed_at TIMESTAMPTZ NULL -- used by TTL decay
|
||||||
|
);
|
||||||
|
CREATE INDEX memory_relations_user_subject_idx ON memory_relations(user_id, subject_label);
|
||||||
|
CREATE INDEX memory_relations_user_predicate_idx ON memory_relations(user_id, predicate);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** `alembic upgrade head` clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.2: `MemoryRelation` ORM model
|
||||||
|
|
||||||
|
**File:** [api/app/models.py](api/app/models.py)
|
||||||
|
|
||||||
|
Mirror the table above. `subject_label` / `object_label` are **plaintext** (entity names — treated as identifiers, not content). `notes_encrypted` uses Fernet like other tiers.
|
||||||
|
|
||||||
|
**Done signal:** Import of `MemoryRelation` resolves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.3: Relational middleware methods
|
||||||
|
|
||||||
|
**File:** [api/app/core/memory_middleware.py](api/app/core/memory_middleware.py)
|
||||||
|
|
||||||
|
Add:
|
||||||
|
- `async def upsert_relation(user_id, subject, subject_type, predicate, object_, object_type, *, confidence=0.7, source_episode_id=None, notes=None) -> None`
|
||||||
|
- `async def query_relations(user_id, subject=None, predicate=None, object_=None, limit=20) -> list[MemoryRelation]`
|
||||||
|
- Extend `enrich_context` return dict with key `relational_memory` — list of short strings `"{subject} --{predicate}--> {object}"` filtered by recent/confident (top 10).
|
||||||
|
- Tier-gate: Free tier → skip (empty list). Pro = base (person/project predicates only). Power = all predicates incl. custom. Use new `TierFeature.RELATIONAL_MEMORY`.
|
||||||
|
|
||||||
|
**Done signal:** Unit tests: upsert then query returns row; tier gating enforces limits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.4: Orchestrator prompt injection
|
||||||
|
|
||||||
|
**File:** `api/app/core/deep_agent.py`
|
||||||
|
|
||||||
|
Where `core_memory` / `episodic` already injected into system prompt, add a new paragraph labelled **"Known people & projects:"** listing the `relational_memory` strings. Keep under 800 chars (truncate if longer).
|
||||||
|
|
||||||
|
**Done signal:** Running a turn with seeded relations — agent uses the info (verified via Langfuse trace + test).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.5: Hook into extraction pipeline
|
||||||
|
|
||||||
|
**File:** `api/app/core/memory_extraction.py`
|
||||||
|
|
||||||
|
When `candidate.type == "relation"` → call `upsert_relation(...)` instead of `update_core` / `store_associative`.
|
||||||
|
|
||||||
|
**Done signal:** End-to-end test: turn saying "Marco is the PM on Project Acme" produces a `person --stakeholder_of--> project` row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.6: TTL + decay job
|
||||||
|
|
||||||
|
**File:** `api/app/core/memory_extraction.py` (or new `memory_maintenance.py`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def decay_relations(db, user_id) -> None:
|
||||||
|
# confidence *= 0.95 every 30 days since last_confirmed_at
|
||||||
|
# delete rows with confidence < 0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire into the same daily batch cron as Free extraction (Phase 5 introduces scheduler — OK to define function now and call it from a stub).
|
||||||
|
|
||||||
|
**Done signal:** Function exists + has unit test on a seeded fixture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.7: Phase 3 checks
|
||||||
|
|
||||||
|
- `cd api && ruff check . --fix`
|
||||||
|
- `cd api && pytest -q tests/test_memory_relations.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 4 — Settings > Memory UI (Electron renderer)
|
||||||
|
|
||||||
|
### TASK 4.1: Backend endpoints for UI
|
||||||
|
|
||||||
|
**File:** `api/app/api/routes/auth.py` (memory sub-section) or new `api/app/api/routes/memory.py`
|
||||||
|
|
||||||
|
Routes (all `@require_auth`, return user-scoped data only):
|
||||||
|
- `GET /auth/me/memory/core` → `dict[str, str]` (plaintext, decrypted).
|
||||||
|
- `GET /auth/me/memory/relational` → `list[RelationOut]` (subject/pred/obj/confidence/last_confirmed_at).
|
||||||
|
- `PATCH /auth/me/memory/relational/{id}` → edit label/confidence; body validates predicate ∈ allowed set.
|
||||||
|
- `DELETE /auth/me/memory/relational/{id}` → hard delete (GDPR Art. 17).
|
||||||
|
- `DELETE /auth/me/memory/core/{key}` → remove a core k/v.
|
||||||
|
- `POST /auth/me/memory/forget-all` → wipe all 4 tiers for user; audit log entry. Requires `X-Confirm: true` header — reject 400 otherwise. Do NOT delete the User row.
|
||||||
|
|
||||||
|
**Done signal:** OpenAPI schema shows all 6 routes; pytest green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 4.2: tRPC + auth-manager wrappers
|
||||||
|
|
||||||
|
**File:** [adiuvAI/src/main/auth/auth-manager.ts](adiuvAI/src/main/auth/auth-manager.ts) + [adiuvAI/src/main/router/index.ts](adiuvAI/src/main/router/index.ts)
|
||||||
|
|
||||||
|
Add auth-manager methods (6) wrapping each HTTP endpoint. Add tRPC procedures in a new `memoryRouter` merged into app router.
|
||||||
|
|
||||||
|
**Done signal:** `trpc.memory.listRelational.useQuery()` resolves from renderer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 4.3: `MemorySection` settings page
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/components/settings/MemorySection.tsx` (new)
|
||||||
|
|
||||||
|
Sections in order:
|
||||||
|
1. **Core preferences** — table of k/v from `trpc.memory.getCore`. Each row: key, value, edit pencil (inline input), trash icon (`deleteCore`). Add-row form at bottom.
|
||||||
|
2. **People & relationships** — table of relations. Columns: subject, predicate (select), object, confidence (progress bar), last confirmed (formatted via `formatRow`). Pencil → edit in drawer. Trash → `deleteRelation`.
|
||||||
|
3. **Danger zone** — red Card with "Forget everything" button. Confirm dialog (typed "forget" to enable) → calls `forgetAll` with `X-Confirm: true`.
|
||||||
|
|
||||||
|
Wire into `SECTIONS` in [adiuvAI/src/renderer/components/settings/types.ts](adiuvAI/src/renderer/components/settings/types.ts) as `{ id: 'memory', label: 'Memory', icon: Brain }`. Use `Brain` from `lucide-react`.
|
||||||
|
|
||||||
|
**Free tier gating:** if `profile.tier === 'free'` → relational table hidden with upgrade CTA instead. Use `usePlatform()` + profile tier check.
|
||||||
|
|
||||||
|
**Done signal:** `/settings` → Memory tab renders all three sections, edits/deletes round-trip to backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 4.4: i18n keys
|
||||||
|
|
||||||
|
Add translation keys to all 5 JSON files under namespace `settings.memory.*`:
|
||||||
|
- `corePreferences`, `peopleRelationships`, `dangerZone`, `forgetEverything`, `forgetConfirm`, `addEntry`, `noEntries`, `upgradeToSeePeople`.
|
||||||
|
|
||||||
|
Keep `common.*` reuse for `save`/`cancel`/`delete`/`edit` (already present).
|
||||||
|
|
||||||
|
**Done signal:** All 5 locale files include the new keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 4.5: Phase 4 checks
|
||||||
|
|
||||||
|
- `cd adiuvAI && npx eslint . --fix`
|
||||||
|
- `cd adiuvAI && npx tsc --noEmit`
|
||||||
|
- Manual: run `npm run start`, log in, open Settings > Memory, edit a core key, verify persisted via `GET /auth/me` memory echo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 5 — Proactive mining (Power tier only)
|
||||||
|
|
||||||
|
### TASK 5.1: Scheduler skeleton
|
||||||
|
|
||||||
|
**File:** `api/app/core/memory_maintenance.py`
|
||||||
|
|
||||||
|
Two entrypoints, callable from a cron runner (APScheduler already a dep — if not, add):
|
||||||
|
- `drain_extraction_queue()` — processes `extraction_queue` rows (Phase 2.4) for Free tier users, batched.
|
||||||
|
- `mine_proactive_patterns(user_id)` — for Power tier users only. Reads last 30 days episodic, runs a single `gpt-4o-mini` call: "Identify recurring temporal/behavioral patterns". Writes results to `memory_proactive` with `confidence`. Applies decay (conf *= 0.9 per 7 days since last sighting).
|
||||||
|
|
||||||
|
Register jobs in `app/main.py` startup (only if `settings.SCHEDULER_ENABLED=True`, default True; false in tests).
|
||||||
|
|
||||||
|
**Done signal:** `pytest -q` green (scheduler disabled). Manual: setting `SCHEDULER_ENABLED=True` + dev run logs "memory cron tick" every 1h.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 5.2: Surfacing proactive hints
|
||||||
|
|
||||||
|
**File:** `api/app/core/deep_agent.py` + `adiuvAI/src/renderer/components/home/DailyBrief.tsx` (if exists)
|
||||||
|
|
||||||
|
Backend already injects `proactive_hints` into prompt (middleware). Confirm still works after changes; add unit test with seeded proactive row → assert string present in final system prompt.
|
||||||
|
|
||||||
|
On renderer, if daily brief component exists, show proactive hints as chips under "I noticed…" header. If not, skip — not a regression.
|
||||||
|
|
||||||
|
**Done signal:** System prompt includes proactive line when row exists + confidence ≥ threshold.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 5.3: Tier gate
|
||||||
|
|
||||||
|
Add `TierFeature.PROACTIVE_MINING` to tier_manager — Power + Team only.
|
||||||
|
|
||||||
|
**Done signal:** Free/Pro user → no cron row for them; Power user → mining runs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 5.4: Phase 5 checks
|
||||||
|
|
||||||
|
- `cd api && ruff check . --fix`
|
||||||
|
- `cd api && pytest -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 6 — Completion
|
||||||
|
|
||||||
|
### TASK 6.1: Verify all files exist / modified
|
||||||
|
|
||||||
|
New files:
|
||||||
|
- [ ] `api/alembic/versions/*_associative_pgvector.py`
|
||||||
|
- [ ] `api/alembic/versions/*_memory_relations.py`
|
||||||
|
- [ ] `api/app/core/embeddings.py`
|
||||||
|
- [ ] `api/app/core/memory_extraction.py`
|
||||||
|
- [ ] `api/app/core/memory_maintenance.py`
|
||||||
|
- [ ] `api/app/api/routes/memory.py` (or new routes appended in `auth.py`)
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/settings/MemorySection.tsx`
|
||||||
|
|
||||||
|
Modified files:
|
||||||
|
- [ ] `api/app/models.py` (MemoryAssociative.embedding Vector(1536), MemoryRelation class)
|
||||||
|
- [ ] `api/app/core/memory_middleware.py` (real pgvector path, relational methods, enrich_context extended, dispatch extraction after store_episode)
|
||||||
|
- [ ] `api/app/billing/tier_manager.py` (REAL_EMBEDDINGS, REALTIME_EXTRACTION, RELATIONAL_MEMORY, PROACTIVE_MINING features)
|
||||||
|
- [ ] `api/app/core/deep_agent.py` (relational injection)
|
||||||
|
- [ ] `api/app/main.py` (scheduler startup)
|
||||||
|
- [ ] `api/requirements.txt` (pgvector, APScheduler)
|
||||||
|
- [ ] `adiuvAI/src/main/auth/auth-manager.ts` (6 memory methods)
|
||||||
|
- [ ] `adiuvAI/src/main/router/index.ts` (memoryRouter merged)
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/settings/types.ts` (memory section entry)
|
||||||
|
- [ ] `adiuvAI/src/renderer/locales/{en,it,es,fr,de}/translation.json` (settings.memory.* keys)
|
||||||
|
|
||||||
|
### TASK 6.2: Full gauntlet
|
||||||
|
|
||||||
|
Run all four commands, expect exit 0:
|
||||||
|
```bash
|
||||||
|
cd api && ruff check . --fix
|
||||||
|
cd api && pytest -q
|
||||||
|
cd adiuvAI && npx eslint . --fix
|
||||||
|
cd adiuvAI && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
### TASK 6.3: Output completion promise
|
||||||
|
|
||||||
|
If gauntlet green and file checklist complete:
|
||||||
|
|
||||||
|
```
|
||||||
|
<promise>MEMORY EVOLUTION COMPLETE</promise>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DO NOT
|
||||||
|
|
||||||
|
- Skip the per-iteration caveman preamble — it is part of the contract of this loop.
|
||||||
|
- Break zero-trust: never log / return plaintext user content in error paths. Relation `subject_label`/`object_label` ARE treated as identifiers — log OK. `notes_encrypted` never logged.
|
||||||
|
- Introduce A-Mem-style retroactive memory rewrites. Explicitly out of scope (strategy doc §3.3).
|
||||||
|
- Introduce AutoGPT-style reflective loops. Out of scope.
|
||||||
|
- Store format prefs or device-specific UI data in core memory — that's electron-store territory (see PROMPT-onboarding.md for precedent).
|
||||||
|
- Use Neo4j or any external graph DB — plain Postgres table is the spec.
|
||||||
|
- Call OpenAI embeddings for Free-tier users.
|
||||||
|
- Ship proactive mining (Phase 5) before Phase 3 (relational) is green — order matters.
|
||||||
|
- Delete user rows in `forget-all` — only memory rows.
|
||||||
|
- Let extraction pipeline or LLM normalization raise into the request path — always try/except, log, swallow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REFERENCE — Existing patterns to reuse
|
||||||
|
|
||||||
|
| Pattern | Source | Reuse for |
|
||||||
|
|---------|--------|-----------|
|
||||||
|
| Fernet per-user enc/dec | [api/app/core/memory_middleware.py](api/app/core/memory_middleware.py) `_get_fernet`, `_safe_decrypt` | New relational `notes_encrypted`, extraction writes |
|
||||||
|
| LLM factory | [api/app/core/llm.py](api/app/core/llm.py) `get_llm` | Extraction + normalization + proactive mining |
|
||||||
|
| Tier check | `api/app/billing/tier_manager.py` `has_feature` | All tier gates in this plan |
|
||||||
|
| Alembic async URL split | [api/alembic/env.py](api/alembic/env.py) | New migrations |
|
||||||
|
| tRPC procedure + authManager wrap | [adiuvAI/src/main/router/index.ts](adiuvAI/src/main/router/index.ts), [auth-manager.ts](adiuvAI/src/main/auth/auth-manager.ts) | 6 memory routes |
|
||||||
|
| Settings section pattern | [adiuvAI/src/renderer/components/settings/ProfileSection.tsx](adiuvAI/src/renderer/components/settings/ProfileSection.tsx) | MemorySection shape |
|
||||||
|
| shadcn table + drawer + confirm | Existing Settings sections | Memory tables + forget confirm |
|
||||||
|
| i18n labelKey pattern | See CLAUDE.md i18n section | All new strings |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CAVEMAN MODE REMINDER
|
||||||
|
|
||||||
|
This document's plan is executed **under caveman:caveman ultra**. Every iteration: activate the skill first, then work. Terse prose in all user-facing text emitted during the loop. Code + commit messages + migration SQL stay normal per caveman plugin boundaries.
|
||||||
|
|
||||||
|
If caveman plugin unavailable for any reason, STOP the iteration and report instead of proceeding in default mode — the loop contract requires it.
|
||||||
713
docs/PROMPT-onboarding.md
Normal file
713
docs/PROMPT-onboarding.md
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
# RALPH LOOP PROMPT — First-Run Onboarding Wizard
|
||||||
|
|
||||||
|
> **How to run:**
|
||||||
|
> ```
|
||||||
|
> /ralph-loop "Implement the onboarding wizard exactly as specified in docs/PROMPT-onboarding.md. Output <promise>ONBOARDING COMPLETE</promise> when all phases pass lint." --max-iterations 25 --completion-promise "ONBOARDING COMPLETE"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## INSTRUCTIONS FOR CLAUDE
|
||||||
|
|
||||||
|
You are implementing a first-run onboarding wizard for the adiuvAI Electron app. This is a **multi-file, multi-iteration** task. On each iteration:
|
||||||
|
|
||||||
|
1. **Read this file** in full.
|
||||||
|
2. **Inspect which tasks are already done** by checking if the target files exist and contain the expected code.
|
||||||
|
3. **Pick the next incomplete task** (always in phase order: Phase 1 → 2 → 3 → 4).
|
||||||
|
4. **Implement it**, then **run the relevant lint command** before exiting.
|
||||||
|
5. When ALL phases are complete AND both lint commands pass, output `<promise>ONBOARDING COMPLETE</promise>`.
|
||||||
|
|
||||||
|
**DO NOT** skip phases. **DO NOT** implement out of order — backend must exist before the FE can call it.
|
||||||
|
|
||||||
|
**LINT COMMANDS** (run after each phase):
|
||||||
|
- Backend: `cd api && ruff check . --fix`
|
||||||
|
- Frontend: `cd adiuvAI && npx eslint . --fix`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WHAT THIS FEATURE DOES
|
||||||
|
|
||||||
|
After login, new users see a chat-styled wizard that collects 5 fields:
|
||||||
|
- `job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`
|
||||||
|
|
||||||
|
These are stored encrypted in `MemoryCore` (backend) so the AI agents personalize responses. Three formatting prefs (`timezone`, `date_format`, `time_format`) are auto-detected from the OS and stored in electron-store (FE only) — the LLM never sees them. The FE formats all timestamp columns in tool-result rows before sending them back to the backend.
|
||||||
|
|
||||||
|
**Storage split:**
|
||||||
|
| Field | Where | Why |
|
||||||
|
|-------|-------|-----|
|
||||||
|
| job_role, industry, primary_use_case, tone_preference, language | `MemoryCore` (backend, encrypted) | LLM needs these for personalization |
|
||||||
|
| timezone, date_format, time_format | electron-store (FE) | FE formatter only — LLM must never see raw timestamps |
|
||||||
|
|
||||||
|
**Key architectural fact:** `memory_middleware.py` `enrich_context()` already injects `core_memory` into every orchestrator call. Writing to `MemoryCore` is sufficient — no system-prompt changes needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 1 — Backend (api/)
|
||||||
|
|
||||||
|
### TASK 1.1: Alembic migration — `onboarding_completed_at` column
|
||||||
|
|
||||||
|
**File:** `api/alembic/versions/XXX_add_onboarding_completed_at.py` (new)
|
||||||
|
|
||||||
|
Create a new Alembic migration that adds:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE users ADD COLUMN onboarding_completed_at TIMESTAMPTZ NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the existing migrations in `api/alembic/versions/` as a pattern reference. The revision ID should be sequential (check the latest existing migration number and increment).
|
||||||
|
|
||||||
|
**Done signal:** File exists in `api/alembic/versions/` with the column add.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.2: Add column to User model
|
||||||
|
|
||||||
|
**File:** `api/app/models.py`
|
||||||
|
|
||||||
|
Find the `User` class (around line 63-94). Add:
|
||||||
|
```python
|
||||||
|
onboarding_completed_at: Mapped[datetime | None] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=True, default=None
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `DateTime` from sqlalchemy if not already imported.
|
||||||
|
|
||||||
|
**Done signal:** `User` model has `onboarding_completed_at` field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.3: Extend UserProfile schema
|
||||||
|
|
||||||
|
**File:** `api/app/schemas.py`
|
||||||
|
|
||||||
|
Find `UserProfile` (around line 27-33). Add two fields:
|
||||||
|
```python
|
||||||
|
onboarding_completed_at: int | None = None # epoch ms, null = not onboarded
|
||||||
|
memory: dict[str, str] = Field(default_factory=dict) # decrypted core memory k/v
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** `UserProfile` has both new fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.4: Extend `get_current_user` to return memory + onboarding flag
|
||||||
|
|
||||||
|
**File:** `api/app/api/middleware/auth.py`
|
||||||
|
|
||||||
|
In `get_current_user()`, after fetching the user row and resolving the tier:
|
||||||
|
1. Read `user.onboarding_completed_at` — convert to epoch ms (int) or None.
|
||||||
|
2. Use `MemoryMiddleware(db).enrich_context(user.id)` to load decrypted core memory. Extract the `core` dict → `{label: value}` pairs.
|
||||||
|
3. Return `UserProfile(..., onboarding_completed_at=..., memory=...)`.
|
||||||
|
|
||||||
|
This requires `get_current_user` to also receive the `db: AsyncSession` dependency. Check if it already does — if not, add `Depends(get_session)`.
|
||||||
|
|
||||||
|
**Done signal:** `GET /api/v1/auth/me` returns `onboarding_completed_at` and `memory` fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.5: New route — `PUT /auth/me/memory`
|
||||||
|
|
||||||
|
**File:** `api/app/api/routes/auth.py`
|
||||||
|
|
||||||
|
Add a new route (do NOT modify `_UpdateProfileRequest`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class _UpdateMemoryRequest(BaseModel):
|
||||||
|
memory: dict[str, str] = Field(default_factory=dict)
|
||||||
|
mark_onboarded: bool = False
|
||||||
|
|
||||||
|
@router.put("/me/memory", response_model=UserProfile)
|
||||||
|
async def update_memory(
|
||||||
|
body: _UpdateMemoryRequest,
|
||||||
|
current_user: UserProfile = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_session),
|
||||||
|
) -> UserProfile:
|
||||||
|
mw = MemoryMiddleware(db)
|
||||||
|
for key, value in body.memory.items():
|
||||||
|
await mw.update_core(current_user.id, key, value)
|
||||||
|
if body.mark_onboarded:
|
||||||
|
result = await db.execute(select(User).where(User.id == current_user.id))
|
||||||
|
user = result.scalar_one()
|
||||||
|
user.onboarding_completed_at = datetime.now(timezone.utc)
|
||||||
|
await db.commit()
|
||||||
|
# Re-fetch profile and return
|
||||||
|
return await get_current_user(...) # use same logic as GET /me
|
||||||
|
```
|
||||||
|
|
||||||
|
Also add a companion route to reset onboarding (for "Re-run onboarding" in Settings):
|
||||||
|
```python
|
||||||
|
@router.post("/me/onboarding/reset")
|
||||||
|
async def reset_onboarding(
|
||||||
|
current_user: UserProfile = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
result = await db.execute(select(User).where(User.id == current_user.id))
|
||||||
|
user = result.scalar_one()
|
||||||
|
user.onboarding_completed_at = None
|
||||||
|
await db.commit()
|
||||||
|
return {"status": "reset"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** Both routes exist and are syntactically correct.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.6: New route — `POST /auth/onboarding/normalize`
|
||||||
|
|
||||||
|
**File:** `api/app/api/routes/auth.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class _NormalizeRequest(BaseModel):
|
||||||
|
inputs: dict[str, str] # {"job_role": "i build websites"}
|
||||||
|
|
||||||
|
class _NormalizeResponse(BaseModel):
|
||||||
|
normalized: dict[str, str]
|
||||||
|
|
||||||
|
@router.post("/onboarding/normalize", response_model=_NormalizeResponse)
|
||||||
|
async def normalize_onboarding(
|
||||||
|
body: _NormalizeRequest,
|
||||||
|
current_user: UserProfile = Depends(get_current_user),
|
||||||
|
) -> _NormalizeResponse:
|
||||||
|
"""One-shot LLM normalization for free-text onboarding answers."""
|
||||||
|
if not body.inputs:
|
||||||
|
return _NormalizeResponse(normalized={})
|
||||||
|
try:
|
||||||
|
llm = get_llm("gpt-4o-mini", temperature=0)
|
||||||
|
prompt = (
|
||||||
|
"You normalize user onboarding answers into clean, ≤3-word canonical labels.\n"
|
||||||
|
"Return a JSON object with the same keys and normalized values.\n"
|
||||||
|
"Examples: 'i build websites' → 'Web Developer', 'tech-ish stuff' → 'Technology'\n"
|
||||||
|
f"Input: {json.dumps(body.inputs)}"
|
||||||
|
)
|
||||||
|
response = await llm.ainvoke(
|
||||||
|
[{"role": "system", "content": "You normalize user inputs. Return JSON only."},
|
||||||
|
{"role": "user", "content": prompt}],
|
||||||
|
)
|
||||||
|
normalized = json.loads(response.content)
|
||||||
|
return _NormalizeResponse(normalized=normalized)
|
||||||
|
except Exception:
|
||||||
|
# LLM failure must never block onboarding — return inputs unchanged
|
||||||
|
return _NormalizeResponse(normalized=body.inputs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `get_llm` from `app.core.llm`. Use `json` stdlib. The `try/except` is critical — flaky LLM must never block the wizard.
|
||||||
|
|
||||||
|
**Done signal:** Route exists, has the safety try/except, returns inputs on failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 1.7: Backend lint check
|
||||||
|
|
||||||
|
Run: `cd api && ruff check . --fix`
|
||||||
|
|
||||||
|
Fix any issues before proceeding to Phase 2.
|
||||||
|
|
||||||
|
**Done signal:** `ruff check .` exits 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 2 — Electron Main Process (adiuvAI/src/main/)
|
||||||
|
|
||||||
|
### TASK 2.1: Extend `UserProfileSchema`
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/shared/api-types.ts`
|
||||||
|
|
||||||
|
Find `UserProfileSchema` (Zod schema). Add:
|
||||||
|
```ts
|
||||||
|
onboardingCompletedAt: z.number().int().nullable().optional(),
|
||||||
|
memory: z.record(z.string(), z.string()).default({}),
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** Schema has both fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.2: Add formatPrefs to electron-store
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/store.ts`
|
||||||
|
|
||||||
|
Extend the `AppSettings` interface:
|
||||||
|
```ts
|
||||||
|
formatPrefs: {
|
||||||
|
timezone: string;
|
||||||
|
dateFormat: string; // 'dd/MM/yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd'
|
||||||
|
timeFormat: '12h' | '24h';
|
||||||
|
} | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Default to `null` in the store defaults.
|
||||||
|
|
||||||
|
Add helpers:
|
||||||
|
```ts
|
||||||
|
export function getFormatPrefs(): FormatPrefs | null {
|
||||||
|
return getStore().get('formatPrefs', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setFormatPrefs(prefs: FormatPrefs): void {
|
||||||
|
getStore().set('formatPrefs', prefs);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Export `FormatPrefs` as a type.
|
||||||
|
|
||||||
|
**Done signal:** `getFormatPrefs()` and `setFormatPrefs()` exported from store.ts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.3: Create locale-defaults helper
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/auth/locale-defaults.ts` (new)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
|
export interface FormatPrefs {
|
||||||
|
timezone: string;
|
||||||
|
dateFormat: string;
|
||||||
|
timeFormat: '12h' | '24h';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectFormatPrefs(): FormatPrefs {
|
||||||
|
const locale = app.getLocale();
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const hour12 = Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12;
|
||||||
|
const timeFormat = hour12 ? '12h' : '24h';
|
||||||
|
const dateFormat = inferDateFormat(locale);
|
||||||
|
return { timezone, timeFormat, dateFormat };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectLanguage(): string {
|
||||||
|
return app.getLocale(); // e.g. 'it-IT', 'en-US'
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferDateFormat(locale: string): string {
|
||||||
|
// MDY locales
|
||||||
|
const mdyLocales = ['en-US', 'en-PH', 'en-BZ'];
|
||||||
|
if (mdyLocales.some(l => locale.startsWith(l))) return 'MM/dd/yyyy';
|
||||||
|
// YMD locales (CJK, ISO-oriented)
|
||||||
|
const ymdPrefixes = ['ja', 'zh', 'ko', 'hu', 'lt', 'sv', 'fi'];
|
||||||
|
if (ymdPrefixes.some(p => locale.startsWith(p))) return 'yyyy-MM-dd';
|
||||||
|
// Default: DMY (most of the world)
|
||||||
|
return 'dd/MM/yyyy';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** File exists with `detectFormatPrefs()` and `detectLanguage()` exported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.4: Create format-row helper
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/api/format-row.ts` (new)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { FormatPrefs } from '../auth/locale-defaults';
|
||||||
|
|
||||||
|
const TIMESTAMP_COLUMNS = new Set([
|
||||||
|
'createdAt', 'updatedAt', 'dueDate', 'date', 'endDate',
|
||||||
|
'lastRunAt', 'startedAt', 'completedAt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function formatRows<T extends Record<string, unknown>>(
|
||||||
|
rows: T[],
|
||||||
|
prefs: FormatPrefs,
|
||||||
|
): T[] {
|
||||||
|
return rows.map(row => formatRow(row, prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRow<T extends Record<string, unknown>>(
|
||||||
|
row: T,
|
||||||
|
prefs: FormatPrefs,
|
||||||
|
): T {
|
||||||
|
const result = { ...row };
|
||||||
|
for (const col of TIMESTAMP_COLUMNS) {
|
||||||
|
if (col in result && typeof result[col] === 'number') {
|
||||||
|
(result as Record<string, unknown>)[col] = formatTimestamp(
|
||||||
|
result[col] as number,
|
||||||
|
prefs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(epochMs: number, prefs: FormatPrefs): string {
|
||||||
|
const date = new Date(epochMs);
|
||||||
|
const hour12 = prefs.timeFormat === '12h';
|
||||||
|
|
||||||
|
const opts: Intl.DateTimeFormatOptions = {
|
||||||
|
timeZone: prefs.timezone,
|
||||||
|
hour12,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
};
|
||||||
|
|
||||||
|
const timePart = date.toLocaleTimeString('en-US', opts);
|
||||||
|
|
||||||
|
const day = date.toLocaleDateString('en-CA', { timeZone: prefs.timezone, day: '2-digit' }).slice(-2);
|
||||||
|
const month = date.toLocaleDateString('en-CA', { timeZone: prefs.timezone, month: '2-digit' }).slice(-2);
|
||||||
|
const year = date.toLocaleDateString('en-CA', { timeZone: prefs.timezone, year: 'numeric' }).slice(0, 4);
|
||||||
|
|
||||||
|
let datePart: string;
|
||||||
|
switch (prefs.dateFormat) {
|
||||||
|
case 'MM/dd/yyyy': datePart = `${month}/${day}/${year}`; break;
|
||||||
|
case 'yyyy-MM-dd': datePart = `${year}-${month}-${day}`; break;
|
||||||
|
default: datePart = `${day}/${month}/${year}`; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${datePart} ${timePart}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** File exists with `formatRow` and `formatRows` exported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.5: Wire format-row into drizzle-executor
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/api/drizzle-executor.ts`
|
||||||
|
|
||||||
|
Import `formatRows`, `formatRow` from `./format-row` and `getFormatPrefs` from `../store` and `detectFormatPrefs` from `../auth/locale-defaults`.
|
||||||
|
|
||||||
|
Find every place that returns `{ rows }` or `{ row }` results. Wrap them:
|
||||||
|
- `rows` → `formatRows(rows, getFormatPrefs() ?? detectFormatPrefs())`
|
||||||
|
- `row` → `formatRow(row, getFormatPrefs() ?? detectFormatPrefs())`
|
||||||
|
|
||||||
|
The `?? detectFormatPrefs()` fallback handles the edge case where executor runs before first auth.status seed.
|
||||||
|
|
||||||
|
**Important:** Only format on `handleList`, `handleGet`, `handleInsert`, `handleUpdate` return paths — NOT on delete. Do not mutate the original rows — `formatRow` returns a new object.
|
||||||
|
|
||||||
|
**Done signal:** All select/get/insert/update returns pass through formatRow(s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.6: Add auth methods to auth-manager
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/auth/auth-manager.ts`
|
||||||
|
|
||||||
|
Add two methods to the `AuthManager` class:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async updateMemory(
|
||||||
|
memory: Record<string, string>,
|
||||||
|
markOnboarded = false,
|
||||||
|
): Promise<UserProfile> {
|
||||||
|
return this.put('/api/v1/auth/me/memory', {
|
||||||
|
memory,
|
||||||
|
mark_onboarded: markOnboarded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async normalizeOnboarding(
|
||||||
|
inputs: Record<string, string>,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const res = await this.post('/api/v1/auth/onboarding/normalize', { inputs });
|
||||||
|
return res.normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetOnboarding(): Promise<void> {
|
||||||
|
await this.post('/api/v1/auth/me/onboarding/reset', {});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the existing `this.put()` / `this.post()` helpers (they handle auth headers and camelCase/snakeCase conversion).
|
||||||
|
|
||||||
|
**Done signal:** All three methods exist on AuthManager.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.7: Extend tRPC authRouter
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/router/index.ts`
|
||||||
|
|
||||||
|
In the `authRouter`, add these procedures:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
updateMemory: t.procedure
|
||||||
|
.input(z.object({
|
||||||
|
memory: z.record(z.string(), z.string()),
|
||||||
|
markOnboarded: z.boolean().optional().default(false),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return authManager.updateMemory(input.memory, input.markOnboarded);
|
||||||
|
}),
|
||||||
|
|
||||||
|
normalizeOnboarding: t.procedure
|
||||||
|
.input(z.object({
|
||||||
|
inputs: z.record(z.string(), z.string()),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return authManager.normalizeOnboarding(input.inputs);
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetOnboarding: t.procedure
|
||||||
|
.mutation(async () => {
|
||||||
|
return authManager.resetOnboarding();
|
||||||
|
}),
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, in the existing `auth.status` procedure, add **silent auto-seeding** logic:
|
||||||
|
- After fetching the profile, if `getFormatPrefs()` returns null → call `setFormatPrefs(detectFormatPrefs())`.
|
||||||
|
- If `profile.memory.language` is missing/empty → call `authManager.updateMemory({ language: detectLanguage() })` silently (fire-and-forget, don't block the return).
|
||||||
|
|
||||||
|
**Done signal:** Three new procedures exist. Status procedure auto-seeds format prefs and language.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.8: Add settings routes for formatPrefs
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/main/router/index.ts`
|
||||||
|
|
||||||
|
In `settingsRouter` (or create one if it doesn't exist), add:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
getFormatPrefs: t.procedure.query(() => {
|
||||||
|
return getFormatPrefs();
|
||||||
|
}),
|
||||||
|
|
||||||
|
setFormatPrefs: t.procedure
|
||||||
|
.input(z.object({
|
||||||
|
timezone: z.string(),
|
||||||
|
dateFormat: z.string(),
|
||||||
|
timeFormat: z.enum(['12h', '24h']),
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
setFormatPrefs(input);
|
||||||
|
return input;
|
||||||
|
}),
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** Both procedures exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 2.9: Frontend lint check
|
||||||
|
|
||||||
|
Run: `cd adiuvAI && npx eslint . --fix`
|
||||||
|
|
||||||
|
Fix any TypeScript/ESLint issues before proceeding to Phase 3.
|
||||||
|
|
||||||
|
**Done signal:** ESLint exits 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 3 — Electron Renderer (adiuvAI/src/renderer/)
|
||||||
|
|
||||||
|
### TASK 3.1: Create onboarding chip options
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/components/onboarding/onboardingOptions.ts` (new)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const JOB_ROLES = ['Developer', 'Designer', 'Consultant', 'Founder', 'Project Manager'] as const;
|
||||||
|
export const INDUSTRIES = ['Tech', 'Design', 'Consulting', 'Legal', 'Marketing', 'Education'] as const;
|
||||||
|
export const USE_CASES = ['Solo freelancer', 'Client manager', 'Team lead', 'Personal productivity'] as const;
|
||||||
|
export const TONES = ['Casual', 'Formal', 'Concise', 'Detailed'] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** File exists with all four arrays exported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.2: Create OnboardingFlow component
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/components/onboarding/OnboardingFlow.tsx` (new)
|
||||||
|
|
||||||
|
This is the most complex file. Key requirements:
|
||||||
|
|
||||||
|
**State machine:**
|
||||||
|
```ts
|
||||||
|
type Step = 'welcome' | 'jobRole' | 'industry' | 'useCase' | 'tone' | 'language' | 'reviewing' | 'done';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
```ts
|
||||||
|
interface OnboardingFlowProps {
|
||||||
|
profile: UserProfile;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Visual style — must match AIChatPanel:**
|
||||||
|
- Chat bubble layout: AI messages in `rounded-2xl` bubbles with a Sparkles icon (from lucide-react).
|
||||||
|
- Use glassmorphism: `bg-white/5 backdrop-blur-md border border-white/10`.
|
||||||
|
- Spring transitions (framer-motion) for each step entering.
|
||||||
|
- Use shadcn components: `Button`, `Input`, `Card`.
|
||||||
|
|
||||||
|
**Each wizard step shows:**
|
||||||
|
1. An "AI bubble" with the question text.
|
||||||
|
2. 3–6 preset chip buttons (from onboardingOptions.ts).
|
||||||
|
3. An optional "Type your own" text input (for `job_role` and `industry` only).
|
||||||
|
4. A "Skip" link at the bottom.
|
||||||
|
5. Previous answers appear above as "user bubbles" (right-aligned).
|
||||||
|
|
||||||
|
**Step details:**
|
||||||
|
| Step | Question | Chips | Free text? |
|
||||||
|
|------|----------|-------|-----------|
|
||||||
|
| welcome | "Hi {name}! I'm your AI assistant. Let me learn a few things about you so I can help better." | Just a "Let's go" button | No |
|
||||||
|
| jobRole | "What's your role?" | JOB_ROLES | Yes |
|
||||||
|
| industry | "What industry do you work in?" | INDUSTRIES | Yes |
|
||||||
|
| useCase | "How will you mainly use adiuvAI?" | USE_CASES | No |
|
||||||
|
| tone | "How should I talk to you?" | TONES | No |
|
||||||
|
| language | "I'll respond in {detected}. Want to change it?" | Show detected language pre-selected; allow typing a different one | Yes |
|
||||||
|
| reviewing | Review screen (see below) | — | — |
|
||||||
|
| done | Redirect (never renders) | — | — |
|
||||||
|
|
||||||
|
**Reviewing step logic:**
|
||||||
|
1. Partition answers: chip selections (already clean) vs free-text answers (need normalization).
|
||||||
|
2. If free-text map is non-empty → call `trpc.auth.normalizeOnboarding.useMutation`. Show "Tidying up…" spinner on those fields only.
|
||||||
|
3. Show a Card titled "Here's what I'll save" with all 5 fields as rows.
|
||||||
|
4. Each row has an Edit pencil icon → converts to inline input → Enter saves → back to read-only.
|
||||||
|
5. If LLM changed a value, show grey hint: `auto-tidied from "original text"`.
|
||||||
|
6. Primary button: "Looks good — save" → calls `trpc.auth.updateMemory.useMutation({ memory: finalMap, markOnboarded: true })` → `utils.auth.status.invalidate()`.
|
||||||
|
7. Secondary link: "Back to wizard" → resets to `jobRole` step with values pre-filled.
|
||||||
|
8. **Failure modes:**
|
||||||
|
- Normalization fails → show raw values + banner "Couldn't auto-tidy — review and save". Save still works.
|
||||||
|
- Save fails → toast error, stay on review screen.
|
||||||
|
|
||||||
|
**Skip behavior:** Clicking Skip on any step → calls `updateMemory({}, markOnboarded: true)` with empty map → wizard closes. Language was already auto-seeded by `auth.status`.
|
||||||
|
|
||||||
|
**Done signal:** Component file exists, renders a multi-step wizard, handles reviewing + save.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.3: Gate OnboardingFlow in AppShell
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/components/layout/AppShell.tsx`
|
||||||
|
|
||||||
|
After the `authenticated === false` → `LoginForm` branch, add:
|
||||||
|
```tsx
|
||||||
|
if (
|
||||||
|
authStatusQuery.data?.profile &&
|
||||||
|
authStatusQuery.data.profile.onboardingCompletedAt == null
|
||||||
|
) {
|
||||||
|
return <OnboardingFlow profile={authStatusQuery.data.profile} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `OnboardingFlow` from `../onboarding/OnboardingFlow`.
|
||||||
|
|
||||||
|
**Done signal:** AppShell conditionally renders OnboardingFlow when onboardingCompletedAt is null.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.4: Add 'profile' to settings sections
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/components/settings/types.ts`
|
||||||
|
|
||||||
|
Add `'profile'` to the `SectionId` type and `{ id: 'profile', label: 'Profile' }` to `SECTIONS` array — insert it before `'account'`.
|
||||||
|
|
||||||
|
**Done signal:** SECTIONS includes 'profile' as the first or second entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.5: Create ProfileSection component
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/components/settings/ProfileSection.tsx` (new)
|
||||||
|
|
||||||
|
Plain form (no chat aesthetic — this is Settings, not the wizard). Two cards:
|
||||||
|
|
||||||
|
**Card 1 — "About you"** (writes to MemoryCore via `auth.updateMemory`):
|
||||||
|
- Fields: job_role, industry, primary_use_case (select from USE_CASES), tone_preference (select from TONES), language (text input).
|
||||||
|
- Pre-populate from `authStatusQuery.data.profile.memory`.
|
||||||
|
- Save button → `trpc.auth.updateMemory.useMutation`.
|
||||||
|
- "Re-run onboarding" button → `trpc.auth.resetOnboarding.useMutation` → `utils.auth.status.invalidate()` (triggers wizard via AppShell gate).
|
||||||
|
|
||||||
|
**Card 2 — "Display preferences"** (writes to electron-store via `trpc.settings.setFormatPrefs`):
|
||||||
|
- Timezone: searchable select, populated from `Intl.supportedValuesOf('timeZone')`.
|
||||||
|
- Date format: select with options: `dd/MM/yyyy`, `MM/dd/yyyy`, `yyyy-MM-dd`.
|
||||||
|
- Time format: radio group — 12h / 24h.
|
||||||
|
- Pre-populate from `trpc.settings.getFormatPrefs` query.
|
||||||
|
- Save button → `trpc.settings.setFormatPrefs.useMutation`.
|
||||||
|
|
||||||
|
Use shadcn `Card`, `Input`, `Select`, `Button`, `Label`, `RadioGroup` components.
|
||||||
|
|
||||||
|
**Done signal:** Component exists with both cards, save functionality wired.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.6: Wire ProfileSection into settings route
|
||||||
|
|
||||||
|
**File:** `adiuvAI/src/renderer/routes/settings.tsx`
|
||||||
|
|
||||||
|
Add the import and the conditional render:
|
||||||
|
```tsx
|
||||||
|
{section === 'profile' && <ProfileSection />}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** ProfileSection renders when section=profile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TASK 3.7: Final lint check
|
||||||
|
|
||||||
|
Run both:
|
||||||
|
```bash
|
||||||
|
cd api && ruff check . --fix
|
||||||
|
cd adiuvAI && npx eslint . --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done signal:** Both exit 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE 4 — Completion
|
||||||
|
|
||||||
|
### TASK 4.1: Verify all files exist
|
||||||
|
|
||||||
|
Check that these files exist:
|
||||||
|
- [ ] `api/alembic/versions/*_add_onboarding_completed_at.py`
|
||||||
|
- [ ] `adiuvAI/src/main/auth/locale-defaults.ts`
|
||||||
|
- [ ] `adiuvAI/src/main/api/format-row.ts`
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/onboarding/onboardingOptions.ts`
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/onboarding/OnboardingFlow.tsx`
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/settings/ProfileSection.tsx`
|
||||||
|
|
||||||
|
Check that these files were modified:
|
||||||
|
- [ ] `api/app/models.py` (has `onboarding_completed_at`)
|
||||||
|
- [ ] `api/app/schemas.py` (UserProfile has `memory` + `onboarding_completed_at`)
|
||||||
|
- [ ] `api/app/api/middleware/auth.py` (get_current_user returns memory)
|
||||||
|
- [ ] `api/app/api/routes/auth.py` (3 new routes)
|
||||||
|
- [ ] `adiuvAI/src/shared/api-types.ts` (UserProfileSchema extended)
|
||||||
|
- [ ] `adiuvAI/src/main/store.ts` (formatPrefs + helpers)
|
||||||
|
- [ ] `adiuvAI/src/main/auth/auth-manager.ts` (3 new methods)
|
||||||
|
- [ ] `adiuvAI/src/main/router/index.ts` (5 new procedures + auto-seed in status)
|
||||||
|
- [ ] `adiuvAI/src/main/api/drizzle-executor.ts` (formatRow wiring)
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/layout/AppShell.tsx` (onboarding gate)
|
||||||
|
- [ ] `adiuvAI/src/renderer/components/settings/types.ts` (profile section)
|
||||||
|
- [ ] `adiuvAI/src/renderer/routes/settings.tsx` (ProfileSection render)
|
||||||
|
|
||||||
|
### TASK 4.2: Output completion promise
|
||||||
|
|
||||||
|
If everything above is done and lint passes:
|
||||||
|
|
||||||
|
```
|
||||||
|
<promise>ONBOARDING COMPLETE</promise>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REFERENCE — Existing patterns to reuse
|
||||||
|
|
||||||
|
**DO NOT reinvent these. Copy their patterns:**
|
||||||
|
|
||||||
|
| Pattern | Source file | Reuse for |
|
||||||
|
|---------|-----------|-----------|
|
||||||
|
| Chat bubble + Sparkles + glass | `src/renderer/components/ai/AIChatPanel.tsx` | OnboardingFlow bubbles |
|
||||||
|
| Stepper state machine | `InlineAgentCreationStepper` in renderer | Wizard step transitions |
|
||||||
|
| MemoryMiddleware.update_core | `api/app/core/memory_middleware.py:137-173` | PUT /me/memory route |
|
||||||
|
| get_llm() | `api/app/core/llm.py` | Normalize route |
|
||||||
|
| electron-store helpers | `src/main/store.ts` (getDeviceId pattern) | getFormatPrefs/setFormatPrefs |
|
||||||
|
| tRPC procedure pattern | `src/main/router/index.ts` (auth.status) | New procedures |
|
||||||
|
| shadcn form components | Existing settings sections | ProfileSection |
|
||||||
|
| toCamelCase / toSnakeCase | `auth-manager.ts` proxy helpers | Automatic key conversion |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DO NOT
|
||||||
|
|
||||||
|
- Add features not described here (no avatar upload, no i18n framework, no animation library beyond framer-motion if already installed).
|
||||||
|
- Modify the orchestrator or system prompts — MemoryCore injection is already handled.
|
||||||
|
- Add foreign key constraints to the migration.
|
||||||
|
- Store formatting prefs in MemoryCore.
|
||||||
|
- Let the LLM normalization route throw on failure — it MUST return inputs unchanged.
|
||||||
|
- Skip the reviewing step in the wizard.
|
||||||
|
- Run both lint checks and fix issues before claiming completion.
|
||||||
212
docs/PROMPT-sonner-notifications.md
Normal file
212
docs/PROMPT-sonner-notifications.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# Sonner Global Notification System — Ralph Loop Prompt
|
||||||
|
|
||||||
|
You are implementing a global toast notification system in the adiuvAI Electron app using shadcn's sonner component.
|
||||||
|
|
||||||
|
**Full plan:** Read `docs/plan-sonner-notifications.md` for the complete architecture, file list, i18n keys, and categorization of every mutation.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Always read the plan first** at `docs/plan-sonner-notifications.md` before doing any work.
|
||||||
|
- **Always read a file before editing it.** Never edit blind.
|
||||||
|
- **One phase per iteration.** Complete one phase fully, verify it compiles, then move on.
|
||||||
|
- **Run `cd adiuvAI && npx tsc --noEmit` after each phase** to catch type errors early.
|
||||||
|
- **Run `cd adiuvAI && npm run lint` after Phase 3 and Phase 4** to catch lint issues.
|
||||||
|
- **Commit after each phase** with a descriptive message.
|
||||||
|
- **i18n: add keys to ALL 5 language files** (`en`, `it`, `es`, `fr`, `de`). The plan has complete translations for each.
|
||||||
|
- **Do NOT touch silent mutations** (note auto-save, kanban drag, sidebar toggle, AI chat/streaming). For these, add `onError` only if missing.
|
||||||
|
- **When removing `saved`/`setSaved` state patterns:** also remove the `setTimeout`, the button text ternary, and any `setSaved(false)` in `onChange` handlers. Replace button text with `{t('common.save')}`.
|
||||||
|
- **Import path for useNotify:** `import { useNotify } from '@/hooks/useNotify';`
|
||||||
|
- **Import path for toast (direct):** `import { toast } from 'sonner';` (only in useNotify.ts itself)
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
Check the state of the codebase to determine which phase to work on:
|
||||||
|
|
||||||
|
1. **If `src/renderer/components/ui/sonner.tsx` does NOT exist** → Start Phase 1
|
||||||
|
2. **If `sonner.tsx` exists but settings components still have `setSaved`** → Do Phase 2
|
||||||
|
3. **If settings are done but CRUD components lack `useNotify`** → Do Phase 3
|
||||||
|
4. **If CRUD is done but auth/onboarding lack `useNotify`** → Do Phase 4
|
||||||
|
5. **If all phases are done and `npx tsc --noEmit` + `npm run lint` pass** → Do Phase 5 (verification)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Foundation
|
||||||
|
|
||||||
|
### Step 1: Install sonner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx shadcn@latest add sonner --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Fix theme import in generated `sonner.tsx`
|
||||||
|
|
||||||
|
The generated file imports `useTheme` from `next-themes`. This app does NOT use next-themes. Fix the import:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// WRONG (generated):
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
// CORRECT:
|
||||||
|
import { useTheme } from "@/components/theme-provider"
|
||||||
|
```
|
||||||
|
|
||||||
|
Also set `position="bottom-right"` and add `richColors` on the `<Sonner>` component.
|
||||||
|
|
||||||
|
### Step 3: Add `<Toaster />` to `src/renderer/index.tsx`
|
||||||
|
|
||||||
|
Import `Toaster` from `@/components/ui/sonner` and render it as the last child inside `<QueryClientProvider>`, AFTER `<RouterProvider />`. This ensures toasts work during login, onboarding, AND normal app usage.
|
||||||
|
|
||||||
|
### Step 4: Create `src/renderer/hooks/useNotify.ts`
|
||||||
|
|
||||||
|
Create the hook exactly as specified in the plan (Section 1.4). The hook exports `{ notify, notifyError, notifyPromise }`.
|
||||||
|
|
||||||
|
### Step 5: Add i18n keys to all 5 translation files
|
||||||
|
|
||||||
|
Add the `"toast"` top-level key with all sub-keys to:
|
||||||
|
- `src/renderer/locales/en/translation.json` (English — from plan)
|
||||||
|
- `src/renderer/locales/it/translation.json` (Italian — from plan)
|
||||||
|
- `src/renderer/locales/es/translation.json` (Spanish — from plan)
|
||||||
|
- `src/renderer/locales/fr/translation.json` (French — from plan)
|
||||||
|
- `src/renderer/locales/de/translation.json` (German — from plan)
|
||||||
|
|
||||||
|
### Step 6: Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && git add -A && git commit -m "feat(notifications): add sonner toast foundation with useNotify hook and i18n keys"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Settings Components
|
||||||
|
|
||||||
|
For each of these 5 files, apply the pattern: add `useNotify()`, remove `saved`/`setSaved` state, remove `setTimeout`, replace button text ternary, add `notify()` in `onSuccess`, add `notifyError()` in `onError`.
|
||||||
|
|
||||||
|
### Files (in order):
|
||||||
|
|
||||||
|
1. **`src/renderer/components/settings/GeneralSection.tsx`**
|
||||||
|
- Remove: `saved`, `setSaved`, `error`, `setError`, `setTimeout`, inline `<p>` error, `setSaved(false)` in onChange
|
||||||
|
- Add: `notify('success', 'toast.profile.updated')` in handleSave onSuccess
|
||||||
|
- Add: `notifyError('toast.profile.updateError', err)` in handleSave onError
|
||||||
|
- Add: `notify('info', 'toast.settings.languageChanged')` in handleLanguageChange
|
||||||
|
- Button text: `{t('common.save')}`
|
||||||
|
|
||||||
|
2. **`src/renderer/components/settings/ProfileSection.tsx`**
|
||||||
|
- Remove: `profileSaved`, `displaySaved` states and their `setTimeout`s
|
||||||
|
- Profile save → `notify('success', 'toast.settings.memorySaved')`
|
||||||
|
- Display save → `notify('success', 'toast.settings.formatPrefsSaved')`
|
||||||
|
- Reset onboarding → `notify('info', 'toast.onboarding.reset')`
|
||||||
|
|
||||||
|
3. **`src/renderer/components/settings/AccountSection.tsx`**
|
||||||
|
- Remove: `urlSaved`, `setUrlSaved` state and `setTimeout`
|
||||||
|
- Backend URL save → `notify('success', 'toast.settings.backendUrlSaved')`
|
||||||
|
- Add onError → `notifyError('toast.settings.backendUrlError', err)`
|
||||||
|
- Logout → `notify('info', 'toast.auth.loggedOut')`
|
||||||
|
|
||||||
|
4. **`src/renderer/components/settings/LocalAgentConfigPanel.tsx`**
|
||||||
|
- Remove: `saved` state and `setTimeout`
|
||||||
|
- Save → `notify('success', 'toast.agent.updated')`
|
||||||
|
- Add onError → `notifyError('toast.agent.updateError', err)`
|
||||||
|
|
||||||
|
5. **`src/renderer/components/settings/CloudAgentConfigPanel.tsx`**
|
||||||
|
- Same as LocalAgentConfigPanel
|
||||||
|
|
||||||
|
### Verify and Commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx tsc --noEmit && npm run lint
|
||||||
|
cd adiuvAI && git add -A && git commit -m "feat(notifications): replace settings saved-state patterns with sonner toasts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: CRUD Operations
|
||||||
|
|
||||||
|
Add `useNotify()` to each component and wire `notify` / `notifyError` into existing `onSuccess` / `onError` callbacks. If `onError` doesn't exist, add it.
|
||||||
|
|
||||||
|
### Files and mutations:
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- `src/renderer/components/tasks/NewTaskDialog.tsx` → `tasks.create`: success `toast.task.created` + inline `clients.create`: success `toast.client.created`
|
||||||
|
- `src/renderer/components/tasks/EditTaskDialog.tsx` → `tasks.update`: success `toast.task.updated`
|
||||||
|
- `src/renderer/components/tasks/TaskDetailDialog.tsx` → `taskComments.create`: success `toast.comment.created`, `taskComments.delete`: warning `toast.comment.deleted`
|
||||||
|
- `src/renderer/routes/tasks.tsx` → `tasks.delete`: warning `toast.task.deleted`, `tasks.update` status toggle: **onError only**
|
||||||
|
- `src/renderer/components/projects/KanbanBoard.tsx` → `tasks.update` drag: **onError only**, `tasks.delete`: warning `toast.task.deleted`
|
||||||
|
- `src/renderer/components/projects/ProjectDetail.tsx` → `tasks.delete`: warning `toast.task.deleted`, `tasks.update` toggle: **onError only**, `notes.create`: success `toast.note.created`
|
||||||
|
- `src/renderer/components/ai/blocks/ChatEntityBlock.tsx` → `tasks.delete`: warning `toast.task.deleted`, `tasks.update` toggle: **onError only**
|
||||||
|
|
||||||
|
**Projects & Clients:**
|
||||||
|
- `src/renderer/components/projects/ProjectSidebar.tsx` → `projects.create`: success, `projects.update`: success, `projects.delete`: warning, `projects.archiveByClient`: warning (check if archiving or unarchiving), `clients.create`: success, `clients.update`: success, `clients.deleteWithCascade`: warning
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `src/renderer/routes/notes.$noteId.tsx` → `notes.delete`: warning `toast.note.deleted`, `notes.update` auto-save: **onError only** (SILENT)
|
||||||
|
|
||||||
|
**Timeline:**
|
||||||
|
- `src/renderer/components/timeline/AddEventDialog.tsx` → `timelineEvents.create`: success
|
||||||
|
- `src/renderer/components/timeline/EditEventDialog.tsx` → `timelineEvents.update`: success
|
||||||
|
- `src/renderer/routes/timeline.tsx` → `timelineEvents.delete`: warning, `timelineEvents.update`: success
|
||||||
|
|
||||||
|
**Agents:**
|
||||||
|
- `src/renderer/components/settings/AgentsSection.tsx` → `agent.*.delete`: warning, `agent.*.update` toggle: **onError only**, `agent.runNow`: use `notifyPromise`
|
||||||
|
- `src/renderer/components/settings/InlineAgentCreationStepper.tsx` → `agent.*.create`: success
|
||||||
|
|
||||||
|
### Verify and Commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx tsc --noEmit && npm run lint
|
||||||
|
cd adiuvAI && git add -A && git commit -m "feat(notifications): add sonner toasts to all CRUD operations"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Auth + Onboarding
|
||||||
|
|
||||||
|
### Files:
|
||||||
|
|
||||||
|
1. **`src/renderer/components/auth/LoginForm.tsx`**
|
||||||
|
- `auth.login` onError → `notifyError('toast.auth.loginError', err)` (KEEP inline error too)
|
||||||
|
- `auth.register` onError → `notifyError('toast.auth.registerError', err)` (KEEP inline error too)
|
||||||
|
- `auth.loginWithOAuth` onError → `notifyError('toast.auth.oauthError', err)`
|
||||||
|
|
||||||
|
2. **`src/renderer/components/layout/AppShell.tsx`**
|
||||||
|
- `auth.logout` onSuccess → `notify('info', 'toast.auth.loggedOut')` (add before `utils.auth.status.invalidate()`)
|
||||||
|
|
||||||
|
3. **`src/renderer/components/onboarding/OnboardingFlow.tsx`**
|
||||||
|
- Final save onSuccess → `notify('success', 'toast.onboarding.completed', { descriptionKey: 'toast.onboarding.completedDescription' })`
|
||||||
|
- Final save onError → `notifyError('toast.onboarding.error', err)`
|
||||||
|
- Normalize call → use `notifyPromise` with loading/success/error keys
|
||||||
|
|
||||||
|
### Verify and Commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx tsc --noEmit && npm run lint
|
||||||
|
cd adiuvAI && git add -A && git commit -m "feat(notifications): add sonner toasts to auth and onboarding flows"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Final Verification
|
||||||
|
|
||||||
|
Run these checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx tsc --noEmit
|
||||||
|
cd adiuvAI && npm run lint
|
||||||
|
cd adiuvAI && npm run knip
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
- No remaining `setSaved` or `setTimeout.*setSaved` patterns in `src/renderer/components/settings/`
|
||||||
|
- All 5 translation files have the `toast` key with matching sub-keys
|
||||||
|
- `sonner.tsx` imports from `@/components/theme-provider` (NOT `next-themes`)
|
||||||
|
- `<Toaster />` renders in `index.tsx` inside `<ThemeProvider>`
|
||||||
|
- `useNotify.ts` exists in `src/renderer/hooks/`
|
||||||
|
|
||||||
|
If everything passes:
|
||||||
|
|
||||||
|
<promise>SONNER NOTIFICATIONS COMPLETE</promise>
|
||||||
BIN
docs/Task.jpg
Normal file
BIN
docs/Task.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/adiuvAI-pitch.pptx
Normal file
BIN
docs/adiuvAI-pitch.pptx
Normal file
Binary file not shown.
BIN
docs/adiuvAI.pptx
Normal file
BIN
docs/adiuvAI.pptx
Normal file
Binary file not shown.
191
docs/agent-runner-sequence.html
Normal file
191
docs/agent-runner-sequence.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Agent Runner — UML Sequence Diagram</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&display=swap');
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #0f1117;
|
||||||
|
color: #c9cdd8;
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #e4e7ef;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #5b6078;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #3d4156;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Agent Runner — Batch File Processing</h1>
|
||||||
|
<p class="subtitle">UML Sequence · Electron FE ↔ FastAPI BE ↔ LLM</p>
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
|
||||||
|
participant FE as Electron FE<br/>(AgentScheduler · DrizzleExecutor)
|
||||||
|
participant BE as FastAPI BE<br/>(AgentRunner · routes/agents)
|
||||||
|
participant LLM as LLM<br/>(gpt-4.1 via LiteLLM)
|
||||||
|
|
||||||
|
note over FE: AgentScheduler cron tick<br/>or manual "Run Now"
|
||||||
|
|
||||||
|
rect rgb(30, 33, 48)
|
||||||
|
note right of FE: PHASE 1 — TRIGGER
|
||||||
|
FE->>+BE: POST /agents/trigger<br/>(agent config, directories, schedule)
|
||||||
|
BE->>BE: billing check · concurrency guard<br/>create AgentRunLog (status=running)
|
||||||
|
BE-->>-FE: 202 Accepted { run_id }
|
||||||
|
note over FE: store runId in local agentRuns
|
||||||
|
note over BE: asyncio.create_task(run_local_agent)<br/>fire-and-forget background task
|
||||||
|
end
|
||||||
|
|
||||||
|
rect rgb(25, 37, 35)
|
||||||
|
note right of FE: PHASE 2 — DIRECTORY SCAN (via WS → FE filesystem)
|
||||||
|
loop For each directory in agent config (max depth=5)
|
||||||
|
BE->>FE: WS tool_call: list_directory { path }
|
||||||
|
FE->>FE: fs.readdir(path)
|
||||||
|
FE-->>BE: WS tool_result: { entries[] }
|
||||||
|
BE->>FE: WS tool_call: get_file_metadata { path }
|
||||||
|
FE->>FE: fs.stat(file)
|
||||||
|
FE-->>BE: WS tool_result: { modifiedAt, size }
|
||||||
|
note over BE: Skip if modifiedAt ≤ last_run_at<br/>(first run: last_run_at=null → all pass)
|
||||||
|
end
|
||||||
|
note over BE: Result: file_list[] — paths passing<br/>extension + date filters (e.g., 22 files)
|
||||||
|
end
|
||||||
|
|
||||||
|
BE->>FE: WS tool_call: select { table: "projects" }
|
||||||
|
FE-->>BE: WS tool_result: { rows[] }
|
||||||
|
note over BE: Cache project list for prompt context
|
||||||
|
|
||||||
|
rect rgb(35, 30, 22)
|
||||||
|
note right of FE: PHASE 3+4 — FOR EACH FILE: READ → PREPROCESS → LLM
|
||||||
|
loop For each file in file_list (sequential)
|
||||||
|
|
||||||
|
rect rgb(40, 35, 25)
|
||||||
|
note over BE: Phase 3 — Read + Preprocess
|
||||||
|
BE->>FE: WS tool_call: read_file_content { path }
|
||||||
|
FE->>FE: fs.readFile(path)
|
||||||
|
FE-->>BE: WS tool_result: { content }
|
||||||
|
BE->>BE: detect_content_type(filename, content)<br/>preprocess() → clean text + metadata<br/>(Python only, zero LLM calls)
|
||||||
|
end
|
||||||
|
|
||||||
|
rect rgb(30, 28, 42)
|
||||||
|
note over BE: Phase 4 — LLM Agent Tool Loop
|
||||||
|
BE->>+LLM: system prompt + clean text<br/>+ available tools + project list
|
||||||
|
loop Max 12 tool-call iterations
|
||||||
|
LLM-->>BE: tool_call (e.g., list_tasks, create_note)
|
||||||
|
BE->>FE: WS tool_call: select/insert/update<br/>{ table, data }
|
||||||
|
FE->>FE: DrizzleExecutor<br/>local SQLite CRUD
|
||||||
|
FE-->>BE: WS tool_result: { rows }
|
||||||
|
BE->>LLM: tool_result → continue
|
||||||
|
end
|
||||||
|
LLM-->>-BE: final text response (no more tool_calls)
|
||||||
|
end
|
||||||
|
|
||||||
|
note over BE: log: file processed, created=N entities
|
||||||
|
opt If create_project was called
|
||||||
|
BE->>FE: WS tool_call: select { table: "projects" }
|
||||||
|
FE-->>BE: WS tool_result: { rows[] }
|
||||||
|
note over BE: Refresh project list cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rect rgb(22, 35, 32)
|
||||||
|
note right of FE: PHASE 5 — COMPLETION
|
||||||
|
BE->>BE: _finalize_run()<br/>update AgentRunLog in PostgreSQL<br/>status = success | partial | error
|
||||||
|
BE->>FE: WS run_complete: { run_id, status }
|
||||||
|
FE->>FE: update local agentRuns table<br/>{ status, completedAt }
|
||||||
|
end
|
||||||
|
|
||||||
|
note over FE,LLM: ⚠ No file-path journal exists. On re-trigger,<br/>BE re-scans all files. Date filter (modifiedAt > last_run_at)<br/>skips unchanged files, but LLM dedup is the only guard<br/>if last_run_at is null or files are unmodified.
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
adiuvAI · Agent Runner Sequence · April 2026 ·
|
||||||
|
FE = Electron (filesystem + SQLite owner) · BE = FastAPI (orchestrator) · LLM = gpt-4.1 via LiteLLM
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'dark',
|
||||||
|
themeVariables: {
|
||||||
|
actorBkg: '#1e2130',
|
||||||
|
actorBorder: '#3b82f6',
|
||||||
|
actorTextColor: '#e4e7ef',
|
||||||
|
actorLineColor: '#3d4156',
|
||||||
|
signalColor: '#c9cdd8',
|
||||||
|
signalTextColor: '#c9cdd8',
|
||||||
|
noteBkgColor: '#1c1f2e',
|
||||||
|
noteTextColor: '#a0a6be',
|
||||||
|
noteBorderColor: '#2e3248',
|
||||||
|
activationBkgColor: '#252840',
|
||||||
|
activationBorderColor: '#4f5580',
|
||||||
|
sequenceNumberColor: '#0f1117',
|
||||||
|
loopTextColor: '#8b8fc4',
|
||||||
|
labelBoxBkgColor: '#1a1d2e',
|
||||||
|
labelBoxBorderColor: '#2e3248',
|
||||||
|
labelTextColor: '#a0a6be',
|
||||||
|
},
|
||||||
|
sequence: {
|
||||||
|
diagramMarginX: 20,
|
||||||
|
diagramMarginY: 20,
|
||||||
|
actorMargin: 80,
|
||||||
|
width: 220,
|
||||||
|
height: 50,
|
||||||
|
boxMargin: 6,
|
||||||
|
boxTextMargin: 6,
|
||||||
|
noteMargin: 12,
|
||||||
|
messageMargin: 40,
|
||||||
|
mirrorActors: true,
|
||||||
|
useMaxWidth: true,
|
||||||
|
wrap: true,
|
||||||
|
},
|
||||||
|
fontFamily: '"JetBrains Mono", monospace',
|
||||||
|
fontSize: 13,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
504
docs/build-deck-commercialista.js
Normal file
504
docs/build-deck-commercialista.js
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
// adiuvAI — Presentazione generica dell'applicazione
|
||||||
|
// Stile: Light canvas (app light mode)
|
||||||
|
const pptxgen = require("pptxgenjs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const ASSETS = "C:/_temp/_adiuvai_workspace/adiuvAI/assets";
|
||||||
|
const LOGO_ICON = `${ASSETS}/logo/logo-icon.png`;
|
||||||
|
const SHOT_HOME = `${ASSETS}/screenshot/home.png`;
|
||||||
|
const SHOT_PROJECTS = `${ASSETS}/screenshot/projects.png`;
|
||||||
|
const SHOT_TASK = `${ASSETS}/screenshot/task.png`;
|
||||||
|
const SHOT_CHAT = `${ASSETS}/screenshot/home_chat.png`;
|
||||||
|
|
||||||
|
// Palette light mode (app)
|
||||||
|
const C = {
|
||||||
|
bg: "F4EDF3", // pinkish-white canvas
|
||||||
|
surface: "FFFFFF", // cards
|
||||||
|
surface2: "EDE5EC", // subtle header row
|
||||||
|
gold: "FBC881", // primary accent
|
||||||
|
goldDark: "C79A5B", // darker gold for contrast on light bg
|
||||||
|
ink: "0C0C0C", // near-black
|
||||||
|
ink2: "323232", // body text dark
|
||||||
|
muted: "8A8EA9", // slate blue-gray
|
||||||
|
border: "C8C3CD", // dusty lavender border
|
||||||
|
borderSoft: "E5DFE4",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FONT_H = "Calibri";
|
||||||
|
const FONT_B = "Calibri";
|
||||||
|
|
||||||
|
const pres = new pptxgen();
|
||||||
|
pres.layout = "LAYOUT_WIDE"; // 13.333 x 7.5
|
||||||
|
pres.author = "adiuvAI";
|
||||||
|
pres.title = "adiuvAI";
|
||||||
|
|
||||||
|
const SW = 13.333;
|
||||||
|
const SH = 7.5;
|
||||||
|
|
||||||
|
const DARK = { bg: "0C0C0C", surface: "181818", surface2: "222222", text: "FBFBFB", muted: "8A8EA9", border: "2A2A2A" };
|
||||||
|
|
||||||
|
function bgLight(slide) { slide.background = { color: C.bg }; }
|
||||||
|
function bgDark(slide) { slide.background = { color: DARK.bg }; }
|
||||||
|
|
||||||
|
function footer(slide, pageNum, total, dark) {
|
||||||
|
slide.addImage({ path: LOGO_ICON, x: 0.5, y: 0.35, w: 0.35, h: 0.35 });
|
||||||
|
slide.addText(
|
||||||
|
[
|
||||||
|
{ text: "adiuv", options: { color: dark ? DARK.text : C.ink, fontFace: FONT_H, fontSize: 11 } },
|
||||||
|
{ text: "AI", options: { color: dark ? C.gold : C.goldDark, fontFace: FONT_H, fontSize: 11, bold: true } },
|
||||||
|
],
|
||||||
|
{ x: 0.9, y: 0.33, w: 2.5, h: 0.4, margin: 0, valign: "middle" }
|
||||||
|
);
|
||||||
|
slide.addText(`${pageNum} / ${total}`, {
|
||||||
|
x: SW - 1.5, y: 0.33, w: 1.0, h: 0.4,
|
||||||
|
color: dark ? DARK.muted : C.muted, fontFace: FONT_B, fontSize: 10, align: "right", valign: "middle", margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function slideTitle(slide, eyebrow, title, dark) {
|
||||||
|
if (eyebrow) {
|
||||||
|
slide.addText(eyebrow.toUpperCase(), {
|
||||||
|
x: 0.8, y: 1.05, w: 10, h: 0.35,
|
||||||
|
color: dark ? C.gold : C.goldDark, fontFace: FONT_H, fontSize: 11, bold: true, charSpacing: 6, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
slide.addText(title, {
|
||||||
|
x: 0.8, y: 1.4, w: 11.5, h: 1.0,
|
||||||
|
color: dark ? DARK.text : C.ink, fontFace: FONT_H, fontSize: 36, bold: true, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goldDot(slide, x, y) {
|
||||||
|
slide.addShape(pres.shapes.OVAL, {
|
||||||
|
x, y, w: 0.12, h: 0.12, fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOTAL = 9;
|
||||||
|
let page = 0;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 1 — COVER
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
|
||||||
|
s.addImage({ path: LOGO_ICON, x: 1.1, y: 2.4, w: 2.6, h: 2.6 });
|
||||||
|
|
||||||
|
s.addText(
|
||||||
|
[
|
||||||
|
{ text: "adiuv", options: { color: C.ink, fontFace: FONT_H, fontSize: 72 } },
|
||||||
|
{ text: "AI", options: { color: C.goldDark, fontFace: FONT_H, fontSize: 72, bold: true } },
|
||||||
|
],
|
||||||
|
{ x: 4.0, y: 2.7, w: 7.5, h: 1.3, margin: 0, valign: "middle" }
|
||||||
|
);
|
||||||
|
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 4.1, y: 4.05, w: 0.6, h: 0.04, fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addText("Meet your new chief of staff.", {
|
||||||
|
x: 4.0, y: 4.15, w: 8.5, h: 0.6,
|
||||||
|
color: C.ink, fontFace: FONT_H, fontSize: 24, italic: true, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addText("Una segretaria AI che legge la tua posta, organizza il tuo lavoro, e ogni mattina ti dice cosa conta — tutto sul tuo computer.", {
|
||||||
|
x: 4.0, y: 4.85, w: 8.5, h: 1.4,
|
||||||
|
color: C.ink2, fontFace: FONT_B, fontSize: 16, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 2 — L'IDEA: UNA SEGRETARIA
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
footer(s, page, TOTAL);
|
||||||
|
slideTitle(s, "L'idea", "Non un altro tool. Una segretaria.");
|
||||||
|
|
||||||
|
s.addText(
|
||||||
|
[
|
||||||
|
{ text: "Gli strumenti di produttività si aspettano che tu li usi.\n", options: { color: C.muted, fontSize: 18 } },
|
||||||
|
{ text: "adiuvAI lavora per te.", options: { color: C.ink, fontSize: 26, bold: true } },
|
||||||
|
],
|
||||||
|
{ x: 0.8, y: 2.8, w: 11.8, h: 1.5, fontFace: FONT_H, margin: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Metafora: cosa fa una segretaria reale
|
||||||
|
const duties = [
|
||||||
|
{ t: "Legge la tua posta", d: "Filtra, prioritizza, segnala solo ciò che richiede la tua attenzione." },
|
||||||
|
{ t: "Tiene in ordine l'agenda", d: "Scadenze, impegni, follow-up — tutto tracciato senza chiederti nulla." },
|
||||||
|
{ t: "Prepara il briefing", d: "Ogni mattina arriva con un piano chiaro: ecco cosa conta oggi." },
|
||||||
|
{ t: "Ti aiuta a eseguire", d: "Prepara bozze, organizza documenti, ti accompagna mentre lavori." },
|
||||||
|
];
|
||||||
|
const cardW = 5.85, gap = 0.2;
|
||||||
|
duties.forEach((d, i) => {
|
||||||
|
const col = i % 2, row = Math.floor(i / 2);
|
||||||
|
const x = 0.8 + col * (cardW + gap);
|
||||||
|
const y = 4.45 + row * 1.35;
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x, y, w: cardW, h: 1.2,
|
||||||
|
fill: { color: C.surface }, line: { color: C.borderSoft, width: 0.75 },
|
||||||
|
});
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x, y, w: 0.08, h: 1.2, fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
s.addText(d.t, {
|
||||||
|
x: x + 0.3, y: y + 0.15, w: cardW - 0.4, h: 0.4,
|
||||||
|
color: C.ink, fontFace: FONT_H, fontSize: 16, bold: true, margin: 0,
|
||||||
|
});
|
||||||
|
s.addText(d.d, {
|
||||||
|
x: x + 0.3, y: y + 0.55, w: cardW - 0.4, h: 0.65,
|
||||||
|
color: C.ink2, fontFace: FONT_B, fontSize: 12, margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 3 — DAILY BRIEF + CAROUSEL (hero feature)
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgDark(s);
|
||||||
|
footer(s, page, TOTAL, true);
|
||||||
|
slideTitle(s, "Il cuore dell'esperienza", "Il briefing del mattino, poi ti prende per mano.", true);
|
||||||
|
|
||||||
|
// Left: screenshot home (mostra il daily brief)
|
||||||
|
s.addImage({ path: SHOT_HOME, x: 0.8, y: 2.8, w: 6.4, h: 3.6 });
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 0.8, y: 2.8, w: 6.4, h: 3.6,
|
||||||
|
fill: { type: "solid", color: DARK.bg, transparency: 100 },
|
||||||
|
line: { color: DARK.border, width: 1 },
|
||||||
|
});
|
||||||
|
s.addText("Daily Brief", {
|
||||||
|
x: 0.8, y: 6.5, w: 6.4, h: 0.35,
|
||||||
|
color: DARK.muted, fontFace: FONT_H, fontSize: 11, bold: true, align: "center", charSpacing: 4, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Right: caption + carousel feature
|
||||||
|
s.addText("Ogni mattina, un briefing personalizzato ti racconta cosa è cambiato, cosa scade e cosa conta di più.", {
|
||||||
|
x: 7.5, y: 2.75, w: 5.2, h: 1.4,
|
||||||
|
color: DARK.text, fontFace: FONT_H, fontSize: 16, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Carousel card (feature)
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 7.5, y: 4.25, w: 5.2, h: 2.6,
|
||||||
|
fill: { color: DARK.surface }, line: { color: DARK.border, width: 0.75 },
|
||||||
|
});
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 7.5, y: 4.25, w: 0.08, h: 2.6,
|
||||||
|
fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
s.addText("CAROSELLO DELLE ATTIVITÀ", {
|
||||||
|
x: 7.75, y: 4.4, w: 5, h: 0.4,
|
||||||
|
color: C.gold, fontFace: FONT_H, fontSize: 10, bold: true, charSpacing: 4, margin: 0,
|
||||||
|
});
|
||||||
|
s.addText(
|
||||||
|
[
|
||||||
|
{ text: "Dalla home, avvii il carosello: ", options: { color: DARK.text, bold: true, breakLine: true } },
|
||||||
|
{ text: "ogni scheda è un'attività che l'AI ritiene prioritaria per la giornata. Ti guida passo passo con le indicazioni per completarla, e puoi chattare con lei mentre lavori — come se avessi la tua segretaria al fianco.",
|
||||||
|
options: { color: DARK.muted } },
|
||||||
|
],
|
||||||
|
{ x: 7.75, y: 4.8, w: 5, h: 1.95, fontFace: FONT_B, fontSize: 13, margin: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 4 — CHAT CONTESTUALE
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgDark(s);
|
||||||
|
footer(s, page, TOTAL, true);
|
||||||
|
slideTitle(s, "Chat", "Parla con la tua segretaria. In italiano, in linguaggio naturale.", true);
|
||||||
|
|
||||||
|
// left: examples
|
||||||
|
const examples = [
|
||||||
|
"« Qual è la prossima attività su cui concentrarmi? »",
|
||||||
|
"« Riassumi le email arrivate stamattina. »",
|
||||||
|
"« Crea un'attività: richiamare Luca giovedì. »",
|
||||||
|
"« Cosa è cambiato sul progetto Patient Portal? »",
|
||||||
|
];
|
||||||
|
examples.forEach((e, i) => {
|
||||||
|
const y = 2.8 + i * 0.65;
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 0.8, y, w: 5.6, h: 0.55,
|
||||||
|
fill: { color: DARK.surface }, line: { color: DARK.border, width: 0.75 },
|
||||||
|
});
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 0.8, y, w: 0.06, h: 0.55, fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
s.addText(e, {
|
||||||
|
x: 1.0, y, w: 5.3, h: 0.55,
|
||||||
|
color: DARK.text, fontFace: FONT_B, fontSize: 12, italic: true, valign: "middle", margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addText("Niente prompt engineering. Niente modelli da scegliere. L'AI giusta lavora in background e ti risponde con il contesto del tuo workspace.", {
|
||||||
|
x: 0.8, y: 5.65, w: 5.6, h: 1.0,
|
||||||
|
color: DARK.muted, fontFace: FONT_B, fontSize: 13, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// right: screenshot
|
||||||
|
s.addImage({ path: SHOT_CHAT, x: 6.8, y: 2.6, w: 6.0, h: 3.375 });
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 6.8, y: 2.6, w: 6.0, h: 3.375,
|
||||||
|
fill: { type: "solid", color: DARK.bg, transparency: 100 },
|
||||||
|
line: { color: DARK.border, width: 1 },
|
||||||
|
});
|
||||||
|
s.addText("Chat contestuale sul workspace", {
|
||||||
|
x: 6.8, y: 6.05, w: 6.0, h: 0.35,
|
||||||
|
color: DARK.muted, fontFace: FONT_H, fontSize: 11, bold: true, align: "center", charSpacing: 4, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 5 — FUNZIONALITÀ (compattate)
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
footer(s, page, TOTAL);
|
||||||
|
slideTitle(s, "Cosa fa", "Tutto il lavoro quotidiano, in un unico posto.");
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ t: "Email → Attività", d: "Legge Gmail, Outlook, cartelle locali ed estrae automaticamente task, promemoria e note." },
|
||||||
|
{ t: "Progetti e clienti", d: "Timeline, milestone, riepiloghi AI per ogni progetto. Tutto collegato." },
|
||||||
|
{ t: "Note con ricerca semantica", d: "Editor markdown e ricerca vettoriale su tutto ciò che scrivi." },
|
||||||
|
{ t: "Timeline e milestone", d: "Panoramica visiva delle scadenze e degli stati di avanzamento." },
|
||||||
|
{ t: "Agenti locali", d: "Sorveglianza file, monitor cartelle, integrazione Telegram." },
|
||||||
|
{ t: "Voce in riunione", d: "Prende note durante le call, estrae action item.", soon: true },
|
||||||
|
];
|
||||||
|
const cardW = 3.95, gap = 0.2;
|
||||||
|
const cols = 3;
|
||||||
|
items.forEach((it, i) => {
|
||||||
|
const col = i % cols, row = Math.floor(i / cols);
|
||||||
|
const x = 0.8 + col * (cardW + gap);
|
||||||
|
const y = 2.8 + row * 1.9;
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x, y, w: cardW, h: 1.7,
|
||||||
|
fill: { color: C.surface }, line: { color: C.borderSoft, width: 0.75 },
|
||||||
|
});
|
||||||
|
if (it.soon) {
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: x + cardW - 1.1, y: y + 0.18, w: 0.95, h: 0.3,
|
||||||
|
fill: { color: C.bg }, line: { color: C.gold, width: 0.75 },
|
||||||
|
});
|
||||||
|
s.addText("COMING SOON", {
|
||||||
|
x: x + cardW - 1.1, y: y + 0.18, w: 0.95, h: 0.3,
|
||||||
|
color: C.goldDark, fontFace: FONT_H, fontSize: 8, bold: true, align: "center", valign: "middle", charSpacing: 2, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
goldDot(s, x + 0.25, y + 0.3);
|
||||||
|
s.addText(it.t, {
|
||||||
|
x: x + 0.5, y: y + 0.2, w: cardW - 1.5, h: 0.4,
|
||||||
|
color: C.ink, fontFace: FONT_H, fontSize: 15, bold: true, margin: 0,
|
||||||
|
});
|
||||||
|
s.addText(it.d, {
|
||||||
|
x: x + 0.25, y: y + 0.7, w: cardW - 0.5, h: 0.95,
|
||||||
|
color: C.ink2, fontFace: FONT_B, fontSize: 11.5, margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 5 — RISERVATEZZA
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
footer(s, page, TOTAL);
|
||||||
|
slideTitle(s, "Riservatezza", "I tuoi dati non lasciano il tuo computer.");
|
||||||
|
|
||||||
|
s.addText("Local-first.", {
|
||||||
|
x: 0.8, y: 2.8, w: 6, h: 0.9,
|
||||||
|
color: C.goldDark, fontFace: FONT_H, fontSize: 48, bold: true, margin: 0,
|
||||||
|
});
|
||||||
|
s.addText("Tutto gira in locale. Il database è sul tuo disco, cifrato. Nessun server adiuvAI vede i contenuti di email, file o documenti.", {
|
||||||
|
x: 0.8, y: 3.9, w: 6, h: 2.0,
|
||||||
|
color: C.ink2, fontFace: FONT_B, fontSize: 15, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const comp = [
|
||||||
|
{ t: "GDPR", d: "I dati non vengono mai trasferiti a terzi. Conformità per architettura." },
|
||||||
|
{ t: "EU AI Act", d: "Progettato dall'inizio per il nuovo quadro normativo europeo." },
|
||||||
|
{ t: "Cifratura end-to-end", d: "Backup e sincronizzazione opzionali con cifratura client-side." },
|
||||||
|
{ t: "No training", d: "I tuoi dati non vengono mai usati per addestrare modelli AI." },
|
||||||
|
];
|
||||||
|
comp.forEach((c, i) => {
|
||||||
|
const col = i % 2, row = Math.floor(i / 2);
|
||||||
|
const x = 7.2 + col * 2.95;
|
||||||
|
const y = 2.8 + row * 1.85;
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x, y, w: 2.85, h: 1.65,
|
||||||
|
fill: { color: C.surface }, line: { color: C.borderSoft, width: 0.75 },
|
||||||
|
});
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x, y, w: 2.85, h: 0.05, fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
s.addText(c.t, {
|
||||||
|
x: x + 0.2, y: y + 0.15, w: 2.6, h: 0.4,
|
||||||
|
color: C.goldDark, fontFace: FONT_H, fontSize: 15, bold: true, margin: 0,
|
||||||
|
});
|
||||||
|
s.addText(c.d, {
|
||||||
|
x: x + 0.2, y: y + 0.6, w: 2.6, h: 1.0,
|
||||||
|
color: C.ink2, fontFace: FONT_B, fontSize: 11, margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 6 — POSIZIONAMENTO
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
footer(s, page, TOTAL);
|
||||||
|
slideTitle(s, "Posizionamento", "Perché non Motion, Notion AI o Microsoft Copilot?");
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
["", "adiuvAI", "Motion", "Notion AI", "Copilot"],
|
||||||
|
["Locale, dati sul tuo PC", "Sì", "No", "No", "No"],
|
||||||
|
["Conforme EU AI Act", "Sì", "n/d", "n/d", "Parziale"],
|
||||||
|
["Legge email + file + chat", "Sì", "Parziale", "No", "Sì"],
|
||||||
|
["Daily Brief proattivo", "Sì", "No", "No", "No"],
|
||||||
|
["AI invisibile (zero prompt)", "Sì", "No", "No", "No"],
|
||||||
|
];
|
||||||
|
const tableData = rows.map((r, ri) =>
|
||||||
|
r.map((cell, ci) => {
|
||||||
|
if (ri === 0) {
|
||||||
|
return {
|
||||||
|
text: cell,
|
||||||
|
options: {
|
||||||
|
bold: true, color: ci === 1 ? C.goldDark : C.ink,
|
||||||
|
fill: { color: C.surface2 },
|
||||||
|
align: "center", fontFace: FONT_H, fontSize: 13, valign: "middle",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (ci === 0) {
|
||||||
|
return {
|
||||||
|
text: cell,
|
||||||
|
options: {
|
||||||
|
fontFace: FONT_B, fontSize: 12, color: C.ink, bold: true,
|
||||||
|
fill: { color: C.surface }, valign: "middle",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const isYes = cell === "Sì";
|
||||||
|
return {
|
||||||
|
text: cell,
|
||||||
|
options: {
|
||||||
|
fontFace: FONT_B, fontSize: 12, align: "center", valign: "middle",
|
||||||
|
color: ci === 1 ? (isYes ? C.goldDark : C.muted) : (isYes ? C.ink : C.muted),
|
||||||
|
bold: ci === 1,
|
||||||
|
fill: { color: ci === 1 ? C.surface2 : C.surface },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
s.addTable(tableData, {
|
||||||
|
x: 0.8, y: 2.8, w: 11.7,
|
||||||
|
colW: [4.5, 1.8, 1.8, 1.8, 1.8],
|
||||||
|
rowH: 0.55,
|
||||||
|
border: { pt: 1, color: C.borderSoft },
|
||||||
|
fontFace: FONT_B,
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addText("Gli altri sono cloud-first generalisti. adiuvAI è locale, proattivo, pensato per chi lavora con dati propri.", {
|
||||||
|
x: 0.8, y: 6.55, w: 12.0, h: 0.5,
|
||||||
|
color: C.goldDark, fontFace: FONT_B, fontSize: 13, italic: true, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 7 — ROADMAP
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
footer(s, page, TOTAL);
|
||||||
|
slideTitle(s, "Roadmap", "Dove siamo, dove stiamo andando.");
|
||||||
|
|
||||||
|
const tlY = 4.2;
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 1.2, y: tlY, w: 11.0, h: 0.04,
|
||||||
|
fill: { color: C.border }, line: { color: C.border },
|
||||||
|
});
|
||||||
|
|
||||||
|
const milestones = [
|
||||||
|
{ x: 1.6, label: "Oggi — Beta privata", items: ["Daily Brief", "Carosello attività", "Email → Task", "Progetti & Note"] },
|
||||||
|
{ x: 5.6, label: "Giugno 2026 — Beta pubblica", items: ["Telegram bot", "Outlook / Teams", "App mobile companion"] },
|
||||||
|
{ x: 9.6, label: "Oltre", items: ["Assistente vocale in riunione", "Workspace di team", "SSO e ruoli"] },
|
||||||
|
];
|
||||||
|
milestones.forEach((m) => {
|
||||||
|
s.addShape(pres.shapes.OVAL, {
|
||||||
|
x: m.x - 0.12, y: tlY - 0.1, w: 0.25, h: 0.25,
|
||||||
|
fill: { color: C.gold }, line: { color: C.goldDark, width: 1 },
|
||||||
|
});
|
||||||
|
s.addText(m.label, {
|
||||||
|
x: m.x - 0.2, y: tlY - 0.9, w: 4.2, h: 0.5,
|
||||||
|
color: C.goldDark, fontFace: FONT_H, fontSize: 13, bold: true, margin: 0,
|
||||||
|
});
|
||||||
|
s.addText(
|
||||||
|
m.items.map((it, idx) => ({
|
||||||
|
text: it,
|
||||||
|
options: { bullet: { code: "25A0" }, color: C.ink2, breakLine: idx < m.items.length - 1, paraSpaceAfter: 4 },
|
||||||
|
})),
|
||||||
|
{ x: m.x - 0.2, y: tlY + 0.3, w: 4.0, h: 2.5, fontFace: FONT_B, fontSize: 12, margin: 0 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 8 — CLOSING
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
page++;
|
||||||
|
const s = pres.addSlide();
|
||||||
|
bgLight(s);
|
||||||
|
|
||||||
|
s.addImage({ path: LOGO_ICON, x: (SW - 1.5) / 2, y: 1.5, w: 1.5, h: 1.5 });
|
||||||
|
|
||||||
|
s.addText("Meet your new chief of staff.", {
|
||||||
|
x: 1.0, y: 3.3, w: SW - 2, h: 0.9,
|
||||||
|
color: C.ink, fontFace: FONT_H, fontSize: 32, bold: true, italic: true, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: (SW - 0.6) / 2, y: 4.25, w: 0.6, h: 0.04, fill: { color: C.gold }, line: { color: C.gold },
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addText("Beta in arrivo a Giugno 2026. Gli early adopter otterranno accesso prioritario e potranno guidare il roadmap.", {
|
||||||
|
x: 1.5, y: 4.45, w: SW - 3, h: 1.4,
|
||||||
|
color: C.ink2, fontFace: FONT_B, fontSize: 16, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
s.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: (SW - 7) / 2, y: 6.0, w: 7, h: 1.1,
|
||||||
|
fill: { color: C.surface }, line: { color: C.gold, width: 1.5 },
|
||||||
|
});
|
||||||
|
s.addText(
|
||||||
|
[
|
||||||
|
{ text: "Iscriviti alla waitlist · ", options: { color: C.ink, bold: true } },
|
||||||
|
{ text: "adiuvai.com", options: { color: C.goldDark, bold: true } },
|
||||||
|
],
|
||||||
|
{ x: (SW - 7) / 2, y: 6.0, w: 7, h: 1.1,
|
||||||
|
fontFace: FONT_H, fontSize: 20, align: "center", valign: "middle", margin: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
const OUT = path.resolve("C:/_temp/_adiuvai_workspace/docs/adiuvAI.pptx");
|
||||||
|
pres.writeFile({ fileName: OUT }).then((f) => console.log("WROTE:", f));
|
||||||
624
docs/build-deck-geopop.js
Normal file
624
docs/build-deck-geopop.js
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
"use strict";
|
||||||
|
const pptxgen = require("pptxgenjs");
|
||||||
|
|
||||||
|
// ── Paths ──────────────────────────────────────────────────────────────────
|
||||||
|
const LOGO = "C:/Users/PC-Roby/Documents/_adiuvai_workspace/adiuvAI/assets/logo/logo-icon.png";
|
||||||
|
const SCR_H = "C:/Users/PC-Roby/Documents/_adiuvai_workspace/adiuvAI/assets/screenshot/home.png";
|
||||||
|
const SCR_HC = "C:/Users/PC-Roby/Documents/_adiuvai_workspace/adiuvAI/assets/screenshot/home_chat.png";
|
||||||
|
const OUTPUT = "C:/Users/PC-Roby/Documents/_adiuvai_workspace/docs/adiuvAI-pitch-geopop.pptx";
|
||||||
|
|
||||||
|
// ── Color tokens (NO # prefix) ─────────────────────────────────────────────
|
||||||
|
const C = {
|
||||||
|
canvas: "f4edf3", // light bg
|
||||||
|
ink: "040404", // primary text
|
||||||
|
golden: "fbc881", // accent
|
||||||
|
slate: "8a8ea9", // secondary text
|
||||||
|
lavender: "c8c3cd", // borders
|
||||||
|
void: "0c0c0c", // dark bg
|
||||||
|
paper: "fbfbfb", // text-on-dark / card fill
|
||||||
|
graphite: "323232", // dark card
|
||||||
|
green: "2e7d32",
|
||||||
|
red: "c62828",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Shadow factory (NEVER reuse same object) ────────────────────────────────
|
||||||
|
const mkShadow = () => ({ type: "outer", blur: 8, offset: 2, angle: 135, color: "000000", opacity: 0.08 });
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
function eyebrow(sl, text, x, y, w, align = "left") {
|
||||||
|
sl.addText(text, {
|
||||||
|
x, y, w, h: 0.28,
|
||||||
|
fontFace: "Calibri", fontSize: 9, bold: true,
|
||||||
|
color: C.golden, charSpacing: 2, align, margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
const pres = new pptxgen();
|
||||||
|
pres.layout = "LAYOUT_16x9"; // 10" × 5.625"
|
||||||
|
pres.title = "adiuvAI — Pitch Deck";
|
||||||
|
pres.author = "Roberto Musso";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 1 — COVER (dark)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.void };
|
||||||
|
|
||||||
|
// Logo top-left
|
||||||
|
sl.addImage({ path: LOGO, x: 0.4, y: 0.28, w: 0.75, h: 0.75 });
|
||||||
|
|
||||||
|
// Eyebrow top-right
|
||||||
|
eyebrow(sl, "BETA · GIUGNO 2026", 1.5, 0.35, 8, "right");
|
||||||
|
|
||||||
|
// Main headline
|
||||||
|
sl.addText([
|
||||||
|
{ text: "Incontra la tua nuova", options: { color: C.paper, breakLine: true } },
|
||||||
|
{ text: "segretaria.", options: { color: C.golden } },
|
||||||
|
], {
|
||||||
|
x: 0.5, y: 1.3, w: 9, h: 2.0,
|
||||||
|
fontFace: "Calibri", fontSize: 54, bold: true, align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
sl.addText(
|
||||||
|
"Una AI che legge la tua posta, organizza il tuo lavoro,\ne ogni mattina ti dice cosa conta davvero. Tutto sul tuo computer.",
|
||||||
|
{
|
||||||
|
x: 1, y: 3.4, w: 8, h: 1.15,
|
||||||
|
fontFace: "Calibri", fontSize: 18, color: C.slate, align: "center",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
sl.addText("adiuvai.com", {
|
||||||
|
x: 0, y: 5.28, w: 10, h: 0.28,
|
||||||
|
fontFace: "Calibri", fontSize: 11, color: C.slate, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 2 — CHI PARLA (light)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.canvas };
|
||||||
|
|
||||||
|
// Left golden accent bar
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 0.35, y: 0.45, w: 0.06, h: 4.7,
|
||||||
|
fill: { color: C.golden }, line: { color: C.golden },
|
||||||
|
});
|
||||||
|
|
||||||
|
eyebrow(sl, "IL FONDATORE", 0.62, 0.5, 5);
|
||||||
|
|
||||||
|
sl.addText("Roberto Musso", {
|
||||||
|
x: 0.62, y: 0.82, w: 9, h: 0.9,
|
||||||
|
fontFace: "Calibri", fontSize: 40, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("AI Senior Architect @ Hewlett Packard Enterprise", {
|
||||||
|
x: 0.62, y: 1.72, w: 9, h: 0.45,
|
||||||
|
fontFace: "Calibri", fontSize: 18, color: C.slate, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
sl.addShape(pres.shapes.LINE, {
|
||||||
|
x: 0.62, y: 2.3, w: 8.7, h: 0,
|
||||||
|
line: { color: C.lavender, width: 0.75 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bullets
|
||||||
|
sl.addText([
|
||||||
|
{ text: "In Hewlett Packard Enterprise dal 2018", options: { bullet: true, breakLine: true } },
|
||||||
|
{ text: "AI Delivery Team Lead — guido un team di 6 persone", options: { bullet: true, breakLine: true } },
|
||||||
|
{ text: "Progetto e consegno soluzioni AI enterprise", options: { bullet: true } },
|
||||||
|
], {
|
||||||
|
x: 0.62, y: 2.48, w: 8.8, h: 1.55,
|
||||||
|
fontFace: "Calibri", fontSize: 17, color: C.ink,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Closing line
|
||||||
|
sl.addText(
|
||||||
|
"adiuvAI nasce da quello che faccio ogni giorno: trasformare l\u2019AI in qualcosa che funziona davvero.",
|
||||||
|
{
|
||||||
|
x: 0.62, y: 4.25, w: 8.8, h: 0.85,
|
||||||
|
fontFace: "Calibri", fontSize: 16, italic: true, color: C.slate, margin: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 3 — HOOK (dark)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.void };
|
||||||
|
|
||||||
|
sl.addText("Quante app hai aperto adesso?", {
|
||||||
|
x: 0.4, y: 0.5, w: 9.2, h: 1.5,
|
||||||
|
fontFace: "Calibri", fontSize: 52, bold: true, color: C.paper, align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("Ottimista: sei.", {
|
||||||
|
x: 0.4, y: 1.95, w: 9.2, h: 0.65,
|
||||||
|
fontFace: "Calibri", fontSize: 26, color: C.slate, align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
// App pills — 8 × (1.0" wide + 0.1" gap) = 8.8" total; start at 0.6
|
||||||
|
const apps = ["Gmail", "Outlook", "Teams", "Slack", "Notion", "Trello", "OneDrive", "WhatsApp"];
|
||||||
|
const pW = 1.0, pH = 0.38, pGap = 0.115, pY = 2.82, pX0 = 0.58;
|
||||||
|
|
||||||
|
apps.forEach((app, i) => {
|
||||||
|
const px = pX0 + i * (pW + pGap);
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: px, y: pY, w: pW, h: pH,
|
||||||
|
fill: { color: "1c1c1c" }, line: { color: C.golden, width: 1 },
|
||||||
|
});
|
||||||
|
sl.addText(app, {
|
||||||
|
x: px, y: pY, w: pW, h: pH,
|
||||||
|
fontFace: "Calibri", fontSize: 12, bold: true,
|
||||||
|
color: C.golden, align: "center", valign: "middle", margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Punchline
|
||||||
|
sl.addText(
|
||||||
|
"Otto posti dove si nasconde il tuo lavoro importante.\nE tu salti da uno all\u2019altro. Tutto. Il. Giorno.",
|
||||||
|
{
|
||||||
|
x: 0.5, y: 3.45, w: 9, h: 1.85,
|
||||||
|
fontFace: "Calibri", fontSize: 22, bold: true, color: C.golden, align: "center",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 4 — PAIN POINTS (light)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.canvas };
|
||||||
|
|
||||||
|
eyebrow(sl, "COSA SUCCEDE DAVVERO", 0.4, 0.25, 6);
|
||||||
|
|
||||||
|
sl.addText("Il tuo lavoro importante ti sta scappando.", {
|
||||||
|
x: 0.4, y: 0.55, w: 9.2, h: 0.75,
|
||||||
|
fontFace: "Calibri", fontSize: 30, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ icon: "\u2709\uFE0F", title: "Email importanti", body: "Si nascondono tra le newsletter.\nLe leggi tardi o non le leggi affatto." },
|
||||||
|
{ icon: "\u2705", title: "I tuoi task", body: "Vivono in tre app diverse.\nNe perderai sempre uno." },
|
||||||
|
{ icon: "\uD83D\uDCDD", title: "Note riunione", body: "Stanno in un doc che non riaprirai mai.\nLe azioni restano senza seguito." },
|
||||||
|
];
|
||||||
|
|
||||||
|
const cW = 2.93, cH = 2.75, cY = 1.5, cGap = 0.2, cX0 = 0.41;
|
||||||
|
|
||||||
|
cards.forEach((card, i) => {
|
||||||
|
const cx = cX0 + i * (cW + cGap);
|
||||||
|
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: cx, y: cY, w: cW, h: cH,
|
||||||
|
fill: { color: C.paper }, line: { color: C.lavender, width: 1.5 },
|
||||||
|
shadow: mkShadow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Golden top bar
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: cx, y: cY, w: cW, h: 0.05,
|
||||||
|
fill: { color: C.golden }, line: { color: C.golden },
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(card.icon, {
|
||||||
|
x: cx + 0.18, y: cY + 0.18, w: 0.7, h: 0.7,
|
||||||
|
fontFace: "Segoe UI Emoji", fontSize: 28, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(card.title, {
|
||||||
|
x: cx + 0.18, y: cY + 0.98, w: cW - 0.3, h: 0.45,
|
||||||
|
fontFace: "Calibri", fontSize: 16, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(card.body, {
|
||||||
|
x: cx + 0.18, y: cY + 1.48, w: cW - 0.3, h: 1.1,
|
||||||
|
fontFace: "Calibri", fontSize: 14, color: C.slate, margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Punchline
|
||||||
|
sl.addText("Spoiler: il problema non sei tu. Sono gli strumenti.", {
|
||||||
|
x: 0.4, y: 4.52, w: 9.2, h: 0.72,
|
||||||
|
fontFace: "Calibri", fontSize: 19, bold: true, color: C.golden, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 5 — TWIST (dark, minimal)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.void };
|
||||||
|
|
||||||
|
sl.addText("Non ti serve un altro tool.", {
|
||||||
|
x: 0.5, y: 0.95, w: 9, h: 1.3,
|
||||||
|
fontFace: "Calibri", fontSize: 50, bold: false, color: C.paper, align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("Ti serve una segretaria.", {
|
||||||
|
x: 0.5, y: 2.35, w: 9, h: 1.6,
|
||||||
|
fontFace: "Calibri", fontSize: 64, bold: true, color: C.golden, align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("Una che legge tutto al posto tuo. E ti dice dove guardare.", {
|
||||||
|
x: 1, y: 4.25, w: 8, h: 0.65,
|
||||||
|
fontFace: "Calibri", fontSize: 18, color: C.slate, align: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 6 — SOLUZIONE (light)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.canvas };
|
||||||
|
|
||||||
|
eyebrow(sl, "LA SOLUZIONE", 0.4, 0.22, 5);
|
||||||
|
|
||||||
|
sl.addText("adiuvAI \u2014 la tua segretaria AI.", {
|
||||||
|
x: 0.4, y: 0.52, w: 9.2, h: 0.85,
|
||||||
|
fontFace: "Calibri", fontSize: 36, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("Gli altri tool aspettano che tu li usi. adiuvAI lavora per te.", {
|
||||||
|
x: 0.4, y: 1.38, w: 9.2, h: 0.48,
|
||||||
|
fontFace: "Calibri", fontSize: 17, italic: true, color: C.slate, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cols = [
|
||||||
|
{ icon: "\uD83D\uDCE7", title: "Legge la tua posta", body: "Filtra, prioritizza, segnala solo ci\u00F2 che richiede la tua attenzione." },
|
||||||
|
{ icon: "\uD83D\uDCC5", title: "Tiene in ordine l\u2019agenda", body: "Scadenze, impegni, follow-up \u2014 tutto tracciato senza chiederti nulla." },
|
||||||
|
{ icon: "\uD83D\uDCCB", title: "Prepara il briefing", body: "Ogni mattina un piano chiaro: ecco cosa conta oggi." },
|
||||||
|
{ icon: "\uD83D\uDE80", title: "Ti aiuta a eseguire", body: "Prepara bozze, organizza documenti, ti accompagna mentre lavori." },
|
||||||
|
];
|
||||||
|
|
||||||
|
const coW = 2.2, coH = 3.05, coY = 2.0, coGap = 0.17, coX0 = 0.41;
|
||||||
|
|
||||||
|
cols.forEach((col, i) => {
|
||||||
|
const cx = coX0 + i * (coW + coGap);
|
||||||
|
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: cx, y: coY, w: coW, h: coH,
|
||||||
|
fill: { color: C.paper }, line: { color: C.lavender, width: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Left golden accent bar
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: cx, y: coY, w: 0.05, h: coH,
|
||||||
|
fill: { color: C.golden }, line: { color: C.golden },
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(col.icon, {
|
||||||
|
x: cx + 0.15, y: coY + 0.18, w: 0.55, h: 0.6,
|
||||||
|
fontFace: "Segoe UI Emoji", fontSize: 26, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(col.title, {
|
||||||
|
x: cx + 0.1, y: coY + 0.88, w: coW - 0.18, h: 0.7,
|
||||||
|
fontFace: "Calibri", fontSize: 14, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(col.body, {
|
||||||
|
x: cx + 0.1, y: coY + 1.65, w: coW - 0.18, h: 1.22,
|
||||||
|
fontFace: "Calibri", fontSize: 13, color: C.slate, margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 7 — DAILY BRIEF + CAROSELLO (light + screenshot)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.canvas };
|
||||||
|
|
||||||
|
eyebrow(sl, "LA MATTINA, DIVERSA", 0.35, 0.2, 4.3);
|
||||||
|
|
||||||
|
sl.addText("Il briefing del mattino.\nPoi ti prende per mano.", {
|
||||||
|
x: 0.35, y: 0.5, w: 4.4, h: 1.25,
|
||||||
|
fontFace: "Calibri", fontSize: 26, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(
|
||||||
|
"Ogni mattina, un briefing personalizzato: cosa \u00E8 cambiato, cosa scade, cosa conta oggi.",
|
||||||
|
{
|
||||||
|
x: 0.35, y: 1.85, w: 4.4, h: 0.75,
|
||||||
|
fontFace: "Calibri", fontSize: 14, color: C.ink, margin: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
sl.addText([
|
||||||
|
{ text: "Il carosello attivit\u00E0 ti guida scheda per scheda, passo passo.", options: { bullet: true, breakLine: true } },
|
||||||
|
{ text: "Chatti con lei mentre lavori. Come se ti fosse al fianco.", options: { bullet: true } },
|
||||||
|
], {
|
||||||
|
x: 0.35, y: 2.7, w: 4.4, h: 0.85,
|
||||||
|
fontFace: "Calibri", fontSize: 14, color: C.ink,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quote box
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 0.35, y: 3.68, w: 4.4, h: 1.5,
|
||||||
|
fill: { color: C.paper }, line: { color: C.golden, width: 2 },
|
||||||
|
shadow: mkShadow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(
|
||||||
|
"\u00ABCliente X di solito paga in ritardo.\nLa tua fattura \u00E8 ancora aperta.\u00BB",
|
||||||
|
{
|
||||||
|
x: 0.55, y: 3.8, w: 4.0, h: 0.78,
|
||||||
|
fontFace: "Calibri", fontSize: 14, italic: true, color: C.ink, margin: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
sl.addText("\u2014 il Daily Brief, con memoria relazionale", {
|
||||||
|
x: 0.55, y: 4.65, w: 4.0, h: 0.38,
|
||||||
|
fontFace: "Calibri", fontSize: 11, color: C.slate, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Screenshot right half
|
||||||
|
sl.addImage({
|
||||||
|
path: SCR_H,
|
||||||
|
x: 4.95, y: 0.18, w: 4.75, h: 5.25,
|
||||||
|
sizing: { type: "contain", w: 4.75, h: 5.25 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 8 — CHAT (light + screenshot)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.canvas };
|
||||||
|
|
||||||
|
sl.addText("Parla con lei.\nIn italiano. In linguaggio naturale.", {
|
||||||
|
x: 0.35, y: 0.28, w: 4.4, h: 1.2,
|
||||||
|
fontFace: "Calibri", fontSize: 24, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bubbles = [
|
||||||
|
"\u00ABQual \u00E8 la prossima attivit\u00E0 su cui concentrarmi?\u00BB",
|
||||||
|
"\u00ABRiassumi le email arrivate stamattina.\u00BB",
|
||||||
|
"\u00ABCrea un\u2019attivit\u00E0: richiamare Luca gioved\u00EC.\u00BB",
|
||||||
|
"\u00ABCosa \u00E8 cambiato sul progetto Patient Portal?\u00BB",
|
||||||
|
];
|
||||||
|
|
||||||
|
bubbles.forEach((bub, i) => {
|
||||||
|
const by = 1.62 + i * 0.77;
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 0.35, y: by, w: 4.4, h: 0.6,
|
||||||
|
fill: { color: C.paper }, line: { color: C.golden, width: 1.5 },
|
||||||
|
});
|
||||||
|
sl.addText(bub, {
|
||||||
|
x: 0.5, y: by, w: 4.1, h: 0.6,
|
||||||
|
fontFace: "Calibri", fontSize: 13, italic: true, color: C.ink, valign: "middle", margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(
|
||||||
|
"Niente prompt engineering. Niente modelli da scegliere.\nL\u2019AI giusta lavora in background.",
|
||||||
|
{
|
||||||
|
x: 0.35, y: 4.82, w: 4.4, h: 0.65,
|
||||||
|
fontFace: "Calibri", fontSize: 12, italic: true, color: C.slate, margin: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Screenshot right
|
||||||
|
sl.addImage({
|
||||||
|
path: SCR_HC,
|
||||||
|
x: 4.95, y: 0.18, w: 4.75, h: 5.25,
|
||||||
|
sizing: { type: "contain", w: 4.75, h: 5.25 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 9 — 11 AGENTI (dark)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.void };
|
||||||
|
|
||||||
|
eyebrow(sl, "NON \u00C8 UN WRAPPER DI CHATGPT", 0.5, 0.2, 9, "center");
|
||||||
|
|
||||||
|
sl.addText("Sono 11 agenti AI specializzati che lavorano insieme.", {
|
||||||
|
x: 0.4, y: 0.5, w: 9.2, h: 0.85,
|
||||||
|
fontFace: "Calibri", fontSize: 32, bold: true, color: C.paper, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agents = [
|
||||||
|
"Intent Classifier", "Home Agent", "Floating Agent", "Unified Processor",
|
||||||
|
"Cloud Processor", "Brief Agent", "Setup Agent", "Memory Extractor",
|
||||||
|
"Memory Miner", "Memory Auditor", "Embeddings",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Layout: row1=[0..3], row2=[4..7], row3=[8..10] centered
|
||||||
|
const bW = 2.12, bH = 0.48, bGX = 0.17, bGY = 0.18;
|
||||||
|
const rowDefs = [
|
||||||
|
{ items: [0,1,2,3], y: 1.58 },
|
||||||
|
{ items: [4,5,6,7], y: 1.58 + bH + bGY },
|
||||||
|
{ items: [8,9,10], y: 1.58 + 2 * (bH + bGY) },
|
||||||
|
];
|
||||||
|
|
||||||
|
rowDefs.forEach(({ items, y }) => {
|
||||||
|
const rowW = items.length * bW + (items.length - 1) * bGX;
|
||||||
|
const x0 = (10 - rowW) / 2;
|
||||||
|
|
||||||
|
items.forEach((idx, ci) => {
|
||||||
|
const bx = x0 + ci * (bW + bGX);
|
||||||
|
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: bx, y, w: bW, h: bH,
|
||||||
|
fill: { color: C.graphite }, line: { color: C.golden, width: 1 },
|
||||||
|
});
|
||||||
|
sl.addText(agents[idx], {
|
||||||
|
x: bx, y, w: bW, h: bH,
|
||||||
|
fontFace: "Calibri", fontSize: 13, bold: true,
|
||||||
|
color: C.paper, align: "center", valign: "middle", margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("Una tua richiesta \u2192 cinque agenti al lavoro.", {
|
||||||
|
x: 0.5, y: 3.88, w: 9, h: 1.38,
|
||||||
|
fontFace: "Calibri", fontSize: 28, bold: true, color: C.golden, align: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 10 — PRIVACY + COMPLIANCE (dark)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.void };
|
||||||
|
|
||||||
|
sl.addText("I tuoi dati non lasciano il tuo computer.", {
|
||||||
|
x: 0.4, y: 0.28, w: 9.2, h: 1.2,
|
||||||
|
fontFace: "Calibri", fontSize: 42, bold: true, color: C.paper, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("Local-first. Non un claim di marketing, un\u2019architettura.", {
|
||||||
|
x: 0.4, y: 1.52, w: 9.2, h: 0.5,
|
||||||
|
fontFace: "Calibri", fontSize: 18, color: C.golden, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pcCards = [
|
||||||
|
{ icon: "\uD83D\uDD12", title: "Local-first", body: "DB cifrato sul tuo disco.\nNessun server adiuvAI vede i tuoi contenuti." },
|
||||||
|
{ icon: "\uD83C\uDDEA\uD83C\uDDFA", title: "EU AI Act", body: "Conforme by design,\nnon adattato a posteriori." },
|
||||||
|
{ icon: "\uD83D\uDEE1\uFE0F", title: "GDPR", body: "Zero trasferimenti a terzi.\nDPA art. 28 con ogni provider LLM." },
|
||||||
|
{ icon: "\uD83D\uDEAB", title: "No Training", body: "I tuoi dati non addestrano modelli AI.\nZero Data Retention contrattuale." },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pCW = 4.35, pCH = 1.55, pCGX = 0.25, pCGY = 0.18;
|
||||||
|
const pCPositions = [
|
||||||
|
{ x: 0.4, y: 2.18 },
|
||||||
|
{ x: 0.4 + pCW + pCGX, y: 2.18 },
|
||||||
|
{ x: 0.4, y: 2.18 + pCH + pCGY },
|
||||||
|
{ x: 0.4 + pCW + pCGX, y: 2.18 + pCH + pCGY },
|
||||||
|
];
|
||||||
|
|
||||||
|
pcCards.forEach((card, i) => {
|
||||||
|
const { x, y } = pCPositions[i];
|
||||||
|
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x, y, w: pCW, h: pCH,
|
||||||
|
fill: { color: C.graphite }, line: { color: C.golden, width: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(card.icon + " " + card.title, {
|
||||||
|
x: x + 0.18, y: y + 0.12, w: pCW - 0.3, h: 0.45,
|
||||||
|
fontFace: "Calibri", fontSize: 16, bold: true, color: C.golden, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(card.body, {
|
||||||
|
x: x + 0.18, y: y + 0.62, w: pCW - 0.3, h: 0.82,
|
||||||
|
fontFace: "Calibri", fontSize: 13, color: C.paper, margin: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer note
|
||||||
|
sl.addText("Private by design, not by promise.", {
|
||||||
|
x: 0, y: 5.27, w: 10, h: 0.28,
|
||||||
|
fontFace: "Calibri", fontSize: 12, italic: true, color: C.slate, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 11 — POSIZIONAMENTO vs COMPETITOR (light)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.canvas };
|
||||||
|
|
||||||
|
eyebrow(sl, "PERCH\u00C9 NON GLI ALTRI", 0.4, 0.2, 6);
|
||||||
|
|
||||||
|
sl.addText("Motion, Notion AI, Copilot: tutti cloud-first, tutti generalisti.", {
|
||||||
|
x: 0.4, y: 0.5, w: 9.2, h: 0.72,
|
||||||
|
fontFace: "Calibri", fontSize: 26, bold: true, color: C.ink, margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build table rows — fresh cell objects per row (no shared references)
|
||||||
|
const hdrOpts = (txt, al = "center") => ({ text: txt, options: { bold: true, fontSize: 13, color: C.void, fill: { color: C.golden }, align: al } });
|
||||||
|
const dataOpts = (txt, clr = C.ink, al = "left") => ({ text: txt, options: { fontSize: 13, color: clr, fill: { color: C.paper }, align: al } });
|
||||||
|
const emoOpts = (txt) => ({ text: txt, options: { fontSize: 15, fill: { color: C.paper }, align: "center" } });
|
||||||
|
|
||||||
|
const tableData = [
|
||||||
|
[ hdrOpts("Funzionalit\u00E0", "left"), hdrOpts("adiuvAI"), hdrOpts("Motion"), hdrOpts("Notion AI"), hdrOpts("Copilot") ],
|
||||||
|
[ dataOpts("Locale, dati sul tuo PC"), emoOpts("\u2705"), emoOpts("\u274C"), emoOpts("\u274C"), emoOpts("\u274C") ],
|
||||||
|
[ dataOpts("EU AI Act compliant"), emoOpts("\u2705"), dataOpts("\u2014", C.slate, "center"), dataOpts("\u2014", C.slate, "center"), emoOpts("\u26A0\uFE0F") ],
|
||||||
|
[ dataOpts("Legge email + file + chat"), emoOpts("\u2705"), emoOpts("\u26A0\uFE0F"), emoOpts("\u274C"), emoOpts("\u2705") ],
|
||||||
|
[ dataOpts("Daily Brief proattivo"), emoOpts("\u2705"), emoOpts("\u274C"), emoOpts("\u274C"), emoOpts("\u274C") ],
|
||||||
|
[ dataOpts("AI invisibile (zero prompt)"), emoOpts("\u2705"), emoOpts("\u274C"), emoOpts("\u274C"), emoOpts("\u274C") ],
|
||||||
|
];
|
||||||
|
|
||||||
|
sl.addTable(tableData, {
|
||||||
|
x: 0.4, y: 1.38, w: 9.2, h: 3.45,
|
||||||
|
colW: [3.4, 1.45, 1.45, 1.45, 1.45],
|
||||||
|
border: { pt: 1, color: C.lavender },
|
||||||
|
fontFace: "Calibri",
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("adiuvAI \u00E8 locale, proattiva, pensata per chi lavora con dati propri.", {
|
||||||
|
x: 0.4, y: 5.08, w: 9.2, h: 0.4,
|
||||||
|
fontFace: "Calibri", fontSize: 16, bold: true, color: C.golden, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// SLIDE 12 — CTA / CLOSING (dark)
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
const sl = pres.addSlide();
|
||||||
|
sl.background = { color: C.void };
|
||||||
|
|
||||||
|
eyebrow(sl, "BETA \u00B7 GIUGNO 2026", 0.5, 0.28, 9, "center");
|
||||||
|
|
||||||
|
sl.addText("Gli early adopter\nguidano il roadmap.", {
|
||||||
|
x: 0.4, y: 0.65, w: 9.2, h: 2.0,
|
||||||
|
fontFace: "Calibri", fontSize: 46, bold: true, color: C.paper, align: "center", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText(
|
||||||
|
"Accesso prioritario, canale diretto con il team,\nvoce in capitolo sulle prossime feature.",
|
||||||
|
{
|
||||||
|
x: 1, y: 2.75, w: 8, h: 0.9,
|
||||||
|
fontFace: "Calibri", fontSize: 17, color: C.slate, align: "center",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// CTA button
|
||||||
|
sl.addShape(pres.shapes.RECTANGLE, {
|
||||||
|
x: 3.2, y: 3.78, w: 3.6, h: 0.7,
|
||||||
|
fill: { color: C.golden }, line: { color: C.golden },
|
||||||
|
});
|
||||||
|
sl.addText("Iscriviti alla waitlist \u2192", {
|
||||||
|
x: 3.2, y: 3.78, w: 3.6, h: 0.7,
|
||||||
|
fontFace: "Calibri", fontSize: 18, bold: true,
|
||||||
|
color: C.ink, align: "center", valign: "middle", margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sl.addText("adiuvai.com", {
|
||||||
|
x: 0.5, y: 4.62, w: 9, h: 0.55,
|
||||||
|
fontFace: "Calibri", fontSize: 28, bold: true, color: C.paper, align: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logo small bottom-left watermark
|
||||||
|
sl.addImage({ path: LOGO, x: 0.4, y: 4.95, w: 0.55, h: 0.55 });
|
||||||
|
|
||||||
|
sl.addText("Roberto Musso \u00B7 roby9115@gmail.com", {
|
||||||
|
x: 1.1, y: 5.12, w: 8.5, h: 0.28,
|
||||||
|
fontFace: "Calibri", fontSize: 11, color: C.slate, align: "left", margin: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Write output ────────────────────────────────────────────────────────────
|
||||||
|
pres.writeFile({ fileName: OUTPUT })
|
||||||
|
.then(() => console.log("OK deck generated:", OUTPUT))
|
||||||
|
.catch(err => { console.error("ERROR:", err); process.exit(1); });
|
||||||
153
docs/creative-brief.md
Normal file
153
docs/creative-brief.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# adiuvAI — Creative Brief: Waitlist Landing Page
|
||||||
|
|
||||||
|
> **Purpose:** Design direction for scrollytelling waitlist page
|
||||||
|
> **Status:** Proposals for review — pick a direction before I build
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brand Audit Summary
|
||||||
|
|
||||||
|
| Element | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| **Logo Mark** | Compass needle — golden north (AI), dark south (human). Animates with gentle oscillation. |
|
||||||
|
| **Primary Color** | `#fbc881` — Golden Yellow (the "AI" accent) |
|
||||||
|
| **Dark Base** | `#0c0c0c` — Near-black |
|
||||||
|
| **Light Base** | `#f4edf3` — Pinkish-white canvas |
|
||||||
|
| **Secondary** | `#8a8ea9` — Slate blue-gray |
|
||||||
|
| **Muted** | `#c8c3cd` — Light gray-purple |
|
||||||
|
| **App Font** | Geist (400/600/700) |
|
||||||
|
| **App Screenshot** | Daily brief view: "Good evening, Roberto" + greeting, chat, quick actions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Style Direction — Two Options
|
||||||
|
|
||||||
|
### Option A: "The Dark Executive" (Recommended)
|
||||||
|
|
||||||
|
> Think **Linear.app meets Stripe's documentation** — but warmer, more human, with the golden compass as a guiding light through darkness.
|
||||||
|
|
||||||
|
**Mood:** Premium, confident, dark. Like a private briefing room at midnight.
|
||||||
|
**Background:** Near-black (`#0c0c0c`) with subtle radial gradients and noise texture.
|
||||||
|
**Typography:** Geist for headings (tight letter-spacing, large scale). DM Sans or Satoshi for body.
|
||||||
|
**Color play:** Gold (`#fbc881`) is the ONLY color accent — glowing, warm against the dark void. Everything else is white/gray hierarchy. The gold appears sparingly: on the compass, on CTAs, on scrollytelling highlights.
|
||||||
|
**Scrollytelling philosophy:** Each scroll "chapter" reveals one concept — like pages of a briefing document. Text fades in from below, sections pin and transform, the compass needle rotates as you scroll deeper.
|
||||||
|
**Animation:** Scroll-triggered reveals (GSAP ScrollTrigger), the compass needle slowly settling as you scroll, golden light trails connecting sections like a thread of attention.
|
||||||
|
|
||||||
|
**Why it works for adiuvAI:**
|
||||||
|
- Dark mode signals "this is serious technology for professionals, not a toy"
|
||||||
|
- Gold-on-dark is inherently premium — it says "personal secretary," not "productivity tool"
|
||||||
|
- The restraint (one accent color) mirrors the USP: "complex AI, invisible to you"
|
||||||
|
- The scrollytelling parallels the daily brief: information revealed progressively, just for you
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option B: "The Warm Canvas"
|
||||||
|
|
||||||
|
> Think **Granola.ai meets Apple product pages** — light, airy, warm.
|
||||||
|
|
||||||
|
**Mood:** Clean, human, warm. Like a fresh notebook on a sunny desk.
|
||||||
|
**Background:** Light canvas (`#f4edf3`) with soft shadows and paper-like texture.
|
||||||
|
**Typography:** Geist for headings, system body. Generous whitespace.
|
||||||
|
**Color play:** Gold accents on light backgrounds, dark cards for contrast sections.
|
||||||
|
**Scrollytelling:** Smooth vertical reveals, product screenshot floats in center, feature cards slide in from sides.
|
||||||
|
|
||||||
|
**Why it could work:** Friendlier, less intimidating, closer to the app's light mode.
|
||||||
|
**Why I don't recommend it:** Every AI productivity tool (Motion, Granola, Reclaim) uses light backgrounds. You'd blend in.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 💬 USER REVIEW: Pick Option A or Option B, or ask me to combine elements. -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Page Architecture (Scrollytelling Flow)
|
||||||
|
|
||||||
|
The page scrolls through **7 chapters**. Each pinned section transitions to the next with a scroll-driven animation.
|
||||||
|
|
||||||
|
```
|
||||||
|
CHAPTER 1: THE HOOK
|
||||||
|
├── Full-screen dark canvas
|
||||||
|
├── Compass needle animates in (settles from rotation)
|
||||||
|
├── Tagline types in: "Meet your new chief of staff."
|
||||||
|
├── Headline fades up: "What if AI could be your secretary?"
|
||||||
|
├── Subheadline fades up
|
||||||
|
├── Email input + CTA
|
||||||
|
└── Scroll indicator: subtle down-arrow pulse
|
||||||
|
|
||||||
|
CHAPTER 2: THE PROBLEM (scroll-triggered)
|
||||||
|
├── Text fades in line by line as you scroll:
|
||||||
|
│ "Your important emails hide between newsletters."
|
||||||
|
│ "Your tasks live in three different apps."
|
||||||
|
│ "Meeting notes sit in a doc you'll never open again."
|
||||||
|
├── Each line appears with a slight delay, dimming the previous
|
||||||
|
└── Final line glows gold: "You need a secretary, not another tool."
|
||||||
|
|
||||||
|
CHAPTER 3: THE REVEAL (pinned section, parallax)
|
||||||
|
├── App screenshot rises from below (parallax entrance)
|
||||||
|
├── Three pillars appear around the screenshot:
|
||||||
|
│ 🧭 AI Secretary | 🔒 Private by Design | 🇪🇺 EU AI Act
|
||||||
|
├── Each pillar highlights on scroll with gold underline
|
||||||
|
└── Screenshot subtly shifts to show Daily Brief
|
||||||
|
|
||||||
|
CHAPTER 4: HOW IT WORKS (horizontal scroll-within-scroll)
|
||||||
|
├── 3 steps slide horizontally as you scroll vertically:
|
||||||
|
│ Step 1: Connect (Gmail/Outlook icons float in)
|
||||||
|
│ Step 2: Extract (animated lines flow from email → task cards)
|
||||||
|
│ Step 3: Brief (morning brief card materializes)
|
||||||
|
└── Progress bar at bottom fills gold as you move through steps
|
||||||
|
|
||||||
|
CHAPTER 5: FEATURES (staggered grid reveal)
|
||||||
|
├── Feature cards fade in one by one as you scroll
|
||||||
|
├── Each card: icon + title + one-line description
|
||||||
|
├── ✅ Beta features have a subtle green dot
|
||||||
|
├── 🔜 Coming Soon features have a pulsing amber dot
|
||||||
|
└── Hover/tap reveals expanded description
|
||||||
|
|
||||||
|
CHAPTER 6: THE FOUNDER (personal touch)
|
||||||
|
├── Clean section, minimal
|
||||||
|
├── Blockquote style with Roberto's note
|
||||||
|
├── "Built by an AI Enterprise Solution Architect"
|
||||||
|
└── Compass needle icon as pull-quote mark
|
||||||
|
|
||||||
|
CHAPTER 7: THE FINAL CTA (pinned, full-screen)
|
||||||
|
├── Dark full-bleed section
|
||||||
|
├── Large headline: "Be the first to meet your AI secretary."
|
||||||
|
├── Email input with animated border (gold shimmer)
|
||||||
|
├── "Beta launches June 2026"
|
||||||
|
└── Footer fades in below
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- 💬 USER REVIEW: Does this flow feel right? Any chapter you'd cut or add? -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Approach
|
||||||
|
|
||||||
|
| Concern | Solution |
|
||||||
|
|---------|----------|
|
||||||
|
| **Scrollytelling engine** | GSAP 3 + ScrollTrigger (already used on current site, proven, lightweight) |
|
||||||
|
| **Icons** | Lucide (already used on current site) |
|
||||||
|
| **Font** | Geist via Google Fonts or CDN (not @fontsource since this is a static HTML page) |
|
||||||
|
| **Email collection** | Simple `<form>` that POSTs to your API endpoint or a service like Buttondown/Mailchimp. I'll build the form shell — you wire it to your backend. |
|
||||||
|
| **Image** | Embed the app screenshot (`home.png`) inline. SVG logos embedded directly. |
|
||||||
|
| **Mobile** | Fully responsive. Scrollytelling degrades gracefully — pinned sections become normal scroll on mobile. |
|
||||||
|
| **Performance** | Single HTML file, no build step. GSAP loaded from CDN. Total page weight < 200KB. |
|
||||||
|
| **Accessibility** | `prefers-reduced-motion` disables scroll animations. Focus states on form. Contrast ratios verified. |
|
||||||
|
|
||||||
|
<!-- 💬 USER REVIEW: Any technical constraints I should know? Where should the waitlist emails go? -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverable
|
||||||
|
|
||||||
|
Once you approve a direction, I'll build the complete `website/index.html` — a single self-contained HTML file with:
|
||||||
|
- Embedded CSS (all custom properties from your app's palette)
|
||||||
|
- Embedded SVG logos (compass mark + wordmark)
|
||||||
|
- GSAP scrollytelling animations
|
||||||
|
- Responsive layout
|
||||||
|
- Working email form (shell)
|
||||||
|
- The app screenshot as the centerpiece visual
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Review the options above and tell me your direction. I'll start building immediately.*
|
||||||
35
docs/executive_assistant_scout.md
Normal file
35
docs/executive_assistant_scout.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
Ho bisogno che mi analizzi il documento `./docs/executive_assistant_scout.md`. Si tratta di una proposta di evoluzione del _UNIFIED_PROCESSING. Dovresti valutarla in funzione dell'attuale implementazione e rispondere alle domande nel file. Una volta consolidate le domande, devi generarmi un piano per l'implementazione.
|
||||||
|
|
||||||
|
### Gestione Agenti Autonomi
|
||||||
|
|
||||||
|
L'utente deve avere la possibilità di creare degli *Executive Assistant Scout*. Questi agenti devono essere autonomi nella ricerca e nell'estrapolazione di:
|
||||||
|
|
||||||
|
* **Task:** Creazione di attività che riguardano l'utente o modifica di quelle già presenti, con relative variazioni o inserimento di commenti. I task possono essere di competenza diretta dell'utente o di suo interesse (da assegnare quindi a un collaboratore, ma sempre afferenti al suo ruolo).
|
||||||
|
> *Ad esempio: se l'utente è Project Manager (PM) di un progetto e c'è un task importante che deve essere svolto dalla risorsa "X", il sistema deve creare il task all'interno di quel progetto e assegnarlo a "X".*
|
||||||
|
* **Eventi:** Tutte le fasi di un progetto, includendo attività (da data a data), milestone e checkpoint.
|
||||||
|
* **Note:** Informazioni riguardanti un progetto (ad esempio, le decisioni prese o l'architettura della soluzione).
|
||||||
|
* **Progetti:** L'insieme organizzato di task, eventi e note.
|
||||||
|
|
||||||
|
#### Funzionamento e Limitazioni
|
||||||
|
|
||||||
|
L'utente deve poter indicare una cartella (folder) di riferimento.
|
||||||
|
|
||||||
|
Una volta indicata la cartella, l'*Scout* deve poterla esaminare per identificare le tipologie di file presenti, analizzarli e definire una strategia di estrazione per task, timeline, note e progetti, adattandola in base al formato dei file.
|
||||||
|
|
||||||
|
Questa fase di configurazione deve avvenire in modo naturale per l'utente, similmente alla fase di on-boarding di OpenClaw. Una volta configurato, l'*Scout* dovrà procedere all'estrazione in totale autonomia. Idealmente, l'agente dovrebbe poter analizzare tutti i tipi di file.
|
||||||
|
|
||||||
|
#### Frequenza e Tracciamento
|
||||||
|
|
||||||
|
* **Sincronizzazione:** L'utente deve poter indicare la frequenza con cui l'*Scout* analizzerà i file. Nei cicli successivi, l'agente dovrà processare solamente le differenze (diff) rispetto al controllo precedente.
|
||||||
|
* **Tracciabilità:** Per ogni elemento estratto, il sistema deve mantenere il riferimento al documento sorgente da cui è stato ricavato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Questioni Aperte e Valutazioni
|
||||||
|
|
||||||
|
Per proseguire con la corretta implementazione, occorre definire i seguenti punti:
|
||||||
|
|
||||||
|
* **Navigazione Note di Progetto:** È preferibile implementare una Wiki oppure generare un file indice per le note di progetto, in modo da permettere una facile navigazione all'AI tra le informazioni (es. decisioni, architettura)?
|
||||||
|
* **Limiti di Sistema sulla Cartella (Folder):** Bisognerà stabilire come applicare dei limiti all'utente sulla cartella di riferimento. Quali parametri usiamo? Ad esempio: numero massimo di file, dimensione massima dei file, profondità e numero di sottocartelle, o limite complessivo di token utilizzabili per giorno o esecuzione?
|
||||||
|
* **Gestione Consumo Token:** Consentendo all'agente di analizzare tutti i tipi di file, occorre considerare che il consumo di token potrebbe aumentare drasticamente (es. elaborando documenti PDF o Word molto pesanti). Come vogliamo gestire o limitare questo aspetto?
|
||||||
|
* **Autonomia vs Controllo (Human-in-the-Loop):** Gli agenti devono essere completamente autonomi nella creazione di task, eventi e note a sistema, oppure dobbiamo prevedere l'implementazione di un meccanismo *Human-in-the-Loop* in cui l'utente riceve una proposta e deve approvarne preventivamente la creazione?
|
||||||
@@ -623,7 +623,7 @@
|
|||||||
<div class="section-head reveal">
|
<div class="section-head reveal">
|
||||||
<p class="label">Architettura</p>
|
<p class="label">Architettura</p>
|
||||||
<h2>Funzionalità Agentiche</h2>
|
<h2>Funzionalità Agentiche</h2>
|
||||||
<p class="subtitle">Cinque componenti AI distinti, ognuno con requisiti specifici di modello.</p>
|
<p class="subtitle">Sei componenti AI distinti, ognuno con requisiti specifici di modello.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
|
|
||||||
@@ -662,12 +662,23 @@
|
|||||||
|
|
||||||
<div class="feature-card reveal">
|
<div class="feature-card reveal">
|
||||||
<div class="feature-icon">⚙</div>
|
<div class="feature-icon">⚙</div>
|
||||||
<h3>Batch Agents</h3>
|
<h3>Background Agents</h3>
|
||||||
<p>Agenti schedulati per raccolta dati da filesystem locale e cloud (Gmail, Teams, Outlook). Cron-based.</p>
|
<p>Agenti schedulati per raccolta dati da filesystem locale e cloud (Gmail, Teams, Outlook). Loop tool-calling multi-turno, API Standard.</p>
|
||||||
<div class="feature-reqs">
|
<div class="feature-reqs">
|
||||||
|
<span class="req-tag">Tool Calling Multi-Turno</span>
|
||||||
<span class="req-tag">Output Strutturato</span>
|
<span class="req-tag">Output Strutturato</span>
|
||||||
<span class="req-tag">Tool Calling Robusto</span>
|
<span class="req-tag">API Standard</span>
|
||||||
<span class="req-tag">Esecuzione Lunga</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card reveal">
|
||||||
|
<div class="feature-icon">🛠</div>
|
||||||
|
<h3>Setup Agent</h3>
|
||||||
|
<p>Journey conversazionale interattiva per configurare un agente. L’utente risponde a domande guidate, il LLM esplora la directory e produce un <code>AgentConfig</code> JSON validato.</p>
|
||||||
|
<div class="feature-reqs">
|
||||||
|
<span class="req-tag">Conversazionale</span>
|
||||||
|
<span class="req-tag">Qualità Linguistica</span>
|
||||||
|
<span class="req-tag">Tool Calling + Reasoning</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -788,7 +799,8 @@
|
|||||||
<button class="tab-btn active" data-tab="home">Home Chat</button>
|
<button class="tab-btn active" data-tab="home">Home Chat</button>
|
||||||
<button class="tab-btn" data-tab="floating">Floating Chat</button>
|
<button class="tab-btn" data-tab="floating">Floating Chat</button>
|
||||||
<button class="tab-btn" data-tab="brief">Daily Brief</button>
|
<button class="tab-btn" data-tab="brief">Daily Brief</button>
|
||||||
<button class="tab-btn" data-tab="batch">Batch Agents</button>
|
<button class="tab-btn" data-tab="batch">Background Agents</button>
|
||||||
|
<button class="tab-btn" data-tab="setup">Setup Agent</button>
|
||||||
<button class="tab-btn" data-tab="embed">Embeddings</button>
|
<button class="tab-btn" data-tab="embed">Embeddings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -986,10 +998,13 @@
|
|||||||
<div class="table-wrap reveal">
|
<div class="table-wrap reveal">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>⚙ Batch Agents</h3>
|
<h3>⚙ Background Agents</h3>
|
||||||
<span class="desc">Tool Calling Robusto + Output Strutturato</span>
|
<span class="desc">Tool Calling Multi-Turno — API Standard (non Batch)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin: 0 0 16px; padding: 12px 18px; background: var(--warn-bg); border: 1px solid var(--warn-border); border-radius: var(--radius-sm); font-size: 0.85rem; color: var(--warn); line-height: 1.6;">
|
||||||
|
<strong>⚠ Nota architetturale:</strong> Il Batch API dei provider LLM <em>non è compatibile</em> con gli agenti di processing (<code>unified-processor</code>, <code>cloud-processor</code>). Il loop di tool-calling (fino a 12 turni per file) richiede risultati sincroni dal client Electron via WebSocket — un round-trip interattivo che il Batch API asincrono non supporta. Usare esclusivamente <strong>API Standard</strong>, a prezzi di listino senza sconto batch.
|
||||||
|
</div>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -998,24 +1013,24 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr style="background: rgba(52,211,153,0.04);">
|
<tr style="background: rgba(52,211,153,0.04);">
|
||||||
<td><strong>OpenAI</strong></td>
|
<td><strong>OpenAI</strong></td>
|
||||||
<td><span class="model-name">GPT-4.1 (Batch)</span></td>
|
<td><span class="model-name">GPT-4.1 Mini</span></td>
|
||||||
<td><span class="price" style="color:var(--green)">$1.00</span></td>
|
<td><span class="price" style="color:var(--green)">$0.40</span></td>
|
||||||
<td><span class="price" style="color:var(--green)">$4.00</span></td>
|
<td><span class="price" style="color:var(--green)">$1.60</span></td>
|
||||||
<td><strong>50% sconto batch</strong>, eccellente output strutturato</td>
|
<td><strong>Ottimo rapporto qualità/costo</strong>, tool calling affidabile, API Standard</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Anthropic</td>
|
<td>Anthropic</td>
|
||||||
<td><span class="model-name">Claude Sonnet 4.6 (Batch)</span></td>
|
<td><span class="model-name">Claude Sonnet 4.6</span></td>
|
||||||
<td><span class="price">$1.50</span></td>
|
<td><span class="price">$3.00</span></td>
|
||||||
<td><span class="price">$7.50</span></td>
|
<td><span class="price">$15.00</span></td>
|
||||||
<td>50% batch, tool use superiore, 300K output</td>
|
<td>Miglior tool use del mercato, se qualità è priorità assoluta</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Google</td>
|
<td>Google</td>
|
||||||
<td><span class="model-name">Gemini 2.5 Pro (Batch)</span></td>
|
<td><span class="model-name">Gemini 2.5 Flash</span></td>
|
||||||
<td><span class="price">$0.625</span></td>
|
<td><span class="price">$0.30</span></td>
|
||||||
<td><span class="price">$5.00</span></td>
|
<td><span class="price">$2.50</span></td>
|
||||||
<td>50% batch, alta qualità reasoning</td>
|
<td>Ottimo reasoning, tool calling affidabile, costo input molto basso</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Mistral</td>
|
<td>Mistral</td>
|
||||||
@@ -1026,10 +1041,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Groq</td>
|
<td>Groq</td>
|
||||||
<td><span class="model-name">Qwen3 32B (Batch)</span></td>
|
<td><span class="model-name">Qwen3 32B</span></td>
|
||||||
<td><span class="price">$0.145</span></td>
|
<td><span class="price">$0.29</span></td>
|
||||||
<td><span class="price">$0.295</span></td>
|
<td><span class="price">$0.59</span></td>
|
||||||
<td>50% batch, molto economico</td>
|
<td>Molto economico, velocità elevata; qualità tool calling inferiore ai proprietari</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Cerebras</td>
|
<td>Cerebras</td>
|
||||||
@@ -1044,6 +1059,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SETUP AGENT -->
|
||||||
|
<div class="tab-panel" id="tab-setup">
|
||||||
|
<div class="table-wrap reveal">
|
||||||
|
<div class="table-header">
|
||||||
|
<div>
|
||||||
|
<h3>🛠 Setup Agent</h3>
|
||||||
|
<span class="desc">Journey Conversazionale — Qualità Linguistica + Reasoning</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin: 0 0 16px; padding: 12px 18px; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.25); border-radius: var(--radius-sm); font-size: 0.85rem; color: var(--accent-3); line-height: 1.6;">
|
||||||
|
<strong>ℹ Profilo diverso dai Background Agents:</strong> Il setup è un’interazione <em>real-time con l’utente</em> (3–15 turni, <code>temperature=0.4</code>). Il volume è basso (poche sessioni per utente nel tempo), quindi il costo è trascurabile anche con modelli premium. Priorità: qualità della conversazione e accuratezza nel produrre l’<code>AgentConfig</code> JSON finale.
|
||||||
|
</div>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Provider</th><th>Modello</th><th>Input $/MTok</th><th>Output $/MTok</th><th>Motivazione</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="background: rgba(52,211,153,0.04);">
|
||||||
|
<td><strong>OpenAI</strong></td>
|
||||||
|
<td><span class="model-name">GPT-4.1</span></td>
|
||||||
|
<td><span class="price" style="color:var(--green)">$2.00</span></td>
|
||||||
|
<td><span class="price" style="color:var(--green)">$8.00</span></td>
|
||||||
|
<td><strong>Ottimo bilanciamento</strong> qualità/costo per conversazioni guidate, JSON output affidabile</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Anthropic</td>
|
||||||
|
<td><span class="model-name">Claude Sonnet 4.6</span></td>
|
||||||
|
<td><span class="price">$3.00</span></td>
|
||||||
|
<td><span class="price">$15.00</span></td>
|
||||||
|
<td>Massima qualità conversazionale e instruction-following; costo giustificato dalla rarità delle sessioni</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Google</td>
|
||||||
|
<td><span class="model-name">Gemini 2.5 Flash</span></td>
|
||||||
|
<td><span class="price">$0.30</span></td>
|
||||||
|
<td><span class="price">$2.50</span></td>
|
||||||
|
<td>Buona qualità conversazionale a costo molto basso; opzione se si vuole contenere ogni spesa</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>OpenAI</td>
|
||||||
|
<td><span class="model-name">GPT-4.1 Mini</span></td>
|
||||||
|
<td><span class="price">$0.40</span></td>
|
||||||
|
<td><span class="price">$1.60</span></td>
|
||||||
|
<td>Alternativa budget; qualità conversazionale sufficiente, JSON output meno affidabile</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Mistral</td>
|
||||||
|
<td><span class="model-name">Mistral Large 3</span></td>
|
||||||
|
<td><span class="price">$2.00</span></td>
|
||||||
|
<td><span class="price">$6.00</span></td>
|
||||||
|
<td>EU data residency; buona qualità per il setup journey</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Groq / Cerebras</td>
|
||||||
|
<td class="na-cell">—</td>
|
||||||
|
<td class="na-cell">—</td>
|
||||||
|
<td class="na-cell">—</td>
|
||||||
|
<td class="na-cell">Non consigliati: qualità conversazionale insufficiente per journey multi-turno</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- EMBEDDINGS -->
|
<!-- EMBEDDINGS -->
|
||||||
<div class="tab-panel" id="tab-embed">
|
<div class="tab-panel" id="tab-embed">
|
||||||
<div class="table-wrap reveal">
|
<div class="table-wrap reveal">
|
||||||
@@ -1097,14 +1178,14 @@
|
|||||||
<div class="section-head reveal">
|
<div class="section-head reveal">
|
||||||
<p class="label">Simulazione</p>
|
<p class="label">Simulazione</p>
|
||||||
<h2>Stima Costi Mensili per Utente</h2>
|
<h2>Stima Costi Mensili per Utente</h2>
|
||||||
<p class="subtitle">Basata su un utilizzo tipico: 500 home, 300 floating, 210 brief, 100 batch, 1000 embeddings al mese.</p>
|
<p class="subtitle">Basata su un utilizzo tipico: 500 home, 300 floating, 210 brief, 100 background agent runs, 10 setup turns (≈2 sessioni), 1000 embeddings al mese.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-wrap reveal">
|
<div class="table-wrap reveal">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>Calcolo Dettagliato</h3>
|
<h3>Calcolo Dettagliato</h3>
|
||||||
<span class="desc">Token medi: Home 2K/1K • Floating 500/300 • Brief 1.5K/500 • Batch 3K/2K • Embed 500</span>
|
<span class="desc">Token medi: Home 2K/1K • Floating 500/300 • Brief 1.5K/500 • Background Agent 3K/2K • Setup 4K/500 • Embed 500</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cost-bars" id="costChart">
|
<div class="cost-bars" id="costChart">
|
||||||
@@ -1152,13 +1233,22 @@
|
|||||||
<td><span class="price">$0.032 + $0.042 = $0.07</span></td>
|
<td><span class="price">$0.032 + $0.042 = $0.07</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Batch Agents</td>
|
<td>Background Agents</td>
|
||||||
<td>OpenAI</td>
|
<td>OpenAI</td>
|
||||||
<td><span class="model-name">GPT-4.1 (Batch)</span></td>
|
<td><span class="model-name">GPT-4.1 Mini</span></td>
|
||||||
<td>100</td>
|
<td>100</td>
|
||||||
<td>300K</td>
|
<td>300K</td>
|
||||||
<td>200K</td>
|
<td>200K</td>
|
||||||
<td><span class="price">$0.30 + $0.80 = $1.10</span></td>
|
<td><span class="price">$0.12 + $0.32 = $0.44</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Setup Agent</td>
|
||||||
|
<td>OpenAI</td>
|
||||||
|
<td><span class="model-name">GPT-4.1</span></td>
|
||||||
|
<td>10 turns</td>
|
||||||
|
<td>40K</td>
|
||||||
|
<td>5K</td>
|
||||||
|
<td><span class="price">$0.08 + $0.04 = $0.12</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Embeddings</td>
|
<td>Embeddings</td>
|
||||||
@@ -1171,7 +1261,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="background: var(--surface-2);">
|
<tr style="background: var(--surface-2);">
|
||||||
<td colspan="6" style="text-align:right; font-weight:600; color:var(--ink);">Totale Mensile per Utente</td>
|
<td colspan="6" style="text-align:right; font-weight:600; color:var(--ink);">Totale Mensile per Utente</td>
|
||||||
<td><span class="price" style="color:var(--green); font-size:1rem;">~$2.78</span></td>
|
<td><span class="price" style="color:var(--green); font-size:1rem;">~$2.24</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -1202,12 +1292,13 @@
|
|||||||
<li><span class="fn">Home Chat</span> <span class="mdl">Gemini 2.5 Flash</span></li>
|
<li><span class="fn">Home Chat</span> <span class="mdl">Gemini 2.5 Flash</span></li>
|
||||||
<li><span class="fn">Floating Chat</span> <span class="mdl">Gemini 2.5 Flash-Lite</span></li>
|
<li><span class="fn">Floating Chat</span> <span class="mdl">Gemini 2.5 Flash-Lite</span></li>
|
||||||
<li><span class="fn">Daily Brief</span> <span class="mdl">GPT-4.1 Nano</span></li>
|
<li><span class="fn">Daily Brief</span> <span class="mdl">GPT-4.1 Nano</span></li>
|
||||||
<li><span class="fn">Batch Agents</span> <span class="mdl">GPT-4.1 Batch</span></li>
|
<li><span class="fn">Background Agents</span> <span class="mdl">GPT-4.1 Mini</span></li>
|
||||||
|
<li><span class="fn">Setup Agent</span> <span class="mdl">GPT-4.1</span></li>
|
||||||
<li><span class="fn">Embeddings</span> <span class="mdl">text-embedding-3-small</span></li>
|
<li><span class="fn">Embeddings</span> <span class="mdl">text-embedding-3-small</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="strategy-cost">
|
<div class="strategy-cost">
|
||||||
<span class="cost-label">Costo stimato/utente/mese</span>
|
<span class="cost-label">Costo stimato/utente/mese</span>
|
||||||
<span class="cost-value highlight">~$2.78</span>
|
<span class="cost-value highlight">~$2.24</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="strategy-pros"><strong>Pro:</strong> Costo ottimale, qualità massima per feature. <strong>Contro:</strong> 2 API key da gestire (Google + OpenAI).</p>
|
<p class="strategy-pros"><strong>Pro:</strong> Costo ottimale, qualità massima per feature. <strong>Contro:</strong> 2 API key da gestire (Google + OpenAI).</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1221,14 +1312,15 @@
|
|||||||
<li><span class="fn">Home Chat</span> <span class="mdl">Llama 3.3 70B</span></li>
|
<li><span class="fn">Home Chat</span> <span class="mdl">Llama 3.3 70B</span></li>
|
||||||
<li><span class="fn">Floating Chat</span> <span class="mdl">Llama 4 Scout</span></li>
|
<li><span class="fn">Floating Chat</span> <span class="mdl">Llama 4 Scout</span></li>
|
||||||
<li><span class="fn">Daily Brief</span> <span class="mdl">Llama 3.1 8B</span></li>
|
<li><span class="fn">Daily Brief</span> <span class="mdl">Llama 3.1 8B</span></li>
|
||||||
<li><span class="fn">Batch Agents</span> <span class="mdl">Qwen3 32B Batch</span></li>
|
<li><span class="fn">Background Agents</span> <span class="mdl">Qwen3 32B</span></li>
|
||||||
|
<li><span class="fn">Setup Agent</span> <span class="mdl">GPT-4.1 Mini</span></li>
|
||||||
<li><span class="fn">Embeddings</span> <span class="mdl">OpenAI (esterno)</span></li>
|
<li><span class="fn">Embeddings</span> <span class="mdl">OpenAI (esterno)</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="strategy-cost">
|
<div class="strategy-cost">
|
||||||
<span class="cost-label">Costo stimato/utente/mese</span>
|
<span class="cost-label">Costo stimato/utente/mese</span>
|
||||||
<span class="cost-value" style="color:var(--blue);">~$1.05</span>
|
<span class="cost-value" style="color:var(--blue);">~$1.30</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="strategy-pros"><strong>Pro:</strong> Ultra economico, velocità record (394–840 TPS). <strong>Contro:</strong> Qualità tool calling inferiore ai proprietari. Serve OpenAI per embeddings.</p>
|
<p class="strategy-pros"><strong>Pro:</strong> Ultra economico, velocità record (394–840 TPS). <strong>Contro:</strong> Qualità tool calling inferiore ai proprietari. Serve OpenAI per embeddings e setup.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ENTERPRISE -->
|
<!-- ENTERPRISE -->
|
||||||
@@ -1240,14 +1332,15 @@
|
|||||||
<li><span class="fn">Home Chat</span> <span class="mdl">GPT-4.1</span></li>
|
<li><span class="fn">Home Chat</span> <span class="mdl">GPT-4.1</span></li>
|
||||||
<li><span class="fn">Floating Chat</span> <span class="mdl">GPT-4.1 Mini</span></li>
|
<li><span class="fn">Floating Chat</span> <span class="mdl">GPT-4.1 Mini</span></li>
|
||||||
<li><span class="fn">Daily Brief</span> <span class="mdl">GPT-4.1 Nano</span></li>
|
<li><span class="fn">Daily Brief</span> <span class="mdl">GPT-4.1 Nano</span></li>
|
||||||
<li><span class="fn">Batch Agents</span> <span class="mdl">GPT-4.1 Batch</span></li>
|
<li><span class="fn">Background Agents</span> <span class="mdl">GPT-4.1 Mini</span></li>
|
||||||
|
<li><span class="fn">Setup Agent</span> <span class="mdl">GPT-4.1</span></li>
|
||||||
<li><span class="fn">Embeddings</span> <span class="mdl">text-embedding-3-small</span></li>
|
<li><span class="fn">Embeddings</span> <span class="mdl">text-embedding-3-small</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="strategy-cost">
|
<div class="strategy-cost">
|
||||||
<span class="cost-label">Costo stimato/utente/mese</span>
|
<span class="cost-label">Costo stimato/utente/mese</span>
|
||||||
<span class="cost-value" style="color:var(--warn);">~$6.20</span>
|
<span class="cost-value" style="color:var(--warn);">~$6.85</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="strategy-pros"><strong>Pro:</strong> Ecosistema unificato, ZDR, affidabilità massima, 1 sola API key. <strong>Contro:</strong> Costo 2–6x superiore alle alternative.</p>
|
<p class="strategy-pros"><strong>Pro:</strong> Ecosistema unificato, ZDR, affidabilità massima, 1 sola API key. <strong>Contro:</strong> Costo 3–7x superiore alle alternative.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1283,8 +1376,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="why-card reveal">
|
<div class="why-card reveal">
|
||||||
<h4>⚙ GPT-4.1 Batch per Agenti</h4>
|
<h4>⚙ GPT-4.1 Mini (Standard) per Background Agents</h4>
|
||||||
<p>Gli agenti batch non richiedono risposta in tempo reale. Lo <strong>sconto batch 50%</strong> di OpenAI rende GPT-4.1 imbattibile a <span class="highlight-model">$1.00/$4.00</span>. Il suo output strutturato e tool calling sono tra i migliori del mercato, cruciali per operazioni CRUD affidabili.</p>
|
<p>Il Batch API dei provider LLM <strong>non è applicabile</strong> agli agenti di processing: il loop tool-calling (<code>unified-processor</code>, <code>cloud-processor</code>) richiede fino a 12 turni sincroni per file, con ogni risultato di tool restituito dal client Electron via WebSocket prima che parta il turno successivo — incompatibile con il modello asincrono e fire-and-forget del Batch API. Si usa quindi l’<strong>API Standard</strong>. GPT-4.1 Mini a <span class="highlight-model">$0.40/$1.60</span> offre un ottimo bilanciamento: tool calling affidabile per operazioni CRUD multi-step, output strutturato consistente, e costo contenuto che non subisce la moltiplicazione del loop (ogni file può generare più chiamate LLM in sequenza).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="why-card reveal">
|
||||||
|
<h4>🛠 GPT-4.1 per Setup Agent</h4>
|
||||||
|
<p>Il setup journey è fondamentalmente diverso dagli agenti di processing: è una <strong>conversazione interattiva real-time</strong> con l’utente (3–15 turni, <code>temperature=0.4</code>) che deve guidare con domande sensate, esplorare la directory con tool calling e produrre un <code>AgentConfig</code> JSON valido alla fine. GPT-4.1 a <span class="highlight-model">$2.00/$8.00</span> è la scelta giusta: qualità conversazionale e instruction-following superiori a Mini, con un impatto sul costo <strong>trascurabile</strong> dato il basso volume (≈2 sessioni/mese per utente). Usare GPT-4.1 Mini per risparmiare $0.09/mese non vale la degradazione nell’UX del setup.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="why-card reveal">
|
<div class="why-card reveal">
|
||||||
@@ -1374,12 +1472,14 @@
|
|||||||
|
|
||||||
// ── Cost Chart ────────────────────────────────────
|
// ── Cost Chart ────────────────────────────────────
|
||||||
// Usage: 500 home (2K in + 1K out), 300 floating (500 in + 300 out),
|
// Usage: 500 home (2K in + 1K out), 300 floating (500 in + 300 out),
|
||||||
// 210 brief (1.5K in + 500 out), 100 batch (3K in + 2K out), 1000 embeds (500 in)
|
// 210 brief (1.5K in + 500 out), 100 background agent runs (3K in + 2K out),
|
||||||
|
// 10 setup turns (2 sessioni × 5 turni, 4K in + 500 out), 1000 embeds (500 in)
|
||||||
const usage = {
|
const usage = {
|
||||||
home: { msgs: 500, inTok: 2000, outTok: 1000 },
|
home: { msgs: 500, inTok: 2000, outTok: 1000 },
|
||||||
float: { msgs: 300, inTok: 500, outTok: 300 },
|
float: { msgs: 300, inTok: 500, outTok: 300 },
|
||||||
brief: { msgs: 210, inTok: 1500, outTok: 500 },
|
brief: { msgs: 210, inTok: 1500, outTok: 500 },
|
||||||
batch: { msgs: 100, inTok: 3000, outTok: 2000 },
|
batch: { msgs: 100, inTok: 3000, outTok: 2000 },
|
||||||
|
setup: { msgs: 10, inTok: 4000, outTok: 500 },
|
||||||
embed: { msgs: 1000, inTok: 500 }
|
embed: { msgs: 1000, inTok: 500 }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1390,36 +1490,45 @@
|
|||||||
return inCost + outCost;
|
return inCost + outCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nota: il Batch API LLM non è compatibile con gli agenti di processing (loop
|
||||||
|
// tool-calling sincrono). I prezzi degli agenti usano l'API Standard, non batch.
|
||||||
|
// Setup agent usa un modello di qualità superiore (interattivo, basso volume).
|
||||||
const strategies = [
|
const strategies = [
|
||||||
{
|
{
|
||||||
name: 'Multi-Provider',
|
name: 'Multi-Provider',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
cost: calcCost('home', 0.30, 2.50) + calcCost('float', 0.10, 0.40) + calcCost('brief', 0.10, 0.40) + calcCost('batch', 1.00, 4.00) + (1000 * 500 / 1e6) * 0.02
|
// agents: GPT-4.1 Mini ($0.40/$1.60) | setup: GPT-4.1 ($2.00/$8.00)
|
||||||
|
cost: calcCost('home', 0.30, 2.50) + calcCost('float', 0.10, 0.40) + calcCost('brief', 0.10, 0.40) + calcCost('batch', 0.40, 1.60) + calcCost('setup', 2.00, 8.00) + (1000 * 500 / 1e6) * 0.02
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Groq Budget',
|
name: 'Groq Budget',
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
cost: calcCost('home', 0.59, 0.79) + calcCost('float', 0.11, 0.34) + calcCost('brief', 0.05, 0.08) + calcCost('batch', 0.145, 0.295) + (1000 * 500 / 1e6) * 0.02
|
// agents: Qwen3 32B ($0.29/$0.59) | setup: GPT-4.1 Mini ($0.40/$1.60, esterno)
|
||||||
|
cost: calcCost('home', 0.59, 0.79) + calcCost('float', 0.11, 0.34) + calcCost('brief', 0.05, 0.08) + calcCost('batch', 0.29, 0.59) + calcCost('setup', 0.40, 1.60) + (1000 * 500 / 1e6) * 0.02
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OpenAI Enterprise',
|
name: 'OpenAI Enterprise',
|
||||||
color: 'amber',
|
color: 'amber',
|
||||||
cost: calcCost('home', 2.00, 8.00) + calcCost('float', 0.40, 1.60) + calcCost('brief', 0.10, 0.40) + calcCost('batch', 1.00, 4.00) + (1000 * 500 / 1e6) * 0.02
|
// agents: GPT-4.1 Mini ($0.40/$1.60) | setup: GPT-4.1 ($2.00/$8.00)
|
||||||
|
cost: calcCost('home', 2.00, 8.00) + calcCost('float', 0.40, 1.60) + calcCost('brief', 0.10, 0.40) + calcCost('batch', 0.40, 1.60) + calcCost('setup', 2.00, 8.00) + (1000 * 500 / 1e6) * 0.02
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Anthropic Full',
|
name: 'Anthropic Full',
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
cost: calcCost('home', 3.00, 15.00) + calcCost('float', 1.00, 5.00) + calcCost('brief', 1.00, 5.00) + calcCost('batch', 1.50, 7.50) + (1000 * 500 / 1e6) * 0.02
|
// agents: Claude Sonnet 4.6 ($3.00/$15.00) | setup: Claude Sonnet 4.6
|
||||||
|
cost: calcCost('home', 3.00, 15.00) + calcCost('float', 1.00, 5.00) + calcCost('brief', 1.00, 5.00) + calcCost('batch', 3.00, 15.00) + calcCost('setup', 3.00, 15.00) + (1000 * 500 / 1e6) * 0.02
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Mistral EU',
|
name: 'Mistral EU',
|
||||||
color: 'teal',
|
color: 'teal',
|
||||||
cost: calcCost('home', 1.00, 3.00) + calcCost('float', 0.20, 0.60) + calcCost('brief', 0.20, 0.60) + calcCost('batch', 2.00, 6.00) + (1000 * 500 / 1e6) * 0.02
|
// agents: Mistral Large 3 ($2.00/$6.00) | setup: Mistral Large 3
|
||||||
|
cost: calcCost('home', 1.00, 3.00) + calcCost('float', 0.20, 0.60) + calcCost('brief', 0.20, 0.60) + calcCost('batch', 2.00, 6.00) + calcCost('setup', 2.00, 6.00) + (1000 * 500 / 1e6) * 0.02
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Google Full',
|
name: 'Google Full',
|
||||||
color: 'pink',
|
color: 'pink',
|
||||||
cost: calcCost('home', 0.30, 2.50) + calcCost('float', 0.10, 0.40) + calcCost('brief', 0.10, 0.40) + calcCost('batch', 0.625, 5.00) + (1000 * 500 / 1e6) * 0.15
|
// agents: Gemini 2.5 Flash ($0.30/$2.50) | setup: Gemini 2.5 Flash
|
||||||
|
cost: calcCost('home', 0.30, 2.50) + calcCost('float', 0.10, 0.40) + calcCost('brief', 0.10, 0.40) + calcCost('batch', 0.30, 2.50) + calcCost('setup', 0.30, 2.50) + (1000 * 500 / 1e6) * 0.15
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
418
docs/llm-provider-report.md
Normal file
418
docs/llm-provider-report.md
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
# Report Provider LLM per adiuvAI — Aprile 2026
|
||||||
|
|
||||||
|
> Analisi comparativa dei provider per i **11 agenti AI** configurati in `api/.env.example`. Selezione ottimizzata per costo, qualità, latenza e privacy dei dati, con **due mapping distinti**: Production (Zero Data Retention obbligatorio) e Development (cost-efficient).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architettura — Gli 11 Agenti
|
||||||
|
|
||||||
|
Ogni variabile `LLM_MODEL_*` in `api/.env.example` controlla un agente con profilo d'uso specifico. Analisi dei requisiti emersa dall'ispezione del codice in `api/app/agents/`, `api/app/core/` e `api/app/memory/`.
|
||||||
|
|
||||||
|
| # | Env Var | Agente | Tool Calling | Latenza | Qualità | Volume/utente | Real-time? |
|
||||||
|
|---|---------|--------|--------------|---------|---------|--------------|------------|
|
||||||
|
| 1 | `LLM_MODEL_CLASSIFIER` | **Intent Classifier** — smista i messaggi del floating panel verso task/project/note/timeline | No, output JSON | Alta (<200 ms) | Bassa (output deterministico) | Alto | ✓ Sì |
|
||||||
|
| 2 | `LLM_MODEL_HOME_AGENT` | **Home Agent** — chat principale con tutti i tool (CRUD task/project/note) | Multi-turno (≤6 step) | Alta (<3 s perceived) | Alta (user-facing) | Alto | ✓ Sì, WS stream |
|
||||||
|
| 3 | `LLM_MODEL_FLOATING_AGENT` | **Floating Agent** — chat contestuale da task/project/note | Multi-turno (≤6 step) | Alta | Media–alta | Alto | ✓ Sì, WS stream |
|
||||||
|
| 4 | `LLM_MODEL_UNIFIED_PROCESSOR` | **Unified Processor** — processa file del filesystem locale | Tool loop (≤12 step) | Bassa (batch) | Media | Medio/occasionale | ✗ Background |
|
||||||
|
| 5 | `LLM_MODEL_CLOUD_PROCESSOR` | **Cloud Processor** — fetch e processing di Gmail/Teams/Outlook | Tool loop (≤12 step) | Bassa | Media | Schedulato | ✗ Background |
|
||||||
|
| 6 | `LLM_MODEL_BRIEF_AGENT` | **Brief Agent** — daily brief home + project (streaming, read-only tool) | Singolo step, tool read-only | Alta (<4 s) | Alta (prosa curata) | Medio | ✓ Sì |
|
||||||
|
| 7 | `LLM_MODEL_SETUP_AGENT` | **Setup Agent** — journey conversazionale per costruire `AgentConfig` JSON | Multi-turno (≤15) | Media | Alta (UX critica) | Basso (una tantum) | ✓ Sì, WS |
|
||||||
|
| 8 | `LLM_MODEL_MEMORY_EXTRACTOR` | **Memory Extractor** — pipeline Mem0 extract+decide (2 call/turno) | No, JSON strutturato | Bassa (off-path) | Bassa (filtrato a valle) | Alto (ogni turno chat) | ✗ Background |
|
||||||
|
| 9 | `LLM_MODEL_MEMORY_MINER` | **Memory Miner** — pattern mining orario su storia episodica (Power+) | No | Bassa | Media | Orario (Power+) | ✗ Cron |
|
||||||
|
| 10 | `LLM_MODEL_MEMORY_AUDITOR` | **Memory Auditor** — audit settimanale: contraddizioni + canonicalizzazione relazioni | No, reasoning su fatti | Bassa | Alta (richiede reasoning) | Settimanale | ✗ Cron |
|
||||||
|
| 11 | `LLM_EMBED_MODEL` | **Embeddings** — vettori 1536-dim per ricerca semantica (LanceDB/Qdrant) | — | Media | Deterministico | Alto | ✓ In-request |
|
||||||
|
|
||||||
|
> **Nota architetturale (Processors):** Il Batch API dei provider LLM **non è utilizzabile** per Unified e Cloud Processor: il loop tool-calling richiede risultati sincroni dal client Electron via WebSocket. Si usa **API Standard** a prezzi di listino.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conformità — Policy Privacy dei Provider
|
||||||
|
|
||||||
|
Per Production è richiesto un **strict Zero Data Retention** (ZDR): nessuna conservazione prompt/response, nessun logging, nessun uso per training — garantito contrattualmente. Per Development è sufficiente l'opt-out di default dal training.
|
||||||
|
|
||||||
|
| Provider | Sede | ZDR strict (prod) | Opt-out training (dev) | Note |
|
||||||
|
|----------|------|:-----------------:|:----------------------:|------|
|
||||||
|
| 🇺🇸 OpenAI | USA | ✓ con **ZDR addendum Enterprise** | ✓ default API | Standard API: 30gg retention logs |
|
||||||
|
| 🇺🇸 Anthropic | USA | ✓ con **Enterprise ZDR** | ✓ default API | 30gg retention su standard tier |
|
||||||
|
| 🇺🇸 Google Vertex AI | USA | ✓ **contrattuale** (Vertex, non AI Studio) | ✓ paid tier | Free AI Studio usa dati per training |
|
||||||
|
| 🇫🇷 Mistral | Francia (EU) | ✓ **ZDR disponibile** | ✓ default | GDPR-native, ottimo per EU residency |
|
||||||
|
| 🇺🇸 Groq | USA | ✓ **via DPA dedicato** | ✓ default | Cloud inference Llama/Qwen |
|
||||||
|
| 🇺🇸 Cerebras | USA | ✓ **nessuna conservazione by default** | ✓ | ZDR out-of-the-box — il più rigoroso |
|
||||||
|
| 🇺🇸 Voyage AI | USA | ✓ ZDR enterprise | ✓ | Embeddings only |
|
||||||
|
| 🇨🇳 DeepSeek | Cina | ✗ | ⚠️ opt-out limitato, dati in Cina | **Solo Dev, con dati sintetici** |
|
||||||
|
| 🇨🇳 Zhipu (GLM) | Cina | ✗ | ⚠️ non verificabile | **Solo Dev** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Confronto Modelli — Miglior Modello per Agente
|
||||||
|
|
||||||
|
Prezzi in USD per milione di token (MTok), aggiornati Aprile 2026.
|
||||||
|
|
||||||
|
### 1. Intent Classifier
|
||||||
|
*Output JSON deterministico, latenza critica, volume alto*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| OpenAI | `GPT-4.1 Nano` | $0.10 | $0.40 | ✓ ent | Veloce, JSON mode affidabile |
|
||||||
|
| **Google Vertex** ⭐ | `Gemini 2.5 Flash-Lite` | **$0.10** | **$0.40** | ✓ | **Migliore prezzo+ZDR+latenza** |
|
||||||
|
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ✓ ent | Overkill per pura classificazione |
|
||||||
|
| Groq | `Llama 3.1 8B` | $0.05 | $0.08 | ✓ DPA | Economico, 840 TPS |
|
||||||
|
| Cerebras | `Llama 3.1 8B` | $0.10 | $0.10 | ✓ | ZDR by default, velocissimo |
|
||||||
|
|
||||||
|
### 2. Home Agent
|
||||||
|
*Multi-turno + tool calling completo + streaming*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **Anthropic** ⭐ | `Claude Sonnet 4.6` | $3.00 | $15.00 | ✓ ent | **Top tool use**, caching -90%, 1M ctx |
|
||||||
|
| OpenAI | `GPT-4.1` | $2.00 | $8.00 | ✓ ent | Solido, JSON mode, 1M ctx |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | ✓ | Miglior rapporto Q/P per prod |
|
||||||
|
| Mistral | `Mistral Medium 3` | $1.00 | $3.00 | ✓ | EU residency |
|
||||||
|
| Groq | `Llama 3.3 70B` | $0.59 | $0.79 | ✓ DPA | Tool calling inferiore ai proprietari |
|
||||||
|
|
||||||
|
### 3. Floating Agent
|
||||||
|
*Single+multi-turno, contestuale, più compatto del Home*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| OpenAI | `GPT-4.1 Mini` | $0.40 | $1.60 | ✓ ent | Bilanciato |
|
||||||
|
| **Anthropic** ⭐ | `Claude Haiku 4.5` | $1.00 | $5.00 | ✓ ent | **Tool use affidabile, bassa latenza** |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash-Lite` | $0.10 | $0.40 | ✓ | Economico; qualità tool inferiore a Haiku |
|
||||||
|
| Mistral | `Mistral Small 3.1` | $0.20 | $0.60 | ✓ | EU |
|
||||||
|
|
||||||
|
### 4. Unified Processor (locale)
|
||||||
|
*Batch, multi-turno (≤12), qualità estrazione importa*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ✓ ent | **Tool loop affidabile, costo contenuto** |
|
||||||
|
| Anthropic | `Claude Sonnet 4.6` | $3.00 | $15.00 | ✓ ent | Qualità top se budget permette |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | ✓ | Valida alternativa, input economico |
|
||||||
|
| Mistral | `Mistral Large 3` | $2.00 | $6.00 | ✓ | EU residency |
|
||||||
|
|
||||||
|
### 5. Cloud Processor (Gmail/Teams/Outlook)
|
||||||
|
*Batch, multi-turno (≤12), dati sensibili*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ✓ ent | **Robusto su email parsing** |
|
||||||
|
| Anthropic | `Claude Sonnet 4.6` | $3.00 | $15.00 | ✓ ent | Miglior reasoning su thread email |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | ✓ | 1M context utile per thread lunghi |
|
||||||
|
|
||||||
|
### 6. Brief Agent (daily brief)
|
||||||
|
*Singolo step, prosa curata, read-only tool*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ✓ ent | **Prosa di qualità, streaming affidabile** |
|
||||||
|
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ✓ ent | Prosa eccellente, più costoso |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | ✓ | Alternativa economica |
|
||||||
|
| Mistral | `Mistral Small 3.1` | $0.20 | $0.60 | ✓ | Economico con EU residency |
|
||||||
|
|
||||||
|
### 7. Setup Agent (journey di configurazione)
|
||||||
|
*Conversazione multi-turno (≤15), JSON finale, UX critica*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **Anthropic** ⭐ | `Claude Sonnet 4.6` | **$3.00** | **$15.00** | ✓ ent | **Miglior instruction-following, JSON affidabile** |
|
||||||
|
| OpenAI | `GPT-4.1` | $2.00 | $8.00 | ✓ ent | Eccellente bilanciamento |
|
||||||
|
| Google Vertex | `Gemini 2.5 Pro` | $1.25 | $10.00 | ✓ | Reasoning solido |
|
||||||
|
|
||||||
|
> Volume bassissimo (≈2 sessioni/mese per utente): il costo è trascurabile anche col modello premium.
|
||||||
|
|
||||||
|
### 8. Memory Extractor (Mem0 extract+decide)
|
||||||
|
*2 call/turno, JSON strutturato, deterministico, off request-path*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `GPT-4.1 Nano` | **$0.10** | **$0.40** | ✓ ent | **Cheapest OpenAI, JSON mode affidabile** |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash-Lite` | $0.10 | $0.40 | ✓ | Pari prezzo, valido |
|
||||||
|
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ✓ ent | Troppo costoso per volume alto |
|
||||||
|
|
||||||
|
### 9. Memory Miner (cron orario, Power+)
|
||||||
|
*Pattern mining su episodi, input medio, occasionale*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ✓ ent | **Reasoning sufficiente per pattern detection** |
|
||||||
|
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | ✓ | Buona alternativa |
|
||||||
|
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ✓ ent | Qualità superiore, costo +2x |
|
||||||
|
|
||||||
|
### 10. Memory Auditor (cron settimanale)
|
||||||
|
*Reasoning per contraddizioni + canonicalizzazione, rarissimo*
|
||||||
|
|
||||||
|
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|
||||||
|
|----------|---------|----------|-----------|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `GPT-4.1` | **$2.00** | **$8.00** | ✓ ent | **Reasoning robusto, volume trascurabile** |
|
||||||
|
| Anthropic | `Claude Sonnet 4.6` | $3.00 | $15.00 | ✓ ent | Alternativa premium |
|
||||||
|
| Google Vertex | `Gemini 2.5 Pro` | $1.25 | $10.00 | ✓ | Ottimo reasoning |
|
||||||
|
|
||||||
|
### 11. Embeddings
|
||||||
|
*Semantic search, 1536-dim, volume alto*
|
||||||
|
|
||||||
|
| Provider | Modello | $/MTok | Dim | ZDR | Note |
|
||||||
|
|----------|---------|--------|-----|:---:|------|
|
||||||
|
| **OpenAI** ⭐ | `text-embedding-3-small` | **$0.02** | 1536 | ✓ ent | **Standard de facto, già in uso (LanceDB 1536-dim)** |
|
||||||
|
| Voyage AI | `voyage-3.5-lite` | $0.02 | 1024 | ✓ | Qualità superiore ma richiede reindex |
|
||||||
|
| Google Vertex | `Gemini Embedding` | $0.15 | variabile | ✓ | 7.5x più costoso, nessun vantaggio |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Mapping Production (Zero Data Retention obbligatorio)
|
||||||
|
|
||||||
|
Tutti i provider selezionati hanno ZDR contrattualmente garantito. Prevalgono qualità e affidabilità; il costo è ottimizzato entro il vincolo ZDR.
|
||||||
|
|
||||||
|
| # | Agente | Provider | Modello | Razionale |
|
||||||
|
|---|--------|----------|---------|-----------|
|
||||||
|
| 1 | Classifier | **OpenAI** | `gpt-4.1-nano` | $0.10/$0.40, JSON mode affidabile, stesso contratto ZDR del resto OpenAI |
|
||||||
|
| 2 | Home Agent | **Anthropic** | `claude-sonnet-4-6` | Miglior tool calling del mercato; caching -90% riduce il costo; esperienza chat premium |
|
||||||
|
| 3 | Floating Agent | **Anthropic** | `claude-haiku-4-5` | Tool calling affidabile + bassa latenza; qualità coerente con Home Agent |
|
||||||
|
| 4 | Unified Processor | **OpenAI** | `gpt-4.1-mini` | Tool loop affidabile a costo contenuto; critico visto che il loop moltiplica le call |
|
||||||
|
| 5 | Cloud Processor | **OpenAI** | `gpt-4.1-mini` | Stesso profilo del locale; parsing di email/chat consolidato |
|
||||||
|
| 6 | Brief Agent | **OpenAI** | `gpt-4.1-mini` | Prosa curata, streaming, read-only tools — ottimo bilanciamento |
|
||||||
|
| 7 | Setup Agent | **Anthropic** | `claude-sonnet-4-6` | Journey conversazionale critica per UX; volume bassissimo giustifica il premium |
|
||||||
|
| 8 | Memory Extractor | **OpenAI** | `gpt-4.1-nano` | 2 call per turno chat: servono i prezzi più bassi con JSON mode |
|
||||||
|
| 9 | Memory Miner | **OpenAI** | `gpt-4.1-mini` | Cron orario su Power+: reasoning sufficiente, costo contenuto |
|
||||||
|
| 10 | Memory Auditor | **OpenAI** | `gpt-4.1` | Reasoning più avanzato per contraddizioni; frequenza settimanale = costo trascurabile |
|
||||||
|
| 11 | Embeddings | **OpenAI** | `text-embedding-3-small` | Già in uso, 1536-dim compatibile con schema LanceDB/Qdrant |
|
||||||
|
|
||||||
|
### Valori `.env` — Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default fallback
|
||||||
|
LLM_MODEL=gpt-4.1-mini
|
||||||
|
LLM_EMBED_MODEL=text-embedding-3-small
|
||||||
|
|
||||||
|
# Per-agent overrides (LiteLLM model IDs)
|
||||||
|
LLM_MODEL_CLASSIFIER=gpt-4.1-nano
|
||||||
|
LLM_MODEL_HOME_AGENT=anthropic/claude-sonnet-4-6
|
||||||
|
LLM_MODEL_FLOATING_AGENT=anthropic/claude-haiku-4-5
|
||||||
|
LLM_MODEL_UNIFIED_PROCESSOR=gpt-4.1-mini
|
||||||
|
LLM_MODEL_CLOUD_PROCESSOR=gpt-4.1-mini
|
||||||
|
LLM_MODEL_BRIEF_AGENT=gpt-4.1-mini
|
||||||
|
LLM_MODEL_SETUP_AGENT=anthropic/claude-sonnet-4-6
|
||||||
|
LLM_MODEL_MEMORY_EXTRACTOR=gpt-4.1-nano
|
||||||
|
LLM_MODEL_MEMORY_MINER=gpt-4.1-mini
|
||||||
|
LLM_MODEL_MEMORY_AUDITOR=gpt-4.1
|
||||||
|
```
|
||||||
|
|
||||||
|
> **2 API key richieste**: OpenAI (Enterprise + ZDR addendum) e Anthropic (Commercial + ZDR addendum). Vedi sezione **[Come attivare ZDR](#come-attivare-zdr-con-openai-e-anthropic)** per la procedura contrattuale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 Mapping Development (cost-efficient, ZDR non richiesto)
|
||||||
|
|
||||||
|
Priorità: costo minimo e velocità di iterazione. **Niente dati utente reali in questo ambiente** — solo dati sintetici o mock. Nessun vincolo ZDR consente di includere Groq, Cerebras e opzionali DeepSeek.
|
||||||
|
|
||||||
|
| # | Agente | Provider | Modello | Razionale |
|
||||||
|
|---|--------|----------|---------|-----------|
|
||||||
|
| 1 | Classifier | **Groq** | `llama-3.1-8b-instant` | $0.05/$0.08: il più economico con 840 TPS |
|
||||||
|
| 2 | Home Agent | **Google AI Studio** | `gemini-2.5-flash` | 6–7x meno di Sonnet, tool use nativo, 1M ctx |
|
||||||
|
| 3 | Floating Agent | **Google AI Studio** | `gemini-2.5-flash-lite` | $0.10/$0.40 sufficiente per single-turn |
|
||||||
|
| 4 | Unified Processor | **Google AI Studio** | `gemini-2.5-flash` | Tool loop funzionante a costo minimo |
|
||||||
|
| 5 | Cloud Processor | **DeepSeek** | `deepseek-chat` ($0.28/$0.42) | Costo minimo per batch con dati sintetici |
|
||||||
|
| 6 | Brief Agent | **Groq** | `llama-3.1-8b-instant` | $0.05/$0.08, prosa accettabile per QA |
|
||||||
|
| 7 | Setup Agent | **Google AI Studio** | `gemini-2.5-flash` | Conversazione decente a costo minimo |
|
||||||
|
| 8 | Memory Extractor | **Groq** | `llama-3.1-8b-instant` | JSON extraction funziona con fallback retry |
|
||||||
|
| 9 | Memory Miner | **Groq** | `llama-3.3-70b-versatile` | Pattern mining richiede reasoning; 70B a $0.59/$0.79 |
|
||||||
|
| 10 | Memory Auditor | **Google AI Studio** | `gemini-2.5-flash` | Reasoning accettabile, quasi gratis a scala dev |
|
||||||
|
| 11 | Embeddings | **OpenAI** | `text-embedding-3-small` | Stesso dim del prod (1536) — evita reindex al promote |
|
||||||
|
|
||||||
|
### Valori `.env` — Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default fallback
|
||||||
|
LLM_MODEL=gemini/gemini-2.5-flash
|
||||||
|
LLM_EMBED_MODEL=text-embedding-3-small
|
||||||
|
|
||||||
|
# Per-agent overrides
|
||||||
|
LLM_MODEL_CLASSIFIER=groq/llama-3.1-8b-instant
|
||||||
|
LLM_MODEL_HOME_AGENT=gemini/gemini-2.5-flash
|
||||||
|
LLM_MODEL_FLOATING_AGENT=gemini/gemini-2.5-flash-lite
|
||||||
|
LLM_MODEL_UNIFIED_PROCESSOR=gemini/gemini-2.5-flash
|
||||||
|
LLM_MODEL_CLOUD_PROCESSOR=deepseek/deepseek-chat
|
||||||
|
LLM_MODEL_BRIEF_AGENT=groq/llama-3.1-8b-instant
|
||||||
|
LLM_MODEL_SETUP_AGENT=gemini/gemini-2.5-flash
|
||||||
|
LLM_MODEL_MEMORY_EXTRACTOR=groq/llama-3.1-8b-instant
|
||||||
|
LLM_MODEL_MEMORY_MINER=groq/llama-3.3-70b-versatile
|
||||||
|
LLM_MODEL_MEMORY_AUDITOR=gemini/gemini-2.5-flash
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Non immettere dati utente reali**. Gli embeddings restano `text-embedding-3-small` per non dover reindicizzare passando in Production (stesso schema 1536-dim).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Simulazione — Costo Mensile per Utente
|
||||||
|
|
||||||
|
Utilizzo tipico: 500 Home, 300 Floating, 210 Brief, 100 Unified Processor, 80 Cloud Processor, 10 Setup turn (≈2 sessioni), 1500 Memory Extractor turn, 720 Miner (30gg × 24h Power+), 4 Auditor, 1000 embeddings.
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
| Agente | Modello | In tok | Out tok | $/mese |
|
||||||
|
|--------|---------|--------|---------|--------|
|
||||||
|
| Classifier | GPT-4.1 Nano | 150K | 30K | $0.027 |
|
||||||
|
| Home Agent | Sonnet 4.6 | 1M | 500K | $10.50 |
|
||||||
|
| Floating | Haiku 4.5 | 150K | 90K | $0.60 |
|
||||||
|
| Unified Processor | GPT-4.1 Mini | 300K | 200K | $0.44 |
|
||||||
|
| Cloud Processor | GPT-4.1 Mini | 240K | 160K | $0.35 |
|
||||||
|
| Brief Agent | GPT-4.1 Mini | 315K | 105K | $0.29 |
|
||||||
|
| Setup Agent | Sonnet 4.6 | 40K | 5K | $0.20 |
|
||||||
|
| Memory Extractor | GPT-4.1 Nano | 750K | 150K | $0.14 |
|
||||||
|
| Memory Miner | GPT-4.1 Mini | 1.4M | 150K | $0.80 |
|
||||||
|
| Memory Auditor | GPT-4.1 | 20K | 5K | $0.08 |
|
||||||
|
| Embeddings | text-embedding-3-small | 500K | — | $0.01 |
|
||||||
|
| | | | **Totale Production** | **~$13.48/utente/mese** |
|
||||||
|
|
||||||
|
> Con prompt caching Anthropic al 90% sui system prompt ripetuti, Home Agent scende a ~$4–5/mese → totale **~$7–8/utente/mese**.
|
||||||
|
|
||||||
|
### Development (dev team ≈ 100 sessioni test/mese totali, non per utente)
|
||||||
|
|
||||||
|
| Agente | Modello | $/mese totali |
|
||||||
|
|--------|---------|--------------|
|
||||||
|
| Classifier | Groq Llama 3.1 8B | $0.004 |
|
||||||
|
| Home Agent | Gemini 2.5 Flash | $0.58 |
|
||||||
|
| Floating | Gemini 2.5 Flash-Lite | $0.04 |
|
||||||
|
| Unified Processor | Gemini 2.5 Flash | $0.09 |
|
||||||
|
| Cloud Processor | DeepSeek Chat | $0.13 |
|
||||||
|
| Brief Agent | Groq Llama 3.1 8B | $0.02 |
|
||||||
|
| Setup Agent | Gemini 2.5 Flash | $0.02 |
|
||||||
|
| Memory Extractor | Groq Llama 3.1 8B | $0.05 |
|
||||||
|
| Memory Miner | Groq Llama 3.3 70B | $0.35 |
|
||||||
|
| Memory Auditor | Gemini 2.5 Flash | $0.02 |
|
||||||
|
| Embeddings | text-embedding-3-small | $0.01 |
|
||||||
|
| | **Totale Dev team** | **~$1.35/mese** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Motivazioni — Decisioni Chiave
|
||||||
|
|
||||||
|
### 🔒 Perché questa separazione Production/Development
|
||||||
|
|
||||||
|
I dati utente di adiuvAI sono E2E-encrypted, ma i prompt agentici contengono metadati operativi (titoli task, nomi progetti, contesto chat) che fluiscono in chiaro verso il LLM provider. Per Production, ZDR contrattuale è non-negoziabile. In Development si usano dati sintetici, quindi i provider più economici senza garanzie ZDR sono perfetti per iterare rapidamente senza bruciare budget.
|
||||||
|
|
||||||
|
### 💬 Claude Sonnet 4.6 per Home Agent (prod) vs Gemini Flash (dev)
|
||||||
|
|
||||||
|
Home Agent è il touchpoint principale: la qualità del tool calling determina la percezione del prodotto. Sonnet 4.6 è il benchmark su tool use. Il caching di Anthropic (-90% sui system prompt) rende il costo sostenibile a scala. In dev, Gemini 2.5 Flash costa **20x meno** con tool calling sufficiente per validare flussi, test di regressione e UI.
|
||||||
|
|
||||||
|
### ⚙️ GPT-4.1 Mini per entrambi i Processor
|
||||||
|
|
||||||
|
Unified e Cloud Processor sono l'unica superficie dove il **tool loop moltiplica il costo** (≤12 turni per file). Il prezzo medio deve essere basso **e** la qualità tool calling alta, altrimenti errori in cascata. GPT-4.1 Mini è lo sweet spot: tool calling OpenAI è robusto, il prezzo è 5x inferiore a Sonnet. Si sconsiglia Groq qui: la qualità tool calling di Llama introduce retry che annullano il risparmio.
|
||||||
|
|
||||||
|
### 🧠 Stratificazione Memory Agents
|
||||||
|
|
||||||
|
- **Extractor** (2 call/turno, volume altissimo) → Nano (cheapest)
|
||||||
|
- **Miner** (orario Power+, reasoning su pattern) → Mini (compromesso)
|
||||||
|
- **Auditor** (settimanale, reasoning avanzato) → GPT-4.1 full (il volume azzera il premium)
|
||||||
|
|
||||||
|
Ogni tier di memory ha un profilo costo/qualità diverso: collassarli tutti su un unico modello spreca o sul basso (Auditor poco accurato) o sull'alto (Extractor 10x più caro del necessario).
|
||||||
|
|
||||||
|
### 🇪🇺 Perché non Mistral in prod di default
|
||||||
|
|
||||||
|
Mistral è un'ottima alternativa EU-residency, ma Sonnet 4.6 e GPT-4.1 hanno tool calling ancora superiore nei benchmark di Aprile 2026. Se la priorità diventa **data residency EU** (clienti enterprise europei, GDPR stretto), raccomando uno switch mirato:
|
||||||
|
- Home Agent → `mistral/mistral-medium-3`
|
||||||
|
- Background processor → `mistral/mistral-large-3`
|
||||||
|
|
||||||
|
### 🚫 Cina esclusa da Production
|
||||||
|
|
||||||
|
DeepSeek e GLM offrono costi imbattibili ma i dati risiedono in Cina senza garanzie ZDR verificabili per utenti internazionali. **Accettabili in Development solo con dati sintetici**.
|
||||||
|
|
||||||
|
### ⚡ Groq e Cerebras come alternative budget
|
||||||
|
|
||||||
|
In Development, Groq domina per pricing + velocità (394–840 TPS). In Production sono qualificati ZDR (tramite DPA) ma la qualità tool calling di Llama rimane inferiore ai modelli proprietari su flussi multi-turno complessi. Cerebras è strict ZDR by default ma il catalogo modelli è limitato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Come attivare ZDR con OpenAI e Anthropic
|
||||||
|
|
||||||
|
**Cosa significa ZDR concretamente:** nessuna conservazione di prompt/output oltre la durata della richiesta, nessun logging di contenuti da parte del provider, nessun uso per training o fine-tuning, abuse monitoring basato su metadati anziché contenuti. Sul tier API standard, invece, OpenAI e Anthropic conservano input/output per **30 giorni** a fini di abuse detection — motivo per cui serve il contratto ZDR esplicito.
|
||||||
|
|
||||||
|
### 🔵 OpenAI — Enterprise Privacy / ZDR Addendum
|
||||||
|
|
||||||
|
**Chi ne ha diritto:** clienti con Enterprise Agreement. Per API a basso volume si può comunque richiedere un **Business Associate Agreement** o **Enterprise Privacy Addendum** senza passare a ChatGPT Enterprise.
|
||||||
|
|
||||||
|
**Procedura:**
|
||||||
|
1. Scrivere a **sales@openai.com** (oppure compilare il form su [openai.com/enterprise](https://openai.com/enterprise/)) indicando:
|
||||||
|
- Ragione sociale, sede legale, P.IVA, DPO/privacy contact
|
||||||
|
- Caso d'uso (per adiuvAI: "agentic SaaS con E2E-encrypted user data, API backend")
|
||||||
|
- Volume stimato mensile (token o $) — utile per pricing
|
||||||
|
- Modelli usati (`gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `text-embedding-3-small`)
|
||||||
|
- Requisito esplicito: **"Zero Data Retention (0-day retention)"** e disabilitazione abuse monitoring sul contenuto
|
||||||
|
2. OpenAI invia **Commercial Agreement + Data Processing Addendum (DPA) + Zero Data Retention Addendum**.
|
||||||
|
3. Firmare via DocuSign. Tempo medio: **2–4 settimane** dalla prima mail al contratto attivo.
|
||||||
|
4. OpenAI abilita ZDR a livello di **Organization ID** dell'API — non serve modificare il codice, vale per tutte le chiamate future.
|
||||||
|
5. Verifica in dashboard: `platform.openai.com` → Settings → Organization → Data Controls. Deve apparire "Zero Data Retention: Enabled".
|
||||||
|
|
||||||
|
**Costo:** l'Enterprise non ha prezzo pubblico; a bassi volumi si può ottenere senza commitment minimo (talvolta con un modesto uplift sulla tariffa API).
|
||||||
|
|
||||||
|
**Documenti utili:** [OpenAI Enterprise Privacy](https://openai.com/enterprise-privacy/), [OpenAI DPA](https://openai.com/policies/data-processing-addendum/), [OpenAI API Data Usage](https://platform.openai.com/docs/models/how-we-use-your-data).
|
||||||
|
|
||||||
|
### 🟣 Anthropic — Commercial Terms + Zero Retention Addendum
|
||||||
|
|
||||||
|
**Chi ne ha diritto:** qualsiasi cliente commerciale. Anthropic è più flessibile di OpenAI: ZDR viene concesso anche a volumi contenuti tramite un addendum al contratto standard.
|
||||||
|
|
||||||
|
**Procedura:**
|
||||||
|
1. Scrivere a **sales@anthropic.com** (oppure via il form [anthropic.com/contact-sales](https://www.anthropic.com/contact-sales)) indicando:
|
||||||
|
- Ragione sociale, sede legale, P.IVA
|
||||||
|
- Caso d'uso e modelli (`claude-sonnet-4-6`, `claude-haiku-4-5`)
|
||||||
|
- Volume stimato mensile
|
||||||
|
- Richiesta esplicita: **"Zero Retention Addendum to the Commercial Terms"**
|
||||||
|
- Se applicabile: GDPR DPA, BAA (per HIPAA)
|
||||||
|
2. Anthropic invia **Commercial Agreement + DPA + Zero Retention Addendum** (clausola dedicata).
|
||||||
|
3. Firma via DocuSign. Tempo medio: **1–3 settimane**.
|
||||||
|
4. Attivazione sull'**Organization** del Claude Console. Verifica in [console.anthropic.com](https://console.anthropic.com) → Settings → Organization → Privacy.
|
||||||
|
5. Dal contratto attivo: 0-day retention di prompt/response, abuse monitoring basato solo su metadati.
|
||||||
|
|
||||||
|
**Costo:** nessun uplift in genere; il contratto ZDR è incluso nel Commercial Agreement.
|
||||||
|
|
||||||
|
**Documenti utili:** [Anthropic Privacy Policy](https://www.anthropic.com/legal/privacy), [Anthropic Commercial Terms](https://www.anthropic.com/legal/commercial-terms), [Anthropic Trust Center](https://trust.anthropic.com) per SOC 2 / ISO 27001.
|
||||||
|
|
||||||
|
### Checklist pre-firma (entrambi)
|
||||||
|
|
||||||
|
Prima di firmare verifica che il contratto copra:
|
||||||
|
|
||||||
|
- [ ] **Zero retention** esplicita (0 giorni, non "short retention" che può significare 24h o 72h)
|
||||||
|
- [ ] **No training** sui prompt/output (default API, confermare per scritto)
|
||||||
|
- [ ] **Abuse monitoring** basato su metadati, non contenuto (altrimenti il provider legge comunque i prompt)
|
||||||
|
- [ ] **Sub-processor list** consultabile (subcontractor del provider: datacenter, CDN, ecc.)
|
||||||
|
- [ ] **DPA art. 28 GDPR** firmato contestualmente (obbligatorio se processi dati di utenti EU)
|
||||||
|
- [ ] **Breach notification SLA** ≤ 72 ore (requisito GDPR)
|
||||||
|
- [ ] **Data residency** — chiedere conferma region processing (US vs EU). Per adiuvAI può valere la pena pretendere routing EU se la clientela è europea
|
||||||
|
- [ ] **Audit right** — possibilità di richiedere audit indipendente (rilevante per clienti enterprise propri)
|
||||||
|
|
||||||
|
### Timeline realistica end-to-end
|
||||||
|
|
||||||
|
| Fase | Durata |
|
||||||
|
|------|--------|
|
||||||
|
| Primo contatto sales + NDA | 3–5 giorni |
|
||||||
|
| Legal review interno contratti provider | 1–2 settimane |
|
||||||
|
| Negoziazione clausole sensibili (residency, audit, pricing) | 1–2 settimane |
|
||||||
|
| Firma DocuSign + attivazione ZDR su Org ID | 2–3 giorni |
|
||||||
|
| **Totale** | **4–7 settimane** per provider |
|
||||||
|
|
||||||
|
> 💡 **Consiglio pratico:** parti **in parallelo** con sales@openai.com e sales@anthropic.com lo stesso giorno. Il processo è indipendente e avere entrambi i contratti attivi contemporaneamente è indispensabile per il mapping proposto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note & Fonti
|
||||||
|
|
||||||
|
Prezzi aggiornati ad Aprile 2026. Verificare sempre le pagine ufficiali prima di decisioni finali — il mercato LLM cambia mensilmente.
|
||||||
|
|
||||||
|
**Fonti:**
|
||||||
|
- [OpenAI API Pricing](https://openai.com/api/pricing/)
|
||||||
|
- [OpenAI Enterprise Privacy + ZDR](https://openai.com/enterprise-privacy/)
|
||||||
|
- [Anthropic Claude Models](https://platform.claude.com/docs/en/docs/about-claude/models)
|
||||||
|
- [Anthropic ZDR](https://www.anthropic.com/legal/privacy)
|
||||||
|
- [Google Vertex AI Pricing](https://cloud.google.com/vertex-ai/pricing)
|
||||||
|
- [Google Vertex Data Governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance)
|
||||||
|
- [Mistral AI Pricing](https://mistral.ai/pricing)
|
||||||
|
- [Mistral Privacy Policy](https://legal.mistral.ai/terms/privacy-policy)
|
||||||
|
- [Groq On-Demand Pricing](https://groq.com/pricing)
|
||||||
|
- [Cerebras Privacy Policy](https://www.cerebras.ai/policies)
|
||||||
|
- [DeepSeek API Pricing](https://api-docs.deepseek.com/quick_start/pricing/)
|
||||||
|
- [LiteLLM Supported Models](https://docs.litellm.ai/docs/providers)
|
||||||
|
- [Best AI for Tool Calling 2026](https://llm-stats.com/leaderboards/best-ai-for-tool-calling)
|
||||||
|
- [AI Cost Board — LLM Pricing 2026](https://aicostboard.com/blog/posts/llm-api-pricing-comparison-2026)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Report generato per adiuvAI · Aprile 2026 · Aggiornato per coprire tutti gli 11 agenti di `api/.env.example` con mapping separati Production/Development.*
|
||||||
@@ -220,13 +220,22 @@ if lf:
|
|||||||
|
|
||||||
**Pattern V3 corretto nel codice produzione (`agent_runner.py`, `deep_agent.py`, `agent_setup.py`):**
|
**Pattern V3 corretto nel codice produzione (`agent_runner.py`, `deep_agent.py`, `agent_setup.py`):**
|
||||||
```python
|
```python
|
||||||
# user_id e session_id vanno in metadata, NON come kwarg diretti
|
# user_id e session_id propagati come attributi first-class Langfuse
|
||||||
lf.start_as_current_observation(
|
# tramite langfuse_context() che wrappa propagate_attributes()
|
||||||
|
from app.core.langfuse_client import langfuse_context
|
||||||
|
|
||||||
|
_lf_ctx = langfuse_context(user_id=user_id, session_id=session_id)
|
||||||
|
_lf_ctx.__enter__()
|
||||||
|
|
||||||
|
# user_id viene hashato con SHA-256 prima dell'invio a Langfuse
|
||||||
|
# session_id arriva dal renderer (home/floating) o dal run_id (batch)
|
||||||
|
|
||||||
|
_span_ctx = lf.start_as_current_observation(
|
||||||
as_type="span",
|
as_type="span",
|
||||||
name="my-span",
|
name="my-span",
|
||||||
metadata={"user_id": user_id, "session_id": session_id},
|
|
||||||
input=...,
|
input=...,
|
||||||
)
|
)
|
||||||
|
# NON mettere user_id/session_id in metadata — propagate_attributes li gestisce
|
||||||
```
|
```
|
||||||
|
|
||||||
### compile_prompt — non usare template.format() direttamente
|
### compile_prompt — non usare template.format() direttamente
|
||||||
|
|||||||
415
docs/marketing-strategy.md
Normal file
415
docs/marketing-strategy.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# adiuvAI — Marketing Strategy & Positioning
|
||||||
|
|
||||||
|
> **Document version:** 1.1 — April 11, 2026
|
||||||
|
> **Status:** Revised (Round 1 feedback applied)
|
||||||
|
> **Changes:** Removed BYOK positioning, merged one-liner options, selected tagline E + hero C + pitch B, added Telegram & mobile app, updated all copy for "just works" philosophy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Market Research & Competitive Landscape](#1-market-research--competitive-landscape)
|
||||||
|
2. [Positioning Strategy](#2-positioning-strategy)
|
||||||
|
3. [Messaging Framework](#3-messaging-framework)
|
||||||
|
4. [Waitlist Landing Page Copy](#4-waitlist-landing-page-copy)
|
||||||
|
5. [Go-to-Market Recommendations](#5-go-to-market-recommendations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Market Research & Competitive Landscape
|
||||||
|
|
||||||
|
### 1.1 Market Category
|
||||||
|
|
||||||
|
adiuvAI sits at the intersection of three booming categories:
|
||||||
|
|
||||||
|
| Category | Market Size (2026 est.) | Growth |
|
||||||
|
|----------|------------------------|--------|
|
||||||
|
| AI Productivity Tools | $14B+ | ~35% CAGR |
|
||||||
|
| Project Management Software | $9B+ | ~13% CAGR |
|
||||||
|
| AI Meeting/Email Assistants | $3B+ | ~40% CAGR |
|
||||||
|
|
||||||
|
The convergence of these three categories into **one AI-first personal assistant** is the core opportunity. No incumbent owns this combined space yet — they all specialize in one slice.
|
||||||
|
|
||||||
|
### 1.2 Competitive Map
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Removed BYOK column per feedback -->
|
||||||
|
|
||||||
|
| Competitor | What They Do | Price | Local-First? | Privacy Model | EU AI Act? |
|
||||||
|
|-----------|-------------|-------|:---:|---------------|:---:|
|
||||||
|
| **Motion** (usemotion.com) | AI tasks + projects + calendar + docs + meetings | $19-34/user/mo | No (cloud SaaS) | SOC2, GDPR | Not stated |
|
||||||
|
| **Reclaim.ai** | AI calendar optimizer + scheduling | Free–$18/user/mo | No (cloud SaaS) | SOC2, GDPR | Not stated |
|
||||||
|
| **Granola** | AI meeting notepad (desktop) | Free–$19/mo | Partial (desktop app, cloud sync) | Standard privacy policy | Not stated |
|
||||||
|
| **Superhuman** | AI email + docs + assistant suite | $25-30/user/mo | No (cloud SaaS) | Standard | Not stated |
|
||||||
|
| **Shortwave** | AI-powered email client (Gmail) | Free–$25/mo | No (cloud SaaS) | CASA Tier 2 | Not stated |
|
||||||
|
| **SaneBox** | AI email filtering/triage | $7-36/mo | No (cloud SaaS) | Google Verified, audited | Not stated |
|
||||||
|
| **Microsoft Copilot** | AI across M365 suite | $30/user/mo | No (Microsoft cloud) | Enterprise compliance | Partial |
|
||||||
|
| **Notion AI** | AI inside Notion workspace | $10/mo add-on | No (cloud SaaS) | SOC2 | Not stated |
|
||||||
|
|
||||||
|
### 1.3 Key Gaps in the Market
|
||||||
|
|
||||||
|
**Gap 1: No one is local-first.** Every competitor stores your data on their servers. adiuvAI stores everything on your device, encrypted. This is a structural advantage, not a feature toggle.
|
||||||
|
|
||||||
|
**Gap 2: No one bridges email + tasks + meetings in one private workspace.** You either use Superhuman for email, Motion for tasks, and Granola for meetings — or you compromise. adiuvAI combines all three with a single AI that understands the full context.
|
||||||
|
|
||||||
|
**Gap 3: EU AI Act compliance is unclaimed territory.** The EU AI Act entered into force in 2024 and is now being enforced. No major competitor prominently advertises compliance. For European buyers (and increasingly, US companies with EU customers), this is a purchasing requirement.
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Replaced BYOK gap with "it just works" philosophy -->
|
||||||
|
|
||||||
|
**Gap 4: AI complexity is always visible.** Every competitor requires you to understand prompts, models, and configurations. adiuvAI makes AI invisible — the right model is automatically selected for optimal cost and performance. The user never thinks about AI. It just works as a personal secretary.
|
||||||
|
|
||||||
|
### 1.4 Competitor Positioning Analysis
|
||||||
|
|
||||||
|
| Competitor | Tagline | Emotional Angle | Weakness vs adiuvAI |
|
||||||
|
|-----------|---------|-----------------|---------------------|
|
||||||
|
| Motion | "Get an unfair advantage by using AI to double productivity" | Ambition, performance | Cloud-only, no privacy story, AI complexity visible |
|
||||||
|
| Reclaim | "#1 AI calendar app for work" | Optimization, control | Calendar-only, no email/meeting/task integration |
|
||||||
|
| Granola | "The AI notepad for people in back-to-back meetings" | Simplicity, focused | Meetings only, no project management |
|
||||||
|
| Superhuman | "Superpowers, everywhere you work" | Aspiration, speed | Email-centric, expensive, no local data |
|
||||||
|
| Shortwave | "Automate your email with AI" | Efficiency, automation | Email-only, Gmail-dependent |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Positioning Strategy
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Merged Option A + B per feedback, added "helps you complete tasks" -->
|
||||||
|
|
||||||
|
### 2.1 One-Liner
|
||||||
|
|
||||||
|
> "adiuvAI is your AI-powered personal secretary that reads your email, organizes your work, helps you complete tasks, and briefs you every morning — turning the chaos of emails, meetings, and files into a clear plan for your day, privately, on your machine."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Kept Option B (Outcome-Led) per feedback -->
|
||||||
|
|
||||||
|
### 2.2 Elevator Pitch (30 seconds)
|
||||||
|
|
||||||
|
> "Imagine starting every morning with a personalized brief: here are your 5 priorities today, here's what changed overnight in your projects, and here's a follow-up email you forgot to send. That's adiuvAI — an AI secretary that runs entirely on your computer, reads your email and files, and tells you exactly what matters. It even helps you complete the work — drafting follow-ups, organizing notes, and keeping your projects on track. No cloud dependency, no data leaks, no AI complexity. Just clarity."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Positioning Statement
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Added "helps you complete your work" per feedback -->
|
||||||
|
|
||||||
|
> **For** busy professionals who lose track of what matters across email, chat, files, and meetings,
|
||||||
|
> **adiuvAI** is the **AI personal secretary**
|
||||||
|
> **that** reads your communications, organizes your work, gives you a clear daily plan, and helps you complete your tasks —
|
||||||
|
> **unlike** Motion, Superhuman, or Notion AI,
|
||||||
|
> **because** it runs entirely on your device, your data never touches a cloud server, and it's compliant with GDPR and the EU AI Act by design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 USP Hierarchy
|
||||||
|
|
||||||
|
| Rank | Feature | User Benefit | Why It Matters |
|
||||||
|
|:---:|---------|-------------|----------------|
|
||||||
|
<!-- ✅ REVISED — Removed BYOK, added Telegram + mobile app per feedback -->
|
||||||
|
|
||||||
|
| **1** | **AI personal secretary (invisible AI)** | You don't configure agents or write prompts — adiuvAI just reads your world and tells you what to do | This is the primary emotional hook. "I want something that just works." THE reason someone joins the waitlist. |
|
||||||
|
| **2** | **Daily brief + activity carousel** | Start every day knowing exactly what matters | Tangible, visualizable, demo-able. This is what you show in the landing page hero. |
|
||||||
|
| **3** | **Local-first / your data stays yours** | Full privacy without sacrificing AI intelligence | Strong differentiator in post-GDPR Europe. Increasingly important globally after repeated data breaches. |
|
||||||
|
| **4** | **Email + files + chat → tasks automatically** | Stop manually copying things from email to your task list | This is the "extraction" magic — the AI reads your email and creates tasks/notes for you. |
|
||||||
|
| **5** | **EU AI Act + GDPR compliant** | Peace of mind for European professionals and companies | Competitive moat — none of the US-based competitors advertise this. |
|
||||||
|
| **6** | **Telegram integration** | Your secretary is in your pocket, in the app you already use | Extends adiuvAI to mobile without building a full app first. Low friction, high reach. |
|
||||||
|
| **7** | **Voice assistant joins your calls** *(coming soon)* | Takes meeting notes, suggests responses, extracts action items | Future feature — creates a complete "secretary" experience. Powerful for waitlist anticipation. |
|
||||||
|
| **8** | **Mobile companion app** *(coming soon)* | Access your daily brief and tasks on the go | Expected by every user — "how do I use this on my phone?" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Messaging Framework
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Selected Option E per feedback -->
|
||||||
|
|
||||||
|
### 3.1 Tagline
|
||||||
|
|
||||||
|
> **"Meet your new chief of staff."**
|
||||||
|
|
||||||
|
Positions adiuvAI as a person, not a tool. "Chief of staff" implies someone who filters information, prioritizes, briefs you, and helps you execute — exactly the secretary metaphor. More premium than "assistant."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Selected Option C (Intrigue/Minimal) per feedback -->
|
||||||
|
|
||||||
|
### 3.2 Hero Copy for Waitlist Landing Page
|
||||||
|
|
||||||
|
**Headline:**
|
||||||
|
"What if AI could be your secretary?"
|
||||||
|
|
||||||
|
**Subheadline:**
|
||||||
|
Not a chatbot. Not another app. A real AI that reads your email, knows your projects, and tells you what to focus on — without ever seeing your data.
|
||||||
|
|
||||||
|
**CTA:** See how it works →
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 Feature-Benefit Mapping
|
||||||
|
|
||||||
|
Use these on the landing page as feature sections below the hero:
|
||||||
|
|
||||||
|
| Feature (Technical) | Benefit (User-Facing) | Landing Page Copy |
|
||||||
|
|---------------------|----------------------|-------------------|
|
||||||
|
| Daily Brief engine | Know what to focus on | **"Start every day with clarity."** Your AI secretary reviews overnight emails, due tasks, and project changes — then gives you a 2-minute briefing with today's priorities. |
|
||||||
|
| Email/file/chat extraction | Stop manual data entry | **"It reads so you don't have to."** adiuvAI scans your inbox, files, and chats. Important items become tasks, notes, or calendar events — automatically. |
|
||||||
|
| Local-first architecture | Your data never leaves | **"Private by design, not by promise."** Everything runs on your device. Your data lives in an encrypted local database. No cloud server ever sees your content. |
|
||||||
|
| EU AI Act + GDPR | Compliance without effort | **"Built for the new rules."** Fully compliant with GDPR and the EU AI Act. No data training on your content. Audit-ready from day one. |
|
||||||
|
<!-- ✅ REVISED — Removed BYOK row, added Telegram + mobile app per feedback -->
|
||||||
|
|
||||||
|
| Voice assistant *(coming soon)* | Meetings handled for you | **"It joins your calls so you can focus."** adiuvAI listens, takes notes, and suggests next steps — all in real-time during your meetings. *(Coming soon)* |
|
||||||
|
| Multi-agent orchestration | Complex tasks handled simply | **"One request, five agents working."** Behind the scenes, specialized AI agents handle tasks, projects, notes, and timelines — you just talk naturally. |
|
||||||
|
| Activity carousel | Visual daily plan | **"Swipe through your day."** A visual carousel of today's key activities. Tap to dive in, swipe to move on. Like stories for your workday. |
|
||||||
|
| Telegram integration | Your secretary in your pocket | **"Talk to your secretary on Telegram."** Send a message, get your brief, check tasks, add notes — all from the app you already have on your phone. |
|
||||||
|
| Mobile companion app *(coming soon)* | adiuvAI on the go | **"Your daily brief, wherever you are."** A lightweight mobile app to review your plan, check off tasks, and stay in sync with your desktop. *(Coming soon)* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 Objection Handling
|
||||||
|
|
||||||
|
| Objection | Response |
|
||||||
|
|-----------|----------|
|
||||||
|
| "I already use Notion/Motion/Todoist for tasks" | Those are task managers you have to maintain. adiuvAI reads your email and creates tasks for you. It's the difference between a notebook and a secretary. |
|
||||||
|
| "How is this different from ChatGPT/Copilot?" | ChatGPT answers questions. adiuvAI *watches your work* — email, files, meetings — and proactively tells you what needs attention. It's not a chatbot, it's a secretary. |
|
||||||
|
| "Can I trust AI with my email/files?" | Your data never leaves your device. adiuvAI runs locally with end-to-end encryption. All AI processing uses privacy-respecting contracts — your data is never used for training. We literally can't see your data. |
|
||||||
|
| "Is this just another AI wrapper?" | adiuvAI is a native desktop app with its own local database, vector search, and multi-agent AI system. It doesn't wrap an API — it orchestrates 5 specialized agents on your behalf. |
|
||||||
|
| "What about team collaboration?" | adiuvAI starts as your personal secretary. Team features (shared workspace, SSO) are on the roadmap for the Team tier. Join the waitlist to help shape what we build. |
|
||||||
|
| "Is it only for developers/tech people?" | Not at all. The entire design philosophy is to *hide* AI complexity. You never see prompts, models, or configurations. It just works like a smart assistant. |
|
||||||
|
<!-- ✅ REVISED — Removed BYOK references, updated AI trust answer -->
|
||||||
|
|
||||||
|
| "Why desktop and not web/mobile?" | Desktop gives us access to your local files, email client, and meetings without routing through a cloud. It's a privacy decision. Mobile app and Telegram integration are on the roadmap. |
|
||||||
|
| "EU AI Act — how are you compliant?" | Local-first architecture means no centralized data processing. We select AI models with privacy-respecting contracts — your data is never used for training. No profiling, no high-risk classification triggers. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Waitlist Landing Page Copy
|
||||||
|
|
||||||
|
### 4.1 Recommended Page Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
1. HERO — Headline + subheadline + email input + CTA
|
||||||
|
2. SOCIAL PROOF BAR — "Built by an AI Enterprise Solution Architect at HPE" + beta timeline
|
||||||
|
3. DAILY BRIEF DEMO — Visual mockup or animation showing the morning brief experience
|
||||||
|
4. 3 PILLARS — AI Secretary | Private by Design | EU Compliant
|
||||||
|
5. HOW IT WORKS — 3-step visual flow (Connect → Extract → Brief)
|
||||||
|
6. FEATURES PREVIEW — 4-6 feature cards with Coming Soon tags where applicable
|
||||||
|
7. FOUNDER NOTE — Short personal message + credibility
|
||||||
|
8. FINAL CTA — Email input + "Join X others on the waitlist"
|
||||||
|
9. FOOTER — Links, legal, social
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 Full Copy Draft
|
||||||
|
|
||||||
|
#### HERO
|
||||||
|
|
||||||
|
**Pre-headline badge:** `Beta launching June 2026`
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Updated hero to match selected Option C + tagline E -->
|
||||||
|
|
||||||
|
**Pre-headline badge:** `Beta launching June 2026`
|
||||||
|
|
||||||
|
**Tagline:** Meet your new chief of staff.
|
||||||
|
|
||||||
|
**Headline:**
|
||||||
|
What if AI could be your secretary?
|
||||||
|
|
||||||
|
**Subheadline:**
|
||||||
|
Not a chatbot. Not another app. A real AI that reads your email, knows your projects, and tells you what to focus on — without ever seeing your data.
|
||||||
|
|
||||||
|
**CTA:** `[Your email] [See how it works →]`
|
||||||
|
|
||||||
|
**Sub-CTA text:** Free to start. No credit card. Early adopters get priority access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SOCIAL PROOF BAR
|
||||||
|
|
||||||
|
> Built by an AI Enterprise Solution Architect · Integrates with Gmail, Outlook, Teams · Runs 100% on your device
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### THE PROBLEM (optional emotional section)
|
||||||
|
|
||||||
|
**Headline:** You're juggling too many tools to stay organized.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
Your important emails hide between newsletters. Your tasks live in three different apps. Meeting notes sit in a doc you'll never open again.
|
||||||
|
|
||||||
|
You don't need another tool. You need someone who reads everything and tells you what matters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3 PILLARS
|
||||||
|
|
||||||
|
**Pillar 1: AI Secretary**
|
||||||
|
adiuvAI reads your email, monitors your files, and watches your calendar. It extracts what's important and creates tasks, notes, and reminders — without you lifting a finger.
|
||||||
|
|
||||||
|
**Pillar 2: Private by Design**
|
||||||
|
Everything runs on your machine. Your data lives in an encrypted local database. No cloud server ever touches your content. You own your data, fully.
|
||||||
|
|
||||||
|
**Pillar 3: EU AI Act Compliant**
|
||||||
|
Built from the ground up for the new regulatory landscape. No training on user data. No profiling. GDPR and EU AI Act compliant by architecture, not by policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### HOW IT WORKS
|
||||||
|
|
||||||
|
**Step 1: Connect**
|
||||||
|
Link your Gmail, Outlook, or local folders. adiuvAI starts learning what matters to you.
|
||||||
|
|
||||||
|
**Step 2: Extract**
|
||||||
|
AI agents scan your email, files, and meetings. They detect tasks, deadlines, and key information — and organize it into your personal workspace.
|
||||||
|
|
||||||
|
**Step 3: Brief**
|
||||||
|
Every morning, get a personalized briefing. Today's priorities, what changed overnight, and what needs your attention. Swipe through your day like stories.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FEATURES PREVIEW
|
||||||
|
|
||||||
|
| Feature | Status |
|
||||||
|
|---------|--------|
|
||||||
|
| Daily Brief & Activity Carousel | ✅ Beta |
|
||||||
|
| Email → Task Extraction (Gmail, Outlook) | ✅ Beta |
|
||||||
|
| Project & Task Management | ✅ Beta |
|
||||||
|
| Markdown Notes with AI Search | ✅ Beta |
|
||||||
|
| Timeline & Milestone Tracking | ✅ Beta |
|
||||||
|
| File & Folder Monitoring Agents | ✅ Beta |
|
||||||
|
| Telegram Bot Integration | ✅ Beta |
|
||||||
|
| Voice: Join Calls & Take Notes | 🔜 Coming Soon |
|
||||||
|
| Teams/Slack Chat Monitoring | 🔜 Coming Soon |
|
||||||
|
| Mobile Companion App | 🔜 Coming Soon |
|
||||||
|
| Team Workspace & SSO | 🔜 Roadmap |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FOUNDER NOTE
|
||||||
|
|
||||||
|
> **From the maker:**
|
||||||
|
> I'm Roberto, an AI Enterprise Solution Architect. I built adiuvAI because I was tired of promising my clients intelligent AI solutions while my own workday was chaos — emails piling up, tasks scattered across apps, meetings with no follow-through.
|
||||||
|
>
|
||||||
|
> adiuvAI is the tool I needed: an AI that actually reads my world and tells me what to do, without shipping my data to someone else's server.
|
||||||
|
>
|
||||||
|
> If that resonates with you, join the waitlist. Early adopters will shape what we build.
|
||||||
|
>
|
||||||
|
> — Roberto
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FINAL CTA
|
||||||
|
|
||||||
|
**Headline:** Be the first to meet your AI secretary.
|
||||||
|
|
||||||
|
**Sub-text:** Beta launches June 2026. Early adopters get free priority access and a voice in what we build next.
|
||||||
|
|
||||||
|
**CTA:** `[Your email] [Join the waitlist →]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Go-to-Market Recommendations
|
||||||
|
|
||||||
|
### 5.1 Launch Channels
|
||||||
|
|
||||||
|
| Channel | Action | Why | Priority |
|
||||||
|
|---------|--------|-----|:---:|
|
||||||
|
| **Product Hunt** | Launch on PH with "AI secretary" angle + privacy story | PH audience loves privacy-first + indie dev stories. Granola, Shortwave, Motion all launched here. | 🔴 High |
|
||||||
|
| **Hacker News** | "Show HN: I built a local-first AI secretary" post | HN audience cares deeply about local-first, E2E encryption, BYOK. Natural fit. | 🔴 High |
|
||||||
|
| **Reddit** (/r/productivity, /r/selfhosted, /r/artificial) | Authentic "I built this" post + engage in comments | Privacy-focused communities will champion a local-first AI tool. | 🔴 High |
|
||||||
|
| **LinkedIn** | Personal posts from your profile (HPE architect building AI tool) | Your credibility as an enterprise AI architect IS the story. LinkedIn loves founder journeys. | 🔴 High |
|
||||||
|
| **Twitter/X** | Build-in-public thread: "I'm building an AI secretary that runs locally" | AI Twitter is hungry for novel approaches. Local-first + privacy-by-design is contrarian. | 🟡 Medium |
|
||||||
|
| **Indie Hackers** | Product launch + revenue/growth updates | Indie audience loves solo founders with real products. | 🟡 Medium |
|
||||||
|
| **EU Tech Communities** | Position as "first EU AI Act compliant AI secretary" | Italian/European tech Twitter, EU startup events, AI regulation communities. | 🟡 Medium |
|
||||||
|
| **YouTube** | 3-5 min demo video: "My AI reads my email every morning" | Visual proof. Show the daily brief, the carousel, email extraction. | 🟡 Medium |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 Content Strategy
|
||||||
|
|
||||||
|
**Pre-launch (now → beta):**
|
||||||
|
|
||||||
|
| Week | Content | Channel |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 1 | "Why I'm building an AI secretary that never touches the cloud" (founder story) | LinkedIn, Twitter |
|
||||||
|
| 2 | "The problem with AI productivity tools: they all want your data" (thought leadership) | LinkedIn, HN |
|
||||||
|
| 3 | Demo video: Daily Brief walkthrough (30 sec) | Twitter, YouTube short |
|
||||||
|
| 4 | "EU AI Act is here — none of the big tools are ready" (positioning) | LinkedIn, Reddit |
|
||||||
|
| 5 | "Building adiuvAI: local-first architecture deep-dive" (technical) | HN, Dev communities |
|
||||||
|
| 6 | "adiuvAI beta is coming June 2026" (waitlist push) | All channels |
|
||||||
|
|
||||||
|
**Post-beta:**
|
||||||
|
- Weekly "What I shipped this week" updates (build in public)
|
||||||
|
- User testimonials from waitlist early adopters
|
||||||
|
- Comparison content: "adiuvAI vs Motion: why local-first matters"
|
||||||
|
- EU AI Act explainer content (SEO play)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 Feature Roadmap Priorities (for Market Impact)
|
||||||
|
|
||||||
|
| Priority | Feature | Market Impact | Effort |
|
||||||
|
|:---:|---------|--------------|--------|
|
||||||
|
| **1** | Daily Brief carousel UI (visual, demo-able) | This is the hero feature for the landing page. You need a visual to show. | Medium |
|
||||||
|
| **2** | Gmail real-time sync (not just fetch) | "It reads my email" needs to actually work automatically for beta | High |
|
||||||
|
| **3** | Voice assistant (call recording + notes) | This is the "wow" future feature that drives waitlist signups | High |
|
||||||
|
| **4** | Outlook/Teams integration | Expands addressable market to enterprise/Microsoft users | Medium |
|
||||||
|
| **5** | Mobile companion (push daily brief to phone) | "How do I use it on the go?" is the first question people will ask | High |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 Quick Wins (Low Effort, High Impact)
|
||||||
|
|
||||||
|
1. **Rename/rebrand the website** — Change from "CLAUDE.md" to "adiuvAI" immediately. The current name is confusing.
|
||||||
|
|
||||||
|
2. **Switch Landing page to English** — You said global reach. The current Italian page limits you.
|
||||||
|
|
||||||
|
3. **Add an email signup form today** — Even before redesigning the full page, add a Waitlist component. Use Buttondown, Mailchimp, or a simple Supabase/Airtable form.
|
||||||
|
|
||||||
|
4. **Record a 60-second Loom** — Show the daily brief, email extraction, and task creation. Embed on the waitlist page.
|
||||||
|
|
||||||
|
5. **Write your "Why I'm building this" LinkedIn post** — Your HPE AI Architect background is your unfair advantage for credibility. Use it.
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Logo already exists in adiuvAI/assets/, updated accordingly -->
|
||||||
|
|
||||||
|
6. **~~Create a brand logo~~** — ✅ Already exists in `adiuvAI/assets/`. Use it on the new waitlist page.
|
||||||
|
|
||||||
|
7. **Claim @adiuvai handles** — Twitter, LinkedIn page, Product Hunt, GitHub, Reddit. Do this now before someone else does.
|
||||||
|
|
||||||
|
**This week's plan:** Items 1 (rebrand to adiuvAI), 2 (English page), and 3 (email signup form) are confirmed as immediate priorities.
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 Pricing Considerations for Waitlist
|
||||||
|
|
||||||
|
For the **waitlist page**, I recommend NOT showing detailed pricing (Free/Pro/Power tiers). Instead:
|
||||||
|
|
||||||
|
- **"Free to get started"** — signals low friction
|
||||||
|
- **"Pro plans for power users"** — signals there's a business model
|
||||||
|
- **"Early adopters get priority access"** — signals exclusivity
|
||||||
|
|
||||||
|
Save the full pricing reveal for the beta launch.
|
||||||
|
|
||||||
|
**Rationale:** Your current pricing (Free/€15 Pro/€29 Power) is competitive with Motion ($19-34), but pricing pages kill waitlist conversion rates. People join waitlists for the vision, not the price. Show pricing when they can actually buy.
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Confirmed: no pricing on waitlist page -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: The adiuvAI Story in One Paragraph
|
||||||
|
|
||||||
|
<!-- ✅ REVISED — Removed BYOK, added task completion, updated tone -->
|
||||||
|
|
||||||
|
> adiuvAI is an AI-powered personal secretary that runs entirely on your desktop. It connects to your email, files, and calendar, automatically extracts what matters, gives you a personalized daily brief every morning, and helps you complete your work — drafting follow-ups, organizing projects, and keeping everything on track. Unlike cloud-based tools like Motion, Superhuman, or Notion AI, adiuvAI stores all data locally with end-to-end encryption — your data literally never leaves your device. It's the first productivity AI built from the ground up for GDPR and EU AI Act compliance. No configuration needed — the AI just works. Beta launches June 2026.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*When you're happy with this strategy, switch to `@creative-director` to turn it into visual deliverables (landing page design, animations, promo materials). The creative director will read this file and brainstorm designs with you.*
|
||||||
120
docs/memory-evolution-strategy.md
Normal file
120
docs/memory-evolution-strategy.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Evoluzione della gestione memoria/personalizzazione di adiuvAI
|
||||||
|
|
||||||
|
> **Versione:** 1.0 — 2026-04-14
|
||||||
|
> **Scope:** analisi architetturale e raccomandazioni per l'evoluzione del sottosistema di memoria di adiuvAI (Electron + FastAPI), con focus sul posizionamento "segretaria personale" (cfr. `docs/marketing-strategy.md`).
|
||||||
|
> **Premessa:** lo stato attuale di `api/app/core/memory_middleware.py` implementa già un modello 4-tier ispirato a MemGPT (core / associative / episodic / proactive) con crittografia Fernet per-utente. Le raccomandazioni partono da qui, non da zero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Stato attuale (sintesi)
|
||||||
|
|
||||||
|
| Tier | Storage | Uso | Gap principale |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `core` | Postgres (k/v) crittografato | Preferenze stabili (lingua, ruolo, tono, ecc.) — già usato dall'onboarding | Crescita non controllata, nessuna gerarchia, nessun limite per tier |
|
||||||
|
| `associative` | `MemoryAssociative` + pgvector (campo `embedding` presente ma **inutilizzato**) | Recupero top-k | Fallback keyword: sta funzionando come "lista recenti", non come semantica |
|
||||||
|
| `episodic` | Summaries conversazione | Iniettato nel contesto | Summary naïf (`message[:200]`), nessuna compressione LLM, nessun decay |
|
||||||
|
| `proactive` | Pattern con `confidence` | Suggerimenti | Nessun ciclo che alimenta la tabella — resta vuota in produzione |
|
||||||
|
|
||||||
|
**Zero-trust:** la crittografia per-utente è un vincolo architetturale forte. Qualunque tecnica che richieda di "leggere" la memoria lato server deve passare dal Fernet dell'utente → esclude servizi gestiti esterni (Mem0 SaaS, Pinecone con payload in chiaro) per il contenuto, ma lascia liberi i **vettori** (già trattati come deterministici da SHA-256 in `vectors.py`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Allineamento con il posizionamento "segretaria"
|
||||||
|
|
||||||
|
Una segretaria umana eccelle in tre dimensioni mnestiche che gli agenti generici trascurano:
|
||||||
|
|
||||||
|
1. **Memoria di ruolo** — sa *chi è* il capo, *cosa gli interessa*, *quali persone* sono VIP, *quali progetti* sono caldi.
|
||||||
|
2. **Memoria di routine** — conosce gli orari, gli stili comunicativi, le abitudini ("di lunedì il capo vuole il brief entro le 8:00").
|
||||||
|
3. **Memoria relazionale** — conosce *le persone intorno al capo*: clienti, colleghi, fornitori, con contesto (ultimo contatto, tono appropriato, argomenti in sospeso).
|
||||||
|
|
||||||
|
Il sistema attuale copre bene (1) tramite `core`, parzialmente (2) via onboarding, **non copre** (3). Questo è il gap più grande rispetto al marketing promise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Raccomandazione architetturale: ibrido mirato
|
||||||
|
|
||||||
|
Nessuno degli approcci citati va adottato in purezza. La proposta è una **combinazione selettiva** guidata dal dominio:
|
||||||
|
|
||||||
|
### 3.1 Base (tutti i tier): MemGPT consolidato + Mem0-style extraction
|
||||||
|
|
||||||
|
Mantenere la struttura 4-tier già presente, ma **sostituire le scritture naïf con una pipeline Mem0**:
|
||||||
|
|
||||||
|
- **Fase Extraction** (post-conversazione, async):
|
||||||
|
- Trigger: dopo `store_episode`, una task in background fa girare `gpt-4o-mini` (economico) su `(last_turn, recency_window, core_memory)`.
|
||||||
|
- Output strutturato: `{candidates: [{type: "fact|preference|relation|routine", content, target_tier}]}`.
|
||||||
|
- **Fase Update** (decisione ADD/UPDATE/DELETE/NOOP):
|
||||||
|
- Per ogni candidato: similarity search nel tier target → l'LLM decide l'azione via tool call.
|
||||||
|
- **Perché Mem0 e non MemGPT puro**: su un'app "segretaria" le informazioni importanti sono *fatti stabili* (il CFO si chiama Giulia, il cliente X paga sempre in ritardo), non conversazioni da rimettere in RAM. Il ciclo Extract/Update è più adatto di una coda FIFO di messaggi.
|
||||||
|
|
||||||
|
### 3.2 Estensione dominio-specifica: Knowledge Graph leggero (Mem0g)
|
||||||
|
|
||||||
|
Aggiungere un **quinto tier** orientato al dominio segretaria:
|
||||||
|
|
||||||
|
- **`relational` tier**: grafo *Entità → Relazione → Entità* memorizzato in Postgres (non serve Neo4j).
|
||||||
|
- Nodi: Person, Company, Project, Topic (già presenti come entità in `agents` dell'Electron — riutilizzabili).
|
||||||
|
- Archi: `works_at`, `reports_to`, `stakeholder_of`, `last_contacted_on`, `owes_followup`.
|
||||||
|
- **Perché un grafo e non solo vettori**: la segretaria deve rispondere a domande tipo *"chi è Marco?"* → embedding testuale confonde "Marco Rossi (cliente)" con "Marco Bianchi (collega)". Il grafo disambigua, i vettori no.
|
||||||
|
- **Implementazione minima**: nuova tabella `memory_relations(user_id, subject_id, predicate, object_id, confidence, source_episode_id, encrypted_notes)`. Popolata dalla stessa pipeline Extraction.
|
||||||
|
|
||||||
|
### 3.3 Dove **non** andare
|
||||||
|
|
||||||
|
- **A-Mem / memory evolution retroattiva**: affascinante ma costoso (ri-analizza il passato a ogni nuova nota). Per una segretaria è *anti-pattern* — introduce non-determinismo dove l'utente si aspetta stabilità ("ma ieri mi dicevi un'altra cosa"). Skippare.
|
||||||
|
- **AutoGPT loop riflessivo**: il dominio è reattivo (brief, follow-up), non goal-seeking autonomo. Over-engineering.
|
||||||
|
- **LangChain `ConversationBufferMemory`** e parenti: già superati dalla struttura attuale. No regressioni.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Differenziazione per tier di prodotto
|
||||||
|
|
||||||
|
La memoria è un asset di differenziazione commerciale **naturale**: più memoria = segretaria più "al corrente". Proposta:
|
||||||
|
|
||||||
|
| Capability | Free | Pro | Power | Team |
|
||||||
|
|---|:-:|:-:|:-:|:-:|
|
||||||
|
| `core` blocks (max k/v) | 20 | 100 | illimitato | illimitato |
|
||||||
|
| `episodic` retention | 7 gg | 90 gg | illimitato | illimitato |
|
||||||
|
| `associative` con pgvector reale (OpenAI embeddings) | ❌ (keyword only) | ✅ | ✅ | ✅ |
|
||||||
|
| `relational` graph (Mem0g) | ❌ | ✅ base (Person/Project) | ✅ completo + custom predicates | ✅ + condivisione team |
|
||||||
|
| Mem0 Extraction pipeline LLM | batch giornaliero | realtime post-turn | realtime + proactive mining | realtime + team-wide |
|
||||||
|
| `proactive` pattern mining | ❌ | ❌ | ✅ (pattern "ogni lunedì…") | ✅ |
|
||||||
|
| Memory export/import cifrato | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Forget/consent UI (GDPR Art. 17) | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
**Rationale commerciale:**
|
||||||
|
- Il Free tier ha una segretaria che "ricorda i fatti base" — sufficiente per la wow-moment del daily brief, non sufficiente per sentirla *tua*.
|
||||||
|
- Il salto Free → Pro si giustifica con "la segretaria inizia a conoscere davvero le persone che tratti" (embeddings reali + grafo base).
|
||||||
|
- Il salto Pro → Power si vende come "la segretaria nota i tuoi pattern e te li anticipa" (proactive tier attivo).
|
||||||
|
- Il Team tier abilita memoria condivisa su entità aziendali comuni (clienti, progetti), mantenendo memoria personale cifrata per-utente.
|
||||||
|
|
||||||
|
**Vincolo zero-trust**: il tier-gating si applica a *quanto* si memorizza e *quali pipeline* girano, **mai** a chi può leggere. Il backend continua a non decifrare nulla che non sia strettamente necessario al turn corrente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Piano di implementazione suggerito (ordine)
|
||||||
|
|
||||||
|
1. **Quick win (1–2 gg)**: attivare davvero `pgvector` sull'`associative` tier (oggi c'è il campo `embedding` ma si usa il fallback keyword). Gate dietro tier ≥ Pro.
|
||||||
|
2. **Extraction pipeline Mem0-style (1 sett)**: task async post-`store_episode` → `gpt-4o-mini` → update strutturato dei tier. Log trace per debug. Gate per tier (batch Free vs realtime Pro+).
|
||||||
|
3. **Relational tier (Mem0g leggero) (1–2 sett)**: schema nuova tabella, alimentazione dalla pipeline, uso nel prompt agent come contesto "persone e relazioni rilevanti".
|
||||||
|
4. **Settings > Memory UI**: pagina dedicata per vedere/modificare `core` + `relational` (la segretaria deve essere *correggibile* — è una feature, non un bug, che l'utente possa dire "no, Giulia è la CFO non la CEO"). GDPR-compliant by design.
|
||||||
|
5. **Proactive mining (opzionale, Power)**: job schedulato che cerca pattern temporali nelle `episodic` e promuove a `proactive` con confidence score.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Rischi e mitigazioni
|
||||||
|
|
||||||
|
| Rischio | Mitigazione |
|
||||||
|
|---|---|
|
||||||
|
| Costo LLM dell'Extraction pipeline esplode sul Free | Batch notturno per Free (1 run/24h con rate cap), realtime solo Pro+ |
|
||||||
|
| Memoria "sporca" (fatti estratti errati) erode fiducia nella segretaria | UI di review/edit obbligatoria (punto 4); mai scrivere `core` senza conferma implicita (es. utente non corregge entro N turni) |
|
||||||
|
| Zero-trust vs embeddings: OpenAI vede il testo dell'embedding | Già accettato dall'architettura attuale per altri flussi (cfr. note su vector search in CLAUDE.md). Documentare esplicitamente nella privacy policy. Opzione BYOK-embedding per tier Power come mitigante marketing. |
|
||||||
|
| Grafo relazionale cresce indefinitamente | TTL per archi con `last_contacted_on` > 18 mesi + decay sulla `confidence` |
|
||||||
|
| Drift tra memoria locale (Electron SQLite) e backend | Già gestito come "backend = source of truth" per `core`. Estendere la stessa regola al nuovo `relational`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. TL;DR
|
||||||
|
|
||||||
|
- **Base architetturale:** manteniamo MemGPT 4-tier (già in casa), sostituiamo le scritture naïf con pipeline **Mem0 Extract/Update**.
|
||||||
|
- **Differenziazione dominio:** aggiungiamo un tier **`relational` (Mem0g leggero)** per modellare persone/progetti — è il vero gap rispetto alla promessa "segretaria".
|
||||||
|
- **Monetizzazione:** la memoria diventa scala di differenziazione tier (retention, embeddings reali, proactive mining, team sharing) senza violare zero-trust.
|
||||||
|
- **Da evitare:** A-Mem (troppo non-deterministico), AutoGPT loop (fuori scope), wrapper LangChain legacy (regressione).
|
||||||
|
- **Primo passo concreto:** accendere pgvector reale sull'`associative` tier — è già pre-cablato e sbloccato da un singolo gate.
|
||||||
852
docs/mockups/2026-05-14-task-form-dialog-mockup.html
Normal file
852
docs/mockups/2026-05-14-task-form-dialog-mockup.html
Normal file
@@ -0,0 +1,852 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Task Form Dialog — keyboard-driven mockup</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f4edf3;
|
||||||
|
--canvas: #ebe4ea;
|
||||||
|
--card: #ffffff;
|
||||||
|
--card-soft: #fbf7fa;
|
||||||
|
--border: #c8c3cd;
|
||||||
|
--border-soft: #d8d4dc;
|
||||||
|
--text: #1a1a1a;
|
||||||
|
--muted: #6e6a73;
|
||||||
|
--primary: #fbc881;
|
||||||
|
--primary-fg: #4a3210;
|
||||||
|
--accent: #e9e5ee;
|
||||||
|
--ring: #8a8ea9;
|
||||||
|
--danger: #c4423a;
|
||||||
|
--green: #5a8a55;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #0c0c0c;
|
||||||
|
--canvas: #161616;
|
||||||
|
--card: #1a1a1a;
|
||||||
|
--card-soft: #202020;
|
||||||
|
--border: #323232;
|
||||||
|
--border-soft: #2a2a2a;
|
||||||
|
--text: #f5f5f5;
|
||||||
|
--muted: #9a9a9a;
|
||||||
|
--primary: #fbc881;
|
||||||
|
--primary-fg: #4a3210;
|
||||||
|
--accent: #2a2a2a;
|
||||||
|
--ring: #8a8ea9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
margin: 0; padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Geist", system-ui, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.page-hint {
|
||||||
|
position: fixed; left: 16px; top: 16px;
|
||||||
|
background: var(--card); border: 1px solid var(--border);
|
||||||
|
border-radius: 8px; padding: 10px 12px;
|
||||||
|
font-size: 12px; max-width: 280px; line-height: 1.5;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.page-hint strong { color: var(--text); }
|
||||||
|
.page-hint kbd {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--accent); border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 4px; padding: 1px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog */
|
||||||
|
.overlay {
|
||||||
|
width: 580px; max-width: 100%;
|
||||||
|
background: rgba(255,255,255,0.92);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 50px -10px rgba(0,0,0,0.18), 0 0 0 1px rgba(255,255,255,0.4) inset;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.overlay { background: rgba(26,26,26,0.92); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header — AddEventDialog style: title + description, no separator */
|
||||||
|
.dlg-header {
|
||||||
|
padding: 18px 22px 8px;
|
||||||
|
}
|
||||||
|
.dlg-title {
|
||||||
|
font-size: 16px; font-weight: 600; margin: 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.dlg-desc {
|
||||||
|
margin: 4px 0 0; font-size: 13px; color: var(--muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body */
|
||||||
|
.dlg-body { padding: 18px 22px 12px; }
|
||||||
|
.title-input {
|
||||||
|
width: 100%;
|
||||||
|
border: none; outline: none; background: transparent;
|
||||||
|
font: inherit; color: inherit;
|
||||||
|
font-size: 22px; font-weight: 500;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.title-input::placeholder { color: var(--muted); opacity: 0.7; }
|
||||||
|
.desc-input {
|
||||||
|
margin-top: 8px;
|
||||||
|
width: 100%;
|
||||||
|
border: none; outline: none; background: transparent;
|
||||||
|
font: inherit; color: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: none;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.desc-input::placeholder { color: var(--muted); opacity: 0.7; }
|
||||||
|
|
||||||
|
/* Properties section */
|
||||||
|
.props-label {
|
||||||
|
font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
|
||||||
|
color: var(--muted); margin: 14px 0 8px;
|
||||||
|
}
|
||||||
|
.pills { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
|
||||||
|
/* Pill */
|
||||||
|
.pill {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
height: 28px; padding: 0 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--card-soft);
|
||||||
|
font-size: 12px; color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 120ms, border-color 120ms, box-shadow 120ms;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pill[data-empty="true"] {
|
||||||
|
border-style: dashed;
|
||||||
|
color: var(--muted);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.pill:focus-visible,
|
||||||
|
.pill[data-focused="true"] {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--ring);
|
||||||
|
box-shadow: 0 0 0 3px rgba(138,142,169,0.25);
|
||||||
|
}
|
||||||
|
.pill .pill-label { color: var(--muted); }
|
||||||
|
.pill .pill-value { font-weight: 500; }
|
||||||
|
.pill .pill-sep { color: var(--muted); opacity: 0.5; }
|
||||||
|
.pill-icon { font-size: 11px; line-height: 1; }
|
||||||
|
.pill .pi-up { color: #c4423a; }
|
||||||
|
.pill .pi-mid { color: #b97a14; }
|
||||||
|
.pill .pi-down { color: var(--muted); }
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.dlg-footer {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 22px;
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
background: rgba(0,0,0,0.015);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.dlg-footer { background: rgba(255,255,255,0.02); }
|
||||||
|
}
|
||||||
|
.kbd-hint { font-size: 11px; color: var(--muted); }
|
||||||
|
.kbd-hint kbd {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
background: var(--accent); border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 4px; padding: 1px 5px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
height: 30px; padding: 0 14px; border-radius: 8px;
|
||||||
|
font: inherit; font-size: 13px; font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: transparent; color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(138,142,169,0.25);
|
||||||
|
border-color: var(--ring);
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--primary-fg);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.footer-actions { display: flex; gap: 6px; }
|
||||||
|
|
||||||
|
/* Popover */
|
||||||
|
.popover {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 220px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 12px 32px -8px rgba(0,0,0,0.2);
|
||||||
|
padding: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.popover[data-open="true"] { display: block; }
|
||||||
|
.pop-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.pop-item:hover,
|
||||||
|
.pop-item:focus,
|
||||||
|
.pop-item[data-active="true"] { background: var(--accent); }
|
||||||
|
.pop-item:focus { box-shadow: inset 0 0 0 1px var(--ring); }
|
||||||
|
.pop-item .check { width: 14px; color: var(--muted); }
|
||||||
|
.pop-item[data-selected="true"] .check::before { content: "✓"; color: var(--text); }
|
||||||
|
|
||||||
|
/* DateField segments */
|
||||||
|
.datefield {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--card-soft);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.datefield:focus-within {
|
||||||
|
border-color: var(--ring);
|
||||||
|
box-shadow: 0 0 0 3px rgba(138,142,169,0.25);
|
||||||
|
}
|
||||||
|
.segment {
|
||||||
|
min-width: 1.8ch; text-align: center; padding: 2px 1px;
|
||||||
|
border-radius: 3px; outline: none; cursor: text;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.segment[data-placeholder="true"] { color: var(--muted); opacity: 0.6; }
|
||||||
|
.segment:focus { background: var(--accent); }
|
||||||
|
.seg-sep { color: var(--muted); padding: 0 1px; user-select: none; }
|
||||||
|
|
||||||
|
.date-pop {
|
||||||
|
padding: 12px;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
.date-pop .field-label {
|
||||||
|
font-size: 11px; color: var(--muted);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.date-pop .cal {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.cal-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
font-size: 12px; margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.cal-head .month { font-weight: 600; }
|
||||||
|
.cal-head button {
|
||||||
|
border: 1px solid var(--border); background: transparent;
|
||||||
|
border-radius: 6px; width: 22px; height: 22px;
|
||||||
|
color: var(--text); cursor: pointer;
|
||||||
|
}
|
||||||
|
.cal-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 2px; font-size: 11px;
|
||||||
|
}
|
||||||
|
.cal-dow {
|
||||||
|
color: var(--muted); text-align: center;
|
||||||
|
padding: 4px 0; font-weight: 500;
|
||||||
|
}
|
||||||
|
.cal-day {
|
||||||
|
text-align: center; padding: 5px 0;
|
||||||
|
border-radius: 5px; cursor: pointer; outline: none;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.cal-day:focus,
|
||||||
|
.cal-day[data-active="true"] { background: var(--accent); }
|
||||||
|
.cal-day[data-selected="true"] {
|
||||||
|
background: var(--primary); color: var(--primary-fg);
|
||||||
|
}
|
||||||
|
.cal-day[data-other-month="true"] { color: var(--muted); opacity: 0.4; }
|
||||||
|
|
||||||
|
.pop-anchor { position: relative; display: inline-flex; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-hint">
|
||||||
|
<strong>Keyboard demo</strong><br>
|
||||||
|
<kbd>Tab</kbd>/<kbd>Shift+Tab</kbd> cycles fields + pills.<br>
|
||||||
|
<kbd>Enter</kbd> opens focused pill.<br>
|
||||||
|
<kbd>↑</kbd>/<kbd>↓</kbd> inside popovers and calendar.<br>
|
||||||
|
<kbd>Esc</kbd> closes popover.<br>
|
||||||
|
Due pill: type date directly (segment edit).
|
||||||
|
<hr style="border:none; border-top:1px solid var(--border-soft); margin:8px 0;">
|
||||||
|
<label style="font-size:11px;">FormatPrefs.dateFormat:
|
||||||
|
<select id="fmt-pref" style="margin-top:4px; width:100%; padding:4px; font: inherit; font-size:11px;">
|
||||||
|
<option value="dd/MM/yyyy">dd/MM/yyyy</option>
|
||||||
|
<option value="MM/dd/yyyy">MM/dd/yyyy</option>
|
||||||
|
<option value="yyyy-MM-dd">yyyy-MM-dd</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overlay" role="dialog" aria-modal="true" aria-labelledby="dlg-title">
|
||||||
|
<header class="dlg-header">
|
||||||
|
<h2 id="dlg-title" class="dlg-title">New task</h2>
|
||||||
|
<p class="dlg-desc">Capture what needs doing. Set properties below or skip and refine later.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dlg-body">
|
||||||
|
<input class="title-input" id="f-title" placeholder="What needs to be done?" autofocus />
|
||||||
|
<textarea class="desc-input" id="f-desc" rows="3" placeholder="Add a description…"></textarea>
|
||||||
|
|
||||||
|
<div class="props-label">Properties</div>
|
||||||
|
<div class="pills" id="pills">
|
||||||
|
|
||||||
|
<!-- Project pill -->
|
||||||
|
<span class="pop-anchor">
|
||||||
|
<button type="button" class="pill" data-pill="project" data-empty="true" tabindex="0">
|
||||||
|
<span class="pill-icon">📁</span>
|
||||||
|
<span class="pill-label">Project</span>
|
||||||
|
</button>
|
||||||
|
<div class="popover" data-popover="project" role="listbox">
|
||||||
|
<div class="pop-item" data-active="true" data-value="">
|
||||||
|
<span class="check"></span>No project
|
||||||
|
</div>
|
||||||
|
<div class="pop-item" data-value="acme-comm">
|
||||||
|
<span class="check"></span>Acme · Communications
|
||||||
|
</div>
|
||||||
|
<div class="pop-item" data-value="testing-bot">
|
||||||
|
<span class="check"></span>Testing · AI ChatBot
|
||||||
|
</div>
|
||||||
|
<div class="pop-item" data-value="adiuvai-app">
|
||||||
|
<span class="check"></span>AdiuvAI · App
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Priority pill -->
|
||||||
|
<span class="pop-anchor">
|
||||||
|
<button type="button" class="pill" data-pill="priority" tabindex="0">
|
||||||
|
<span class="pill-icon pi-mid">→</span>
|
||||||
|
<span class="pill-label">Priority</span>
|
||||||
|
<span class="pill-sep">·</span>
|
||||||
|
<span class="pill-value">Medium</span>
|
||||||
|
</button>
|
||||||
|
<div class="popover" data-popover="priority" role="listbox" style="min-width:160px;">
|
||||||
|
<div class="pop-item" data-value="high"><span class="check"></span>High</div>
|
||||||
|
<div class="pop-item" data-active="true" data-selected="true" data-value="medium"><span class="check"></span>Medium</div>
|
||||||
|
<div class="pop-item" data-value="low"><span class="check"></span>Low</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Status pill -->
|
||||||
|
<span class="pop-anchor">
|
||||||
|
<button type="button" class="pill" data-pill="status" tabindex="0">
|
||||||
|
<span class="pill-icon">○</span>
|
||||||
|
<span class="pill-label">Status</span>
|
||||||
|
<span class="pill-sep">·</span>
|
||||||
|
<span class="pill-value">To do</span>
|
||||||
|
</button>
|
||||||
|
<div class="popover" data-popover="status" role="listbox" style="min-width:170px;">
|
||||||
|
<div class="pop-item" data-active="true" data-selected="true" data-value="todo"><span class="check"></span>To do</div>
|
||||||
|
<div class="pop-item" data-value="in_progress"><span class="check"></span>In progress</div>
|
||||||
|
<div class="pop-item" data-value="done"><span class="check"></span>Done</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Due pill -->
|
||||||
|
<span class="pop-anchor">
|
||||||
|
<button type="button" class="pill" data-pill="due" data-empty="true" tabindex="0">
|
||||||
|
<span class="pill-icon">📅</span>
|
||||||
|
<span class="pill-label">Due</span>
|
||||||
|
</button>
|
||||||
|
<div class="popover date-pop" data-popover="due" role="dialog" style="min-width:300px;">
|
||||||
|
<div class="field-label">Date</div>
|
||||||
|
<div class="datefield" id="datefield" tabindex="-1"><!-- segments injected by JS --></div>
|
||||||
|
<div class="cal" id="calendar">
|
||||||
|
<div class="cal-head">
|
||||||
|
<button type="button" data-nav="-1">‹</button>
|
||||||
|
<span class="month" id="cal-month">May 2026</span>
|
||||||
|
<button type="button" data-nav="1">›</button>
|
||||||
|
</div>
|
||||||
|
<div class="cal-grid" id="cal-grid"><!-- filled by JS --></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Assignees pill -->
|
||||||
|
<span class="pop-anchor">
|
||||||
|
<button type="button" class="pill" data-pill="assignees" data-empty="true" tabindex="0">
|
||||||
|
<span class="pill-icon">+</span>
|
||||||
|
<span class="pill-label">Add assignees</span>
|
||||||
|
</button>
|
||||||
|
<div class="popover" data-popover="assignees" role="listbox">
|
||||||
|
<div class="pop-item" data-active="true" data-value="alex"><span class="check"></span>Alex Morgan</div>
|
||||||
|
<div class="pop-item" data-value="priya"><span class="check"></span>Priya Shah</div>
|
||||||
|
<div class="pop-item" data-value="yo"><span class="check"></span>You</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="dlg-footer">
|
||||||
|
<div></div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button type="button" class="btn">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary">Create task</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ---------- popover open/close + arrow nav ---------- */
|
||||||
|
const pills = document.querySelectorAll('.pill');
|
||||||
|
const popovers = document.querySelectorAll('.popover');
|
||||||
|
|
||||||
|
function closeAllPopovers() {
|
||||||
|
popovers.forEach((p) => p.setAttribute('data-open', 'false'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pillArr = Array.from(pills);
|
||||||
|
pills.forEach((pill) => {
|
||||||
|
pill.addEventListener('click', (e) => openPopoverFor(pill));
|
||||||
|
pill.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
openPopoverFor(pill);
|
||||||
|
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
const idx = pillArr.indexOf(pill);
|
||||||
|
const next = pillArr[Math.min(idx + 1, pillArr.length - 1)];
|
||||||
|
next && next.focus();
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
const idx = pillArr.indexOf(pill);
|
||||||
|
const prev = pillArr[Math.max(idx - 1, 0)];
|
||||||
|
prev && prev.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function openPopoverFor(pill) {
|
||||||
|
const which = pill.dataset.pill;
|
||||||
|
const pop = document.querySelector(`.popover[data-popover="${which}"]`);
|
||||||
|
if (!pop) return;
|
||||||
|
closeAllPopovers();
|
||||||
|
pop.setAttribute('data-open', 'true');
|
||||||
|
if (which === 'due') {
|
||||||
|
// focus first date segment
|
||||||
|
const firstSeg = pop.querySelector('.segment');
|
||||||
|
if (firstSeg) firstSeg.focus();
|
||||||
|
} else {
|
||||||
|
const items = pop.querySelectorAll('.pop-item');
|
||||||
|
items.forEach((i) => i.setAttribute('tabindex', '-1'));
|
||||||
|
const active = pop.querySelector('.pop-item[data-active="true"]') || items[0];
|
||||||
|
if (active) {
|
||||||
|
active.setAttribute('tabindex', '0');
|
||||||
|
active.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const open = document.querySelector('.popover[data-open="true"]');
|
||||||
|
if (open) {
|
||||||
|
e.preventDefault();
|
||||||
|
closePopover(open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- list popover keyboard ---------- */
|
||||||
|
popovers.forEach((pop) => {
|
||||||
|
if (pop.dataset.popover === 'due') return;
|
||||||
|
const items = Array.from(pop.querySelectorAll('.pop-item'));
|
||||||
|
items.forEach((it) => {
|
||||||
|
it.setAttribute('tabindex', '-1');
|
||||||
|
it.addEventListener('click', () => selectPopItem(pop, it));
|
||||||
|
it.addEventListener('keydown', (e) => onPopItemKey(e, pop, items, it));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function onPopItemKey(e, pop, items, item) {
|
||||||
|
const idx = items.indexOf(item);
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
moveFocus(items, Math.min(idx + 1, items.length - 1));
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
moveFocus(items, Math.max(idx - 1, 0));
|
||||||
|
} else if (e.key === 'Home') {
|
||||||
|
e.preventDefault(); moveFocus(items, 0);
|
||||||
|
} else if (e.key === 'End') {
|
||||||
|
e.preventDefault(); moveFocus(items, items.length - 1);
|
||||||
|
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
selectPopItem(pop, item);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closePopover(pop);
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
closePopover(pop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function moveFocus(items, target) {
|
||||||
|
items.forEach((i) => i.setAttribute('tabindex', '-1'));
|
||||||
|
const el = items[target];
|
||||||
|
el.setAttribute('tabindex', '0');
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
function closePopover(pop) {
|
||||||
|
pop.setAttribute('data-open', 'false');
|
||||||
|
const pill = document.querySelector(`.pill[data-pill="${pop.dataset.popover}"]`);
|
||||||
|
pill && pill.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPopItem(pop, item) {
|
||||||
|
const which = pop.dataset.popover;
|
||||||
|
if (which === 'assignees') {
|
||||||
|
item.toggleAttribute('data-selected');
|
||||||
|
} else {
|
||||||
|
pop.querySelectorAll('.pop-item').forEach((i) => i.removeAttribute('data-selected'));
|
||||||
|
item.setAttribute('data-selected', 'true');
|
||||||
|
}
|
||||||
|
updatePillFrom(pop);
|
||||||
|
if (which !== 'assignees') closePopover(pop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePillFrom(pop) {
|
||||||
|
const which = pop.dataset.popover;
|
||||||
|
const pill = document.querySelector(`.pill[data-pill="${which}"]`);
|
||||||
|
if (!pill) return;
|
||||||
|
if (which === 'assignees') {
|
||||||
|
const sel = Array.from(pop.querySelectorAll('.pop-item[data-selected="true"]'));
|
||||||
|
if (sel.length === 0) {
|
||||||
|
pill.setAttribute('data-empty', 'true');
|
||||||
|
pill.innerHTML = '<span class="pill-icon">+</span><span class="pill-label">Add assignees</span>';
|
||||||
|
} else {
|
||||||
|
pill.removeAttribute('data-empty');
|
||||||
|
const names = sel.map((s) => s.textContent.trim());
|
||||||
|
pill.innerHTML = `<span class="pill-icon">👤</span><span class="pill-label">Assignees</span><span class="pill-sep">·</span><span class="pill-value">${names.join(', ')}</span>`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cur = pop.querySelector('.pop-item[data-selected="true"]');
|
||||||
|
if (which === 'project') {
|
||||||
|
if (!cur || cur.dataset.value === '') {
|
||||||
|
pill.setAttribute('data-empty', 'true');
|
||||||
|
pill.innerHTML = '<span class="pill-icon">📁</span><span class="pill-label">Project</span>';
|
||||||
|
} else {
|
||||||
|
pill.removeAttribute('data-empty');
|
||||||
|
pill.innerHTML = `<span class="pill-icon">📁</span><span class="pill-label">Project</span><span class="pill-sep">·</span><span class="pill-value">${cur.textContent.trim()}</span>`;
|
||||||
|
}
|
||||||
|
} else if (which === 'priority') {
|
||||||
|
const v = cur.dataset.value;
|
||||||
|
const icon = v === 'high' ? '<span class="pill-icon pi-up">↑</span>'
|
||||||
|
: v === 'low' ? '<span class="pill-icon pi-down">↓</span>'
|
||||||
|
: '<span class="pill-icon pi-mid">→</span>';
|
||||||
|
const label = v[0].toUpperCase() + v.slice(1);
|
||||||
|
pill.innerHTML = `${icon}<span class="pill-label">Priority</span><span class="pill-sep">·</span><span class="pill-value">${label}</span>`;
|
||||||
|
} else if (which === 'status') {
|
||||||
|
const v = cur.dataset.value;
|
||||||
|
const icon = v === 'done' ? '✓' : v === 'in_progress' ? '◐' : '○';
|
||||||
|
const label = v === 'in_progress' ? 'In progress' : v === 'todo' ? 'To do' : 'Done';
|
||||||
|
pill.innerHTML = `<span class="pill-icon">${icon}</span><span class="pill-label">Status</span><span class="pill-sep">·</span><span class="pill-value">${label}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- DateField — format-aware segments ---------- */
|
||||||
|
const SEG_DEFS = {
|
||||||
|
day: { len: 2, min: 1, max: 31, ph: 'DD' },
|
||||||
|
month: { len: 2, min: 1, max: 12, ph: 'MM' },
|
||||||
|
year: { len: 4, min: 1900, max: 2100, ph: 'YYYY' },
|
||||||
|
hour: { len: 2, min: 0, max: 23, ph: 'HH' },
|
||||||
|
minute: { len: 2, min: 0, max: 59, ph: 'MM' },
|
||||||
|
};
|
||||||
|
const FMT_LAYOUT = {
|
||||||
|
'dd/MM/yyyy': [['day','/'],['month','/'],['year',null]],
|
||||||
|
'MM/dd/yyyy': [['month','/'],['day','/'],['year',null]],
|
||||||
|
'yyyy-MM-dd': [['year','-'],['month','-'],['day',null]],
|
||||||
|
};
|
||||||
|
let currentFmt = 'dd/MM/yyyy';
|
||||||
|
|
||||||
|
function renderDateField() {
|
||||||
|
const df = document.getElementById('datefield');
|
||||||
|
const cur = readDateField();
|
||||||
|
df.innerHTML = '';
|
||||||
|
const layout = FMT_LAYOUT[currentFmt].concat([null, ['hour',':'], ['minute', null]]);
|
||||||
|
layout.forEach((entry) => {
|
||||||
|
if (entry === null) {
|
||||||
|
const sp = document.createElement('span');
|
||||||
|
sp.className = 'seg-sep'; sp.innerHTML = ' ';
|
||||||
|
df.appendChild(sp); return;
|
||||||
|
}
|
||||||
|
const [key, sep] = entry;
|
||||||
|
const def = SEG_DEFS[key];
|
||||||
|
const seg = document.createElement('span');
|
||||||
|
seg.className = 'segment';
|
||||||
|
seg.contentEditable = 'true';
|
||||||
|
seg.dataset.seg = key;
|
||||||
|
seg.dataset.len = def.len; seg.dataset.min = def.min; seg.dataset.max = def.max;
|
||||||
|
const v = cur[key];
|
||||||
|
if (v == null) {
|
||||||
|
seg.dataset.placeholder = 'true';
|
||||||
|
seg.textContent = def.ph;
|
||||||
|
} else {
|
||||||
|
seg.dataset.placeholder = 'false';
|
||||||
|
seg.textContent = String(v).padStart(def.len, '0');
|
||||||
|
}
|
||||||
|
df.appendChild(seg);
|
||||||
|
if (sep) {
|
||||||
|
const s = document.createElement('span');
|
||||||
|
s.className = 'seg-sep'; s.textContent = sep;
|
||||||
|
df.appendChild(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bindDateSegments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDateSegments() {
|
||||||
|
const dfSegments = Array.from(document.querySelectorAll('.segment'));
|
||||||
|
dfSegments.forEach((seg, idx) => {
|
||||||
|
seg.addEventListener('focus', () => {
|
||||||
|
if (seg.dataset.placeholder === 'true') {
|
||||||
|
seg.textContent = '';
|
||||||
|
}
|
||||||
|
// select all
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(seg);
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
});
|
||||||
|
seg.addEventListener('blur', () => {
|
||||||
|
const len = parseInt(seg.dataset.len, 10);
|
||||||
|
const min = parseInt(seg.dataset.min, 10);
|
||||||
|
const max = parseInt(seg.dataset.max, 10);
|
||||||
|
let v = seg.textContent.replace(/\D/g, '');
|
||||||
|
if (!v) {
|
||||||
|
seg.dataset.placeholder = 'true';
|
||||||
|
seg.textContent = seg.dataset.seg.toUpperCase().slice(0,len).padEnd(len, seg.dataset.seg[0].toUpperCase());
|
||||||
|
// reset to nice placeholder
|
||||||
|
const ph = { day:'DD', month:'MM', year:'YYYY', hour:'HH', minute:'MM' }[seg.dataset.seg];
|
||||||
|
seg.textContent = ph;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = parseInt(v, 10);
|
||||||
|
if (n < min) n = min;
|
||||||
|
if (n > max) n = max;
|
||||||
|
seg.dataset.placeholder = 'false';
|
||||||
|
seg.textContent = String(n).padStart(len, '0');
|
||||||
|
refreshSelectedDay();
|
||||||
|
});
|
||||||
|
seg.addEventListener('keydown', (e) => {
|
||||||
|
const len = parseInt(seg.dataset.len, 10);
|
||||||
|
if (e.key === 'ArrowRight' || (e.key === '/' || e.key === ':') ) {
|
||||||
|
e.preventDefault();
|
||||||
|
const next = dfSegments[idx + 1];
|
||||||
|
if (next) next.focus();
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault();
|
||||||
|
const prev = dfSegments[idx - 1];
|
||||||
|
if (prev) prev.focus();
|
||||||
|
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
const min = parseInt(seg.dataset.min, 10);
|
||||||
|
const max = parseInt(seg.dataset.max, 10);
|
||||||
|
const cur = parseInt(seg.textContent.replace(/\D/g,''), 10);
|
||||||
|
const base = isNaN(cur) ? min : cur;
|
||||||
|
let n = base + (e.key === 'ArrowUp' ? 1 : -1);
|
||||||
|
if (n < min) n = max;
|
||||||
|
if (n > max) n = min;
|
||||||
|
seg.dataset.placeholder = 'false';
|
||||||
|
seg.textContent = String(n).padStart(len, '0');
|
||||||
|
refreshSelectedDay();
|
||||||
|
} else if (/^\d$/.test(e.key)) {
|
||||||
|
const cur = seg.textContent.replace(/\D/g,'');
|
||||||
|
if (cur.length >= len) {
|
||||||
|
e.preventDefault();
|
||||||
|
seg.textContent = e.key;
|
||||||
|
// place caret at end
|
||||||
|
}
|
||||||
|
// when reaching len, advance to next segment after this char
|
||||||
|
setTimeout(() => {
|
||||||
|
if ((seg.textContent || '').replace(/\D/g,'').length >= len) {
|
||||||
|
const next = dfSegments[idx + 1];
|
||||||
|
if (next) next.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else if (e.key === 'Backspace' && seg.textContent === '') {
|
||||||
|
const prev = dfSegments[idx - 1];
|
||||||
|
if (prev) { e.preventDefault(); prev.focus(); }
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
seg.blur();
|
||||||
|
const pop = document.querySelector('.popover[data-popover="due"]');
|
||||||
|
closePopover(pop);
|
||||||
|
updateDuePill();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
const pop = document.querySelector('.popover[data-popover="due"]');
|
||||||
|
closePopover(pop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function readDateFieldFromDOM() {
|
||||||
|
return readDateField();
|
||||||
|
}
|
||||||
|
renderDateField();
|
||||||
|
document.getElementById('fmt-pref').addEventListener('change', (e) => {
|
||||||
|
currentFmt = e.target.value;
|
||||||
|
renderDateField();
|
||||||
|
updateDuePill();
|
||||||
|
});
|
||||||
|
|
||||||
|
function readDateField() {
|
||||||
|
const get = (k) => {
|
||||||
|
const s = document.querySelector(`.segment[data-seg="${k}"]`);
|
||||||
|
if (!s || s.dataset.placeholder === 'true') return null;
|
||||||
|
const v = s.textContent.replace(/\D/g,'');
|
||||||
|
return v ? parseInt(v, 10) : null;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
day: get('day'), month: get('month'), year: get('year'),
|
||||||
|
hour: get('hour'), minute: get('minute'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateValue(d) {
|
||||||
|
const day = String(d.day).padStart(2,'0');
|
||||||
|
const month = String(d.month).padStart(2,'0');
|
||||||
|
const year = String(d.year);
|
||||||
|
switch (currentFmt) {
|
||||||
|
case 'MM/dd/yyyy': return `${month}/${day}/${year}`;
|
||||||
|
case 'yyyy-MM-dd': return `${year}-${month}-${day}`;
|
||||||
|
default: return `${day}/${month}/${year}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateDuePill() {
|
||||||
|
const d = readDateField();
|
||||||
|
const pill = document.querySelector('.pill[data-pill="due"]');
|
||||||
|
if (d.day && d.month && d.year) {
|
||||||
|
pill.removeAttribute('data-empty');
|
||||||
|
const time = d.hour != null && d.minute != null
|
||||||
|
? ` ${String(d.hour).padStart(2,'0')}:${String(d.minute).padStart(2,'0')}` : '';
|
||||||
|
pill.innerHTML = `<span class="pill-icon">📅</span><span class="pill-label">Due</span><span class="pill-sep">·</span><span class="pill-value">${formatDateValue(d)}${time}</span>`;
|
||||||
|
} else {
|
||||||
|
pill.setAttribute('data-empty', 'true');
|
||||||
|
pill.innerHTML = '<span class="pill-icon">📅</span><span class="pill-label">Due</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Mini calendar ---------- */
|
||||||
|
let calYear = 2026, calMonth = 5; // May 2026
|
||||||
|
function renderCalendar() {
|
||||||
|
const grid = document.getElementById('cal-grid');
|
||||||
|
document.getElementById('cal-month').textContent =
|
||||||
|
new Date(calYear, calMonth - 1, 1).toLocaleString(undefined, { month: 'long', year: 'numeric' });
|
||||||
|
grid.innerHTML = '';
|
||||||
|
const dows = ['Mo','Tu','We','Th','Fr','Sa','Su'];
|
||||||
|
dows.forEach((d) => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'cal-dow'; el.textContent = d;
|
||||||
|
grid.appendChild(el);
|
||||||
|
});
|
||||||
|
const first = new Date(calYear, calMonth - 1, 1);
|
||||||
|
const offset = (first.getDay() + 6) % 7; // Mon-first
|
||||||
|
const daysInMonth = new Date(calYear, calMonth, 0).getDate();
|
||||||
|
const daysPrev = new Date(calYear, calMonth - 1, 0).getDate();
|
||||||
|
for (let i = offset - 1; i >= 0; i--) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'cal-day';
|
||||||
|
el.dataset.otherMonth = 'true';
|
||||||
|
el.textContent = daysPrev - i;
|
||||||
|
grid.appendChild(el);
|
||||||
|
}
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'cal-day';
|
||||||
|
el.tabIndex = 0;
|
||||||
|
el.textContent = d;
|
||||||
|
el.dataset.day = d;
|
||||||
|
el.addEventListener('click', () => pickCalDay(d));
|
||||||
|
el.addEventListener('keydown', (e) => onCalKey(e, d));
|
||||||
|
grid.appendChild(el);
|
||||||
|
}
|
||||||
|
refreshSelectedDay();
|
||||||
|
}
|
||||||
|
function refreshSelectedDay() {
|
||||||
|
const d = readDateField();
|
||||||
|
const days = document.querySelectorAll('.cal-day[data-day]');
|
||||||
|
days.forEach((el) => el.removeAttribute('data-selected'));
|
||||||
|
if (d.day && d.month === calMonth && d.year === calYear) {
|
||||||
|
const tgt = document.querySelector(`.cal-day[data-day="${d.day}"]`);
|
||||||
|
if (tgt) tgt.setAttribute('data-selected', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pickCalDay(d) {
|
||||||
|
const segDay = document.querySelector('.segment[data-seg="day"]');
|
||||||
|
const segMonth = document.querySelector('.segment[data-seg="month"]');
|
||||||
|
const segYear = document.querySelector('.segment[data-seg="year"]');
|
||||||
|
segDay.dataset.placeholder = 'false'; segDay.textContent = String(d).padStart(2,'0');
|
||||||
|
segMonth.dataset.placeholder = 'false'; segMonth.textContent = String(calMonth).padStart(2,'0');
|
||||||
|
segYear.dataset.placeholder = 'false'; segYear.textContent = String(calYear);
|
||||||
|
refreshSelectedDay();
|
||||||
|
updateDuePill();
|
||||||
|
}
|
||||||
|
function onCalKey(e, d) {
|
||||||
|
const grid = document.getElementById('cal-grid');
|
||||||
|
const days = Array.from(grid.querySelectorAll('.cal-day[data-day]'));
|
||||||
|
const idx = days.findIndex((el) => parseInt(el.dataset.day,10) === d);
|
||||||
|
let target = null;
|
||||||
|
if (e.key === 'ArrowRight') target = days[idx + 1];
|
||||||
|
else if (e.key === 'ArrowLeft') target = days[idx - 1];
|
||||||
|
else if (e.key === 'ArrowDown') target = days[idx + 7];
|
||||||
|
else if (e.key === 'ArrowUp') target = days[idx - 7];
|
||||||
|
else if (e.key === 'Enter') { e.preventDefault(); pickCalDay(d); return; }
|
||||||
|
if (target) { e.preventDefault(); target.focus(); }
|
||||||
|
}
|
||||||
|
document.querySelectorAll('[data-nav]').forEach((b) => {
|
||||||
|
b.addEventListener('click', () => {
|
||||||
|
const dir = parseInt(b.dataset.nav, 10);
|
||||||
|
calMonth += dir;
|
||||||
|
if (calMonth < 1) { calMonth = 12; calYear--; }
|
||||||
|
if (calMonth > 12) { calMonth = 1; calYear++; }
|
||||||
|
renderCalendar();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
renderCalendar();
|
||||||
|
|
||||||
|
/* ---------- click outside closes popovers ---------- */
|
||||||
|
document.addEventListener('mousedown', (e) => {
|
||||||
|
const inPop = e.target.closest('.popover');
|
||||||
|
const inPill = e.target.closest('.pill');
|
||||||
|
if (!inPop && !inPill) closeAllPopovers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
253
docs/multi-region-guide.md
Normal file
253
docs/multi-region-guide.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Guida Multi-Region — adiuvAI API
|
||||||
|
|
||||||
|
> Stato attuale: FastAPI containerizzata (docker-compose) su singolo VPS Hetzner in Europa.
|
||||||
|
> Obiettivo: ridurre la latenza per utenti fuori dall'Europa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fase 1 — Ottimizzare Cloudflare (già in uso)
|
||||||
|
|
||||||
|
### 1.1 Argo Smart Routing
|
||||||
|
|
||||||
|
- **Dashboard → Traffic → Argo** — attivalo (~$5/mese + $0.10/GB)
|
||||||
|
- Usa i backbone privati Cloudflare invece dell'internet pubblico
|
||||||
|
- Riduce la latenza del 30-40% senza toccare nulla lato server
|
||||||
|
- Singolo cambiamento con miglior rapporto costo/beneficio
|
||||||
|
|
||||||
|
### 1.2 SSL/TLS
|
||||||
|
|
||||||
|
- **Dashboard → SSL/TLS → Overview** → mode **Full (Strict)** (non "Flexible", causa redirect loop)
|
||||||
|
- Abilita **TLS 1.3** (meno round-trip nell'handshake)
|
||||||
|
- Abilita **Early Hints** (103) in Speed → Optimization
|
||||||
|
|
||||||
|
### 1.3 Cache Rules
|
||||||
|
|
||||||
|
Di default Cloudflare non cachea le risposte API (Content-Type `application/json`). Per gli endpoint pubblici (es. `/api/v1/health`):
|
||||||
|
|
||||||
|
- **Dashboard → Caching → Cache Rules** → crea regola:
|
||||||
|
- Match: `URI Path starts with /api/v1/health`
|
||||||
|
- Action: Cache, Edge TTL 30s, Browser TTL 10s
|
||||||
|
- Lato codice: aggiungere header `Cache-Control: public, s-maxage=30` e `CDN-Cache-Control: public, max-age=30` all'health endpoint
|
||||||
|
- **NON** cacheate endpoint autenticati (il JWT rende ogni richiesta unica)
|
||||||
|
|
||||||
|
### 1.4 Response Compression
|
||||||
|
|
||||||
|
- **Dashboard → Speed → Optimization → Content Optimization**
|
||||||
|
- Abilita **Brotli** (più efficiente di gzip per payload JSON)
|
||||||
|
- Le risposte JSON vengono compresse automaticamente al transit
|
||||||
|
|
||||||
|
### 1.5 WebSocket
|
||||||
|
|
||||||
|
- **Dashboard → Network** → verifica che **WebSockets** sia ON (default nel piano Free)
|
||||||
|
- Il `/chat/stream` WebSocket viene proxato ma non cacheato
|
||||||
|
- Il keepalive di 30s che già avete mantiene la connessione viva attraverso Cloudflare
|
||||||
|
|
||||||
|
### 1.6 Tiered Cache (piano Pro+)
|
||||||
|
|
||||||
|
- **Dashboard → Caching → Tiered Cache** → attiva **Smart Tiered Caching**
|
||||||
|
- Cloudflare usa data center "upper-tier" come cache intermedia
|
||||||
|
- Riduce le hit al tuo origin server
|
||||||
|
|
||||||
|
### 1.7 Timeout
|
||||||
|
|
||||||
|
- **Dashboard → Network → WebSocket timeout**: aumenta se gli utenti hanno sessioni chat lunghe
|
||||||
|
- **Proxy Read Timeout**: default 100s, sufficiente per le LLM call (il tool loop ha cap 5 iterazioni)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fase 2 — Secondo nodo in US East
|
||||||
|
|
||||||
|
### Architettura target
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─── Cloudflare (Geo Steering) ───┐
|
||||||
|
│ │
|
||||||
|
utenti EU/Africa utenti Americas
|
||||||
|
│ │
|
||||||
|
┌────────▼─────────┐ ┌──────────▼─────────┐
|
||||||
|
│ VPS EU (attuale) │ │ VPS US (nuovo) │
|
||||||
|
│ docker-compose │ │ docker-compose │
|
||||||
|
│ app + PG primary │ │ app + PG replica │
|
||||||
|
└────────┬──────────┘ └──────────┬──────────┘
|
||||||
|
│ │
|
||||||
|
└── PG streaming replication ────────┘
|
||||||
|
(async, read-only replica)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opzione A: Secondo VPS Hetzner (Ashburn) + Cloudflare Load Balancing
|
||||||
|
|
||||||
|
Estensione naturale del setup attuale — minimo cambiamento architetturale.
|
||||||
|
|
||||||
|
#### Step 1 — Provisioning del VPS US
|
||||||
|
|
||||||
|
1. Crea un VPS Hetzner in **Ashburn (us-east)** (stesse specs del nodo EU)
|
||||||
|
2. Setup identico: Docker, Docker Compose, git
|
||||||
|
3. Configura un **tunnel WireGuard** tra EU e US per il traffico DB (mai esporre PG sulla rete pubblica)
|
||||||
|
|
||||||
|
#### Step 2 — PostgreSQL Streaming Replication
|
||||||
|
|
||||||
|
**Sul PRIMARY (EU):**
|
||||||
|
|
||||||
|
1. Creare un utente replication:
|
||||||
|
```sql
|
||||||
|
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '<strong_password>';
|
||||||
|
```
|
||||||
|
2. Creare un replication slot:
|
||||||
|
```sql
|
||||||
|
SELECT pg_create_physical_replication_slot('replica_us_east');
|
||||||
|
```
|
||||||
|
3. Configurare `pg_hba.conf` per permettere connessioni dal VPS US:
|
||||||
|
```
|
||||||
|
host replication replicator <US_VPS_WIREGUARD_IP>/32 scram-sha-256
|
||||||
|
```
|
||||||
|
4. Esporre la porta PG solo sull'IP WireGuard nel `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
ports:
|
||||||
|
- "10.0.0.1:5432:5432" # solo interfaccia WireGuard
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sul REPLICA (US):**
|
||||||
|
|
||||||
|
1. Base backup dal primary:
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-v postgres_data:/var/lib/postgresql/data \
|
||||||
|
pgvector/pgvector:pg16 \
|
||||||
|
bash -c "pg_basebackup -h <PRIMARY_WG_IP> -U replicator \
|
||||||
|
-D /var/lib/postgresql/data -Fp -Xs -P -R"
|
||||||
|
```
|
||||||
|
Il flag `-R` crea automaticamente `standby.signal` e scrive `primary_conninfo` in `postgresql.auto.conf`.
|
||||||
|
|
||||||
|
2. Avviare PG in modalita replica (legge `standby.signal` e si connette al primary)
|
||||||
|
|
||||||
|
3. Verificare:
|
||||||
|
- Sul primary: `SELECT * FROM pg_stat_replication;` (deve mostrare il replica)
|
||||||
|
- Sul replica: `SELECT pg_is_in_recovery();` (deve restituire `t`)
|
||||||
|
|
||||||
|
#### Step 3 — Modifiche al codice FastAPI
|
||||||
|
|
||||||
|
Modifiche necessarie in `app/config/settings.py`:
|
||||||
|
- Aggiungere `DATABASE_READ_URL: str = ""` — URL del replica locale per le letture
|
||||||
|
- Aggiungere `REGION: str = "eu"` — identificativo regione per health check e observability
|
||||||
|
|
||||||
|
Modifiche in `app/db.py`:
|
||||||
|
- Creare un secondo engine `read_engine` che usa `DATABASE_READ_URL` (fallback a `DATABASE_URL` se vuoto)
|
||||||
|
- Esporre un `get_read_session()` dependency da usare nelle query read-only
|
||||||
|
|
||||||
|
Modifiche in `app/main.py`:
|
||||||
|
- L'health endpoint deve restituire `region` nel payload
|
||||||
|
- Aggiungere header `Cache-Control` / `CDN-Cache-Control` per il caching all'edge
|
||||||
|
|
||||||
|
Nelle route, per le query di sola lettura pesanti (es. ricerca, listing):
|
||||||
|
- Usare `db: AsyncSession = Depends(get_read_session)` invece di `get_session`
|
||||||
|
- Le scritture (auth, billing, update) continuano a usare `get_session` (→ primary EU)
|
||||||
|
|
||||||
|
#### Step 4 — Docker Compose per il nodo US
|
||||||
|
|
||||||
|
Creare un `docker-compose.replica.yml` (override) che:
|
||||||
|
- Sovrascrive le env dell'app con `DATABASE_READ_URL` verso il DB locale e `DATABASE_URL` verso il primary EU
|
||||||
|
- Imposta `REGION=us-east`
|
||||||
|
- Avvia PG in modalita replica (con `primary_conninfo` che punta al primary EU via WireGuard)
|
||||||
|
|
||||||
|
Il `.env` sul nodo US:
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql+asyncpg://postgres:<pass>@<PRIMARY_WG_IP>:5432/adiuvai
|
||||||
|
DATABASE_READ_URL=postgresql+asyncpg://postgres:postgres@db:5432/adiuvai
|
||||||
|
REGION=us-east
|
||||||
|
PRIMARY_DB_HOST=<PRIMARY_WG_IP>
|
||||||
|
REPLICATOR_PASSWORD=<strong_password>
|
||||||
|
# ... resto delle variabili (JWT_SECRET, STRIPE, LLM keys, etc.) identiche al nodo EU
|
||||||
|
```
|
||||||
|
|
||||||
|
Avvio: `docker compose -f docker-compose.yml -f docker-compose.replica.yml up -d`
|
||||||
|
|
||||||
|
#### Step 5 — Deploy CI multi-region
|
||||||
|
|
||||||
|
Estendere il workflow `.gitea/workflows/deploy.yaml` con un secondo job `deploy-us`:
|
||||||
|
- Identico a `deploy` ma con SSH verso il VPS US
|
||||||
|
- Usa `secrets.SSH_HOST_US`, `secrets.SSH_USER_US`, `secrets.SSH_KEY_US`
|
||||||
|
- Il comando di deploy usa `-f docker-compose.yml -f docker-compose.replica.yml`
|
||||||
|
- **NON** esegue `alembic upgrade head` — le migrazioni girano solo sul primary (il replica riceve le DDL via replication)
|
||||||
|
|
||||||
|
I due job `deploy` e `deploy-us` possono girare in parallelo (entrambi dipendono solo da `test`).
|
||||||
|
|
||||||
|
#### Step 6 — Cloudflare Geo Steering
|
||||||
|
|
||||||
|
1. **Dashboard → Traffic → Load Balancing** (piano Pro, ~$5/mese per pool)
|
||||||
|
2. Creare due **Origin Pools**:
|
||||||
|
- `eu-pool`: origin = IP del VPS EU, health check = `GET /api/v1/health`
|
||||||
|
- `us-pool`: origin = IP del VPS US, health check = `GET /api/v1/health`
|
||||||
|
3. Creare un **Load Balancer** su `api.adiuvai.com`:
|
||||||
|
- Steering policy: **Geo**
|
||||||
|
- EU/Africa → `eu-pool`
|
||||||
|
- Americas → `us-pool`
|
||||||
|
- Fallback: `eu-pool`
|
||||||
|
4. Impostare health monitor: `GET /api/v1/health`, interval 60s, timeout 5s
|
||||||
|
- Se un nodo va giù, tutto il traffico va al nodo sano (automatic failover)
|
||||||
|
|
||||||
|
### Opzione B: Fly.io (alternativa più semplice, meno controllo)
|
||||||
|
|
||||||
|
Se preferisci evitare la gestione manuale di un secondo VPS:
|
||||||
|
|
||||||
|
1. Crea un `fly.toml` nella root del progetto API
|
||||||
|
2. `fly launch` — Fly rileva il Dockerfile e deploya
|
||||||
|
3. `fly regions add iad` — aggiunge US East (Ashburn)
|
||||||
|
4. Fly gestisce: routing anycast, health checks, TLS, auto-scaling
|
||||||
|
5. Il DB resta su Hetzner EU — Fly non risolve il problema del database, ma elimina tutta la gestione infrastrutturale dell'app layer
|
||||||
|
6. Costo: ~$5-15/mese per region (dipende dalle risorse)
|
||||||
|
7. Contro: meno controllo, vendor lock-in, il DB non ha replica locale
|
||||||
|
|
||||||
|
### Opzione C: Hetzner Cloud Load Balancer + geo DNS esterno
|
||||||
|
|
||||||
|
- Hetzner offre load balancer nativi, ma sono single-region (non cross-region)
|
||||||
|
- Non adatto per geo-routing, utile solo per HA nella stessa region
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fase 3 — Terzo nodo in Asia (futuro)
|
||||||
|
|
||||||
|
Stessa procedura della Fase 2:
|
||||||
|
1. VPS Hetzner Singapore (o AWS ap-southeast-1)
|
||||||
|
2. Secondo PG replica con slot `replica_asia`
|
||||||
|
3. Terzo pool in Cloudflare Load Balancing con geo steering per Asia-Pacific
|
||||||
|
4. Terzo job `deploy-asia` nel CI
|
||||||
|
|
||||||
|
Da valutare solo quando il traffico dall'Asia lo giustifica.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicurezza della rete tra i nodi
|
||||||
|
|
||||||
|
| Metodo | Pro | Contro |
|
||||||
|
|--------|-----|--------|
|
||||||
|
| **WireGuard** | Semplice, veloce, <1ms overhead, kernel-level | Setup manuale per nodo |
|
||||||
|
| **Hetzner vSwitch** | Zero config se entrambi su Hetzner | Solo stessa region |
|
||||||
|
| **Tailscale** | WireGuard gestito, zero config rete | Dipendenza esterna |
|
||||||
|
| **SSH tunnel** | Nessun software extra | Overhead maggiore, meno stabile |
|
||||||
|
|
||||||
|
**Raccomandazione**: WireGuard (o Tailscale per semplicita) tra tutti i nodi. Mai esporre PostgreSQL 5432 sull'IP pubblico.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Considerazioni specifiche per adiuvAI
|
||||||
|
|
||||||
|
- **L'app e local-first**: la maggior parte delle operazioni (tasks, notes, projects) avviene in SQLite locale nell'Electron app. Il backend serve solo auth, chat streaming, cloud storage e billing. Questo significa che la latenza del backend impatta meno di quanto sembrerebbe.
|
||||||
|
- **WebSocket `/chat/stream`**: il geo steering porta l'utente al nodo piu vicino, ma la risposta LLM dipende dalla latenza verso OpenAI/Anthropic (non verso il tuo server). Il beneficio principale e nel tempo di handshake e nel primo token.
|
||||||
|
- **`_pending_states` in-memory per OAuth**: gia documentato come non scalabile su multi-worker. Con multi-region diventa critico — servira Redis condiviso o spostare lo state su DB.
|
||||||
|
- **JWT_SECRET deve essere identico** su tutti i nodi — un token emesso dal nodo EU deve essere validato dal nodo US.
|
||||||
|
- **Alembic migrations**: eseguire SOLO sul primary. Il replica riceve le DDL via streaming replication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stima costi
|
||||||
|
|
||||||
|
| Componente | Costo mensile |
|
||||||
|
|------------|---------------|
|
||||||
|
| Argo Smart Routing | ~$5 + $0.10/GB |
|
||||||
|
| Cloudflare Load Balancing | ~$5/pool |
|
||||||
|
| VPS Hetzner US (CX22) | ~$5-10 |
|
||||||
|
| WireGuard | Gratis |
|
||||||
|
| **Totale Fase 1** | **~$5** |
|
||||||
|
| **Totale Fase 2** | **~$15-20** |
|
||||||
510
docs/plan-brief-agent.md
Normal file
510
docs/plan-brief-agent.md
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
# Dedicated Brief Agent (Home + Project)
|
||||||
|
|
||||||
|
> Ralph-loop plan. Execute one phase per iteration. Each phase is self-contained:
|
||||||
|
> its **Files**, **Tasks**, **Acceptance**, and **Verify** blocks are everything
|
||||||
|
> the agent needs to finish that phase without re-reading earlier phases.
|
||||||
|
> Mark `- [x]` as you complete tasks. Do not start phase N+1 until phase N's
|
||||||
|
> **Acceptance** is fully met.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
All Python commands in this plan (`pytest`, `python`, `ruff`, `alembic`) must
|
||||||
|
be run inside the `api/` project's virtualenv at `api/.venv`.
|
||||||
|
|
||||||
|
- **bash / WSL / macOS / Linux**: `source api/.venv/bin/activate` (or prefix
|
||||||
|
commands with `api/.venv/bin/python -m ...`)
|
||||||
|
- **Windows PowerShell**: `api\.venv\Scripts\Activate.ps1`
|
||||||
|
- **Windows bash shell (this repo's default)**: `source api/.venv/Scripts/activate`
|
||||||
|
|
||||||
|
Do **not** use the system Python or a globally installed `pytest`/`ruff` —
|
||||||
|
dependencies are pinned inside the venv. Every `Verify` step below assumes the
|
||||||
|
venv is active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Today the daily brief is produced by the `home-agent` with a prompt stuffed into
|
||||||
|
`sendHomeRequest()` at [adiuvAI/src/main/ai/orchestrator.ts:160](adiuvAI/src/main/ai/orchestrator.ts#L160). This
|
||||||
|
couples two very different jobs (chat vs summarisation) into one agent and one
|
||||||
|
prompt. Worse: the home agent is wired to emit XML tag wrappers (`<task>`,
|
||||||
|
`<timeline>`, `<project>`) for the UI component renderer — wrappers the brief
|
||||||
|
does not want. We filter them out in post, but the LLM still pays tokens for
|
||||||
|
them and sometimes leaks malformed tags.
|
||||||
|
|
||||||
|
We want a **dedicated brief agent** that:
|
||||||
|
|
||||||
|
- Runs in **two modes** — `home` (daily brief) and `project` (per-project status brief).
|
||||||
|
- Produces **plain text only** — no XML/HTML tag wrappers, no bracketed id lists.
|
||||||
|
- Uses the **same infra pattern** as the other agents: `get_agent_llm(...)`,
|
||||||
|
`.env` override, Langfuse prompt via `get_prompt_or_fallback()`,
|
||||||
|
Langfuse tracing via `langfuse_context` + generation observations.
|
||||||
|
- Is **memory-aware** — core memory and relational memory are injected into the
|
||||||
|
system prompt so the brief can say "Client X usually pays late — your invoice
|
||||||
|
is still out" instead of a generic list.
|
||||||
|
- Is **read-only** — no create/update/delete tools. Tool surface is the minimum
|
||||||
|
needed to answer "what needs attention right now?".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Electron ─ WsFrame{type:"brief_request", mode:"home"|"project", project_id?} ─► device_ws
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
core/brief_agent.py
|
||||||
|
├── run_home_brief()
|
||||||
|
└── run_project_brief()
|
||||||
|
│
|
||||||
|
read-only tool subset │
|
||||||
|
(tasks, projects, notes, │
|
||||||
|
timelines, memory get) │
|
||||||
|
▼
|
||||||
|
plain-text stream ▲
|
||||||
|
back to renderer ─┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key decisions:**
|
||||||
|
|
||||||
|
- New WS frame type `brief_request` — *not* a reuse of `home_request` — so the
|
||||||
|
frame payload stays small and typed, and the server can pick the right agent
|
||||||
|
without sniffing the prompt.
|
||||||
|
- Read-only tools only. Give the LLM access to the same data the UI sees
|
||||||
|
(tasks, projects, notes, timelines, memory **get**). No mutating tools, no
|
||||||
|
memory-write tools — a brief should never change state.
|
||||||
|
- `resolved_project_id` is passed explicitly in the request payload for the
|
||||||
|
`project` mode (no LLM-side resolution). The `home` mode omits it.
|
||||||
|
- Both modes stream — the UI already has streaming rendering for the home
|
||||||
|
brief; we reuse the same pattern for the project brief card.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Improved prompts
|
||||||
|
|
||||||
|
The current prompt tries to do everything in one paragraph. These split the job
|
||||||
|
into role, data rules, voice, and output contract — the structure the model
|
||||||
|
actually follows.
|
||||||
|
|
||||||
|
### `home_brief` (Langfuse prompt, label `production`)
|
||||||
|
|
||||||
|
```
|
||||||
|
You are the user's personal assistant producing a short daily brief.
|
||||||
|
|
||||||
|
ROLE
|
||||||
|
Act like a calm, attentive secretary writing a stand-up note for your boss.
|
||||||
|
Warm and human, never breezy. Never cheerful filler, never emojis, never
|
||||||
|
"here is your brief" meta-text. The user is opening the app mid-workday and
|
||||||
|
is probably stressed — your job is to lower cognitive load, not add noise.
|
||||||
|
|
||||||
|
TOOLS — always call before writing
|
||||||
|
Pull fresh data every run. Do not invent counts or titles. Use at minimum:
|
||||||
|
- list_tasks_due_today — tasks the user owes today
|
||||||
|
- list_timeline_events_today — events starting or ending today
|
||||||
|
- list_active_projects — projects currently in progress or at risk
|
||||||
|
- memory_list_blocks / memory_get — personal context about people, clients,
|
||||||
|
payment habits, working preferences
|
||||||
|
If a tool returns nothing, simply omit that topic. Never report zeros.
|
||||||
|
|
||||||
|
WHAT TO INCLUDE
|
||||||
|
1. Tasks due today (title + priority; group the 1–2 most important).
|
||||||
|
2. Timeline events starting or ending today (and anything that starts/ends
|
||||||
|
tomorrow if the user has a very light day).
|
||||||
|
3. Active projects that need a nudge — stalled, blocked, or awaiting input.
|
||||||
|
4. Memory-aware colour where it sharpens the brief. Examples:
|
||||||
|
- "Client Rossi tends to pay late — the Acme invoice is 6 days out."
|
||||||
|
- "You usually dislike meetings before 10:00 — the call at 09:30 is unusual."
|
||||||
|
Only add a memory line when it changes what the user does. Do not pad.
|
||||||
|
|
||||||
|
WHAT TO OMIT
|
||||||
|
- Zero-counts ("no overdue items", "0 meetings today").
|
||||||
|
- Statistics ("2 active projects, 3 completed tasks").
|
||||||
|
- Headers, titles, greetings, sign-offs, dates, emojis, slang.
|
||||||
|
- Meta-phrases ("here is", "let me know if", "hope this helps").
|
||||||
|
- XML/HTML tags of any kind. Plain prose only.
|
||||||
|
|
||||||
|
LIGHT-DAY CLAUSE
|
||||||
|
If tasks + events + active-project-nudges together produce fewer than two
|
||||||
|
sentences of content, also list 1–2 projects in status `on_hold` or `waiting`
|
||||||
|
and ask a single, specific question about them — e.g. "Is the Bianchi
|
||||||
|
redesign still paused, or ready to pick back up?" One question max, grounded
|
||||||
|
in a real project name.
|
||||||
|
|
||||||
|
VOICE
|
||||||
|
- Calm. Concise. Human. Short sentences.
|
||||||
|
- Use **bold** sparingly for task titles, project names, and people's names.
|
||||||
|
- No bullet lists. Flow as 2–4 sentences of prose.
|
||||||
|
|
||||||
|
LENGTH
|
||||||
|
2–4 sentences total. Hard cap 4. If the day is truly empty, one sentence.
|
||||||
|
|
||||||
|
Respond in the user's language ({{language}}). Today is {{today}}.
|
||||||
|
```
|
||||||
|
|
||||||
|
Variables: `{{language}}` (e.g. "Italian"), `{{today}}` (ISO date).
|
||||||
|
|
||||||
|
### `project_brief` (Langfuse prompt, label `production`)
|
||||||
|
|
||||||
|
```
|
||||||
|
You are the project assistant producing a short status brief for ONE project.
|
||||||
|
|
||||||
|
ROLE
|
||||||
|
A senior project manager summarising state-of-play for the owner. Factual,
|
||||||
|
sharp, forward-looking. Never reassuring filler, never emojis.
|
||||||
|
|
||||||
|
SCOPE
|
||||||
|
Work only with project_id = {{project_id}}. Do not mention or pull data from
|
||||||
|
other projects. Use tools to fetch fresh data:
|
||||||
|
- get_project — current status, dates, description
|
||||||
|
- list_tasks(project_id) — open work, split by status
|
||||||
|
- list_timeline_events(project_id) — milestones hit, upcoming, overdue
|
||||||
|
- list_project_notes(project_id) — any recent decisions or blockers
|
||||||
|
- memory_get — relevant context about the client, collaborators, constraints
|
||||||
|
|
||||||
|
STRUCTURE — follow exactly, one short paragraph per section, no headers
|
||||||
|
1. **State.** One sentence: current phase, health (on track / at risk / blocked),
|
||||||
|
and why. Cite the concrete signal (overdue milestone, stalled tasks, recent
|
||||||
|
blocker note).
|
||||||
|
2. **What's moving.** What was completed or progressed recently. Name specific
|
||||||
|
tasks or milestones.
|
||||||
|
3. **Next steps.** The 1–3 most important things the user should do next, in
|
||||||
|
priority order. Be concrete — task name, who owns it, when due if known.
|
||||||
|
If waiting on someone else, name them and what the ask is.
|
||||||
|
4. **Risks / memory-flagged items.** One line max. Only include when there is
|
||||||
|
a real risk or a relevant memory (e.g. late-paying client, tight deadline,
|
||||||
|
scope change). Omit the section entirely if nothing to say.
|
||||||
|
|
||||||
|
WHAT TO OMIT
|
||||||
|
- Zero-counts ("no overdue tasks").
|
||||||
|
- Generic advice ("keep up the good work").
|
||||||
|
- Greetings, headers, bullet lists, emojis, sign-offs, meta-phrases.
|
||||||
|
- XML/HTML tags or bracketed id lists. Plain prose only.
|
||||||
|
|
||||||
|
VOICE
|
||||||
|
- Direct. Factual. No fluff.
|
||||||
|
- Use **bold** sparingly for task titles, milestone names, and the owner's name.
|
||||||
|
- Short sentences. Prefer verbs over nouns ("Client review is blocking release"
|
||||||
|
not "There is a blocker which is the client review").
|
||||||
|
|
||||||
|
LENGTH
|
||||||
|
4–8 sentences total across the 3–4 sections. Hard cap 8.
|
||||||
|
|
||||||
|
Respond in the user's language ({{language}}). Today is {{today}}.
|
||||||
|
```
|
||||||
|
|
||||||
|
Variables: `{{project_id}}`, `{{language}}`, `{{today}}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — Backend config scaffolding
|
||||||
|
|
||||||
|
**Goal:** `LLM_MODEL_BRIEF_AGENT` resolvable via `get_agent_llm("brief-agent")`.
|
||||||
|
|
||||||
|
**Files**
|
||||||
|
- `api/app/config/settings.py`
|
||||||
|
- `api/app/core/llm.py`
|
||||||
|
- `api/.env.example`
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [ ] Add field `LLM_MODEL_BRIEF_AGENT: str = ""` to `Settings` after `LLM_MODEL_CLOUD_PROCESSOR`.
|
||||||
|
- [ ] Add `"brief-agent": lambda: settings.LLM_MODEL_BRIEF_AGENT or settings.LLM_MODEL` entry to `_AGENT_MODEL_SETTINGS` in `llm.py`.
|
||||||
|
- [ ] Add a commented-out `LLM_MODEL_BRIEF_AGENT=` block in `.env.example`, with a 2-line description mirroring the existing style ("Brief-agent — produces home and project text briefs. A small model (e.g. gpt-4o-mini) is sufficient.").
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- `python -c "from app.core.llm import model_for_agent; print(model_for_agent('brief-agent'))"` prints the default model (matches `LLM_MODEL`) when the override is empty; prints the override when set.
|
||||||
|
- `ruff check .` passes.
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- `cd api && source .venv/Scripts/activate && python -c "from app.core.llm import model_for_agent; print(model_for_agent('brief-agent'))"`
|
||||||
|
- `cd api && source .venv/Scripts/activate && ruff check .`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — Brief-agent module (read-only tool subset)
|
||||||
|
|
||||||
|
**Goal:** `run_home_brief()` and `run_project_brief()` callables exist and work
|
||||||
|
end-to-end against a live backend, producing plain-text streams. No WS wiring
|
||||||
|
yet — exercised via a `scripts/smoke_brief.py` one-liner.
|
||||||
|
|
||||||
|
**Files (new)**
|
||||||
|
- `api/app/core/brief_agent.py`
|
||||||
|
|
||||||
|
**Files (touched)**
|
||||||
|
- `api/app/agents/task_agent.py` — export a `TASK_READ_TOOLS` list
|
||||||
|
(`list_tasks`, `list_tasks_due_today`, `list_task_comments`).
|
||||||
|
- `api/app/agents/project_agent.py` — export a `PROJECT_READ_TOOLS` list
|
||||||
|
(`list_projects`, `list_all_projects`, `get_project`).
|
||||||
|
- `api/app/agents/timeline_agent.py` — export a `TIMELINE_READ_TOOLS` list
|
||||||
|
(`list_timelines`, plus a new `list_timelines_today` that filters by today
|
||||||
|
— add it alongside the existing tools) and a `list_timeline_events` alias
|
||||||
|
scoped by `project_id`.
|
||||||
|
- `api/app/agents/note_agent.py` — export a `NOTE_READ_TOOLS` list
|
||||||
|
(`list_notes`, `get_note`).
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [x] Add the four `*_READ_TOOLS` exports in the agent files. Do not remove the
|
||||||
|
existing `*_TOOLS` exports — the chat agents still use them.
|
||||||
|
- [x] Add `list_timelines_today` in `timeline_agent.py`: returns only timelines
|
||||||
|
whose `date` falls on today (UTC). Mirror the shape of
|
||||||
|
`list_tasks_due_today`.
|
||||||
|
- [x] Create `brief_agent.py` with:
|
||||||
|
- Module-level fallback prompt constants `_HOME_BRIEF_FALLBACK` and
|
||||||
|
`_PROJECT_BRIEF_FALLBACK` — copy the prompts from the plan above verbatim,
|
||||||
|
using `{language}` / `{today}` / `{project_id}` (single-brace) so
|
||||||
|
`.format()` works when Langfuse is unavailable.
|
||||||
|
- Read-only memory tools subset: reuse `_memory_tools()` from `deep_agent.py`
|
||||||
|
but filter to `memory_list_blocks`, `memory_get`, `archival_memory_search`,
|
||||||
|
`conversation_search`. Factor out a small helper `_read_only_memory_tools()`
|
||||||
|
in `deep_agent.py` (or duplicate locally — keep it simple).
|
||||||
|
- `async def run_home_brief(user_id, context) -> AsyncGenerator[tuple[str, Any], None]`
|
||||||
|
- `async def run_project_brief(user_id, project_id, context) -> AsyncGenerator[tuple[str, Any], None]`
|
||||||
|
- Both reuse `_run_single_agent_stream` from `deep_agent.py` by passing
|
||||||
|
`agent_name="brief-agent"` and the relevant prompt. Tool list is the
|
||||||
|
read-only subset.
|
||||||
|
- Inject `_language_instruction`, `_relational_memory_injection`, and
|
||||||
|
`_proactive_hints_injection` into the system prompt — same pattern as
|
||||||
|
`run_home_stream`.
|
||||||
|
- After rendering the system prompt with `compile_prompt`, append a line
|
||||||
|
`"\nToday is YYYY-MM-DD."` only if the Langfuse template did not already
|
||||||
|
include `{{today}}` substitution (safe fallback).
|
||||||
|
- [x] Do **not** call `_normalize_tagged_list_lines` on the output — the brief
|
||||||
|
prompt forbids tags, so skipping the post-processor is a deliberate signal
|
||||||
|
of correctness.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- Importing `from app.core.brief_agent import run_home_brief, run_project_brief` succeeds.
|
||||||
|
- A smoke script `scripts/smoke_brief.py` (create it; git-ignore it; OK to delete afterwards) runs `run_home_brief` against a seeded test user and streams text to stdout. Output contains no `<` or `[uuid]` substrings.
|
||||||
|
- `ruff check .` passes.
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- `cd api && source .venv/Scripts/activate && python scripts/smoke_brief.py home`
|
||||||
|
- `cd api && source .venv/Scripts/activate && python scripts/smoke_brief.py project <uuid>`
|
||||||
|
- `cd api && source .venv/Scripts/activate && ruff check .`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3 — WS frame + REST fallback
|
||||||
|
|
||||||
|
**Goal:** Electron can send `{type:"brief_request", mode, project_id?}` over
|
||||||
|
the device WS and receive a plain-text stream. REST `POST /chat/brief` exists
|
||||||
|
as fallback.
|
||||||
|
|
||||||
|
**Files**
|
||||||
|
- `api/app/schemas.py`
|
||||||
|
- `api/app/api/routes/device_ws.py`
|
||||||
|
- `api/app/api/routes/chat.py`
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [ ] In `schemas.py`: add `brief_request = "brief_request"` to `WsFrameType`,
|
||||||
|
and a `WsBriefRequest` model with fields
|
||||||
|
`type: Literal[WsFrameType.brief_request]`, `request_id: str | None`,
|
||||||
|
`session_id: str | None`, `mode: Literal["home", "project"]`,
|
||||||
|
`project_id: str | None`.
|
||||||
|
- [ ] In `device_ws.py`: add an `elif frame_type == WsFrameType.brief_request:`
|
||||||
|
branch that dispatches to a new `_handle_brief_request` task.
|
||||||
|
- [ ] Implement `_handle_brief_request` by mirroring `_handle_home_request` but:
|
||||||
|
- Call `run_home_brief(user_id, context)` when `mode == "home"`,
|
||||||
|
`run_project_brief(user_id, project_id, context)` when `mode == "project"`
|
||||||
|
(validate `project_id` is a UUID; send `stream_end` with error frame
|
||||||
|
otherwise).
|
||||||
|
- **Skip** episode storage — briefs are not conversations.
|
||||||
|
- Still run `memory.enrich_context(...)` so relational/proactive memory is
|
||||||
|
injected.
|
||||||
|
- [ ] In `chat.py`: add `POST /chat/brief` that accepts `{mode, project_id?}`
|
||||||
|
and returns the full text (collects stream). This is the offline fallback
|
||||||
|
path used when the WS is not ready.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- Electron smoke client opens the WS, sends a `brief_request` with `mode:"home"`,
|
||||||
|
and receives `stream_start` → N × `stream_text` → `stream_end` frames.
|
||||||
|
- `POST /chat/brief` returns `{response: "..."}`.
|
||||||
|
- Malformed `project_id` → WS frame `stream_end` with an error message (no server crash).
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- Run pytest existing suite: `cd api && source .venv/Scripts/activate && pytest -q`.
|
||||||
|
- Add one unit test `tests/test_brief_agent.py` covering: home mode returns
|
||||||
|
non-empty text; project mode with bogus UUID returns an error without
|
||||||
|
crashing; tools called are from the read-only subset (monkeypatch
|
||||||
|
`run_home_brief` to assert the tool list).
|
||||||
|
- Then: `cd api && source .venv/Scripts/activate && pytest tests/test_brief_agent.py -v`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4 — Langfuse prompts
|
||||||
|
|
||||||
|
**Goal:** `home_brief` and `project_brief` prompts exist in Langfuse at label
|
||||||
|
`production`, matching the content in this plan.
|
||||||
|
|
||||||
|
**Files**
|
||||||
|
- None (external config via MCP).
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [x] Use `mcp__langfuse-docs__searchLangfuseDocs` to confirm the text-prompt
|
||||||
|
variable syntax (`{{variable}}`) and that `label="production"` is the label
|
||||||
|
read by `get_prompt_or_fallback`.
|
||||||
|
- [x] Use `mcp__langfuse__createTextPrompt` to create `home_brief` with the
|
||||||
|
content from the "Improved prompts → home_brief" section above. Set label
|
||||||
|
to `production`. Variables: `language`, `today`.
|
||||||
|
- [x] Use `mcp__langfuse__createTextPrompt` to create `project_brief` with the
|
||||||
|
content from the "Improved prompts → project_brief" section above. Set label
|
||||||
|
to `production`. Variables: `language`, `today`, `project_id`.
|
||||||
|
- [x] Use `mcp__langfuse__getPrompt` to round-trip both prompts and verify the
|
||||||
|
raw template matches what was sent.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- Both prompts resolve via `get_prompt_or_fallback("home_brief", "")` and
|
||||||
|
`get_prompt_or_fallback("project_brief", "")` in a Python shell against the
|
||||||
|
real Langfuse instance — return a non-empty `raw_template` and a non-None
|
||||||
|
`prompt_obj`.
|
||||||
|
- `prompt_obj.compile(language="Italian", today="2026-04-17")` returns text
|
||||||
|
containing the Italian directive and the date.
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- `cd api && source .venv/Scripts/activate && python -c "from app.core.langfuse_client import get_prompt_or_fallback; t,p = get_prompt_or_fallback('home_brief', ''); print(len(t), p is not None)"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5 — Electron client: home brief uses new agent
|
||||||
|
|
||||||
|
**Goal:** The existing home-brief UI flow (toast + full card) calls the new
|
||||||
|
brief agent over WS, and the `DAILY_BRIEF_PROMPT` constant is deleted.
|
||||||
|
|
||||||
|
**Files**
|
||||||
|
- `adiuvAI/src/shared/api-types.ts` (or wherever WS types live)
|
||||||
|
- `adiuvAI/src/main/api/backend-client.ts`
|
||||||
|
- `adiuvAI/src/main/ai/orchestrator.ts`
|
||||||
|
- `adiuvAI/src/main/router/index.ts`
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [x] Add `WsBriefRequest` frame shape to shared types, mirroring the API
|
||||||
|
`WsBriefRequest` schema.
|
||||||
|
- [x] In `backend-client.ts`, add `sendBriefRequest(mode, projectId?, callbacks, requestId?)`
|
||||||
|
modeled on `sendHomeRequest`. It sends `{type:"brief_request", mode, project_id}`.
|
||||||
|
- [x] In `orchestrator.ts`:
|
||||||
|
- Delete the `DAILY_BRIEF_PROMPT` constant (and the `langSuffix` hack — the
|
||||||
|
backend now owns language injection).
|
||||||
|
- `generateAndCacheBrief()` → call `client.sendBriefRequest("home", undefined, {...})`.
|
||||||
|
- `dailyBrief()` → call `client.sendBriefRequest("home", undefined, {...}, requestId)`.
|
||||||
|
- [x] `router/index.ts`: no signature change — only the underlying orchestrator
|
||||||
|
was rewired. Leave `ai.dailyBrief` mutation as-is.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- Launch the Electron app, open Home, brief renders within 10s. No
|
||||||
|
`<task>`/`<timeline>` markers appear in the output. Italian UI user gets
|
||||||
|
Italian prose.
|
||||||
|
- Grep confirms `DAILY_BRIEF_PROMPT` no longer exists in the repo.
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- `cd adiuvAI && npm run lint`
|
||||||
|
- Manual: Home page renders a fresh brief. Toggle `isHomePage` nav away/back
|
||||||
|
to check the cache path still works.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6 — Project brief UI card
|
||||||
|
|
||||||
|
**Goal:** Each project page has a "Brief" card that calls `sendBriefRequest("project", id, ...)`
|
||||||
|
and renders streaming plain text.
|
||||||
|
|
||||||
|
**Files**
|
||||||
|
- `adiuvAI/src/renderer/components/projects/ProjectDetail.tsx`
|
||||||
|
- `adiuvAI/src/renderer/components/projects/ProjectBriefCard.tsx` (new)
|
||||||
|
- `adiuvAI/src/main/router/index.ts` (add `ai.projectBrief` mutation)
|
||||||
|
- `adiuvAI/src/main/ai/orchestrator.ts` (add `projectBrief(sender, projectId, requestId)`)
|
||||||
|
- Locale files (all 5).
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [ ] Add `projectBrief(sender, projectId, requestId)` in `orchestrator.ts`
|
||||||
|
mirroring `dailyBrief` but with `mode:"project"` and no cache (cheap enough
|
||||||
|
to regenerate on demand; add a simple in-memory TTL of 5 minutes keyed by
|
||||||
|
`projectId` only if the UX feels laggy).
|
||||||
|
- [ ] Add `ai.projectBrief` mutation in the tRPC router, input
|
||||||
|
`{projectId: z.string().uuid(), requestId: z.string().optional()}`.
|
||||||
|
- [ ] `ProjectBriefCard`: shadcn Card, Sparkles icon, `text-sm` body. States:
|
||||||
|
`idle` (button "Generate brief") → `streaming` (skeleton + partial text) →
|
||||||
|
`ready` (full text + "Refresh" button). Stream via
|
||||||
|
`window.electronAI.onStreamChunk()` by request id.
|
||||||
|
- [ ] Mount `ProjectBriefCard` at the top of `ProjectDetail`, above existing
|
||||||
|
content.
|
||||||
|
- [ ] Add i18n keys under `projects.brief.*`: `title`, `generate`, `refresh`,
|
||||||
|
`generating`, `error`. Add to all 5 locale files.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- On a project with tasks/timelines, the card streams a coherent 4–8 sentence
|
||||||
|
brief with state / what's moving / next steps sections, no XML tags.
|
||||||
|
- On an empty project (no tasks/timelines), the brief is still coherent and
|
||||||
|
does not hallucinate.
|
||||||
|
- Refresh button produces a fresh generation (new `request_id`).
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- `cd adiuvAI && npm run lint`
|
||||||
|
- Manual: navigate `/projects?projectId=<uuid>`, click Generate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7 — Observability + cleanup
|
||||||
|
|
||||||
|
**Goal:** The brief agent is visible in Langfuse as its own generation name,
|
||||||
|
and the old hard-coded prompt is fully removed.
|
||||||
|
|
||||||
|
**Files**
|
||||||
|
- `api/app/core/brief_agent.py`
|
||||||
|
- `adiuvAI/.claude/CLAUDE.md` — document the new agent
|
||||||
|
- `.claude/CLAUDE.md` (root) — add a short line under "api" section
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [ ] Verify that `_run_single_agent_stream` uses `agent_name="brief-agent"`
|
||||||
|
so the Langfuse span/generation is named accordingly. Spot-check one trace
|
||||||
|
in the Langfuse UI.
|
||||||
|
- [ ] In `adiuvAI/.claude/CLAUDE.md`, add under the "AI Subsystem" section a
|
||||||
|
new bullet for `brief-agent` in the agents table (Scope: "Daily home brief
|
||||||
|
and per-project status brief", Tools: read-only subset).
|
||||||
|
- [ ] In the root `.claude/CLAUDE.md`, under the `api/` architecture section,
|
||||||
|
add `brief_agent.py` to the "Orchestration" list with a one-line purpose.
|
||||||
|
- [ ] Delete `scripts/smoke_brief.py` if it was committed.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- A Langfuse trace for a home brief shows span `brief-agent-stream` containing
|
||||||
|
a generation `brief-agent-llm` linked to the `home_brief` prompt version.
|
||||||
|
- `rg -n "DAILY_BRIEF_PROMPT" adiuvAI/` returns no matches.
|
||||||
|
- `rg -n "home_brief|project_brief" api/` shows usages only in `brief_agent.py`.
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- Trigger one home brief and one project brief, open Langfuse, confirm traces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8 — Regression + doc polish
|
||||||
|
|
||||||
|
**Goal:** existing home chat behavior unchanged; brief behavior documented for
|
||||||
|
future contributors.
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
- [ ] Open the home chat, send a normal message ("what are my tasks today?").
|
||||||
|
Response must still include `<task>` tag lines (the chat agent still uses
|
||||||
|
the tag contract; only the brief agent does not).
|
||||||
|
- [ ] Open the floating panel on a task. Response must still be plain text with
|
||||||
|
no tags (existing contract).
|
||||||
|
- [ ] Add a short "Daily Brief" paragraph to the user-facing docs (if any
|
||||||
|
marketing/help doc exists — otherwise skip).
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
- No regressions in home chat or floating chat.
|
||||||
|
- Plan document (`docs/plan-brief-agent.md`) is marked complete: every phase's
|
||||||
|
task checklist is `- [x]`.
|
||||||
|
|
||||||
|
**Verify**
|
||||||
|
- Manual QA pass of: home brief, project brief, home chat, floating chat on
|
||||||
|
task, floating chat on project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (explicitly)
|
||||||
|
|
||||||
|
- Push notifications / proactive brief delivery (already a separate plan).
|
||||||
|
- Weekly / monthly brief variants.
|
||||||
|
- Brief export to PDF / email.
|
||||||
|
- Per-client brief (clients are currently a lightweight table, not a UI page).
|
||||||
|
- Writing tools in the brief agent — it stays read-only. If the user acts on
|
||||||
|
the brief ("create a task for X"), they send that as a normal home-chat
|
||||||
|
message, and the home agent handles it.
|
||||||
312
docs/plan-onboarding-wizard.md
Normal file
312
docs/plan-onboarding-wizard.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# First-Run User Onboarding (Profile → Core Memory + Format Prefs)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Today, after sign-up or login, users land directly on the home chat with no introduction. Signup only collects `name`, `surname`, `email`. The backend AI agents have no idea who they're talking to — generic answers, generic tone, no language match, raw timestamps.
|
||||||
|
|
||||||
|
This change adds a one-time wizard that runs the **first time a user opens the app post-login**. It:
|
||||||
|
1. Seeds the user's **core memory** (encrypted, server-side) with personalization data the AI should reason about (`job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`).
|
||||||
|
2. Auto-detects and stores **formatting preferences** (`timezone`, `time_format`, `date_format`) **on the FE** as electron-store settings — *not* in core memory, because the LLM should never see raw timestamps or have to reason about format strings. Instead, the FE applies these to tool-result rows before they're sent back to the backend.
|
||||||
|
3. Optionally normalizes user free-text answers via a single backend LLM call before persisting, so messy inputs like "i build websites" become clean values like "Web Developer".
|
||||||
|
|
||||||
|
Because [memory_middleware.py:53-94](api/app/core/memory_middleware.py#L53-L94) already auto-injects `core_memory` into every orchestrator call (see [device_ws.py:213](api/app/api/routes/device_ws.py#L213) and [device_ws.py:282](api/app/api/routes/device_ws.py#L282)), no system-prompt code changes — writing to `MemoryCore` is enough for agents to "see" the data on their next call.
|
||||||
|
|
||||||
|
**Decisions made with the user:**
|
||||||
|
- **UI style**: hybrid chat-styled wizard (looks like AIChatPanel — bubbles, chips — but pre-scripted, no LLM calls per step)
|
||||||
|
- **Storage split**: AI-relevant fields in encrypted `MemoryCore`; formatting prefs in FE-local electron-store
|
||||||
|
- **Skippable + editable** in a new Settings → Profile section
|
||||||
|
- **OS-derived defaults**: language, timezone, time format, date format auto-detected from the OS. Language is shown in the wizard for confirmation; the three formatting prefs are seeded silently and editable in Settings.
|
||||||
|
- **Avatar**: comes from Google OAuth (already supported via `users.avatar_url`). Not in this wizard. A manual upload control in Settings → Profile is a nice-to-have but **out of scope** for this change.
|
||||||
|
- **LLM normalization**: yes, but only on free-text answers, in **one batch call at the final 'Done' step**. User sees a "Here's what I saved" review screen and can edit before persisting.
|
||||||
|
|
||||||
|
## Fields collected
|
||||||
|
|
||||||
|
| Key | Lives in | Source | In wizard? | Editable in Settings? | Used by |
|
||||||
|
|---------------------|---------------------------|------------------------------|------------------------|----------------------|------------|
|
||||||
|
| `job_role` | `MemoryCore` (BE, encrypted) | User (chip + free text) | Yes | Yes | LLM |
|
||||||
|
| `industry` | `MemoryCore` | User (chip + free text) | Yes | Yes | LLM |
|
||||||
|
| `primary_use_case` | `MemoryCore` | User (chip) | Yes | Yes | LLM |
|
||||||
|
| `tone_preference` | `MemoryCore` | User (chip) | Yes | Yes | LLM |
|
||||||
|
| `language` | `MemoryCore` | OS `app.getLocale()` → user confirms | Yes — confirm/change | Yes | LLM (response language) + future UI i18n |
|
||||||
|
| `timezone` | electron-store (FE) | `Intl.DateTimeFormat().resolvedOptions().timeZone` | No — silent | Yes | FE formatter |
|
||||||
|
| `time_format` | electron-store (FE) | Derived from locale (12h/24h)| No — silent | Yes | FE formatter |
|
||||||
|
| `date_format` | electron-store (FE) | Derived from locale (e.g. dd/MM/yyyy) | No — silent | Yes | FE formatter |
|
||||||
|
|
||||||
|
The split is the load-bearing decision: **the LLM never sees raw timestamps or format prefs**. Instead, the FE's drizzle executor formats every timestamp column in tool-result rows using the user's preferences before sending the `tool_result` frame back to the backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture notes
|
||||||
|
|
||||||
|
1. **AI orchestration is fully delegated to the backend** via WebSocket — see [orchestrator.ts:87-117](adiuvAI/src/main/ai/orchestrator.ts#L87-L117). The Electron client never builds a system prompt. So all LLM-relevant personalization must live on the backend (in `MemoryCore`).
|
||||||
|
|
||||||
|
2. **Tool calls are FE-executed**: backend sends `WsToolCall` → FE [drizzle-executor.ts](adiuvAI/src/main/api/drizzle-executor.ts) runs the SELECT and returns `{ rows }` → FE [backend-client.ts:652-658](adiuvAI/src/main/api/backend-client.ts#L652-L658) wraps it as a `tool_result` frame. The formatting hook goes **between the executor returning rows and the frame being sent** — this is where raw `dueDate` numbers become `"15/04/2026 14:30"` strings.
|
||||||
|
|
||||||
|
3. **Format prefs are per-device**: `timezone` is inherently per-device (your laptop and phone may be in different cities). For consistency we keep all three format prefs FE-local. If the user wants cross-device sync later, this can migrate to `MemoryCore` without breaking the wire format — but that's not v1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to change
|
||||||
|
|
||||||
|
### Backend (`api/`)
|
||||||
|
|
||||||
|
1. **`api/alembic/versions/<new>_add_onboarded_flag.py`** — new Alembic migration:
|
||||||
|
- `ALTER TABLE users ADD COLUMN onboarding_completed_at TIMESTAMPTZ NULL`
|
||||||
|
The five LLM-relevant values live in the existing `memory_core` table — no new columns.
|
||||||
|
|
||||||
|
2. **[api/app/models.py:63-94](api/app/models.py#L63-L94)** — add `onboarding_completed_at: Mapped[datetime | None]` to `User`.
|
||||||
|
|
||||||
|
3. **[api/app/schemas.py:27-33](api/app/schemas.py#L27-L33)** — extend `UserProfile`:
|
||||||
|
```python
|
||||||
|
class UserProfile(BaseModel):
|
||||||
|
id: str
|
||||||
|
email: str
|
||||||
|
name: str | None = None
|
||||||
|
surname: str | None = None
|
||||||
|
tier: BillingTier
|
||||||
|
avatar_url: str | None = None
|
||||||
|
onboarding_completed_at: int | None = None # epoch ms
|
||||||
|
memory: dict[str, str] = Field(default_factory=dict)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **[api/app/api/middleware/auth.py:74-79](api/app/api/middleware/auth.py#L74-L79)** — extend `get_current_user`:
|
||||||
|
- Read `onboarding_completed_at` from the user row.
|
||||||
|
- Use `MemoryMiddleware(db).list_core_blocks(user_id)` to load decrypted core blocks → `{label: value}` dict, attach as `memory`.
|
||||||
|
|
||||||
|
5. **[api/app/api/routes/auth.py](api/app/api/routes/auth.py)** — add a new route. Do not extend `_UpdateProfileRequest` (keep name/surname separate).
|
||||||
|
```python
|
||||||
|
class _UpdateMemoryRequest(BaseModel):
|
||||||
|
memory: dict[str, str] = Field(default_factory=dict)
|
||||||
|
mark_onboarded: bool = False
|
||||||
|
|
||||||
|
@router.put("/me/memory", response_model=UserProfile)
|
||||||
|
async def update_memory(
|
||||||
|
body: _UpdateMemoryRequest,
|
||||||
|
current_user: UserProfile = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_session),
|
||||||
|
) -> UserProfile:
|
||||||
|
memory = MemoryMiddleware(db)
|
||||||
|
for key, value in body.memory.items():
|
||||||
|
await memory.update_core(current_user.id, key, value)
|
||||||
|
if body.mark_onboarded:
|
||||||
|
result = await db.execute(select(User).where(User.id == current_user.id))
|
||||||
|
user = result.scalar_one()
|
||||||
|
user.onboarding_completed_at = datetime.now(timezone.utc)
|
||||||
|
await db.commit()
|
||||||
|
# Re-fetch via get_current_user-style logic and return UserProfile.
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **`api/app/api/routes/auth.py`** — new normalization route:
|
||||||
|
```python
|
||||||
|
class _NormalizeRequest(BaseModel):
|
||||||
|
inputs: dict[str, str] # e.g. {"job_role": "i build websites"}
|
||||||
|
|
||||||
|
class _NormalizeResponse(BaseModel):
|
||||||
|
normalized: dict[str, str]
|
||||||
|
|
||||||
|
@router.post("/onboarding/normalize", response_model=_NormalizeResponse)
|
||||||
|
async def normalize_onboarding(
|
||||||
|
body: _NormalizeRequest,
|
||||||
|
current_user: UserProfile = Depends(get_current_user),
|
||||||
|
) -> _NormalizeResponse:
|
||||||
|
"""One-shot LLM normalization for free-text onboarding answers."""
|
||||||
|
```
|
||||||
|
Implementation: build a small system prompt ("You normalize user onboarding answers. Return JSON only. Each key maps to a clean, ≤3-word canonical label."), call `get_llm("gpt-4o-mini", temperature=0)` from [api/app/core/llm.py](api/app/core/llm.py) with `response_format={"type": "json_object"}`, parse, return. Must short-circuit and return the inputs unchanged on any LLM error so the wizard never blocks on a flaky model call. Rate-limited by the existing `TierRateLimiter` middleware.
|
||||||
|
|
||||||
|
7. **No orchestrator / prompt changes needed.** `MemoryMiddleware.enrich_context()` already injects `core_memory` into every chat call. **This is the whole point of using `MemoryCore` instead of system-prompt injection.**
|
||||||
|
|
||||||
|
### Electron main (`adiuvAI/src/main/`)
|
||||||
|
|
||||||
|
8. **[src/shared/api-types.ts:26-34](adiuvAI/src/shared/api-types.ts#L26-L34)** — extend `UserProfileSchema` with `onboardingCompletedAt: z.number().int().nullable().optional()` and `memory: z.record(z.string(), z.string()).default({})`.
|
||||||
|
|
||||||
|
9. **[src/main/store.ts:23-38](adiuvAI/src/main/store.ts#L23-L38)** — add a `formatPrefs` block to `AppSettings`:
|
||||||
|
```ts
|
||||||
|
formatPrefs: {
|
||||||
|
timezone: string; // 'Europe/Rome'
|
||||||
|
dateFormat: string; // 'dd/MM/yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd'
|
||||||
|
timeFormat: '12h' | '24h';
|
||||||
|
} | null; // null = not yet seeded
|
||||||
|
```
|
||||||
|
Default to `null`. Add helpers `getFormatPrefs()` and `setFormatPrefs(prefs)`.
|
||||||
|
|
||||||
|
10. **New: `src/main/auth/locale-defaults.ts`** — small helper:
|
||||||
|
```ts
|
||||||
|
export function detectFormatPrefs(): FormatPrefs {
|
||||||
|
const locale = app.getLocale(); // 'it-IT'
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const timeFormat = Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12 ? '12h' : '24h';
|
||||||
|
const dateFormat = inferDateFormatFromLocale(locale); // small lookup: 'en-US'→MM/dd/yyyy, 'en-GB'/'it-IT'/...→dd/MM/yyyy, 'ja-JP'/...→yyyy-MM-dd
|
||||||
|
return { timezone, timeFormat, dateFormat };
|
||||||
|
}
|
||||||
|
export function detectLanguage(): string { return app.getLocale(); } // 'it-IT'
|
||||||
|
```
|
||||||
|
|
||||||
|
11. **New: `src/main/api/format-row.ts`** — pure function called by the executor:
|
||||||
|
```ts
|
||||||
|
const TIMESTAMP_COLUMNS = new Set([
|
||||||
|
'createdAt', 'updatedAt', 'dueDate', 'date', 'endDate',
|
||||||
|
'lastRunAt', 'startedAt', 'completedAt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function formatRow<T extends Record<string, unknown>>(row: T, prefs: FormatPrefs): T;
|
||||||
|
export function formatRows<T extends Record<string, unknown>>(rows: T[], prefs: FormatPrefs): T[];
|
||||||
|
```
|
||||||
|
For each known timestamp column whose value is a `number`, replace it with `formatInstant(value, prefs)` where `formatInstant` uses `Intl.DateTimeFormat(locale, { timeZone: prefs.timezone, hour12: prefs.timeFormat === '12h', ... })` and the `dateFormat` setting. Returns a new object — does not mutate.
|
||||||
|
|
||||||
|
The set of timestamp columns is hard-coded against the Drizzle schema; if a new timestamp column is added, this set must be updated. (Acceptable for v1 — the schema is small. If it grows, we can derive the set from the Drizzle schema's `integer('...', { mode: 'number' })` columns at startup.)
|
||||||
|
|
||||||
|
12. **[src/main/api/drizzle-executor.ts:204-263](adiuvAI/src/main/api/drizzle-executor.ts#L204-L263)** — wrap the executor's `select`/`get`/`insert`/`update` return paths so that `rows`/`row` get passed through `formatRow(s)(..., getFormatPrefs() ?? detectFormatPrefs())`. The `?? detect…` fallback handles the edge case where the executor runs before the first auth.status seed call (e.g. background tool calls during login).
|
||||||
|
|
||||||
|
13. **[src/main/auth/auth-manager.ts:170-174](adiuvAI/src/main/auth/auth-manager.ts#L170-L174)** — add two methods:
|
||||||
|
```ts
|
||||||
|
async updateMemory(memory: Record<string, string>, markOnboarded = false): Promise<UserProfile>
|
||||||
|
async normalizeOnboarding(inputs: Record<string, string>): Promise<Record<string, string>>
|
||||||
|
```
|
||||||
|
Both call the new backend routes via the existing `put`/`post` helpers.
|
||||||
|
|
||||||
|
14. **[src/main/router/index.ts:1059-1098](adiuvAI/src/main/router/index.ts#L1059-L1098)** — extend `authRouter`:
|
||||||
|
- Add `auth.updateMemory` mutation: input `{ memory, markOnboarded? }`.
|
||||||
|
- Add `auth.normalizeOnboarding` mutation: input `{ inputs: Record<string, string> }`.
|
||||||
|
- **Extend `auth.status`** so that immediately after fetching the profile, if `getFormatPrefs()` is `null`, it calls `setFormatPrefs(detectFormatPrefs())` (silent FE seed). If `profile.memory.language` is missing, it also calls `authManager.updateMemory({ language: detectLanguage() })` (silent BE seed). Both run only on first launch — subsequent calls find the values present and short-circuit.
|
||||||
|
|
||||||
|
### Electron renderer (`adiuvAI/src/renderer/`)
|
||||||
|
|
||||||
|
15. **[src/renderer/components/layout/AppShell.tsx:79-119](adiuvAI/src/renderer/components/layout/AppShell.tsx#L79-L119)** — add the first-run gate. After the `authStatusQuery.data?.authenticated === false` branch:
|
||||||
|
```tsx
|
||||||
|
if (authStatusQuery.data?.profile && authStatusQuery.data.profile.onboardingCompletedAt == null) {
|
||||||
|
return <OnboardingFlow profile={authStatusQuery.data.profile} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
16. **New: `src/renderer/components/onboarding/OnboardingFlow.tsx`** — the wizard. Internal state machine:
|
||||||
|
```ts
|
||||||
|
type Step = 'welcome' | 'jobRole' | 'industry' | 'useCase' | 'tone' | 'language' | 'reviewing' | 'done';
|
||||||
|
```
|
||||||
|
Renders chat-bubble layout matching [AIChatPanel.tsx](adiuvAI/src/renderer/components/ai/AIChatPanel.tsx) — Sparkles icon, `rounded-2xl`, glassmorphism, spring transitions per the design context in `adiuvAI/.claude/CLAUDE.md`. Each step shows an "AI" bubble with the question, 3–6 chip presets, an optional "type your own" input, and a Skip link.
|
||||||
|
|
||||||
|
The `language` step pre-selects the value already in `profile.memory.language` (auto-seeded). User confirms or picks a different one.
|
||||||
|
|
||||||
|
**`reviewing` step** (the LLM normalization gate):
|
||||||
|
- On entry, partition the user's answers into two groups:
|
||||||
|
- **Chip selections** — already canonical, skip the LLM entirely.
|
||||||
|
- **Free-text answers** — bundle into a `{key: rawText}` map.
|
||||||
|
- If the free-text map is non-empty, call `trpc.auth.normalizeOnboarding.useMutation` with it. Show a small inline loader on those fields only ("Tidying up…", ~1-2s).
|
||||||
|
|
||||||
|
**Review screen UX** — single card titled "Here's what I'll save", listing all five fields as rows:
|
||||||
|
|
||||||
|
| Row appearance | When |
|
||||||
|
|---------------------------------|---------------------------------------------------|
|
||||||
|
| Read-only label + value | Chip-selected values (`use_case`, `tone`, etc.) — checkmark icon |
|
||||||
|
| Read-only label + value + small grey hint `auto-tidied from "i build websites"` | Free-text values that the LLM normalized |
|
||||||
|
| Read-only label + value | Free-text values that the LLM did NOT change |
|
||||||
|
|
||||||
|
Each row has a small **Edit** pencil icon on the right. Clicking it converts that row in-place into a text input (or a Select for chip-based fields like `tone`/`use_case`, populated with the same chip presets plus a free-text "Other" option). The user types the new value, presses Enter or clicks Save → the row goes back to read-only with the **new value as-typed**.
|
||||||
|
|
||||||
|
**Edited values are stored verbatim — no re-normalization.** Rationale: the LLM normalization exists to clean up the *initial* messy answer; once the user has seen the suggestion and chosen to override it, re-running the LLM would either no-op (their text is already clean) or fight them. The user is the final arbiter. The "auto-tidied from…" hint disappears once a row is edited (the new value is no longer LLM-derived).
|
||||||
|
|
||||||
|
Bottom of the card: a single primary **"Looks good — save"** button → calls `trpc.auth.updateMemory.useMutation` with the final map + `markOnboarded: true` → `utils.auth.status.invalidate()` → AppShell remounts into the normal app. A secondary **"Back to wizard"** link drops the user back to the first wizard step (`jobRole`) with all current values pre-filled — used when the review reveals the answers are wrong enough that re-running the wizard is faster than five inline edits.
|
||||||
|
|
||||||
|
**Failure modes**:
|
||||||
|
- Normalization HTTP call fails → review screen shows raw values with a small banner "Couldn't auto-tidy — review and save". Save still works.
|
||||||
|
- User clicks "Looks good — save" and `updateMemory` fails → toast error, user stays on the review screen, can retry.
|
||||||
|
|
||||||
|
**Skip behaviour**: clicking Skip on any step calls `updateMemory({}, markOnboarded: true)` — empty map, just the flag. We don't re-prompt next launch.
|
||||||
|
|
||||||
|
17. **New: `src/renderer/components/onboarding/onboardingOptions.ts`** — preset chip lists:
|
||||||
|
```ts
|
||||||
|
export const JOB_ROLES = ['Developer', 'Designer', 'Consultant', 'Founder', 'Project Manager'];
|
||||||
|
export const INDUSTRIES = ['Tech', 'Design', 'Consulting', 'Legal', 'Marketing', 'Education'];
|
||||||
|
export const USE_CASES = ['Solo freelancer', 'Client manager', 'Team lead', 'Personal productivity'];
|
||||||
|
export const TONES = ['Casual', 'Formal', 'Concise', 'Detailed'];
|
||||||
|
```
|
||||||
|
|
||||||
|
18. **[src/renderer/components/settings/types.ts:3-9](adiuvAI/src/renderer/components/settings/types.ts#L3-L9)** — add `'profile'` to `SectionId` and `{ id: 'profile', label: 'Profile' }` to `SECTIONS` (before `'account'`).
|
||||||
|
|
||||||
|
19. **New: `src/renderer/components/settings/ProfileSection.tsx`** — Settings → Profile editor. Plain form (no chat aesthetic in Settings). Two cards:
|
||||||
|
- **"About you"** (writes to `MemoryCore` via `auth.updateMemory`): job_role, industry, primary_use_case, tone_preference, language. "Re-run onboarding" button → small backend route `POST /auth/onboarding/reset` (or just an extension of `update_memory` with `clear_onboarded: true`) that nulls `users.onboarding_completed_at`, then `auth.status.invalidate()` remounts the wizard.
|
||||||
|
- **"Display preferences"** (writes to electron-store via a new `trpc.settings.setFormatPrefs` mutation): timezone (select populated from `Intl.supportedValuesOf('timeZone')`), date_format (select: dd/MM/yyyy, MM/dd/yyyy, yyyy-MM-dd), time_format (radio: 12h / 24h).
|
||||||
|
|
||||||
|
20. **[src/main/router/index.ts](adiuvAI/src/main/router/index.ts)** — `settingsRouter`: add `getFormatPrefs` query and `setFormatPrefs` mutation that read/write to electron-store via `getFormatPrefs()` / `setFormatPrefs()`.
|
||||||
|
|
||||||
|
21. **[src/renderer/routes/settings.tsx:55-58](adiuvAI/src/renderer/routes/settings.tsx#L55-L58)** — add `{section === 'profile' && <ProfileSection />}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Patterns to reuse (do not duplicate)
|
||||||
|
|
||||||
|
- **Stepper state**: `InlineAgentCreationStepper` — `useState<...>` plus conditional rendering.
|
||||||
|
- **Chat bubble aesthetic**: copy bubble + Sparkles + glass styling from [AIChatPanel.tsx](adiuvAI/src/renderer/components/ai/AIChatPanel.tsx). Do **not** invent a new chat shell.
|
||||||
|
- **Form components**: shadcn `Field`/`Input`/`Select`/`Button`/`Card` already used in existing settings sections.
|
||||||
|
- **`MemoryMiddleware.update_core`** ([memory_middleware.py:137-173](api/app/core/memory_middleware.py#L137-L173)) — already used by `deep_agent.py:343`. We just expose it via REST.
|
||||||
|
- **`get_llm()` from [api/app/core/llm.py](api/app/core/llm.py)** — for the normalization route. Use `gpt-4o-mini` with `temperature=0` and JSON response format.
|
||||||
|
- **`toCamelCase` / `toSnakeCase`** in `auth-manager` — handles `mark_onboarded` ↔ `markOnboarded` automatically.
|
||||||
|
- **electron-store helpers** ([store.ts:62-98](adiuvAI/src/main/store.ts#L62-L98)) — same pattern as `getDeviceId` / `getLocalAgents`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. **Backend migration + tests**:
|
||||||
|
```
|
||||||
|
cd api && alembic upgrade head
|
||||||
|
pytest tests/test_auth.py -k "memory or normalize"
|
||||||
|
```
|
||||||
|
Manually `curl PUT /api/v1/auth/me/memory` with `{"memory": {"job_role":"Developer"}, "mark_onboarded": true}` and confirm round-trip via `GET /api/v1/auth/me`.
|
||||||
|
|
||||||
|
2. **LLM normalization route**:
|
||||||
|
- `curl POST /api/v1/auth/onboarding/normalize` with `{"inputs": {"job_role": "i build websites", "industry": "tech-ish stuff"}}`.
|
||||||
|
- Expect `{"normalized": {"job_role": "Web Developer", "industry": "Technology"}}` (or similar — exact phrasing varies).
|
||||||
|
- Stop the LLM provider (or use an invalid `OPENAI_API_KEY`) and re-run — must return inputs unchanged, never 500.
|
||||||
|
|
||||||
|
3. **Locale auto-seed (FE + BE)**:
|
||||||
|
- Fresh user, fresh electron-store. Log in via Electron.
|
||||||
|
- `getFormatPrefs()` should now return the detected `{timezone, dateFormat, timeFormat}`.
|
||||||
|
- `memory_core` should have one row: `language`.
|
||||||
|
- Reload app → no second seed call (idempotent).
|
||||||
|
|
||||||
|
4. **First-run wizard (golden path with chips only)**:
|
||||||
|
- Reset: backend `UPDATE users SET onboarding_completed_at=NULL; DELETE FROM memory_core WHERE user_id=...`. FE: clear electron-store `formatPrefs`.
|
||||||
|
- `npm start` → log in → land on `OnboardingFlow`.
|
||||||
|
- Pick a chip on every step (no free text). Confirm language. Land on review screen — should not show a loading spinner (no normalization needed). Click Confirm.
|
||||||
|
- `auth.status` invalidates, AppShell mounts the home chat.
|
||||||
|
- `SELECT key FROM memory_core WHERE user_id=...` → 5 keys (job_role, industry, primary_use_case, tone_preference, language).
|
||||||
|
- Reload app → does not re-prompt.
|
||||||
|
|
||||||
|
5. **First-run wizard (free-text path)**:
|
||||||
|
- Reset. Walk through wizard typing free text on `job_role` and `industry` (e.g. "i build websites", "tech-ish stuff").
|
||||||
|
- On final step, see ~1-2s "Tidying up…" spinner, then a review screen showing the normalized values plus the chip-selected use_case/tone/language.
|
||||||
|
- Edit one normalized value manually. Confirm. The edited value is what lands in `memory_core`.
|
||||||
|
|
||||||
|
6. **AI uses the data (proof the wiring works)**:
|
||||||
|
- With an onboarded user whose `tone_preference="Formal"` and `language="it-IT"`, ask the home chat "draft a quick status email".
|
||||||
|
- Response should be in Italian and read formal. **If language doesn't match, `enrich_context` is not feeding `core_memory` into the prompt as expected — investigate before declaring done.** This is the single most likely failure point because we don't modify it.
|
||||||
|
|
||||||
|
7. **Format prefs reach the LLM as strings**:
|
||||||
|
- From the home chat, ask "what tasks are due this week?".
|
||||||
|
- Inspect the network/log of the `tool_result` frame the FE sends back. Every `dueDate` field must be a formatted string like `"15/04/2026 14:30"`, not a numeric timestamp.
|
||||||
|
- The AI's response must reference dates in the user's preferred format.
|
||||||
|
- Change `time_format` from 24h to 12h in Settings → Profile. Re-ask. Times should now be `2:30 PM` style.
|
||||||
|
|
||||||
|
8. **Skip flow**:
|
||||||
|
- Reset, log in, click Skip on step 1.
|
||||||
|
- `users.onboarding_completed_at` set; `memory_core` only has `language` (from auto-seed).
|
||||||
|
- Reload → no re-prompt.
|
||||||
|
|
||||||
|
9. **Re-run onboarding**: Settings → Profile → "Re-run onboarding" → wizard mounts immediately.
|
||||||
|
|
||||||
|
10. **Lint**:
|
||||||
|
```
|
||||||
|
cd adiuvAI && source ~/.nvm/nvm.sh && npm run lint
|
||||||
|
cd api && ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (deferred)
|
||||||
|
|
||||||
|
- **UI internationalisation framework**: storing `language` enables future i18n, but no translation library is added. Wizard copy hardcoded in English for v1.
|
||||||
|
- **Avatar upload control** in Settings → Profile: avatar already comes from Google OAuth via `users.avatar_url`. A manual upload UI is a nice-to-have follow-up.
|
||||||
|
- **Working hours**, **top goals** (free-text seeds): same `MemoryCore` pattern — easy to add later.
|
||||||
|
- **Cross-device sync of format prefs**: v1 stores them per-device in electron-store. Migrating to `MemoryCore` later doesn't break the wire format.
|
||||||
|
- **Schema-bump re-prompting**: when we add a new wizard question later we'll need a `core_memory["__onboarding_version__"]` key and a guard. Not needed now.
|
||||||
|
- **Animated typing effect** on AI bubbles.
|
||||||
484
docs/plan-sonner-notifications.md
Normal file
484
docs/plan-sonner-notifications.md
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
# Global Notification System — Sonner Toast Integration
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The adiuvAI Electron app has **52+ user-facing mutations** (create/update/delete for tasks, projects, clients, notes, timeline events, agents, settings, auth) with **no unified feedback system**. Some components show a transient "Saved" button label for 2s via `useState` + `setTimeout`; most mutations are completely silent. Errors are handled inconsistently — some show inline text, many are swallowed.
|
||||||
|
|
||||||
|
This plan adds a global toast notification system using **shadcn's sonner component**, replacing all ad-hoc patterns with a single i18n-aware API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Foundation
|
||||||
|
|
||||||
|
### 1.1 Install sonner via shadcn CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adiuvAI && npx shadcn@latest add sonner
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the `sonner` npm package and generates `src/renderer/components/ui/sonner.tsx`.
|
||||||
|
|
||||||
|
### 1.2 Fix theme import in generated `sonner.tsx`
|
||||||
|
|
||||||
|
The generated file imports `useTheme` from `next-themes` (doesn't exist in this app). Replace with:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTheme } from "@/components/theme-provider"
|
||||||
|
```
|
||||||
|
|
||||||
|
The app's `useTheme()` returns `{ theme: "dark" | "light" | "system" }` — same shape sonner expects.
|
||||||
|
|
||||||
|
Configure `position="bottom-right"` to avoid sidebar collision. Keep `richColors` enabled for variant-specific coloring.
|
||||||
|
|
||||||
|
Full target file:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTheme } from "@/components/theme-provider"
|
||||||
|
import { Toaster as Sonner } from "sonner"
|
||||||
|
|
||||||
|
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
position="bottom-right"
|
||||||
|
richColors
|
||||||
|
className="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton:
|
||||||
|
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton:
|
||||||
|
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Place `<Toaster />` in `src/renderer/index.tsx`
|
||||||
|
|
||||||
|
Add `<Toaster />` as a sibling of `<RouterProvider />` inside `<ThemeProvider>`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
// ...
|
||||||
|
<ThemeProvider defaultTheme="system" storageKey="adiuvai-theme">
|
||||||
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<LanguageSync />
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
<Toaster />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</trpc.Provider>
|
||||||
|
</ThemeProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why here?** The `<Toaster />` must render OUTSIDE all conditional rendering in AppShell.tsx (which gates LoginForm / OnboardingFlow / main app). Placing it in `index.tsx` ensures toasts work in all three states.
|
||||||
|
|
||||||
|
### 1.4 Create `useNotify()` hook
|
||||||
|
|
||||||
|
**New file:** `src/renderer/hooks/useNotify.ts`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type ToastVariant = 'success' | 'error' | 'info' | 'warning';
|
||||||
|
|
||||||
|
interface NotifyOptions {
|
||||||
|
descriptionKey?: string;
|
||||||
|
values?: Record<string, string | number>;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotify() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
function notify(variant: ToastVariant, messageKey: string, options?: NotifyOptions) {
|
||||||
|
const message = t(messageKey, options?.values);
|
||||||
|
const description = options?.descriptionKey
|
||||||
|
? t(options.descriptionKey, options?.values)
|
||||||
|
: undefined;
|
||||||
|
const duration = options?.duration;
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case 'success': toast.success(message, { description, duration: duration ?? 3000 }); break;
|
||||||
|
case 'error': toast.error(message, { description, duration: duration ?? Infinity }); break;
|
||||||
|
case 'info': toast.info(message, { description, duration: duration ?? 3000 }); break;
|
||||||
|
case 'warning': toast.warning(message, { description, duration: duration ?? 4000 }); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyError(messageKey: string, error?: { message?: string }) {
|
||||||
|
toast.error(t(messageKey), { description: error?.message, duration: Infinity });
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyPromise<T>(promise: Promise<T>, keys: { loading: string; success: string; error: string }) {
|
||||||
|
toast.promise(promise, { loading: t(keys.loading), success: t(keys.success), error: t(keys.error) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { notify, notifyError, notifyPromise };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Design rationale:**
|
||||||
|
- Error toasts: `duration: Infinity` — persist until dismissed so users can read/copy errors
|
||||||
|
- Success: 3s auto-dismiss — brief confirmation
|
||||||
|
- Warning (destructive): 4s — slightly longer for delete confirmations
|
||||||
|
- `notifyError`: convenience for `onError` callbacks — title from i18n, description from raw error
|
||||||
|
- `notifyPromise`: wraps `toast.promise()` for long-running ops
|
||||||
|
- All text goes through `t()` for i18n
|
||||||
|
|
||||||
|
### 1.5 Add i18n toast keys
|
||||||
|
|
||||||
|
**Files:** `src/renderer/locales/{en,it,es,fr,de}/translation.json`
|
||||||
|
|
||||||
|
Add a `toast` top-level key. English:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"toast": {
|
||||||
|
"profile": {
|
||||||
|
"updated": "Profile updated",
|
||||||
|
"updateError": "Failed to update profile"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"languageChanged": "Language changed",
|
||||||
|
"backendUrlSaved": "Server URL saved",
|
||||||
|
"backendUrlError": "Failed to save server URL",
|
||||||
|
"formatPrefsSaved": "Display preferences saved",
|
||||||
|
"formatPrefsError": "Failed to save display preferences",
|
||||||
|
"memorySaved": "Preferences saved",
|
||||||
|
"memoryError": "Failed to save preferences"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"loginError": "Sign-in failed",
|
||||||
|
"registerError": "Registration failed",
|
||||||
|
"oauthError": "Google sign-in failed",
|
||||||
|
"loggedOut": "Signed out"
|
||||||
|
},
|
||||||
|
"onboarding": {
|
||||||
|
"completed": "Onboarding complete",
|
||||||
|
"completedDescription": "Your workspace is personalized",
|
||||||
|
"error": "Failed to save onboarding",
|
||||||
|
"reset": "Onboarding reset",
|
||||||
|
"normalizing": "Personalizing your workspace...",
|
||||||
|
"normalized": "Personalization ready"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"created": "Task created",
|
||||||
|
"createError": "Failed to create task",
|
||||||
|
"updated": "Task updated",
|
||||||
|
"updateError": "Failed to update task",
|
||||||
|
"deleted": "Task deleted",
|
||||||
|
"deleteError": "Failed to delete task"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"created": "Project created",
|
||||||
|
"createError": "Failed to create project",
|
||||||
|
"updated": "Project updated",
|
||||||
|
"updateError": "Failed to update project",
|
||||||
|
"deleted": "Project deleted",
|
||||||
|
"deleteError": "Failed to delete project",
|
||||||
|
"archived": "Project archived",
|
||||||
|
"unarchived": "Project unarchived",
|
||||||
|
"archivedAll": "All projects archived",
|
||||||
|
"unarchivedAll": "All projects unarchived"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"created": "Client created",
|
||||||
|
"createError": "Failed to create client",
|
||||||
|
"updated": "Client renamed",
|
||||||
|
"updateError": "Failed to rename client",
|
||||||
|
"deleted": "Client deleted",
|
||||||
|
"deleteError": "Failed to delete client"
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"created": "Note created",
|
||||||
|
"createError": "Failed to create note",
|
||||||
|
"deleted": "Note deleted",
|
||||||
|
"deleteError": "Failed to delete note"
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"created": "Event created",
|
||||||
|
"createError": "Failed to create event",
|
||||||
|
"updated": "Event updated",
|
||||||
|
"updateError": "Failed to update event",
|
||||||
|
"deleted": "Event deleted",
|
||||||
|
"deleteError": "Failed to delete event"
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"created": "Comment added",
|
||||||
|
"createError": "Failed to add comment",
|
||||||
|
"deleted": "Comment deleted",
|
||||||
|
"deleteError": "Failed to delete comment"
|
||||||
|
},
|
||||||
|
"agent": {
|
||||||
|
"created": "Agent created",
|
||||||
|
"createError": "Failed to create agent",
|
||||||
|
"updated": "Agent configuration saved",
|
||||||
|
"updateError": "Failed to save agent configuration",
|
||||||
|
"deleted": "Agent deleted",
|
||||||
|
"deleteError": "Failed to delete agent",
|
||||||
|
"runStarted": "Agent run started",
|
||||||
|
"runError": "Failed to start agent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Italian translations:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
"toast": {
|
||||||
|
"profile": { "updated": "Profilo aggiornato", "updateError": "Impossibile aggiornare il profilo" },
|
||||||
|
"settings": { "languageChanged": "Lingua cambiata", "backendUrlSaved": "URL del server salvato", "backendUrlError": "Impossibile salvare l'URL del server", "formatPrefsSaved": "Preferenze di visualizzazione salvate", "formatPrefsError": "Impossibile salvare le preferenze", "memorySaved": "Preferenze salvate", "memoryError": "Impossibile salvare le preferenze" },
|
||||||
|
"auth": { "loginError": "Accesso fallito", "registerError": "Registrazione fallita", "oauthError": "Accesso con Google fallito", "loggedOut": "Disconnesso" },
|
||||||
|
"onboarding": { "completed": "Onboarding completato", "completedDescription": "Il tuo workspace e' personalizzato", "error": "Impossibile salvare l'onboarding", "reset": "Onboarding ripristinato", "normalizing": "Personalizzazione in corso...", "normalized": "Personalizzazione pronta" },
|
||||||
|
"task": { "created": "Attivita' creata", "createError": "Impossibile creare l'attivita'", "updated": "Attivita' aggiornata", "updateError": "Impossibile aggiornare l'attivita'", "deleted": "Attivita' eliminata", "deleteError": "Impossibile eliminare l'attivita'" },
|
||||||
|
"project": { "created": "Progetto creato", "createError": "Impossibile creare il progetto", "updated": "Progetto aggiornato", "updateError": "Impossibile aggiornare il progetto", "deleted": "Progetto eliminato", "deleteError": "Impossibile eliminare il progetto", "archived": "Progetto archiviato", "unarchived": "Progetto ripristinato", "archivedAll": "Tutti i progetti archiviati", "unarchivedAll": "Tutti i progetti ripristinati" },
|
||||||
|
"client": { "created": "Cliente creato", "createError": "Impossibile creare il cliente", "updated": "Cliente rinominato", "updateError": "Impossibile rinominare il cliente", "deleted": "Cliente eliminato", "deleteError": "Impossibile eliminare il cliente" },
|
||||||
|
"note": { "created": "Nota creata", "createError": "Impossibile creare la nota", "deleted": "Nota eliminata", "deleteError": "Impossibile eliminare la nota" },
|
||||||
|
"timeline": { "created": "Evento creato", "createError": "Impossibile creare l'evento", "updated": "Evento aggiornato", "updateError": "Impossibile aggiornare l'evento", "deleted": "Evento eliminato", "deleteError": "Impossibile eliminare l'evento" },
|
||||||
|
"comment": { "created": "Commento aggiunto", "createError": "Impossibile aggiungere il commento", "deleted": "Commento eliminato", "deleteError": "Impossibile eliminare il commento" },
|
||||||
|
"agent": { "created": "Agente creato", "createError": "Impossibile creare l'agente", "updated": "Configurazione agente salvata", "updateError": "Impossibile salvare la configurazione", "deleted": "Agente eliminato", "deleteError": "Impossibile eliminare l'agente", "runStarted": "Esecuzione agente avviata", "runError": "Impossibile avviare l'agente" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Spanish translations:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
"toast": {
|
||||||
|
"profile": { "updated": "Perfil actualizado", "updateError": "Error al actualizar el perfil" },
|
||||||
|
"settings": { "languageChanged": "Idioma cambiado", "backendUrlSaved": "URL del servidor guardada", "backendUrlError": "Error al guardar la URL del servidor", "formatPrefsSaved": "Preferencias de visualizacion guardadas", "formatPrefsError": "Error al guardar las preferencias", "memorySaved": "Preferencias guardadas", "memoryError": "Error al guardar las preferencias" },
|
||||||
|
"auth": { "loginError": "Error de acceso", "registerError": "Error de registro", "oauthError": "Error de acceso con Google", "loggedOut": "Sesion cerrada" },
|
||||||
|
"onboarding": { "completed": "Configuracion completada", "completedDescription": "Tu espacio de trabajo esta personalizado", "error": "Error al guardar la configuracion", "reset": "Configuracion reiniciada", "normalizing": "Personalizando tu espacio...", "normalized": "Personalizacion lista" },
|
||||||
|
"task": { "created": "Tarea creada", "createError": "Error al crear la tarea", "updated": "Tarea actualizada", "updateError": "Error al actualizar la tarea", "deleted": "Tarea eliminada", "deleteError": "Error al eliminar la tarea" },
|
||||||
|
"project": { "created": "Proyecto creado", "createError": "Error al crear el proyecto", "updated": "Proyecto actualizado", "updateError": "Error al actualizar el proyecto", "deleted": "Proyecto eliminado", "deleteError": "Error al eliminar el proyecto", "archived": "Proyecto archivado", "unarchived": "Proyecto restaurado", "archivedAll": "Todos los proyectos archivados", "unarchivedAll": "Todos los proyectos restaurados" },
|
||||||
|
"client": { "created": "Cliente creado", "createError": "Error al crear el cliente", "updated": "Cliente renombrado", "updateError": "Error al renombrar el cliente", "deleted": "Cliente eliminado", "deleteError": "Error al eliminar el cliente" },
|
||||||
|
"note": { "created": "Nota creada", "createError": "Error al crear la nota", "deleted": "Nota eliminada", "deleteError": "Error al eliminar la nota" },
|
||||||
|
"timeline": { "created": "Evento creado", "createError": "Error al crear el evento", "updated": "Evento actualizado", "updateError": "Error al actualizar el evento", "deleted": "Evento eliminado", "deleteError": "Error al eliminar el evento" },
|
||||||
|
"comment": { "created": "Comentario agregado", "createError": "Error al agregar el comentario", "deleted": "Comentario eliminado", "deleteError": "Error al eliminar el comentario" },
|
||||||
|
"agent": { "created": "Agente creado", "createError": "Error al crear el agente", "updated": "Configuracion del agente guardada", "updateError": "Error al guardar la configuracion", "deleted": "Agente eliminado", "deleteError": "Error al eliminar el agente", "runStarted": "Ejecucion del agente iniciada", "runError": "Error al iniciar el agente" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**French translations:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
"toast": {
|
||||||
|
"profile": { "updated": "Profil mis a jour", "updateError": "Impossible de mettre a jour le profil" },
|
||||||
|
"settings": { "languageChanged": "Langue modifiee", "backendUrlSaved": "URL du serveur enregistree", "backendUrlError": "Impossible d'enregistrer l'URL du serveur", "formatPrefsSaved": "Preferences d'affichage enregistrees", "formatPrefsError": "Impossible d'enregistrer les preferences", "memorySaved": "Preferences enregistrees", "memoryError": "Impossible d'enregistrer les preferences" },
|
||||||
|
"auth": { "loginError": "Echec de la connexion", "registerError": "Echec de l'inscription", "oauthError": "Echec de la connexion Google", "loggedOut": "Deconnecte" },
|
||||||
|
"onboarding": { "completed": "Configuration terminee", "completedDescription": "Votre espace de travail est personnalise", "error": "Impossible d'enregistrer la configuration", "reset": "Configuration reinitialisee", "normalizing": "Personnalisation en cours...", "normalized": "Personnalisation terminee" },
|
||||||
|
"task": { "created": "Tache creee", "createError": "Impossible de creer la tache", "updated": "Tache mise a jour", "updateError": "Impossible de mettre a jour la tache", "deleted": "Tache supprimee", "deleteError": "Impossible de supprimer la tache" },
|
||||||
|
"project": { "created": "Projet cree", "createError": "Impossible de creer le projet", "updated": "Projet mis a jour", "updateError": "Impossible de mettre a jour le projet", "deleted": "Projet supprime", "deleteError": "Impossible de supprimer le projet", "archived": "Projet archive", "unarchived": "Projet restaure", "archivedAll": "Tous les projets archives", "unarchivedAll": "Tous les projets restaures" },
|
||||||
|
"client": { "created": "Client cree", "createError": "Impossible de creer le client", "updated": "Client renomme", "updateError": "Impossible de renommer le client", "deleted": "Client supprime", "deleteError": "Impossible de supprimer le client" },
|
||||||
|
"note": { "created": "Note creee", "createError": "Impossible de creer la note", "deleted": "Note supprimee", "deleteError": "Impossible de supprimer la note" },
|
||||||
|
"timeline": { "created": "Evenement cree", "createError": "Impossible de creer l'evenement", "updated": "Evenement mis a jour", "updateError": "Impossible de mettre a jour l'evenement", "deleted": "Evenement supprime", "deleteError": "Impossible de supprimer l'evenement" },
|
||||||
|
"comment": { "created": "Commentaire ajoute", "createError": "Impossible d'ajouter le commentaire", "deleted": "Commentaire supprime", "deleteError": "Impossible de supprimer le commentaire" },
|
||||||
|
"agent": { "created": "Agent cree", "createError": "Impossible de creer l'agent", "updated": "Configuration de l'agent enregistree", "updateError": "Impossible d'enregistrer la configuration", "deleted": "Agent supprime", "deleteError": "Impossible de supprimer l'agent", "runStarted": "Execution de l'agent lancee", "runError": "Impossible de lancer l'agent" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**German translations:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
"toast": {
|
||||||
|
"profile": { "updated": "Profil aktualisiert", "updateError": "Profil konnte nicht aktualisiert werden" },
|
||||||
|
"settings": { "languageChanged": "Sprache geaendert", "backendUrlSaved": "Server-URL gespeichert", "backendUrlError": "Server-URL konnte nicht gespeichert werden", "formatPrefsSaved": "Anzeigeeinstellungen gespeichert", "formatPrefsError": "Einstellungen konnten nicht gespeichert werden", "memorySaved": "Einstellungen gespeichert", "memoryError": "Einstellungen konnten nicht gespeichert werden" },
|
||||||
|
"auth": { "loginError": "Anmeldung fehlgeschlagen", "registerError": "Registrierung fehlgeschlagen", "oauthError": "Google-Anmeldung fehlgeschlagen", "loggedOut": "Abgemeldet" },
|
||||||
|
"onboarding": { "completed": "Einrichtung abgeschlossen", "completedDescription": "Ihr Arbeitsbereich ist personalisiert", "error": "Einrichtung konnte nicht gespeichert werden", "reset": "Einrichtung zurueckgesetzt", "normalizing": "Personalisierung laeuft...", "normalized": "Personalisierung abgeschlossen" },
|
||||||
|
"task": { "created": "Aufgabe erstellt", "createError": "Aufgabe konnte nicht erstellt werden", "updated": "Aufgabe aktualisiert", "updateError": "Aufgabe konnte nicht aktualisiert werden", "deleted": "Aufgabe geloescht", "deleteError": "Aufgabe konnte nicht geloescht werden" },
|
||||||
|
"project": { "created": "Projekt erstellt", "createError": "Projekt konnte nicht erstellt werden", "updated": "Projekt aktualisiert", "updateError": "Projekt konnte nicht aktualisiert werden", "deleted": "Projekt geloescht", "deleteError": "Projekt konnte nicht geloescht werden", "archived": "Projekt archiviert", "unarchived": "Projekt wiederhergestellt", "archivedAll": "Alle Projekte archiviert", "unarchivedAll": "Alle Projekte wiederhergestellt" },
|
||||||
|
"client": { "created": "Kunde erstellt", "createError": "Kunde konnte nicht erstellt werden", "updated": "Kunde umbenannt", "updateError": "Kunde konnte nicht umbenannt werden", "deleted": "Kunde geloescht", "deleteError": "Kunde konnte nicht geloescht werden" },
|
||||||
|
"note": { "created": "Notiz erstellt", "createError": "Notiz konnte nicht erstellt werden", "deleted": "Notiz geloescht", "deleteError": "Notiz konnte nicht geloescht werden" },
|
||||||
|
"timeline": { "created": "Ereignis erstellt", "createError": "Ereignis konnte nicht erstellt werden", "updated": "Ereignis aktualisiert", "updateError": "Ereignis konnte nicht aktualisiert werden", "deleted": "Ereignis geloescht", "deleteError": "Ereignis konnte nicht geloescht werden" },
|
||||||
|
"comment": { "created": "Kommentar hinzugefuegt", "createError": "Kommentar konnte nicht hinzugefuegt werden", "deleted": "Kommentar geloescht", "deleteError": "Kommentar konnte nicht geloescht werden" },
|
||||||
|
"agent": { "created": "Agent erstellt", "createError": "Agent konnte nicht erstellt werden", "updated": "Agent-Konfiguration gespeichert", "updateError": "Konfiguration konnte nicht gespeichert werden", "deleted": "Agent geloescht", "deleteError": "Agent konnte nicht geloescht werden", "runStarted": "Agent-Ausfuehrung gestartet", "runError": "Agent konnte nicht gestartet werden" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Settings Mutations (replace existing `saved`/`setSaved` patterns)
|
||||||
|
|
||||||
|
These 5 components have existing feedback to **remove and replace**:
|
||||||
|
|
||||||
|
### 2.1 `src/renderer/components/settings/GeneralSection.tsx`
|
||||||
|
|
||||||
|
**Current:** `saved`/`setSaved` state (line 28), `error`/`setError` state (line 29), `setTimeout` (line 44), inline `<p>` error (line 93).
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Add `const { notify, notifyError } = useNotify();`
|
||||||
|
2. **Remove** `const [saved, setSaved] = useState(false);`
|
||||||
|
3. **Remove** `const [error, setError] = useState('');`
|
||||||
|
4. In `handleSave` `onSuccess`: remove `setSaved(true); setTimeout(...)` → add `notify('success', 'toast.profile.updated');`
|
||||||
|
5. In `handleSave` `onError`: replace `setError(err.message)` → `notifyError('toast.profile.updateError', err);`
|
||||||
|
6. **Remove** inline error `<p>` tag (line 93)
|
||||||
|
7. **Remove** `setSaved(false)` from `onChange` handlers (lines 83, 89)
|
||||||
|
8. Button text: `{saved ? t('settings.saved') : t('common.save')}` → `{t('common.save')}`
|
||||||
|
9. In `handleLanguageChange`: add `notify('info', 'toast.settings.languageChanged');`
|
||||||
|
|
||||||
|
### 2.2 `src/renderer/components/settings/ProfileSection.tsx`
|
||||||
|
|
||||||
|
**Current:** `profileSaved`/`displaySaved` states, both with `setTimeout`.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Add `useNotify()`, remove both `saved` states and `setTimeout`s
|
||||||
|
2. Profile save `onSuccess` → `notify('success', 'toast.settings.memorySaved')`
|
||||||
|
3. Display save `onSuccess` → `notify('success', 'toast.settings.formatPrefsSaved')`
|
||||||
|
4. Reset onboarding `onSuccess` → `notify('info', 'toast.onboarding.reset')`
|
||||||
|
5. Both save buttons: replace ternary with `{t('common.save')}`
|
||||||
|
|
||||||
|
### 2.3 `src/renderer/components/settings/AccountSection.tsx`
|
||||||
|
|
||||||
|
**Current:** `urlSaved`/`setUrlSaved` state, `setTimeout`.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Add `useNotify()`, remove `urlSaved` state and `setTimeout`
|
||||||
|
2. Backend URL save `onSuccess` → `notify('success', 'toast.settings.backendUrlSaved')`
|
||||||
|
3. Add `onError` → `notifyError('toast.settings.backendUrlError', err)`
|
||||||
|
4. Logout `onSuccess` → `notify('info', 'toast.auth.loggedOut')`
|
||||||
|
5. Button text: replace ternary with `{t('common.save')}`
|
||||||
|
|
||||||
|
### 2.4 `src/renderer/components/settings/LocalAgentConfigPanel.tsx`
|
||||||
|
|
||||||
|
**Current:** `saved`/`setSaved` state, `setTimeout`.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Add `useNotify()`, remove `saved` state and `setTimeout`
|
||||||
|
2. Save `onSuccess` → `notify('success', 'toast.agent.updated')`
|
||||||
|
3. Add `onError` → `notifyError('toast.agent.updateError', err)`
|
||||||
|
4. Button text: `{t('common.save')}`
|
||||||
|
|
||||||
|
### 2.5 `src/renderer/components/settings/CloudAgentConfigPanel.tsx`
|
||||||
|
|
||||||
|
Identical pattern to 2.4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: CRUD Operations
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/tasks/NewTaskDialog.tsx` | `tasks.create` | success | `toast.task.created` |
|
||||||
|
| `components/tasks/EditTaskDialog.tsx` | `tasks.update` | success | `toast.task.updated` |
|
||||||
|
| `components/tasks/TaskDetailDialog.tsx` | `taskComments.create` | success | `toast.comment.created` |
|
||||||
|
| `components/tasks/TaskDetailDialog.tsx` | `taskComments.delete` | warning | `toast.comment.deleted` |
|
||||||
|
| `routes/tasks.tsx` | `tasks.delete` | warning | `toast.task.deleted` |
|
||||||
|
| `components/projects/KanbanBoard.tsx` | `tasks.delete` | warning | `toast.task.deleted` |
|
||||||
|
| `components/projects/ProjectDetail.tsx` | `tasks.delete` | warning | `toast.task.deleted` |
|
||||||
|
| `components/ai/blocks/ChatEntityBlock.tsx` | `tasks.delete` | warning | `toast.task.deleted` |
|
||||||
|
|
||||||
|
**Error-only (no success toast):** Status toggle mutations in `routes/tasks.tsx`, `KanbanBoard.tsx`, `ProjectDetail.tsx`, `ChatEntityBlock.tsx` — visual feedback (badge/card move) IS the confirmation.
|
||||||
|
|
||||||
|
### Projects
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `projects.create` | success | `toast.project.created` |
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `projects.update` | success | `toast.project.updated` |
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `projects.delete` | warning | `toast.project.deleted` |
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `projects.archiveByClient` | warning | `toast.project.archivedAll` / `unarchivedAll` |
|
||||||
|
|
||||||
|
### Clients
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `clients.create` | success | `toast.client.created` |
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `clients.update` | success | `toast.client.updated` |
|
||||||
|
| `components/projects/ProjectSidebar.tsx` | `clients.deleteWithCascade` | warning | `toast.client.deleted` |
|
||||||
|
| `components/tasks/NewTaskDialog.tsx` | `clients.create` (inline) | success | `toast.client.created` |
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/projects/ProjectDetail.tsx` | `notes.create` | success | `toast.note.created` |
|
||||||
|
| `routes/notes.$noteId.tsx` | `notes.delete` | warning | `toast.note.deleted` |
|
||||||
|
|
||||||
|
### Timeline Events
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/timeline/AddEventDialog.tsx` | `timelineEvents.create` | success | `toast.timeline.created` |
|
||||||
|
| `components/timeline/EditEventDialog.tsx` | `timelineEvents.update` | success | `toast.timeline.updated` |
|
||||||
|
| `routes/timeline.tsx` | `timelineEvents.delete` | warning | `toast.timeline.deleted` |
|
||||||
|
| `routes/timeline.tsx` | `timelineEvents.update` | success | `toast.timeline.updated` |
|
||||||
|
|
||||||
|
### Agents
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/settings/AgentsSection.tsx` | `agent.*.delete` | warning | `toast.agent.deleted` |
|
||||||
|
| `components/settings/AgentsSection.tsx` | `agent.runNow` | promise | `toast.agent.runStarted` / `runError` |
|
||||||
|
| `components/settings/InlineAgentCreationStepper.tsx` | `agent.*.create` | success | `toast.agent.created` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Auth + Onboarding
|
||||||
|
|
||||||
|
| File | Mutation | Toast | Key |
|
||||||
|
|------|----------|-------|-----|
|
||||||
|
| `components/auth/LoginForm.tsx` | `auth.login` error | error | `toast.auth.loginError` |
|
||||||
|
| `components/auth/LoginForm.tsx` | `auth.register` error | error | `toast.auth.registerError` |
|
||||||
|
| `components/auth/LoginForm.tsx` | `auth.loginWithOAuth` error | error | `toast.auth.oauthError` |
|
||||||
|
| `components/layout/AppShell.tsx` | `auth.logout` success | info | `toast.auth.loggedOut` |
|
||||||
|
| `components/onboarding/OnboardingFlow.tsx` | final save success | success | `toast.onboarding.completed` |
|
||||||
|
| `components/onboarding/OnboardingFlow.tsx` | normalize call | promise | `toast.onboarding.normalizing` / `normalized` |
|
||||||
|
|
||||||
|
**Auth note:** Keep inline form errors in LoginForm alongside toast — form-level positional context is valuable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Explicitly SILENT Mutations (no toast)
|
||||||
|
|
||||||
|
| Mutation | Reason |
|
||||||
|
|----------|--------|
|
||||||
|
| `notes.update` (auto-save debounced 2s) | Would spam a toast every 2s while typing |
|
||||||
|
| `tasks.update` status toggle (kanban drag, checkbox, status cycle) | Card movement / badge change IS the feedback |
|
||||||
|
| `settings.setSidebarCollapsed` | Sidebar animation IS the feedback |
|
||||||
|
| `ai.chat` / `ai.dailyBrief` | Has own streaming UI |
|
||||||
|
| `agent.journey.*` | Has own conversational UI |
|
||||||
|
|
||||||
|
For these: add `onError` callback only (no success toast).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## All Files Modified (Summary)
|
||||||
|
|
||||||
|
**New files (2):**
|
||||||
|
- `src/renderer/components/ui/sonner.tsx` — generated by CLI + theme fix
|
||||||
|
- `src/renderer/hooks/useNotify.ts` — custom hook
|
||||||
|
|
||||||
|
**Modified files (~25):**
|
||||||
|
- `package.json` — `sonner` dependency (automatic via CLI)
|
||||||
|
- `src/renderer/index.tsx` — add `<Toaster />`
|
||||||
|
- 5x `locales/{en,it,es,fr,de}/translation.json` — add `toast` keys
|
||||||
|
- 5x Settings components — replace saved state with toast
|
||||||
|
- ~14x CRUD/auth/onboarding components — add toast calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. **All app states:** Trigger toasts during login (error), onboarding (completion), and normal usage (CRUD)
|
||||||
|
2. **Theme:** Switch dark/light/system — toast backgrounds should follow semantic tokens
|
||||||
|
3. **i18n:** Switch to Italian → trigger save → toast should read "Profilo aggiornato"
|
||||||
|
4. **Error persistence:** Error toasts stay until dismissed (test with invalid backend URL)
|
||||||
|
5. **Silent mutations:** Drag task on kanban, type in notes, toggle sidebar — NO toasts
|
||||||
|
6. **Removed patterns:** Settings Save buttons stay as "Save" (no "Saved" flash) + toast appears
|
||||||
|
7. **Position:** Bottom-right, not overlapping sidebar
|
||||||
|
8. **Lint:** `npm run lint` passes
|
||||||
|
9. **Build:** `npm run package` succeeds
|
||||||
3035
docs/superpowers/plans/2026-05-11-project-folder-integration.md
Normal file
3035
docs/superpowers/plans/2026-05-11-project-folder-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
1579
docs/superpowers/plans/2026-05-13-timeline-batch-add.md
Normal file
1579
docs/superpowers/plans/2026-05-13-timeline-batch-add.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,386 @@
|
|||||||
|
# Project Folder Integration — Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-11
|
||||||
|
**Status:** Approved (brainstorming complete)
|
||||||
|
**Author:** Roberto + Claude
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Let users link a local (or shared-PC) folder to an adiuvAI project. Adiuvai scans the folder, generates per-file summaries via LLM, and exposes the resulting manifest to the Home, Brief, and Task-Brief agents so they can answer project questions with awareness of the user's local files.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Multi-folder linking per project (deferred — 1 folder per project for now).
|
||||||
|
- Full-text RAG over file contents (we use lightweight per-file summaries instead).
|
||||||
|
- File editing from inside adiuvAI (read-only).
|
||||||
|
- Token-usage display in the project UI (recorded backend-side; dedicated Settings page comes later).
|
||||||
|
- Web SPA support (Electron-only — web SPA has no filesystem access).
|
||||||
|
|
||||||
|
## Strategy
|
||||||
|
|
||||||
|
**Hybrid AI File System (manifest first, optional wiki tier later).**
|
||||||
|
|
||||||
|
Phase 1 (this spec): build a lightweight manifest — for each indexable file record `(relativePath, kind, size, mtime, 1-line LLM summary)`. The agent receives the manifest pre-injected into its system prompt and reads full file contents lazily via a scoped tool.
|
||||||
|
|
||||||
|
Phase 2 (future, out of scope): for folders above N files or on user opt-in, generate per-folder + per-file wiki summaries written to a structured index. Not built now.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────── adiuvAI (Electron) ───────────────────────┐
|
||||||
|
│ Renderer (React) │
|
||||||
|
│ • Project hero: <FolderChip> (status glance) │
|
||||||
|
│ • <FilesTab>: link/unlink, browse, rescan, progress, browser │
|
||||||
|
│ • <FolderBrowser>: tree view of manifest │
|
||||||
|
│ │
|
||||||
|
│ Main (Node) │
|
||||||
|
│ • db/schema.ts: +projectFolderFiles, +projects.folderPath │
|
||||||
|
│ • files/scanner.ts: walk + filter + mtime delta │
|
||||||
|
│ • files/indexer.ts: orchestrates WS index session │
|
||||||
|
│ • files/daily-rescan.ts: 24h-stale check on app start │
|
||||||
|
│ • router/projectFolders.ts: tRPC procedures │
|
||||||
|
│ • api/backend-client.ts: +sendIndexBatch frame │
|
||||||
|
│ • api/drizzle-executor.ts: +read_project_folder_manifest, │
|
||||||
|
│ +read_project_folder_file actions │
|
||||||
|
└───────────────────────────────────────────────────────────────────┘
|
||||||
|
│ /api/v1/device WS
|
||||||
|
▼
|
||||||
|
┌────────────────────────── api (FastAPI) ─────────────────────────┐
|
||||||
|
│ device_ws.py: +index_file_batch / +index_file_result frames │
|
||||||
|
│ core/folder_indexer.py: summarize text / vision per file │
|
||||||
|
│ core/deep_agent.py: pre-inject manifest when project context set │
|
||||||
|
│ agents/folder_agent.py: scoped read_project_folder_file tool │
|
||||||
|
│ billing/tier_manager.py: +folder_max_files, +folder_monthly_tokens│
|
||||||
|
│ models.py: +AgentRunLog.tokens_used, +MonthlyTokenUsage table │
|
||||||
|
└───────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Privacy invariant:** file content travels to the backend only transiently — for summarization — and is never persisted there. Summaries and manifest entries live in the local SQLite database. Token usage is recorded backend-side because it gates the user's tier quota.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
|-------|----------|
|
||||||
|
| Retrieval strategy | Hybrid: manifest first, optional wiki tier later (phase 2, out of scope) |
|
||||||
|
| File scope | Text whitelist (.md, .txt, .pdf, .docx, .csv, code) + images (.png/.jpg) summarized via gpt-4o-mini vision |
|
||||||
|
| Cardinality | One folder per project |
|
||||||
|
| Rescan triggers | Manual button + daily auto (24h staleness check on app start) + on-demand mtime delta when manifest is read |
|
||||||
|
| Rate-limit metric | Tokens-per-month per user **and** total file-count cap per folder, both tier-gated |
|
||||||
|
| Indexing pipeline | WS streaming over existing `/api/v1/device` with new frame types |
|
||||||
|
| Agent access | Pre-inject manifest into system prompt; lazy reads via scoped `read_project_folder_file` tool |
|
||||||
|
| UI placement | Hero chip + dedicated "Files" tab in `ProjectTabBar` |
|
||||||
|
| Platform | Electron-only (web SPA: tab disabled) |
|
||||||
|
| Token-usage display | Out of scope (record backend-side, surface in Settings later) |
|
||||||
|
|
||||||
|
## Schema Changes
|
||||||
|
|
||||||
|
### adiuvAI local SQLite (`src/main/db/schema.ts`)
|
||||||
|
|
||||||
|
Extend `projects`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
projects: {
|
||||||
|
// existing columns...
|
||||||
|
folderPath: text('folder_path'), // nullable absolute path or UNC
|
||||||
|
folderLastScannedAt: integer('folder_last_scanned_at'),// ms, nullable
|
||||||
|
folderLastScanStatus: text('folder_last_scan_status'), // 'idle' | 'scanning' | 'error'
|
||||||
|
folderTotalFiles: integer('folder_total_files').default(0),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
New table `projectFolderFiles`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
projectFolderFiles: {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
projectId: text('project_id').notNull(), // FK projects.id (no DB constraint per convention)
|
||||||
|
relativePath: text('relative_path').notNull(), // path relative to folderPath
|
||||||
|
ext: text('ext').notNull(), // '.md', '.png', ...
|
||||||
|
kind: text('kind').notNull(), // 'text' | 'image' | 'pdf' | 'docx' | 'skipped' | 'error'
|
||||||
|
sizeBytes: integer('size_bytes').notNull(),
|
||||||
|
mtimeMs: integer('mtime_ms').notNull(),
|
||||||
|
summary: text('summary'), // nullable, ≤500 chars
|
||||||
|
summaryUpdatedAt: integer('summary_updated_at'),
|
||||||
|
// Unique index: (projectId, relativePath)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### api Postgres (alembic migration)
|
||||||
|
|
||||||
|
```python
|
||||||
|
op.add_column('agent_run_logs',
|
||||||
|
sa.Column('tokens_used', sa.Integer(), nullable=False, server_default='0'))
|
||||||
|
|
||||||
|
op.create_table('monthly_token_usage',
|
||||||
|
sa.Column('user_id', UUID(as_uuid=False), ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
|
||||||
|
sa.Column('year_month', sa.String(7), nullable=False), # 'YYYY-MM'
|
||||||
|
sa.Column('feature', sa.String(64), nullable=False), # 'folder_index'
|
||||||
|
sa.Column('tokens_used', sa.Integer, nullable=False, server_default='0'),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'year_month', 'feature'),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tier matrix (`app/billing/tier_manager.py`)
|
||||||
|
|
||||||
|
| Feature | Free | Pro | Power | Team |
|
||||||
|
|-------------------------|------|-----|-------|------|
|
||||||
|
| `folder_max_files` | 200 | 5000| -1 | -1 |
|
||||||
|
| `folder_monthly_tokens` | 100k | 2M | -1 | -1 |
|
||||||
|
|
||||||
|
## Indexing Pipeline
|
||||||
|
|
||||||
|
### New WS frame types on `/api/v1/device`
|
||||||
|
|
||||||
|
| Direction | Frame | Payload |
|
||||||
|
|-----------|---------------------------|---------|
|
||||||
|
| C → S | `index_session_start` | `{ sessionId, projectId, totalFiles }` |
|
||||||
|
| C → S | `index_file_batch` | `{ sessionId, files: [{relPath, kind, content/imageB64, sizeBytes, mtimeMs}] }` (batches of 5) |
|
||||||
|
| S → C | `index_file_result` | `{ sessionId, relPath, summary, tokensUsed, error? }` |
|
||||||
|
| S → C | `index_session_progress` | `{ sessionId, processed, total }` |
|
||||||
|
| C → S | `index_session_cancel` | `{ sessionId }` |
|
||||||
|
| S → C | `index_session_done` | `{ sessionId, status: 'completed' \| 'cancelled' \| 'quota_exceeded' \| 'error' }` |
|
||||||
|
|
||||||
|
### Flow (Electron `files/indexer.ts`)
|
||||||
|
|
||||||
|
1. tRPC `projectFolders.startScan({ projectId })`.
|
||||||
|
2. `scanner.ts` walks `folderPath`:
|
||||||
|
- Filter by whitelist (text exts + .png/.jpg).
|
||||||
|
- Apply size cap (1 MB / file).
|
||||||
|
- Compute mtime delta vs `projectFolderFiles`.
|
||||||
|
- Returns `{ newFiles[], changedFiles[], deletedFiles[] }`.
|
||||||
|
3. Backend pre-flight: `POST /api/v1/billing/quota/check { feature: 'folder_index', estimated_files: N }`:
|
||||||
|
- Rejects 402 if `folder_max_files` exceeded for the user's tier.
|
||||||
|
- Rejects 402 if `folder_monthly_tokens` already exhausted.
|
||||||
|
4. Open `index_session_start` over WS.
|
||||||
|
5. For each batch of 5 files:
|
||||||
|
- Read content (text) or base64-encode (image).
|
||||||
|
- Send `index_file_batch`.
|
||||||
|
- Await `index_file_result × 5`.
|
||||||
|
- Upsert `projectFolderFiles` row with the returned summary.
|
||||||
|
- Backend atomically increments `MonthlyTokenUsage` and writes a row in `AgentRunLog` with `tokens_used`.
|
||||||
|
6. Send `index_session_done`. Update `projects.folderLastScannedAt`, `.folderTotalFiles`, `.folderLastScanStatus = 'idle'`.
|
||||||
|
7. Delete `projectFolderFiles` rows for `deletedFiles`.
|
||||||
|
|
||||||
|
### Backend (`core/folder_indexer.py`)
|
||||||
|
|
||||||
|
- `summarize_text(content, ext) → (summary, tokens)` via `gpt-4o-mini`, Langfuse prompt `folder_file_summary_text`.
|
||||||
|
- `summarize_image(b64) → (summary, tokens)` via `gpt-4o-mini` vision, Langfuse prompt `folder_file_summary_image`.
|
||||||
|
- After each summarization, atomically increment `MonthlyTokenUsage(user_id, year_month, 'folder_index', +tokens)`. If the increment would exceed cap, the call returns a `quota_exceeded` error in `index_file_result`, and the session sends `index_session_done(status='quota_exceeded')`.
|
||||||
|
|
||||||
|
### Rescan triggers
|
||||||
|
|
||||||
|
- **Manual button** → tRPC `projectFolders.startScan` mutation.
|
||||||
|
- **On-demand mtime check** → inside `read_project_folder_manifest` drizzle-executor action: if any tracked mtime is stale, fire-and-forget `startScan` before returning the current manifest.
|
||||||
|
- **Daily auto** → `app.on('ready')` iterates user projects; if `folderLastScannedAt < now − 24h` and `folderPath != null`, queue `startScan`.
|
||||||
|
|
||||||
|
`projects.folderLastScanStatus === 'scanning'` blocks new scan triggers (manual button disabled, daily auto + mtime on-demand both skip).
|
||||||
|
|
||||||
|
## Agent Integration
|
||||||
|
|
||||||
|
### Manifest pre-injection
|
||||||
|
|
||||||
|
In `core/deep_agent.py`, every agent run that has a resolved `projectId` builds a compact manifest block and prepends it to the system prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
<linked_folder>
|
||||||
|
path: D:\Clients\Acme\Brand (214 files, scanned 2h ago)
|
||||||
|
files:
|
||||||
|
- /briefs/kickoff.md [text] Project kickoff notes; scope, stakeholders, deadlines
|
||||||
|
- /logos/logo-v3.png [image] Final logo, golden-yellow palette on white
|
||||||
|
- /research/competitor.pdf [pdf] Competitor brand audit, 12 entries
|
||||||
|
...
|
||||||
|
</linked_folder>
|
||||||
|
```
|
||||||
|
|
||||||
|
Format: `relativePath [kind] summary`. If the rendered block exceeds ~3000 tokens, truncate to the top N files by `mtimeMs DESC` and append:
|
||||||
|
|
||||||
|
```
|
||||||
|
… {M} more files omitted, use read_project_folder_file to access by path
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend pulls the manifest via the new drizzle-executor action:
|
||||||
|
|
||||||
|
```
|
||||||
|
action: read_project_folder_manifest
|
||||||
|
data: { projectId }
|
||||||
|
returns: { folderPath, lastScannedAt, files: [{relPath, kind, summary}] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### projectId resolution per agent
|
||||||
|
|
||||||
|
- `run_task_brief_research_stream` — `task.projectId`.
|
||||||
|
- `run_home` — null unless the user message is project-scoped (via `@project` mention or active project context passed from renderer).
|
||||||
|
- `run_brief` — backend cannot enumerate projects directly because projects live in the local SQLite. It calls a new `execute_on_client` action `list_projects_with_folder_manifests` that returns `[{ projectId, projectName, folderPath, lastScannedAt, files: [{relPath, kind, summary}] }]` for every project that has a linked folder. The backend then builds a **multi-project compact manifest** (top 5 most-recently-modified files per project).
|
||||||
|
|
||||||
|
### New scoped tool (`agents/folder_agent.py`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@tool
|
||||||
|
async def read_project_folder_file(project_id: str, relative_path: str) -> str:
|
||||||
|
"""Read full content of a file inside the project's linked folder."""
|
||||||
|
result = await execute_on_client(
|
||||||
|
action="read_project_folder_file",
|
||||||
|
data={"projectId": project_id, "relativePath": relative_path},
|
||||||
|
)
|
||||||
|
return result.get("content", "") or f"File not found: {relative_path}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Backed by a new `drizzle-executor` action that:
|
||||||
|
1. Looks up `projects.folderPath` for the projectId.
|
||||||
|
2. Resolves `path.join(folderPath, relativePath)` with traversal guard (`..` and absolute paths rejected).
|
||||||
|
3. Reads the file via the existing fs helpers. Image → returns base64. Text → returns content (size-capped).
|
||||||
|
|
||||||
|
The existing journey-only `FILESYSTEM_TOOLS` are not added to home/brief/task-brief; only the new scoped tool is bound.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
### Hero chip (`ProjectDetail.tsx`)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<FolderChip
|
||||||
|
projectId={project.id}
|
||||||
|
folderPath={project.folderPath}
|
||||||
|
totalFiles={project.folderTotalFiles}
|
||||||
|
lastScannedAt={project.folderLastScannedAt}
|
||||||
|
scanStatus={project.folderLastScanStatus}
|
||||||
|
onClick={() => scrollToTab('files')}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
States:
|
||||||
|
- **Unlinked:** dashed pill "📁 Link folder" + Sparkles icon.
|
||||||
|
- **Linked idle:** "📁 214 files · 2h ago" with soft golden-yellow background.
|
||||||
|
- **Scanning:** "📁 indexing 47/214" + spinner.
|
||||||
|
- **Error:** "📁 Scan failed" red-tinted; click → Files tab.
|
||||||
|
|
||||||
|
### Files tab
|
||||||
|
|
||||||
|
Add `'files'` to `SECTIONS` in `ProjectTabBar.tsx`. The tab body:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ Linked folder │
|
||||||
|
│ ┌────────────────────────────────────────────┐ │
|
||||||
|
│ │ 📁 D:\Clients\Acme\Brand [⋯ menu] │ │
|
||||||
|
│ │ 214 files · last scanned 2h ago │ │
|
||||||
|
│ │ [Rescan] [Unlink] │ │
|
||||||
|
│ └────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Files (filter: [All] [Text] [Images] [PDF]) │
|
||||||
|
│ ┌────────────────────────────────────────────┐ │
|
||||||
|
│ │ briefs/kickoff.md │ │
|
||||||
|
│ │ Project kickoff notes; scope, deadlines │ │
|
||||||
|
│ │ logos/logo-v3.png │ │
|
||||||
|
│ │ Final logo, golden-yellow on white │ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ └────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty state (no folder linked)
|
||||||
|
|
||||||
|
```
|
||||||
|
<Empty>
|
||||||
|
Sparkles
|
||||||
|
Link a project folder
|
||||||
|
Connect a local folder so AI agents can read its files
|
||||||
|
when answering questions about this project.
|
||||||
|
[Choose folder...] ← opens Electron dialog.showOpenDialog
|
||||||
|
</Empty>
|
||||||
|
```
|
||||||
|
|
||||||
|
### New components (`src/renderer/components/projects/folder/`)
|
||||||
|
|
||||||
|
- `FolderChip.tsx`
|
||||||
|
- `FilesSection.tsx` (mounts inside `ProjectDetail`)
|
||||||
|
- `FolderLinkCard.tsx` (path + actions)
|
||||||
|
- `FolderFileList.tsx` (virtualized list of manifest entries)
|
||||||
|
- `FolderUnlinkDialog.tsx`
|
||||||
|
|
||||||
|
### Platform gating
|
||||||
|
|
||||||
|
Feature is **Electron-only**. Wrap entry points in `platform.isElectron`. On the web SPA, the Files tab renders disabled with a tooltip "Folder linking available in desktop app".
|
||||||
|
|
||||||
|
### Folder dialog
|
||||||
|
|
||||||
|
New tRPC `projectFolders.chooseFolder` mutation invokes `dialog.showOpenDialog({ properties: ['openDirectory'] })` in the main process and returns the selected path.
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
Add `projects.folder.*` keys (title, link CTA, browse, rescan, unlink, status strings, empty state copy, error toasts) to all 5 locale JSON files: en, it, es, fr, de.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Quota exhaustion
|
||||||
|
|
||||||
|
- Pre-flight 402 → toast `"Folder too big for {tier} plan — max {N} files"` or `"Monthly token budget exhausted (resets {date})"`. Folder not linked.
|
||||||
|
- Mid-scan `quota_exceeded` frame → partial manifest kept, scan marked `error`, toast as above, banner in Files tab `"Indexing paused — quota exhausted"`.
|
||||||
|
|
||||||
|
### Path errors
|
||||||
|
|
||||||
|
- Folder no longer exists at scan start → tRPC throws → toast `"Folder not found: {path}"`. `folderLastScanStatus = 'error'`. User offered Unlink or Re-link.
|
||||||
|
- Permission denied on a file during scan → file skipped, logged in `projectFolderFiles` with `kind='skipped'`, no summary. Skipped files appear greyed in the Files tab.
|
||||||
|
- Path traversal attempt in `read_project_folder_file` (relativePath contains `..` or is absolute) → tool returns `"Access denied"`; backend logs a warning. Hard fail, no fallback.
|
||||||
|
|
||||||
|
### Network / WS failures
|
||||||
|
|
||||||
|
- WS drop mid-scan: the in-flight session is abandoned server-side and the local `folderLastScanStatus` is flipped from `'scanning'` to `'error'`. The next trigger (manual rescan, daily auto, or the next on-demand mtime check) starts a **new** session; because the scanner's mtime delta only re-summarizes files whose `mtimeMs` changed (or that have no row yet), already-indexed files are skipped naturally — there is no explicit session-resume protocol.
|
||||||
|
- Backend 5xx on summarize → file marked `kind='error'`, retried in the next rescan, not auto-retried inline.
|
||||||
|
|
||||||
|
### File-type fallbacks
|
||||||
|
|
||||||
|
- PDF parse fails (corrupt) → skipped, `kind='skipped'` with `summary='Could not extract text'`.
|
||||||
|
- Image too large (>5 MB) → skipped with reason. Cap is a constant in `files/scanner.ts`.
|
||||||
|
- DOCX or other unsupported types → skipped silently with extension noted.
|
||||||
|
|
||||||
|
### Concurrent scan guard
|
||||||
|
|
||||||
|
`projects.folderLastScanStatus === 'scanning'` blocks new scan triggers. Manual button shows "Scanning..." disabled; daily auto + mtime on-demand both check the status flag first.
|
||||||
|
|
||||||
|
### Manifest size overflow
|
||||||
|
|
||||||
|
If the agent's pre-injected `<linked_folder>` block would exceed ~3000 tokens, the backend truncates to the top N files by `mtimeMs DESC` and appends an "M more files omitted" hint.
|
||||||
|
|
||||||
|
### Tool call on unlinked project
|
||||||
|
|
||||||
|
`read_project_folder_file` when `folderPath === null` returns `"No folder linked to project {projectId}"`. The agent can recover and answer without folder context.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### API (`api/tests/`)
|
||||||
|
|
||||||
|
| File | Coverage |
|
||||||
|
|------|----------|
|
||||||
|
| `test_folder_indexer.py` | `summarize_text` / `summarize_image` happy path, token recording, Langfuse prompt linking |
|
||||||
|
| `test_folder_quota.py` | Pre-flight 402 rejects (max_files + monthly_tokens), atomic increment + `quota_exceeded` mid-stream, monthly reset at `year_month` rollover |
|
||||||
|
| `test_ws_index_session.py` | Session lifecycle, cancel mid-stream, abandoned-on-disconnect (next scan skips already-indexed files via mtime delta), bad batch payload validation |
|
||||||
|
| `test_folder_agent_tool.py` | `read_project_folder_file` happy path, unlinked project, traversal guard (`../`, absolute) |
|
||||||
|
| `test_manifest_injection.py` | `<linked_folder>` block formatting, truncation past 3k tokens, multi-project brief manifest, null projectId skips injection |
|
||||||
|
|
||||||
|
Reuse fixtures in `tests/conftest.py` and WS test helpers (`ws_unified` already covers session lifecycle).
|
||||||
|
|
||||||
|
### Electron / adiuvAI
|
||||||
|
|
||||||
|
No automated test suite currently. Manual smoke checks during development:
|
||||||
|
|
||||||
|
- Link folder → manifest populated → unlink → manifest rows deleted.
|
||||||
|
- Scan a synthetic dir with mixed text/image/binary → only whitelisted indexed.
|
||||||
|
- mtime delta: change one file, rescan only re-indexes that file.
|
||||||
|
- Disconnect WS mid-scan → status flips to `'error'`; next manual rescan re-indexes only the remaining files (mtime delta).
|
||||||
|
|
||||||
|
### Eval (Langfuse)
|
||||||
|
|
||||||
|
Build a test set of 10 representative folders (mix of markdown, code, PDFs, images). Score summary quality (LLM-as-judge) and token efficiency. Link scores to the prompt version per the existing `LOCAL_AGENT_V2_PLAN.md` pattern.
|
||||||
|
|
||||||
|
## Out of scope (this spec)
|
||||||
|
|
||||||
|
- Phase-2 wiki tier (per-folder + per-file structured summaries).
|
||||||
|
- Multi-folder per project.
|
||||||
|
- Web SPA support.
|
||||||
|
- Token-usage display UI (Settings page comes later).
|
||||||
|
- File editing from inside adiuvAI.
|
||||||
|
- Live file watcher (chokidar). Daily + manual + on-demand mtime is enough for now.
|
||||||
|
|
||||||
|
## Open questions (none)
|
||||||
|
|
||||||
|
All resolved during brainstorming.
|
||||||
299
docs/superpowers/specs/2026-05-13-timeline-batch-add-design.md
Normal file
299
docs/superpowers/specs/2026-05-13-timeline-batch-add-design.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Timeline Batch Add — Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-13
|
||||||
|
**Status:** Draft, awaiting user review
|
||||||
|
**Scope:** `adiuvAI/` submodule only
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Adding timeline events today goes through `AddEventDialog.tsx` one event at a time. The dialog already supports a sequential "add then add another" loop, but:
|
||||||
|
|
||||||
|
- Each event commits immediately on Enter (no client-side staging).
|
||||||
|
- The project picker appears at the bottom, after type/title/date.
|
||||||
|
- Date entry requires opening a calendar popover — keyboard-hostile.
|
||||||
|
- A user planning a project (kickoff + milestones + activities) clicks through the dialog 5–10 times to seed a project's timeline.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Single dialog session lets the user pick a project, stage multiple timeline events of mixed types, review, and commit the batch. Fully operable without a mouse.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- New backend endpoint. Re-use `trpc.timelineEvents.create` per event.
|
||||||
|
- Reordering staged events. Order is "as added".
|
||||||
|
- Bulk import (CSV/paste). Out of scope.
|
||||||
|
- Cross-project batch. One batch = one project.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Refactor `adiuvAI/src/renderer/components/timeline/AddEventDialog.tsx` in place. Same callsites (`routes/timeline.tsx`, `components/projects/ProjectDetail.tsx`), same props (`open`, `onOpenChange`, `defaultProjectId?`, `onRecordHistory?`).
|
||||||
|
|
||||||
|
Two new shared primitives extracted from this work:
|
||||||
|
|
||||||
|
- `adiuvAI/src/renderer/lib/parseDate.ts` — pure date parser, locale-aware.
|
||||||
|
- `adiuvAI/src/renderer/components/ui/date-field.tsx` — controlled date input with typed entry + popover fallback.
|
||||||
|
|
||||||
|
Existing `EditEventDialog.tsx` migrates to `<DateField>` as part of this work. `TaskFormDialog.tsx` is **out of scope** — it uses `TZDate` plus time-of-day (H/M) selectors, which DateField does not cover. A follow-up pass should add `timezone` + `showTime` props to DateField, then migrate TaskFormDialog.
|
||||||
|
|
||||||
|
## State model
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type StagedEvent = {
|
||||||
|
id: string; // nanoid, local-only key
|
||||||
|
title: string;
|
||||||
|
type: 'milestone' | 'checkpoint' | 'activity';
|
||||||
|
date: Date;
|
||||||
|
endDate?: Date; // activity only
|
||||||
|
};
|
||||||
|
|
||||||
|
type Mode = { kind: 'add' } | { kind: 'edit'; id: string };
|
||||||
|
|
||||||
|
// In AddEventDialog
|
||||||
|
const [projectId, setProjectId] = useState(defaultProjectId ?? '');
|
||||||
|
const [staged, setStaged] = useState<StagedEvent[]>([]);
|
||||||
|
const [mode, setMode] = useState<Mode>({ kind: 'add' });
|
||||||
|
// Form fields:
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [type, setType] = useState<TimelineEventType>('milestone');
|
||||||
|
const [date, setDate] = useState<Date | undefined>();
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>();
|
||||||
|
const [focusedRowId, setFocusedRowId] = useState<string | null>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─── Add timeline events ─────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Project [ Search project… ▾ ] │ ← hidden when defaultProjectId set
|
||||||
|
│ │ locked when staged.length > 0
|
||||||
|
│ ┌─ Staged list (scrollable, max ~6) ─┐ │
|
||||||
|
│ │ ✓ Kickoff milestone 15/03 ✕│ │
|
||||||
|
│ │ ✓ Phase 1 checkpoint 22/03 ✕│ │
|
||||||
|
│ └───────────────────────────────────┘ │
|
||||||
|
│ ───────────────────────────────────── │
|
||||||
|
│ ( Milestone | Checkpoint | Activity ) │
|
||||||
|
│ [ Event title… ] │
|
||||||
|
│ [ Date ] [End date ] │ ← end shown only for activity
|
||||||
|
│ │
|
||||||
|
│ [Cancel] [Add ↵] [Save N] │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
States:
|
||||||
|
1. **Fresh open** — empty staged list with hint text, form ready, focus on project picker (or title if `defaultProjectId`).
|
||||||
|
2. **N staged, form ready** — staged list visible, form empty, focus on title.
|
||||||
|
3. **Row focused** — form dimmed (`opacity-50 pointer-events-none`), staged row has focus ring.
|
||||||
|
4. **Editing row** — form populated from row, "Add ↵" button reads "Update ↵", row in list highlighted.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Shared primitives (new)
|
||||||
|
|
||||||
|
**`lib/parseDate.ts`** — pure functions, no React.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function parseDate(
|
||||||
|
input: string,
|
||||||
|
prefs: FormatPrefs,
|
||||||
|
baseDate?: Date,
|
||||||
|
): Date | null;
|
||||||
|
|
||||||
|
export function parseDateRange(
|
||||||
|
input: string,
|
||||||
|
prefs: FormatPrefs,
|
||||||
|
baseDate?: Date,
|
||||||
|
): { from: Date; to?: Date } | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Accepts:
|
||||||
|
- Keywords: `today`, `tomorrow`, `yesterday` (i18n-aware via current `i18n.language`)
|
||||||
|
- Relative: `+Nd`, `+Nw`, `+Nm`, `-Nd`
|
||||||
|
- Weekday names in current UI language (next occurrence): `mon`/`monday`, `lun`/`lunedì`, etc.
|
||||||
|
- Partial date: `DD/MM` or `MM/DD` (per `prefs.dateFormat`) → current year, year-rollover if past
|
||||||
|
- Full date: `DD/MM/YYYY`, `MM/DD/YYYY`, `YYYY-MM-DD` (per prefs)
|
||||||
|
|
||||||
|
Returns `null` on unparseable. No date library — small regex + native `Date`.
|
||||||
|
|
||||||
|
**`components/ui/date-field.tsx`** — controlled input.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type DateFieldProps = {
|
||||||
|
value: Date | undefined;
|
||||||
|
onChange: (d: Date | undefined) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
minDate?: Date;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
invalidMessage?: string;
|
||||||
|
className?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
|
id?: string;
|
||||||
|
onCommit?: (d: Date) => void; // fired on Enter after valid parse
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Internal: text input + calendar icon button → Popover wrapping shadcn `Calendar`. Reads `useFormatPrefs()` internally.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Display formatted value (via `formatDate(prefs)`) when input not focused and value valid.
|
||||||
|
- Show raw user text while focused.
|
||||||
|
- Parse on blur and on Enter — if valid, call `onChange(date)`; if invalid, set `aria-invalid="true"` and red ring.
|
||||||
|
- Alt+↓ opens popover. Calendar selection commits via `onChange` and closes popover.
|
||||||
|
- `Enter` inside input: `e.preventDefault()`, parse, call `onChange(parsed)`, then call optional `onCommit?: (d: Date) => void` prop synchronously with the parsed value. Parent uses `onCommit` to stage without relying on `useState` flush. If invalid, no `onCommit` call, no propagation.
|
||||||
|
|
||||||
|
### Internal to AddEventDialog (not exported)
|
||||||
|
|
||||||
|
**`<ProjectPickerRow>`** — shadcn `Command` inside `Popover`. Typeable filter. Disabled when `staged.length > 0` (visual: muted, tooltip "Project locked after first event"). Hidden when `defaultProjectId` set.
|
||||||
|
|
||||||
|
**`<StagedList>`** — `<ul role="listbox" aria-label="Staged events">`. Empty state: muted hint `t('timeline.emptyStagedHint')`. Each row `<li role="option" tabIndex={-1}>` with:
|
||||||
|
- Type badge (color from existing palette — chart-1/2/3 mapping by type)
|
||||||
|
- Title (truncate)
|
||||||
|
- Date(s), formatted per prefs
|
||||||
|
- ✕ icon button (hover-visible only) for mouse users; aria-label `t('timeline.removeRow')`
|
||||||
|
|
||||||
|
Roving tabindex managed by `focusedRowId`. List itself has `tabIndex={0}` when no row focused, so Tab reaches it.
|
||||||
|
|
||||||
|
**`<EventForm>`** — wraps:
|
||||||
|
- `ToggleGroup` for type (existing pattern)
|
||||||
|
- `Input` for title (autoFocus when `mode.kind === 'add'`)
|
||||||
|
- `<DateField>` for `date`
|
||||||
|
- `<DateField>` for `endDate`, mounted only when `type === 'activity'`, `minDate` = `date`
|
||||||
|
|
||||||
|
## Keyboard map
|
||||||
|
|
||||||
|
| Context | Key | Action |
|
||||||
|
|----------------------|----------------|--------|
|
||||||
|
| Project picker open | type | filter list |
|
||||||
|
| | ↑/↓ | nav results |
|
||||||
|
| | Enter | select, focus title |
|
||||||
|
| | Esc | close picker |
|
||||||
|
| Form, any field | Tab/Shift+Tab | cycle: project → type → title → date → endDate → footer buttons |
|
||||||
|
| | Enter (valid) | stage event (add) or update row (edit), focus title |
|
||||||
|
| | Ctrl+Enter | save batch (if N ≥ 1) |
|
||||||
|
| | Esc | close dialog (confirm if staged > 0) |
|
||||||
|
| Title field | ↑ (caret at 0) | focus last staged row |
|
||||||
|
| Type toggle | ←/→ | cycle types |
|
||||||
|
| Date field | Alt+↓ | open calendar popover |
|
||||||
|
| | Enter | parse + commit + advance focus |
|
||||||
|
| Staged row | ↑/↓ | move focus |
|
||||||
|
| | Enter | load row → form, mode=edit |
|
||||||
|
| | Del/Backspace | remove row, focus next or form |
|
||||||
|
| | Esc | focus form title |
|
||||||
|
| Footer Save button | Enter/Space | save batch |
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
type+title+date entered, Enter pressed
|
||||||
|
→ validateForm()
|
||||||
|
→ if mode.add: setStaged([...staged, newEvent]); resetForm(); focusTitle()
|
||||||
|
→ if mode.edit: setStaged(staged.map(e => e.id===mode.id ? newEvent : e)); setMode({kind:'add'}); resetForm(); focusTitle()
|
||||||
|
|
||||||
|
Save N pressed (or Ctrl+Enter)
|
||||||
|
→ for each staged event:
|
||||||
|
results = await Promise.allSettled(
|
||||||
|
staged.map(e => createEvent.mutateAsync({
|
||||||
|
title: e.title,
|
||||||
|
date: e.date.getTime(),
|
||||||
|
endDate: e.endDate?.getTime(),
|
||||||
|
type: e.type,
|
||||||
|
projectId: defaultProjectId || projectId || undefined,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
→ for each fulfilled: onRecordHistory?.({kind:'create', id, payload:...})
|
||||||
|
→ utils.timelineEvents.list.invalidate() // once, not per event
|
||||||
|
→ if all fulfilled: notify success, close
|
||||||
|
→ if partial: keep rejected rows in staged, mark with error tooltip, notify warning
|
||||||
|
→ if all rejected: notify error, no rows removed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Per-field (inline, no toast):
|
||||||
|
- Empty title → submit disabled, Enter no-op.
|
||||||
|
- Unparseable date → red ring on `DateField`, `aria-invalid="true"`, submit disabled.
|
||||||
|
- Activity `endDate < date` → red ring on end field, message `timeline.endBeforeStart`, submit disabled.
|
||||||
|
- No project selected (picker shown) → submit disabled, picker gets focus ring.
|
||||||
|
|
||||||
|
Batch failure (per data flow above):
|
||||||
|
- All success → toast + close.
|
||||||
|
- Partial → keep failed rows with error tooltip, toast warns count.
|
||||||
|
- All fail → toast error, dialog stays open.
|
||||||
|
|
||||||
|
Edge cases:
|
||||||
|
- Dialog closed mid-batch: fire-and-forget mutations continue server-side; UI suppresses their toasts after close (track via local `closedRef`).
|
||||||
|
- Project deleted between selection and submit → falls into partial-fail path. Acceptable.
|
||||||
|
- `defaultProjectId` for deleted project → already handled by existing callsite contracts.
|
||||||
|
|
||||||
|
## i18n keys (added to all 5 locales)
|
||||||
|
|
||||||
|
```
|
||||||
|
timeline.endBeforeStart "End must be after start"
|
||||||
|
timeline.dateInvalid "Unrecognized date"
|
||||||
|
timeline.batchCreated_one "1 event created"
|
||||||
|
timeline.batchCreated_other "{{count}} events created"
|
||||||
|
timeline.batchPartial "{{ok}} created, {{failed}} failed"
|
||||||
|
timeline.batchFailed "Could not create events"
|
||||||
|
timeline.staged_one "1 event staged"
|
||||||
|
timeline.staged_other "{{count}} events staged"
|
||||||
|
timeline.emptyStagedHint "Type a title, set a date, press Enter"
|
||||||
|
timeline.editRow "Edit"
|
||||||
|
timeline.removeRow "Remove"
|
||||||
|
timeline.projectLocked "Project locked after first event"
|
||||||
|
timeline.confirmCloseStaged "Discard {{count}} staged events?"
|
||||||
|
timeline.saveAll "Save {{count}}"
|
||||||
|
timeline.update "Update"
|
||||||
|
common.add existing — re-use
|
||||||
|
common.cancel existing — re-use
|
||||||
|
```
|
||||||
|
|
||||||
|
Date parser keywords (per locale):
|
||||||
|
```
|
||||||
|
date.keyword.today
|
||||||
|
date.keyword.tomorrow
|
||||||
|
date.keyword.yesterday
|
||||||
|
date.keyword.weekdays array, mon..sun in locale (short + long)
|
||||||
|
```
|
||||||
|
|
||||||
|
## File touch list
|
||||||
|
|
||||||
|
New:
|
||||||
|
- `adiuvAI/src/renderer/lib/parseDate.ts`
|
||||||
|
- `adiuvAI/src/renderer/components/ui/date-field.tsx`
|
||||||
|
|
||||||
|
Modified:
|
||||||
|
- `adiuvAI/src/renderer/components/timeline/AddEventDialog.tsx` — full rewrite to staged-batch model.
|
||||||
|
- `adiuvAI/src/renderer/components/timeline/EditEventDialog.tsx` — swap popover+Calendar for `<DateField>`.
|
||||||
|
- `adiuvAI/src/renderer/locales/{en,it,es,fr,de}/translation.json` — add i18n keys above.
|
||||||
|
|
||||||
|
Untouched:
|
||||||
|
- Backend (`api/`): no schema, no router changes.
|
||||||
|
- tRPC contracts: re-use `timelineEvents.create`.
|
||||||
|
- DB schema: no migration.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Repo has no automated test suite (per `adiuvAI/.claude/CLAUDE.md`). Manual verification before merge:
|
||||||
|
|
||||||
|
- [ ] Open from `/timeline`: project picker visible, locks after first staged.
|
||||||
|
- [ ] Open from `ProjectDetail`: project picker hidden, preset used.
|
||||||
|
- [ ] Parse — type and verify: `today`, `tomorrow`, `+3d`, `+1w`, `mon`, `15/03`, `15/03/26`, `2026-03-15`. Repeat with `dateFormat` switched in Settings.
|
||||||
|
- [ ] Switch UI language to IT, type `oggi`, `domani`, `lun` — parse works.
|
||||||
|
- [ ] Stage 3 mixed-type events, Save → all created, history records 3 entries, toast plural correct.
|
||||||
|
- [ ] Stage 2, kill network mid-save → failed row stays with error tooltip, toast warns count.
|
||||||
|
- [ ] Pure keyboard run: open dialog, Tab to project, type+Enter, type title, Tab, type date, Enter (stage), repeat ×3, Ctrl+Enter (save). Mouse never touched.
|
||||||
|
- [ ] ↑ from title moves to last row, Enter loads to form for edit, Esc returns to form.
|
||||||
|
- [ ] Del on focused row removes it, focus advances.
|
||||||
|
- [ ] Esc with staged > 0 shows confirm; cancel keeps dialog, OK closes.
|
||||||
|
- [ ] `EditEventDialog` opens, `DateField` shows existing date formatted, edit and save works.
|
||||||
|
- [ ] Reduced-motion preference respected (no popover spring if user has it).
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
None known at design time. Resolved during brainstorming:
|
||||||
|
- Batch model: stage then commit all.
|
||||||
|
- Project scope: one project per batch, locked after first event.
|
||||||
|
- Date entry: typed input with smart parse, calendar popover as fallback.
|
||||||
|
- Range entry: two fields (start → Tab → end).
|
||||||
|
- Row edit: arrow nav, Enter edit, Del remove.
|
||||||
|
- Components: `DateField` + `parseDate` extracted as shared primitives, migrate `EditEventDialog` in this work. `TaskFormDialog` deferred (needs timezone + time-of-day support on DateField).
|
||||||
1
graphify-out/.graphify_root
Normal file
1
graphify-out/.graphify_root
Normal file
@@ -0,0 +1 @@
|
|||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace
|
||||||
250
graphify-out/.graphify_uncached.txt
Normal file
250
graphify-out/.graphify_uncached.txt
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\index.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\web.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\brand-showcase.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\README.md
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\requirements.txt
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_action.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_date.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_info.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_no_project.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\journey_v2\data\email_action.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\journey_v2\data\email_info.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_action.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_heavy.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_single.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_thread.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\fallback.txt
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\generic_page.html
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\notes.txt
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\favicon.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-black.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-full.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-icon.png
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-icon.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-mark.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-white.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-wordmark.svg
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\home.png
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\home_chat.png
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\projects.png
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\task.png
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\drizzle.config.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\forge.config.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\forge.env.d.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\scripts\seed-fake-data.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\index.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\ipc.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\store.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\agents\agent-scheduler.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\ai\orchestrator.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\api\backend-client.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\api\drizzle-executor.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\auth\auth-manager.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\auth\backup-key.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\auth\locale-defaults.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\db\index.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\db\schema.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\db\vectordb.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\router\index.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\preload\index.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\preload\trpc.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\i18n.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\index.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\router.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routeTree.gen.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\web-main.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\theme-provider.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\agents\AgentRunLog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\AIChatPanel.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\ChatInputBox.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\FloatingChat.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatChartBlock.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatEntityBlock.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatTableBlock.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatTimelineBlock.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\index.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\auth\LoginForm.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\layout\AppShell.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\notes\MilkdownEditor.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\onboarding\OnboardingFlow.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\onboarding\onboardingOptions.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\KanbanBoard.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\ProjectDetail.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\ProjectSidebar.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\ProjectTabBar.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AccountSection.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AgentRow.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AgentRunHistorySheet.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AgentsSection.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AppearanceSection.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AvatarCropDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\BillingSection.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\CloudAgentConfigPanel.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\InlineAgentCreationStepper.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\JourneyDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\LocalAgentConfigPanel.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\MemorySection.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\ProfileSection.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\PromptBuilderChat.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\SettingsCard.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\TemplateSelectCard.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\types.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\EditTaskDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\NewTaskDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\PriorityBadge.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\task-utils.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\TaskCard.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\TaskDetailDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\TaskRow.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\AddEventDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\EditEventDialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\history-types.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\ProjectTimeline.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\ProjectTimelineBox.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\TimelineAxisHeader.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\TimelineGanttView.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\alert-dialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\avatar.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\badge.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\breadcrumb.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\button.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\calendar.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\card.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\chart.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\checkbox.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\collapsible.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\context-menu.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\dialog.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\dropdown-menu.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\empty.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\field.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\gradual-blur.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\input-group.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\input.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\item.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\label.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\popover.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\scroll-area.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\select.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\separator.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\sheet.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\sidebar.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\skeleton.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\slider.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\sonner.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\switch.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\table.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\tabs.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\textarea.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\toggle-group.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\toggle.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\tooltip.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\context\ExpandedClientsContext.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\context\FloatingChatContext.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\use-mobile.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useAIChat.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useDoubleClickAI.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useNotify.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useTimelineHistory.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\date.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\httpLink.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\ipcLink.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\platform.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\trpc.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\utils.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\index.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\notes.$noteId.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\projects.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\settings.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\tasks.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\timeline.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\__root.tsx
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\shared\api-types.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\shared\batch-types.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\shared\casing.ts
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\env.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\001_initial_schema.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\003_agent_tables.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\004_add_memory_tables.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\005_associative_pgvector.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\006_memory_relations.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\1f5975a4f3f4_add_extraction_queue.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\818478c251dc_add_name_and_surname_to_users_table.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\9a1f2d0b6c7e_deprecate_backend_agent_config_tables.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\a3b9c0d1e2f3_add_agent_config_to_local_agents.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\b4c0d1e2f3a4_add_oauth_and_avatar.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\c5d1e2f3a4b5_add_onboarding_completed_at.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\e04100e88ace_avatar_url_varchar_to_text.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\db.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\main.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\models.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\schemas.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\filesystem_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\note_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\project_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\task_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\timeline_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\deps.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\auth.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\rate_limit.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\sanitizer.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\agents.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\agent_setup.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\auth.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\billing.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\chat.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\device_ws.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\memory.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\auth\oauth_providers.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\auth\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\billing\stripe_service.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\billing\tier_manager.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\billing\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\config\settings.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\config\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\agent_registry.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\agent_runner.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\agent_session_buffer.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\brief_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\deep_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\device_manager.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\embeddings.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\langfuse_client.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\llm.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\memory_extraction.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\memory_maintenance.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\memory_middleware.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\output_formatter.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\ws_context.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\preprocessors\base.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\preprocessors\email_html.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\preprocessors\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\integrations\gmail.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\integrations\ms_graph.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\integrations\__init__.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\conftest.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_agent_runner_v2.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_auth.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_brief_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_deep_agent.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_device_ws.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_integrations.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_journey_v2.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_audit.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_extraction.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_middleware.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_models.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_proactive.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_relations.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_middleware.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_output_formatter.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_preprocessors.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_schemas_v3.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_ws_unified.py
|
||||||
|
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\__init__.py
|
||||||
998
graphify-out/GRAPH_REPORT.md
Normal file
998
graphify-out/GRAPH_REPORT.md
Normal file
@@ -0,0 +1,998 @@
|
|||||||
|
# Graph Report - . (2026-05-14)
|
||||||
|
|
||||||
|
## Corpus Check
|
||||||
|
- 166 files · ~405,615 words
|
||||||
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 2908 nodes · 5718 edges · 148 communities detected
|
||||||
|
- Extraction: 55% EXTRACTED · 45% INFERRED · 0% AMBIGUOUS · INFERRED: 2551 edges (avg confidence: 0.59)
|
||||||
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
|
## Community Hubs (Navigation)
|
||||||
|
- [[_COMMUNITY_API Auth + Memory Backbone|API Auth + Memory Backbone]]
|
||||||
|
- [[_COMMUNITY_Agent Runners (deepbrieffolder)|Agent Runners (deep/brief/folder)]]
|
||||||
|
- [[_COMMUNITY_Chat + Device WebSocket|Chat + Device WebSocket]]
|
||||||
|
- [[_COMMUNITY_Email Integrations (GmailMS Graph)|Email Integrations (Gmail/MS Graph)]]
|
||||||
|
- [[_COMMUNITY_Device + Agent Runtime Tests|Device + Agent Runtime Tests]]
|
||||||
|
- [[_COMMUNITY_Filesystem + Client Agents|Filesystem + Client Agents]]
|
||||||
|
- [[_COMMUNITY_Electron Renderer Core (TS)|Electron Renderer Core (TS)]]
|
||||||
|
- [[_COMMUNITY_Alembic Migrations|Alembic Migrations]]
|
||||||
|
- [[_COMMUNITY_Electron Main + Indexer|Electron Main + Indexer]]
|
||||||
|
- [[_COMMUNITY_Billing + Quotas|Billing + Quotas]]
|
||||||
|
- [[_COMMUNITY_HomeProject Brief Agents|Home/Project Brief Agents]]
|
||||||
|
- [[_COMMUNITY_Memory Tests + Seeds|Memory Tests + Seeds]]
|
||||||
|
- [[_COMMUNITY_Agent Config + App Bootstrap|Agent Config + App Bootstrap]]
|
||||||
|
- [[_COMMUNITY_Middleware + Settings|Middleware + Settings]]
|
||||||
|
- [[_COMMUNITY_OAuth Providers|OAuth Providers]]
|
||||||
|
- [[_COMMUNITY_Architecture References|Architecture References]]
|
||||||
|
- [[_COMMUNITY_Agent Setup + Journey|Agent Setup + Journey]]
|
||||||
|
- [[_COMMUNITY_Middleware Tests|Middleware Tests]]
|
||||||
|
- [[_COMMUNITY_Project UI Mockups|Project UI Mockups]]
|
||||||
|
- [[_COMMUNITY_Client List UI|Client List UI]]
|
||||||
|
- [[_COMMUNITY_AI Chat UI Surface|AI Chat UI Surface]]
|
||||||
|
- [[_COMMUNITY_HTML Preprocessor|HTML Preprocessor]]
|
||||||
|
- [[_COMMUNITY_AI Brand UI Patterns|AI Brand UI Patterns]]
|
||||||
|
- [[_COMMUNITY_Chrome DevTools Perf Profile|Chrome DevTools Perf Profile]]
|
||||||
|
- [[_COMMUNITY_Task Context Menu|Task Context Menu]]
|
||||||
|
- [[_COMMUNITY_Renderer Components|Renderer Components]]
|
||||||
|
- [[_COMMUNITY_Timeline Event Issues|Timeline Event Issues]]
|
||||||
|
- [[_COMMUNITY_Brand Identity System|Brand Identity System]]
|
||||||
|
- [[_COMMUNITY_Datei18n Components|Date/i18n Components]]
|
||||||
|
- [[_COMMUNITY_Date Utilities|Date Utilities]]
|
||||||
|
- [[_COMMUNITY_Community 30|Community 30]]
|
||||||
|
- [[_COMMUNITY_Community 31|Community 31]]
|
||||||
|
- [[_COMMUNITY_Community 32|Community 32]]
|
||||||
|
- [[_COMMUNITY_Community 33|Community 33]]
|
||||||
|
- [[_COMMUNITY_Community 34|Community 34]]
|
||||||
|
- [[_COMMUNITY_Community 35|Community 35]]
|
||||||
|
- [[_COMMUNITY_Community 36|Community 36]]
|
||||||
|
- [[_COMMUNITY_Community 38|Community 38]]
|
||||||
|
- [[_COMMUNITY_Community 40|Community 40]]
|
||||||
|
- [[_COMMUNITY_Community 41|Community 41]]
|
||||||
|
- [[_COMMUNITY_Community 42|Community 42]]
|
||||||
|
- [[_COMMUNITY_Community 49|Community 49]]
|
||||||
|
- [[_COMMUNITY_Community 51|Community 51]]
|
||||||
|
- [[_COMMUNITY_Community 52|Community 52]]
|
||||||
|
- [[_COMMUNITY_Community 56|Community 56]]
|
||||||
|
- [[_COMMUNITY_Community 57|Community 57]]
|
||||||
|
- [[_COMMUNITY_Community 58|Community 58]]
|
||||||
|
- [[_COMMUNITY_Community 59|Community 59]]
|
||||||
|
- [[_COMMUNITY_Community 65|Community 65]]
|
||||||
|
- [[_COMMUNITY_Community 77|Community 77]]
|
||||||
|
- [[_COMMUNITY_Community 78|Community 78]]
|
||||||
|
- [[_COMMUNITY_Community 79|Community 79]]
|
||||||
|
- [[_COMMUNITY_Community 80|Community 80]]
|
||||||
|
- [[_COMMUNITY_Community 81|Community 81]]
|
||||||
|
- [[_COMMUNITY_Community 82|Community 82]]
|
||||||
|
- [[_COMMUNITY_Community 83|Community 83]]
|
||||||
|
- [[_COMMUNITY_Community 84|Community 84]]
|
||||||
|
- [[_COMMUNITY_Community 85|Community 85]]
|
||||||
|
- [[_COMMUNITY_Community 86|Community 86]]
|
||||||
|
- [[_COMMUNITY_Community 87|Community 87]]
|
||||||
|
- [[_COMMUNITY_Community 88|Community 88]]
|
||||||
|
- [[_COMMUNITY_Community 89|Community 89]]
|
||||||
|
- [[_COMMUNITY_Community 90|Community 90]]
|
||||||
|
- [[_COMMUNITY_Community 91|Community 91]]
|
||||||
|
- [[_COMMUNITY_Community 92|Community 92]]
|
||||||
|
- [[_COMMUNITY_Community 93|Community 93]]
|
||||||
|
- [[_COMMUNITY_Community 118|Community 118]]
|
||||||
|
- [[_COMMUNITY_Community 119|Community 119]]
|
||||||
|
- [[_COMMUNITY_Community 120|Community 120]]
|
||||||
|
- [[_COMMUNITY_Community 121|Community 121]]
|
||||||
|
- [[_COMMUNITY_Community 122|Community 122]]
|
||||||
|
- [[_COMMUNITY_Community 123|Community 123]]
|
||||||
|
- [[_COMMUNITY_Community 169|Community 169]]
|
||||||
|
- [[_COMMUNITY_Community 170|Community 170]]
|
||||||
|
- [[_COMMUNITY_Community 171|Community 171]]
|
||||||
|
- [[_COMMUNITY_Community 173|Community 173]]
|
||||||
|
- [[_COMMUNITY_Community 174|Community 174]]
|
||||||
|
- [[_COMMUNITY_Community 175|Community 175]]
|
||||||
|
- [[_COMMUNITY_Community 176|Community 176]]
|
||||||
|
- [[_COMMUNITY_Community 177|Community 177]]
|
||||||
|
- [[_COMMUNITY_Community 178|Community 178]]
|
||||||
|
- [[_COMMUNITY_Community 179|Community 179]]
|
||||||
|
- [[_COMMUNITY_Community 180|Community 180]]
|
||||||
|
- [[_COMMUNITY_Community 181|Community 181]]
|
||||||
|
- [[_COMMUNITY_Community 182|Community 182]]
|
||||||
|
- [[_COMMUNITY_Community 183|Community 183]]
|
||||||
|
- [[_COMMUNITY_Community 184|Community 184]]
|
||||||
|
- [[_COMMUNITY_Community 231|Community 231]]
|
||||||
|
- [[_COMMUNITY_Community 232|Community 232]]
|
||||||
|
- [[_COMMUNITY_Community 236|Community 236]]
|
||||||
|
- [[_COMMUNITY_Community 237|Community 237]]
|
||||||
|
- [[_COMMUNITY_Community 238|Community 238]]
|
||||||
|
- [[_COMMUNITY_Community 239|Community 239]]
|
||||||
|
- [[_COMMUNITY_Community 240|Community 240]]
|
||||||
|
- [[_COMMUNITY_Community 241|Community 241]]
|
||||||
|
- [[_COMMUNITY_Community 242|Community 242]]
|
||||||
|
- [[_COMMUNITY_Community 243|Community 243]]
|
||||||
|
- [[_COMMUNITY_Community 244|Community 244]]
|
||||||
|
- [[_COMMUNITY_Community 245|Community 245]]
|
||||||
|
- [[_COMMUNITY_Community 246|Community 246]]
|
||||||
|
- [[_COMMUNITY_Community 247|Community 247]]
|
||||||
|
- [[_COMMUNITY_Community 248|Community 248]]
|
||||||
|
- [[_COMMUNITY_Community 249|Community 249]]
|
||||||
|
- [[_COMMUNITY_Community 250|Community 250]]
|
||||||
|
- [[_COMMUNITY_Community 251|Community 251]]
|
||||||
|
- [[_COMMUNITY_Community 252|Community 252]]
|
||||||
|
- [[_COMMUNITY_Community 253|Community 253]]
|
||||||
|
- [[_COMMUNITY_Community 254|Community 254]]
|
||||||
|
- [[_COMMUNITY_Community 255|Community 255]]
|
||||||
|
- [[_COMMUNITY_Community 256|Community 256]]
|
||||||
|
- [[_COMMUNITY_Community 257|Community 257]]
|
||||||
|
- [[_COMMUNITY_Community 258|Community 258]]
|
||||||
|
- [[_COMMUNITY_Community 259|Community 259]]
|
||||||
|
- [[_COMMUNITY_Community 260|Community 260]]
|
||||||
|
- [[_COMMUNITY_Community 261|Community 261]]
|
||||||
|
- [[_COMMUNITY_Community 262|Community 262]]
|
||||||
|
- [[_COMMUNITY_Community 263|Community 263]]
|
||||||
|
- [[_COMMUNITY_Community 264|Community 264]]
|
||||||
|
- [[_COMMUNITY_Community 265|Community 265]]
|
||||||
|
- [[_COMMUNITY_Community 266|Community 266]]
|
||||||
|
- [[_COMMUNITY_Community 267|Community 267]]
|
||||||
|
- [[_COMMUNITY_Community 268|Community 268]]
|
||||||
|
- [[_COMMUNITY_Community 269|Community 269]]
|
||||||
|
- [[_COMMUNITY_Community 270|Community 270]]
|
||||||
|
- [[_COMMUNITY_Community 271|Community 271]]
|
||||||
|
- [[_COMMUNITY_Community 272|Community 272]]
|
||||||
|
- [[_COMMUNITY_Community 273|Community 273]]
|
||||||
|
- [[_COMMUNITY_Community 274|Community 274]]
|
||||||
|
- [[_COMMUNITY_Community 275|Community 275]]
|
||||||
|
- [[_COMMUNITY_Community 276|Community 276]]
|
||||||
|
- [[_COMMUNITY_Community 277|Community 277]]
|
||||||
|
- [[_COMMUNITY_Community 278|Community 278]]
|
||||||
|
- [[_COMMUNITY_Community 279|Community 279]]
|
||||||
|
- [[_COMMUNITY_Community 280|Community 280]]
|
||||||
|
- [[_COMMUNITY_Community 281|Community 281]]
|
||||||
|
- [[_COMMUNITY_Community 282|Community 282]]
|
||||||
|
- [[_COMMUNITY_Community 283|Community 283]]
|
||||||
|
- [[_COMMUNITY_Community 284|Community 284]]
|
||||||
|
- [[_COMMUNITY_Community 285|Community 285]]
|
||||||
|
- [[_COMMUNITY_Community 286|Community 286]]
|
||||||
|
- [[_COMMUNITY_Community 287|Community 287]]
|
||||||
|
- [[_COMMUNITY_Community 288|Community 288]]
|
||||||
|
- [[_COMMUNITY_Community 289|Community 289]]
|
||||||
|
- [[_COMMUNITY_Community 290|Community 290]]
|
||||||
|
- [[_COMMUNITY_Community 291|Community 291]]
|
||||||
|
- [[_COMMUNITY_Community 292|Community 292]]
|
||||||
|
- [[_COMMUNITY_Community 293|Community 293]]
|
||||||
|
- [[_COMMUNITY_Community 294|Community 294]]
|
||||||
|
|
||||||
|
## God Nodes (most connected - your core abstractions)
|
||||||
|
1. `MemoryMiddleware` - 236 edges
|
||||||
|
2. `User` - 127 edges
|
||||||
|
3. `MemoryProactive` - 107 edges
|
||||||
|
4. `Subscription` - 98 edges
|
||||||
|
5. `MemoryAssociative` - 98 edges
|
||||||
|
6. `MemoryEpisodic` - 93 edges
|
||||||
|
7. `AgentRunLog` - 91 edges
|
||||||
|
8. `MemoryCore` - 90 edges
|
||||||
|
9. `UserProfile` - 79 edges
|
||||||
|
10. `MemoryRelation` - 65 edges
|
||||||
|
|
||||||
|
## Surprising Connections (you probably didn't know these)
|
||||||
|
- `recordRunAction()` --calls--> `getDb()` [INFERRED]
|
||||||
|
adiuvAI\src\main\api\backend-client.ts → adiuvAI\src\main\db\index.ts
|
||||||
|
- `Tier manager: feature matrix and quota enforcement. ``TierManager`` is the si` --uses--> `Subscription` [INFERRED]
|
||||||
|
api\app\billing\tier_manager.py → api\app\models.py
|
||||||
|
- `Centralises tier feature-gating, rate-limit lookups, and quota checks.` --uses--> `Subscription` [INFERRED]
|
||||||
|
api\app\billing\tier_manager.py → api\app\models.py
|
||||||
|
- `Return the current billing tier for ``user_id`` from the DB. Falls ba` --uses--> `Subscription` [INFERRED]
|
||||||
|
api\app\billing\tier_manager.py → api\app\models.py
|
||||||
|
- `Return ``True`` if ``tier`` has ``feature`` enabled. For numeric feat` --uses--> `Subscription` [INFERRED]
|
||||||
|
api\app\billing\tier_manager.py → api\app\models.py
|
||||||
|
|
||||||
|
## Hyperedges (group relationships)
|
||||||
|
- **Task Form Dialog keyboard polish** — kbddesign_rovingfocus_hook, kbddesign_listboxkeys_hook, kbddesign_datefield_withtime, kbddesign_propertypill_button [EXTRACTED 1.00]
|
||||||
|
- **Memory V2 in-house pipeline** — memv2_fact_extraction, memv2_memory_fact_table, memv2_user_profile_table, memv2_forgetting_decay, memv2_episode_summarization [EXTRACTED 1.00]
|
||||||
|
- **Production LLM Agent Stack (ZDR)** — llmreport_home_agent, llmreport_floating_agent, llmreport_brief_agent, llmreport_unified_processor, llmreport_memory_extractor, llmreport_openai_zdr, llmreport_anthropic_zdr [EXTRACTED 1.00]
|
||||||
|
- **Memory Evolution Pipeline (extraction + storage + decision)** — memory_extraction_module, memory_middleware, memory_associative_table, memory_relations_table [EXTRACTED 0.90]
|
||||||
|
- **Folder Indexing Pipeline (scan, WS, summarize, store)** — scanner_module, indexer_module, device_ws, folder_indexer [EXTRACTED 0.90]
|
||||||
|
- **Onboarding Storage Split (encrypted core vs local prefs)** — onboarding_flow_component, memory_core_table, electron_store [EXTRACTED 0.85]
|
||||||
|
|
||||||
|
## Communities
|
||||||
|
|
||||||
|
### Community 0 - "API Auth + Memory Backbone"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (259): Base, Shared declarative base for all ORM models., ExtractionQueue, MemoryAssociative, MemoryCore, MemoryEpisodic, MemoryProactive, MemoryRelation (+251 more)
|
||||||
|
|
||||||
|
### Community 1 - "Agent Runners (deep/brief/folder)"
|
||||||
|
Cohesion: 0.02
|
||||||
|
Nodes (183): make_query_relations_tool(), Relations agent — read-only tool wrapping MemoryMiddleware.query_relations., Return a query_relations tool bound to *user_id*., _as_text(), _build_processing_tools(), _fetch_projects(), _finalize_run(), _make_agent_executor() (+175 more)
|
||||||
|
|
||||||
|
### Community 2 - "Chat + Device WebSocket"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (168): AgentRunLog, AgentCatalogItem, AgentCreationCheckRequest, AgentCreationCheckResponse, AgentRunLogResponse, AgentTriggerRequest, ChatContext, ChatRequest (+160 more)
|
||||||
|
|
||||||
|
### Community 3 - "Email Integrations (Gmail/MS Graph)"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (84): _build_gmail_query(), GmailClient, _parse_body(), _parse_date(), Gmail API client for cloud agent integration. Wraps the Google Gmail REST API, Remove HTML tags and decode entities to get plain text., Recursively extract the plain-text body from a Gmail message payload. Pre, Parse an RFC 2822 email date header into a UTC ``datetime``. (+76 more)
|
||||||
|
|
||||||
|
### Community 4 - "Device + Agent Runtime Tests"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (102): CloudAgentConfig, LocalAgentConfig, _format_entities_for_context(), _format_metadata(), _format_projects(), _get_extraction_rules(), _get_no_match_behavior(), _is_overdue() (+94 more)
|
||||||
|
|
||||||
|
### Community 5 - "Filesystem + Client Agents"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (97): get_client(), list_clients(), Client agent — read-only tools for the clients table., List clients, optionally filtered by a name/email substring search. search:, Get full details for one client by UUID. id: the client's UUID., get_file_metadata(), list_directory(), Filesystem agent — tools for reading local directories and files on Electron. (+89 more)
|
||||||
|
|
||||||
|
### Community 6 - "Electron Renderer Core (TS)"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (19): AuthExpiredError, BackendClient, logHttp(), logHttpResponse(), logWsRecv(), logWsSend(), OfflineError, QuotaError (+11 more)
|
||||||
|
|
||||||
|
### Community 7 - "Alembic Migrations"
|
||||||
|
Cohesion: 0.04
|
||||||
|
Nodes (69): _get_url(), Alembic migration environment — async-compatible. At runtime the app uses ``p, Convert an asyncpg URL to a psycopg2 URL for Alembic CLI., Emit SQL without a live DB connection., Run migrations against a live DB using the async engine., run_migrations_offline(), run_migrations_online(), run_migrations_online_async() (+61 more)
|
||||||
|
|
||||||
|
### Community 8 - "Electron Main + Indexer"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (42): startAgentScheduler(), tickAgentScheduler(), checkConnectivity(), dailyBrief(), generateAndCacheBrief(), getBriefTimeSlot(), getCachedBrief(), getCurrentSlotKey() (+34 more)
|
||||||
|
|
||||||
|
### Community 9 - "Billing + Quotas"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (56): MonthlyTokenUsage, add_token_usage(), check_folder_quota(), _current_year_month(), QuotaExceeded, Quota checks and atomic token-usage accounting for folder integration., Raised when a folder operation cannot proceed under the user's tier., Raise QuotaExceeded if folder_max_files or folder_monthly_tokens would be vi (+48 more)
|
||||||
|
|
||||||
|
### Community 10 - "Home/Project Brief Agents"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (71): home_brief Langfuse prompt, ProjectBriefCard renderer, project_brief Langfuse prompt, Read-only tool subset, run_home_brief() function, run_project_brief() function, WS brief_request frame, Anthropic Zero Retention Addendum (+63 more)
|
||||||
|
|
||||||
|
### Community 11 - "Memory Tests + Seeds"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (58): _uuid(), _normalize_domain_payload(), _proactive_hints_injection(), Return a system-prompt paragraph listing proactive behavioral hints. Retu, embed_text(), OpenAI embedding helper for associative memory tier. Single public function:, Call OpenAI text-embedding-3-small. Return None on failure (caller falls back to, Exception (+50 more)
|
||||||
|
|
||||||
|
### Community 12 - "Agent Config + App Bootstrap"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (43): lifespan(), _memory_audit_cron_tick(), _memory_cron_tick(), Weekly cron: contradiction scan + label canonicalization for all users (Phase 7), Hourly cron: drain Free-tier extraction queue + mine proactive patterns for Powe, In-process TTL buffer for per-session LangChain message history. Stores the ful, _SessionBuffer, audit_memory() (+35 more)
|
||||||
|
|
||||||
|
### Community 13 - "Middleware + Settings"
|
||||||
|
Cohesion: 0.04
|
||||||
|
Nodes (29): Settings, get_session(), Database engine, session factory, and base model. All app code uses the async, FastAPI dependency that yields an async DB session per request., _get_client_ip(), RateLimiter, IP-based sliding-window rate limiter. Cloudflare-aware: uses CF-Connecting-IP, Extract real client IP behind Cloudflare / reverse proxy. (+21 more)
|
||||||
|
|
||||||
|
### Community 14 - "OAuth Providers"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (25): generate_pkce_pair(), OAuthUserInfo, OAuth 2.0 + PKCE provider abstractions. Each provider implements a three-step, Fetch the authenticated user's identity from Google., Normalized user identity returned by any provider., Generate a (code_verifier, code_challenge) pair for PKCE S256. The code_v, Tests for auth routes: register, login, refresh, me, OAuth social login. Exer, POST /api/v1/auth/refresh (+17 more)
|
||||||
|
|
||||||
|
### Community 15 - "Architecture References"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (49): AgentRunLog, Agent Runner UML Sequence Diagram, AgentScheduler, AIChatPanel (visual reference), AppShell Component, AuthManager, Caveman Mode (token compression), deep_agent.py (+41 more)
|
||||||
|
|
||||||
|
### Community 16 - "Agent Setup + Journey"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (44): make_directory_tools(), Return filesystem tools that resolve relative paths against *base_directory*., AgentConfig, Structured agent configuration (replaces freeform prompt_template)., _as_text(), _build_system_prompt(), _call_llm_with_tools(), _extract_agent_config() (+36 more)
|
||||||
|
|
||||||
|
### Community 17 - "Middleware Tests"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (15): _auth_header(), _make_jwt(), _override_db(), Tests for Step 9 middleware: auth, rate limiting, and sanitizer. Auth tests:, Each test uses a fresh unique user_id so windows never collide., POST /auth/register is exempt — 25 calls should never return 429., POST /auth/login is exempt — multiple failed attempts are not rate-limited., Mock ``run_home`` to inject controlled strings into chat responses. (+7 more)
|
||||||
|
|
||||||
|
### Community 18 - "Project UI Mockups"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (41): App Logo (Top Left), Assignee Label, Client Label Tag, Client: Umbrella Labs, Client: Wayne Enterprises, Completed Count Card (8), Design Pattern: Filter Tab Bar, Design Pattern: Inline Metadata Chips (+33 more)
|
||||||
|
|
||||||
|
### Community 19 - "Client List UI"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (37): AI Project Summary Card, Add Button (Timeline / Tasks / Notes), Client: Acme Corp, Client: Globex Inc, Client: Initech Solutions, Client List in Sidebar, Client: Umbrella Labs, Client: Wayne Enterprises (+29 more)
|
||||||
|
|
||||||
|
### Community 20 - "AI Chat UI Surface"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (32): AI Agent Avatar (Sparkles + adiuvAI Label), AI Follow-Up Suggestion Text (setting a due date), AI Response Block (adiuvAI Agent Reply), adiuvAI Brand Name (Wordmark in Chat), App Logo (Golden Diamond Icon), Chat Conversation Area (Scrollable Message List), Chat Input Bar (Ask me anything...), Chat Send Button (Arrow Up, Amber) (+24 more)
|
||||||
|
|
||||||
|
### Community 21 - "HTML Preprocessor"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (24): PreprocessResult, Base types for the preprocessor system., Output of a preprocessor handler. Attributes ---------- content, _extract_metadata(), preprocess_email_html(), Preprocessor for email HTML files. Handles: - HTML stripping via BeautifulSo, Extract Subject/From/To/Date from raw HTML or plain text., Return only the latest message in a threaded email. (+16 more)
|
||||||
|
|
||||||
|
### Community 22 - "AI Brand UI Patterns"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (27): Design Pattern: AI as Quiet Partner, App Logo (Golden Diamond Icon), Brand Personality: Calm, Intelligent, Warm, Ask Me Anything Chat Input, Suggestion Chip: Any overdue tasks?, Suggestion Chip: Suggest next actions, Suggestion Chip: Summarize this week, Suggestion Chip: What's on my plate today? (+19 more)
|
||||||
|
|
||||||
|
### Community 23 - "Chrome DevTools Perf Profile"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (27): Animations Track (purple bars), CPU Track (high utilization), createTask, Chrome DevTools Performance Recording, Evaluate Script Task, Frames Track, Function call frames (deep stacks), GPU Track (+19 more)
|
||||||
|
|
||||||
|
### Community 24 - "Task Context Menu"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (26): Color Submenu, Copy Link Action, Delete Action, Duplicate Action, Edit Action, Mark as Done Action, Progress Submenu (10%-100%), Task Assignee Avatars (+18 more)
|
||||||
|
|
||||||
|
### Community 25 - "Renderer Components"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (15): getTimeGreeting(), relativeDate(), t(), ProjectTabBar(), attemptClose(), formValid(), handleClose(), loadRowIntoForm() (+7 more)
|
||||||
|
|
||||||
|
### Community 26 - "Timeline Event Issues"
|
||||||
|
Cohesion: 0.1
|
||||||
|
Nodes (21): Issue: Dual Date Axes (year-month + short month), Event: Alpha Release (checkpoint), Event: Beta Testing (activity bar), Event: Design Phase Complete, Event: Post-Launch Review, Event: Production Launch, Event: Project Kickoff (milestone, checked), Event Type: activity (rounded bar) (+13 more)
|
||||||
|
|
||||||
|
### Community 27 - "Brand Identity System"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (20): Brand Color: Canvas Dark (#0c0c0c) — dark mode background, Brand Color: Canvas Light (#f4edf3) — light mode background, Brand Color: Golden (#fbc881) — AI/Nord accent, Brand Color: Ink (#040404) — user/Sud/text, Brand Color: Slate (#8a8ea9) — secondary/muted, Compass Settle Animation (5s ease-in-out infinite), adiuvAI Brand Identity System, adiuvAI Color Palette (+12 more)
|
||||||
|
|
||||||
|
### Community 28 - "Date/i18n Components"
|
||||||
|
Cohesion: 0.14
|
||||||
|
Nodes (18): AddEventDialog Component, DateField UI Component, EditEventDialog Component, i18n Translation JSON Files (en/it/es/fr/de), parseDate Utility, Sonner Toast Notifications Plan, Timeline Batch Add Plan, Sonner Notifications Ralph Loop Prompt (+10 more)
|
||||||
|
|
||||||
|
### Community 29 - "Date Utilities"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (10): detectBrowserFormatPrefs(), formatDate(), formatDateTime(), formatDueDate(), formatTime(), formatTs(), inferDateFormat(), useFormatPrefs() (+2 more)
|
||||||
|
|
||||||
|
### Community 30 - "Community 30"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (17): AI Email Drafting Workflow (Focus Tasks Feature), Apply & Continue Button (Top Right), Checklist: Check contract dates (Section A.2), Checklist: Offer 10% discount-based loyalty credit, Checklist: Tone professional & sincere, Command / Prompt Input Footer, Draft Client Email - Follow-up Task, Focus Tasks Email Draft View (+9 more)
|
||||||
|
|
||||||
|
### Community 31 - "Community 31"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (16): AdiuvAI Brand, Pinkish-Lavender Rounded Square Background, Brand Value: Calm, Intelligent, Warm, Brand Value: Precision and Clarity, Color: Golden Amber (#F5C07A), Color: Pinkish-Lavender Background (#F0EBF4), Color: Near-Black (#1A1A1A), Design Style: Flat Minimal Geometric (+8 more)
|
||||||
|
|
||||||
|
### Community 32 - "Community 32"
|
||||||
|
Cohesion: 0.19
|
||||||
|
Nodes (9): handler(), clampPosition(), computeAnchorPosition(), computeDualAnchor(), FloatingChatProvider(), getChatWidth(), useFloatingChat(), useDoubleClickAI() (+1 more)
|
||||||
|
|
||||||
|
### Community 33 - "Community 33"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (14): BeautifulSoup4 + lxml (HTML parsing), Google Auth Libraries (OAuth), Agent Runner V2 — agent execution test harness, Email Type: Action — requires task creation, Email Parsing Pattern — extract headers, type, project linkage, Journey V2 — user journey / end-to-end flow test harness, Content Preprocessor Pipeline — HTML/text normalization before agent processing, Test Fixture: Action Email (agent_runner_v2) — login bug fix request (+6 more)
|
||||||
|
|
||||||
|
### Community 34 - "Community 34"
|
||||||
|
Cohesion: 0.19
|
||||||
|
Nodes (14): Add Note Button, Masonry Card Grid Layout, Inline Checkbox List Items, Desert Road Trip Ideas Note, Home Renovation Tasks Note, Image Attachment in Note Card, Light Theme Design, Mountain Sunset Photography Note (+6 more)
|
||||||
|
|
||||||
|
### Community 35 - "Community 35"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (6): useNotify(), CloudAgentConfigPanel(), LocalAgentConfigPanel(), EditTaskDialog(), NewTaskDialog(), useTaskAttachments()
|
||||||
|
|
||||||
|
### Community 36 - "Community 36"
|
||||||
|
Cohesion: 0.32
|
||||||
|
Nodes (9): handleScanError(), addDays(), addMonths(), parseDate(), parseDateRange(), parseKeyword(), parseNumeric(), pivotYear() (+1 more)
|
||||||
|
|
||||||
|
### Community 38 - "Community 38"
|
||||||
|
Cohesion: 0.23
|
||||||
|
Nodes (12): adiuvAI Greeting Message (Italian), Artifact-Based Text Authoring Pattern, Chat Input Box ('Chiedimi qualsiasi cosa...'), Italian Email Template with Placeholders, Italian UI Localization, Left Sidebar Navigation (Home/Tasks/Projects icons), Bottom Pagination Carousel (artifact versions), Vertical Resize Divider Handle (+4 more)
|
||||||
|
|
||||||
|
### Community 40 - "Community 40"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (4): ABC, BaseAgent, Minimal agent base types retained for compatibility with batch runners., Common base for non-chat agents still using the old base contract.
|
||||||
|
|
||||||
|
### Community 41 - "Community 41"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (8): users.avatar_url column, adiuvai:// deep-link protocol, GoogleOAuthProvider (httpx + PKCE), oauth_accounts table, Backend web-callback bouncer, _pending_states Redis requirement, google-api-python-client, httpx
|
||||||
|
|
||||||
|
### Community 42 - "Community 42"
|
||||||
|
Cohesion: 0.52
|
||||||
|
Nodes (6): absolutePath(), attachmentsRoot(), copyIntoTask(), deleteStored(), deleteTaskDir(), sanitizeFilename()
|
||||||
|
|
||||||
|
### Community 49 - "Community 49"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (2): SidebarMenuButton(), useSidebar()
|
||||||
|
|
||||||
|
### Community 51 - "Community 51"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (7): Project detail page tasks tab, Task Attachments Subsystem, TaskDetailSheet (right-side), TaskListView Component, TaskPager (Pagination), TaskTable (shadcn Table), Replace KanbanBoard with TaskListView
|
||||||
|
|
||||||
|
### Community 52 - "Community 52"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (7): AddEventDialog header style, useListboxKeys Hook, PropertyPill as button forwardRef, useRovingFocus Hook, tasks.newTaskDescription/editTaskDescription keys, Task estimate column, TaskFormDialog (quick capture)
|
||||||
|
|
||||||
|
### Community 56 - "Community 56"
|
||||||
|
Cohesion: 0.4
|
||||||
|
Nodes (6): Compass needle logo (gold/dark), Geist typeface, Option A The Dark Executive, Option B The Warm Canvas, GSAP ScrollTrigger Scrollytelling, 7-chapter waitlist landing page
|
||||||
|
|
||||||
|
### Community 57 - "Community 57"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): Alembic Migrations, Cloudflare WAF + DDoS, FastAPI Framework, SQLAlchemy 2.0 async, adiuvAI Waitlist Service README, Waitlist Service Requirements
|
||||||
|
|
||||||
|
### Community 58 - "Community 58"
|
||||||
|
Cohesion: 0.6
|
||||||
|
Nodes (3): isBriefRelevantTask(), isBriefRelevantTimeline(), isInCurrentWeek()
|
||||||
|
|
||||||
|
### Community 59 - "Community 59"
|
||||||
|
Cohesion: 0.4
|
||||||
|
Nodes (2): useTheme(), Toaster()
|
||||||
|
|
||||||
|
### Community 65 - "Community 65"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): detectFormatPrefs(), inferDateFormat()
|
||||||
|
|
||||||
|
### Community 77 - "Community 77"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Initial schema: users, refresh_tokens, subscriptions. Revision ID: 001 Revis
|
||||||
|
|
||||||
|
### Community 78 - "Community 78"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Add agent config and run log tables: local_agent_configs, cloud_agent_configs, a
|
||||||
|
|
||||||
|
### Community 79 - "Community 79"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Add memory tables and user encryption_key column. Memory tables: memory_co
|
||||||
|
|
||||||
|
### Community 80 - "Community 80"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Phase 1 — confirm pgvector activation on memory_associative. Migration 004 cr
|
||||||
|
|
||||||
|
### Community 81 - "Community 81"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Add memory_relations table (Phase 3 — relational tier). Revision ID: 006 Rev
|
||||||
|
|
||||||
|
### Community 82 - "Community 82"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): add extraction_queue Revision ID: 1f5975a4f3f4 Revises: 005 Create Date: 20
|
||||||
|
|
||||||
|
### Community 83 - "Community 83"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): add name and surname to users table Revision ID: 818478c251dc Revises: 004
|
||||||
|
|
||||||
|
### Community 84 - "Community 84"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Add oauth_accounts table, nullable password_hash, avatar_url to users. Revisi
|
||||||
|
|
||||||
|
### Community 85 - "Community 85"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Add onboarding_completed_at column to users table. Revision ID: c5d1e2f3a4b5
|
||||||
|
|
||||||
|
### Community 86 - "Community 86"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): Add token tracking columns for folder integration. Revision ID: d6e3f4a5b6c7 Re
|
||||||
|
|
||||||
|
### Community 87 - "Community 87"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): avatar_url_varchar_to_text Revision ID: e04100e88ace Revises: c5d1e2f3a4b5
|
||||||
|
|
||||||
|
### Community 88 - "Community 88"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): create waitlist_entries table Revision ID: 001 Revises: Create Date: 2026-0
|
||||||
|
|
||||||
|
### Community 89 - "Community 89"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): add consent_given_at and anonymized_at columns Revision ID: 002 Revises: 001
|
||||||
|
|
||||||
|
### Community 90 - "Community 90"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): add language column to waitlist_entries Revision ID: 003 Revises: 002 Creat
|
||||||
|
|
||||||
|
### Community 91 - "Community 91"
|
||||||
|
Cohesion: 0.83
|
||||||
|
Nodes (3): detectLang(), initI18n(), setLanguage()
|
||||||
|
|
||||||
|
### Community 92 - "Community 92"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (4): LangChain (>=0.3), langchain-litellm, langchain-openai, LiteLLM (>=1.50)
|
||||||
|
|
||||||
|
### Community 93 - "Community 93"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (4): Fly.io alternative, Hetzner VPS (EU primary + US replica), PostgreSQL streaming replication, WireGuard tunnel between nodes
|
||||||
|
|
||||||
|
### Community 118 - "Community 118"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): LangChain + LangChain-OpenAI Dependencies, Langfuse Observability Dependency, LiteLLM Dependency (100+ LLM providers)
|
||||||
|
|
||||||
|
### Community 119 - "Community 119"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (3): Email Type: Info — FYI only, no action required, Test Fixture: Info Email (agent_runner_v2) — FYI policy, no action needed, Test Fixture: Info Email (journey_v2) — remote work policy FYI
|
||||||
|
|
||||||
|
### Community 120 - "Community 120"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): graphify-out Directory, Graphify Workflow Rules, adiuvAI Monorepo Root Instructions
|
||||||
|
|
||||||
|
### Community 121 - "Community 121"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): FastAPI (>=0.115), Gunicorn, Uvicorn (>=0.34)
|
||||||
|
|
||||||
|
### Community 122 - "Community 122"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Competitor: Granola, Competitor: Motion, Competitor: Superhuman
|
||||||
|
|
||||||
|
### Community 123 - "Community 123"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): FormatPrefs (electron-store, device-local), format-row.ts FE timestamp formatter, locale-defaults.ts (OS detection)
|
||||||
|
|
||||||
|
### Community 169 - "Community 169"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Expose tool modules used by deep orchestrator-worker graphs.
|
||||||
|
|
||||||
|
### Community 170 - "Community 170"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Shared FastAPI dependencies. ``get_current_user`` and ``oauth2_scheme`` live
|
||||||
|
|
||||||
|
### Community 171 - "Community 171"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): OAuth provider abstractions and utilities.
|
||||||
|
|
||||||
|
### Community 173 - "Community 173"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): API Dev Server Command (uvicorn), FastAPI Framework Dependency
|
||||||
|
|
||||||
|
### Community 174 - "Community 174"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Pinecone + Qdrant Vector Store Dependencies, SQLAlchemy + asyncpg + Alembic (DB stack)
|
||||||
|
|
||||||
|
### Community 175 - "Community 175"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Email Type: No-Project — irrelevant to any project, Test Fixture: No-Project Email (agent_runner_v2) — newsletter unrelated to project
|
||||||
|
|
||||||
|
### Community 176 - "Community 176"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Email Type: Date — contains scheduled event/date, Test Fixture: Date Email (agent_runner_v2) — kickoff meeting with date
|
||||||
|
|
||||||
|
### Community 177 - "Community 177"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Email Type: Thread — nested reply chain (blockquote structure), Test Fixture: Email Thread (preprocessors) — nested blockquote multi-turn thread
|
||||||
|
|
||||||
|
### Community 178 - "Community 178"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Email Type: Heavy HTML — complex table-based layout email, Test Fixture: Heavy HTML Email (preprocessors) — complex table layout newsletter
|
||||||
|
|
||||||
|
### Community 179 - "Community 179"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Alembic, psycopg2-binary
|
||||||
|
|
||||||
|
### Community 180 - "Community 180"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Device-specific backup key, cryptography (Fernet)
|
||||||
|
|
||||||
|
### Community 181 - "Community 181"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): DateField withTime + flat props, parseDate HH:MM suffix support
|
||||||
|
|
||||||
|
### Community 182 - "Community 182"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): E2E fixture-driven YAML tests, pytest
|
||||||
|
|
||||||
|
### Community 183 - "Community 183"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): Cloudflare Argo Smart Routing, Cloudflare Load Balancing geo steering
|
||||||
|
|
||||||
|
### Community 184 - "Community 184"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): memory_maintenance.py, Proactive Pattern Mining
|
||||||
|
|
||||||
|
### Community 231 - "Community 231"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return updated credential dict if the access token was refreshed. If
|
||||||
|
|
||||||
|
### Community 232 - "Community 232"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return updated credential dict if the access token was refreshed. Ret
|
||||||
|
|
||||||
|
### Community 236 - "Community 236"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: requests a CRUD/vector operation on the local DB.
|
||||||
|
|
||||||
|
### Community 237 - "Community 237"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Client → Server: result of a CRUD/vector operation.
|
||||||
|
|
||||||
|
### Community 238 - "Community 238"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: incremental LLM response text.
|
||||||
|
|
||||||
|
### Community 239 - "Community 239"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Client → Server: device identification on WS connect.
|
||||||
|
|
||||||
|
### Community 240 - "Community 240"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): User display preferences sent by Electron on each request.
|
||||||
|
|
||||||
|
### Community 241 - "Community 241"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Scope for a floating request — narrows the agent to a specific entity.
|
||||||
|
|
||||||
|
### Community 242 - "Community 242"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Client → Server: Floating chat message scoped to an entity.
|
||||||
|
|
||||||
|
### Community 243 - "Community 243"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Client → Server: Request a plain-text brief (home or project).
|
||||||
|
|
||||||
|
### Community 244 - "Community 244"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: signals start of a streaming response.
|
||||||
|
|
||||||
|
### Community 245 - "Community 245"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: signals end of a streaming response.
|
||||||
|
|
||||||
|
### Community 246 - "Community 246"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Structured floating domain payload for UI routing decisions.
|
||||||
|
|
||||||
|
### Community 247 - "Community 247"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: domain determined for a floating request.
|
||||||
|
|
||||||
|
### Community 248 - "Community 248"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Per-type extraction config produced by the journey chatbot.
|
||||||
|
|
||||||
|
### Community 249 - "Community 249"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Structured agent configuration (replaces freeform prompt_template).
|
||||||
|
|
||||||
|
### Community 250 - "Community 250"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Stream a plain-text daily home brief. Yields (event_type, data) tuples id
|
||||||
|
|
||||||
|
### Community 251 - "Community 251"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Stream a plain-text project status brief for project_id. Yields (event_ty
|
||||||
|
|
||||||
|
### Community 252 - "Community 252"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return the resolved model string for *agent_name* (for Langfuse tracking).
|
||||||
|
|
||||||
|
### Community 253 - "Community 253"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return an LLM configured for *agent_name*, respecting per-agent overrides.
|
||||||
|
|
||||||
|
### Community 254 - "Community 254"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return an embedding vector for *text*. Uses ``settings.LLM_EMBED_MODEL``
|
||||||
|
|
||||||
|
### Community 255 - "Community 255"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: requests a CRUD/vector operation on the local DB.
|
||||||
|
|
||||||
|
### Community 256 - "Community 256"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Client → Server: result of a CRUD/vector operation.
|
||||||
|
|
||||||
|
### Community 257 - "Community 257"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: incremental LLM response text.
|
||||||
|
|
||||||
|
### Community 258 - "Community 258"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: signals end of response with the complete text.
|
||||||
|
|
||||||
|
### Community 259 - "Community 259"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): User display preferences sent by Electron on each request.
|
||||||
|
|
||||||
|
### Community 260 - "Community 260"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Scope for a floating request — narrows the agent to a specific entity.
|
||||||
|
|
||||||
|
### Community 261 - "Community 261"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Client → Server: Home chat message.
|
||||||
|
|
||||||
|
### Community 262 - "Community 262"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: signals start of a streaming response.
|
||||||
|
|
||||||
|
### Community 263 - "Community 263"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: streamed text token.
|
||||||
|
|
||||||
|
### Community 264 - "Community 264"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: signals end of a streaming response.
|
||||||
|
|
||||||
|
### Community 265 - "Community 265"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Structured floating domain payload for UI routing decisions.
|
||||||
|
|
||||||
|
### Community 266 - "Community 266"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Server → Client: domain determined for a floating request.
|
||||||
|
|
||||||
|
### Community 267 - "Community 267"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Per-type extraction config produced by the journey chatbot.
|
||||||
|
|
||||||
|
### Community 268 - "Community 268"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): List notes, optionally scoped to a project by project_id.
|
||||||
|
|
||||||
|
### Community 269 - "Community 269"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Fetch a single note by its UUID to read its full Markdown content.
|
||||||
|
|
||||||
|
### Community 270 - "Community 270"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Create a new note. title: note heading (required) content: Markdown bo
|
||||||
|
|
||||||
|
### Community 271 - "Community 271"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Update an existing note. Only pass fields that should change. note_id: UUID
|
||||||
|
|
||||||
|
### Community 272 - "Community 272"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Delete a note permanently by its UUID.
|
||||||
|
|
||||||
|
### Community 273 - "Community 273"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return the resolved model string for *agent_name* (for Langfuse tracking).
|
||||||
|
|
||||||
|
### Community 274 - "Community 274"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return an LLM configured for *agent_name*, respecting per-agent overrides.
|
||||||
|
|
||||||
|
### Community 275 - "Community 275"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Return an embedding vector for *text*. Uses ``settings.LLM_EMBED_MODEL``
|
||||||
|
|
||||||
|
### Community 276 - "Community 276"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Stripe Dependency (billing)
|
||||||
|
|
||||||
|
### Community 277 - "Community 277"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Pydantic (>=2.10)
|
||||||
|
|
||||||
|
### Community 278 - "Community 278"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): python-jose (JWT)
|
||||||
|
|
||||||
|
### Community 279 - "Community 279"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Stripe SDK
|
||||||
|
|
||||||
|
### Community 280 - "Community 280"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): boto3
|
||||||
|
|
||||||
|
### Community 281 - "Community 281"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): slowapi (rate limit)
|
||||||
|
|
||||||
|
### Community 282 - "Community 282"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): bcrypt
|
||||||
|
|
||||||
|
### Community 283 - "Community 283"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): websockets
|
||||||
|
|
||||||
|
### Community 284 - "Community 284"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Pinecone
|
||||||
|
|
||||||
|
### Community 285 - "Community 285"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): qdrant-client
|
||||||
|
|
||||||
|
### Community 286 - "Community 286"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): MSAL (Microsoft Auth)
|
||||||
|
|
||||||
|
### Community 287 - "Community 287"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): APScheduler
|
||||||
|
|
||||||
|
### Community 288 - "Community 288"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): python-docx
|
||||||
|
|
||||||
|
### Community 289 - "Community 289"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): ruff
|
||||||
|
|
||||||
|
### Community 290 - "Community 290"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Mistral (EU residency option)
|
||||||
|
|
||||||
|
### Community 291 - "Community 291"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DeepSeek (dev-only, CN)
|
||||||
|
|
||||||
|
### Community 292 - "Community 292"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Competitor: Reclaim.ai
|
||||||
|
|
||||||
|
### Community 293 - "Community 293"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Telegram bot integration
|
||||||
|
|
||||||
|
### Community 294 - "Community 294"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): LLM Provider Report April 2026
|
||||||
|
|
||||||
|
## Ambiguous Edges - Review These
|
||||||
|
- `Google Auth Libraries (OAuth)` → `Agent Runner V2 — agent execution test harness` [AMBIGUOUS]
|
||||||
|
api/requirements.txt · relation: conceptually_related_to
|
||||||
|
|
||||||
|
## Knowledge Gaps
|
||||||
|
- **519 isolated node(s):** `Seed script: inserts fake clients, projects, tasks, timeline events, and notes`, `Timestamp in ms, optionally shifted into the past.`, `Initial schema: users, refresh_tokens, subscriptions. Revision ID: 001 Revis`, `Add agent config and run log tables: local_agent_configs, cloud_agent_configs, a`, `Add memory tables and user encryption_key column. Memory tables: memory_co` (+514 more)
|
||||||
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
|
- **Thin community `Community 49`** (7 nodes): `sidebar.tsx`, `cn()`, `handleKeyDown()`, `SidebarMenu()`, `SidebarMenuButton()`, `SidebarMenuItem()`, `useSidebar()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 59`** (5 nodes): `theme-provider.tsx`, `sonner.tsx`, `ThemeProvider()`, `useTheme()`, `Toaster()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 65`** (4 nodes): `locale-defaults.ts`, `detectFormatPrefs()`, `detectLanguage()`, `inferDateFormat()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 77`** (4 nodes): `001_initial_schema.py`, `downgrade()`, `Initial schema: users, refresh_tokens, subscriptions. Revision ID: 001 Revis`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 78`** (4 nodes): `003_agent_tables.py`, `downgrade()`, `Add agent config and run log tables: local_agent_configs, cloud_agent_configs, a`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 79`** (4 nodes): `004_add_memory_tables.py`, `downgrade()`, `Add memory tables and user encryption_key column. Memory tables: memory_co`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 80`** (4 nodes): `005_associative_pgvector.py`, `downgrade()`, `Phase 1 — confirm pgvector activation on memory_associative. Migration 004 cr`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 81`** (4 nodes): `006_memory_relations.py`, `downgrade()`, `Add memory_relations table (Phase 3 — relational tier). Revision ID: 006 Rev`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 82`** (4 nodes): `1f5975a4f3f4_add_extraction_queue.py`, `downgrade()`, `add extraction_queue Revision ID: 1f5975a4f3f4 Revises: 005 Create Date: 20`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 83`** (4 nodes): `818478c251dc_add_name_and_surname_to_users_table.py`, `downgrade()`, `add name and surname to users table Revision ID: 818478c251dc Revises: 004`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 84`** (4 nodes): `b4c0d1e2f3a4_add_oauth_and_avatar.py`, `downgrade()`, `Add oauth_accounts table, nullable password_hash, avatar_url to users. Revisi`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 85`** (4 nodes): `c5d1e2f3a4b5_add_onboarding_completed_at.py`, `downgrade()`, `Add onboarding_completed_at column to users table. Revision ID: c5d1e2f3a4b5`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 86`** (4 nodes): `d6e3f4a5b6c7_folder_index_tables.py`, `downgrade()`, `Add token tracking columns for folder integration. Revision ID: d6e3f4a5b6c7 Re`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 87`** (4 nodes): `e04100e88ace_avatar_url_varchar_to_text.py`, `downgrade()`, `avatar_url_varchar_to_text Revision ID: e04100e88ace Revises: c5d1e2f3a4b5`, `upgrade()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 88`** (4 nodes): `downgrade()`, `create waitlist_entries table Revision ID: 001 Revises: Create Date: 2026-0`, `upgrade()`, `001_create_waitlist_entries.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 89`** (4 nodes): `downgrade()`, `add consent_given_at and anonymized_at columns Revision ID: 002 Revises: 001`, `upgrade()`, `002_add_gdpr_fields.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 90`** (4 nodes): `downgrade()`, `add language column to waitlist_entries Revision ID: 003 Revises: 002 Creat`, `upgrade()`, `003_add_language_column.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 169`** (2 nodes): `Expose tool modules used by deep orchestrator-worker graphs.`, `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 170`** (2 nodes): `deps.py`, `Shared FastAPI dependencies. ``get_current_user`` and ``oauth2_scheme`` live`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 171`** (2 nodes): `__init__.py`, `OAuth provider abstractions and utilities.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 173`** (2 nodes): `API Dev Server Command (uvicorn)`, `FastAPI Framework Dependency`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 174`** (2 nodes): `Pinecone + Qdrant Vector Store Dependencies`, `SQLAlchemy + asyncpg + Alembic (DB stack)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 175`** (2 nodes): `Email Type: No-Project — irrelevant to any project`, `Test Fixture: No-Project Email (agent_runner_v2) — newsletter unrelated to project`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 176`** (2 nodes): `Email Type: Date — contains scheduled event/date`, `Test Fixture: Date Email (agent_runner_v2) — kickoff meeting with date`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 177`** (2 nodes): `Email Type: Thread — nested reply chain (blockquote structure)`, `Test Fixture: Email Thread (preprocessors) — nested blockquote multi-turn thread`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 178`** (2 nodes): `Email Type: Heavy HTML — complex table-based layout email`, `Test Fixture: Heavy HTML Email (preprocessors) — complex table layout newsletter`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 179`** (2 nodes): `Alembic`, `psycopg2-binary`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 180`** (2 nodes): `Device-specific backup key`, `cryptography (Fernet)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 181`** (2 nodes): `DateField withTime + flat props`, `parseDate HH:MM suffix support`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 182`** (2 nodes): `E2E fixture-driven YAML tests`, `pytest`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 183`** (2 nodes): `Cloudflare Argo Smart Routing`, `Cloudflare Load Balancing geo steering`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 184`** (2 nodes): `memory_maintenance.py`, `Proactive Pattern Mining`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 231`** (1 nodes): `Return updated credential dict if the access token was refreshed. If`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 232`** (1 nodes): `Return updated credential dict if the access token was refreshed. Ret`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 236`** (1 nodes): `Server → Client: requests a CRUD/vector operation on the local DB.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 237`** (1 nodes): `Client → Server: result of a CRUD/vector operation.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 238`** (1 nodes): `Server → Client: incremental LLM response text.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 239`** (1 nodes): `Client → Server: device identification on WS connect.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 240`** (1 nodes): `User display preferences sent by Electron on each request.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 241`** (1 nodes): `Scope for a floating request — narrows the agent to a specific entity.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 242`** (1 nodes): `Client → Server: Floating chat message scoped to an entity.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 243`** (1 nodes): `Client → Server: Request a plain-text brief (home or project).`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 244`** (1 nodes): `Server → Client: signals start of a streaming response.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 245`** (1 nodes): `Server → Client: signals end of a streaming response.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 246`** (1 nodes): `Structured floating domain payload for UI routing decisions.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 247`** (1 nodes): `Server → Client: domain determined for a floating request.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 248`** (1 nodes): `Per-type extraction config produced by the journey chatbot.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 249`** (1 nodes): `Structured agent configuration (replaces freeform prompt_template).`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 250`** (1 nodes): `Stream a plain-text daily home brief. Yields (event_type, data) tuples id`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 251`** (1 nodes): `Stream a plain-text project status brief for project_id. Yields (event_ty`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 252`** (1 nodes): `Return the resolved model string for *agent_name* (for Langfuse tracking).`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 253`** (1 nodes): `Return an LLM configured for *agent_name*, respecting per-agent overrides.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 254`** (1 nodes): `Return an embedding vector for *text*. Uses ``settings.LLM_EMBED_MODEL```
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 255`** (1 nodes): `Server → Client: requests a CRUD/vector operation on the local DB.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 256`** (1 nodes): `Client → Server: result of a CRUD/vector operation.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 257`** (1 nodes): `Server → Client: incremental LLM response text.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 258`** (1 nodes): `Server → Client: signals end of response with the complete text.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 259`** (1 nodes): `User display preferences sent by Electron on each request.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 260`** (1 nodes): `Scope for a floating request — narrows the agent to a specific entity.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 261`** (1 nodes): `Client → Server: Home chat message.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 262`** (1 nodes): `Server → Client: signals start of a streaming response.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 263`** (1 nodes): `Server → Client: streamed text token.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 264`** (1 nodes): `Server → Client: signals end of a streaming response.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 265`** (1 nodes): `Structured floating domain payload for UI routing decisions.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 266`** (1 nodes): `Server → Client: domain determined for a floating request.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 267`** (1 nodes): `Per-type extraction config produced by the journey chatbot.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 268`** (1 nodes): `List notes, optionally scoped to a project by project_id.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 269`** (1 nodes): `Fetch a single note by its UUID to read its full Markdown content.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 270`** (1 nodes): `Create a new note. title: note heading (required) content: Markdown bo`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 271`** (1 nodes): `Update an existing note. Only pass fields that should change. note_id: UUID`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 272`** (1 nodes): `Delete a note permanently by its UUID.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 273`** (1 nodes): `Return the resolved model string for *agent_name* (for Langfuse tracking).`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 274`** (1 nodes): `Return an LLM configured for *agent_name*, respecting per-agent overrides.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 275`** (1 nodes): `Return an embedding vector for *text*. Uses ``settings.LLM_EMBED_MODEL```
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 276`** (1 nodes): `Stripe Dependency (billing)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 277`** (1 nodes): `Pydantic (>=2.10)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 278`** (1 nodes): `python-jose (JWT)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 279`** (1 nodes): `Stripe SDK`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 280`** (1 nodes): `boto3`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 281`** (1 nodes): `slowapi (rate limit)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 282`** (1 nodes): `bcrypt`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 283`** (1 nodes): `websockets`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 284`** (1 nodes): `Pinecone`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 285`** (1 nodes): `qdrant-client`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 286`** (1 nodes): `MSAL (Microsoft Auth)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 287`** (1 nodes): `APScheduler`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 288`** (1 nodes): `python-docx`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 289`** (1 nodes): `ruff`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 290`** (1 nodes): `Mistral (EU residency option)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 291`** (1 nodes): `DeepSeek (dev-only, CN)`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 292`** (1 nodes): `Competitor: Reclaim.ai`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 293`** (1 nodes): `Telegram bot integration`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 294`** (1 nodes): `LLM Provider Report April 2026`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
|
||||||
|
## Suggested Questions
|
||||||
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
|
- **What is the exact relationship between `Google Auth Libraries (OAuth)` and `Agent Runner V2 — agent execution test harness`?**
|
||||||
|
_Edge tagged AMBIGUOUS (relation: conceptually_related_to) - confidence is low._
|
||||||
|
- **Why does `MemoryMiddleware` connect `API Auth + Memory Backbone` to `Agent Runners (deep/brief/folder)`, `Chat + Device WebSocket`, `Memory Tests + Seeds`?**
|
||||||
|
_High betweenness centrality (0.062) - this node is a cross-community bridge._
|
||||||
|
- **Why does `run_floating_stream()` connect `Agent Runners (deep/brief/folder)` to `Electron Main + Indexer`, `Chat + Device WebSocket`, `Memory Tests + Seeds`?**
|
||||||
|
_High betweenness centrality (0.039) - this node is a cross-community bridge._
|
||||||
|
- **Are the 211 inferred relationships involving `MemoryMiddleware` (e.g. with `User` and `Subscription`) actually correct?**
|
||||||
|
_`MemoryMiddleware` has 211 INFERRED edges - model-reasoned connections that need verification._
|
||||||
|
- **Are the 125 inferred relationships involving `User` (e.g. with `Base` and `Hourly cron: drain Free-tier extraction queue + mine proactive patterns for Powe`) actually correct?**
|
||||||
|
_`User` has 125 INFERRED edges - model-reasoned connections that need verification._
|
||||||
|
- **Are the 104 inferred relationships involving `MemoryProactive` (e.g. with `Base` and `_RegisterRequest`) actually correct?**
|
||||||
|
_`MemoryProactive` has 104 INFERRED edges - model-reasoned connections that need verification._
|
||||||
|
- **Are the 96 inferred relationships involving `Subscription` (e.g. with `Base` and `Auth middleware — JWT validation dependency. ``get_current_user`` is the Fast`) actually correct?**
|
||||||
|
_`Subscription` has 96 INFERRED edges - model-reasoned connections that need verification._
|
||||||
276
graphify-out/graph.html
Normal file
276
graphify-out/graph.html
Normal file
File diff suppressed because one or more lines are too long
97154
graphify-out/graph.json
Normal file
97154
graphify-out/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,20 @@
|
|||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"computedHash": "2621a44fbd9fc2636953d1e6e39e5faeed995f7fb958ec12cc98a2f0576f6fa7"
|
"computedHash": "2621a44fbd9fc2636953d1e6e39e5faeed995f7fb958ec12cc98a2f0576f6fa7"
|
||||||
},
|
},
|
||||||
|
"langfuse": {
|
||||||
|
"source": "langfuse/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"computedHash": "a72c15d6329867b84c4e7456f4e04d8c06ce5298d1d0068dbe864b8b00fb406c"
|
||||||
|
},
|
||||||
|
"remotion-best-practices": {
|
||||||
|
"source": "remotion-dev/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"computedHash": "a6a79c92a109339759d7b465f2d45fe11ce955e307a3e2c03fbcf4f9b624b77a"
|
||||||
|
},
|
||||||
"shadcn": {
|
"shadcn": {
|
||||||
"source": "shadcn/ui",
|
"source": "shadcn/ui",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"computedHash": "642a177bee320618caa49f5106cadb4e7594c606e867f61ba7b56d19cf745cd5"
|
"computedHash": "03365c7543539f6e6689a754e65b3d8ea023ba9d6ea12a9b5550462bf060af8c"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
waitlist
Submodule
1
waitlist
Submodule
Submodule waitlist added at df43f4783a
2
website
2
website
Submodule website updated: c60a9c2b1f...0006f36215
Reference in New Issue
Block a user