Compare commits

..

47 Commits

Author SHA1 Message Date
Roberto
1a8acd08c0 chore: bump api submodule + untrack settings.local.json
- api: folder agent pagination/search + PDF/DOCX extract, WS frame casing compat, Langfuse traces
- .gitignore: add .claude/settings.local.json
- graphify-out: refresh after 166-file incremental update
2026-05-14 14:27:41 +02:00
Roberto
82a7a8dc27 chore: bump adiuvAI submodule — DateTimeField typing perf
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:46:47 +02:00
Roberto
5a90dbc832 chore: bump adiuvAI submodule — DateTimeField typing fix after calendar pick
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:09:21 +02:00
Roberto
f72aaa8424 chore: bump adiuvAI submodule — DateTimeField autocomplete + calendar stay-open
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:56:02 +02:00
Roberto
fa09ed2156 chore: bump adiuvAI submodule — DateTimeField + assignees kbd wrap + header padding
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:38:33 +02:00
Roberto
1341fb3144 chore: bump adiuvAI submodule — Due popover keyboard close fix
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:21:45 +02:00
Roberto
3705316a25 feat: bump adiuvAI submodule — task form keyboard polish
Brings the TaskFormDialog UX in line with the timeline AddEventDialog:
new header (DialogTitle + DialogDescription, no separator), roving
focus across property pills, listbox keyboard inside each popover,
and a typeable DateField (with optional HH:MM suffix) for the Due
field. Two shared hooks (useRovingFocus, useListboxKeys) underpin
the keyboard model.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:18:30 +02:00
Roberto
72d7cc2f6e docs: add task form dialog keyboard polish implementation plan
Step-by-step plan to port AddEventDialog UX (header, full keyboard nav,
date+time via DateField) into TaskFormDialog. Two new shared hooks
(useRovingFocus, useListboxKeys), parseDate time-suffix, DateField
withTime + flat props, and i18n updates across all 5 languages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:51:57 +02:00
Roberto
e1d15b3edd docs: add task form dialog keyboard polish design
Spec for porting AddEventDialog UX patterns into TaskFormDialog: new
header (title + description, no separator), full keyboard navigation
(roving focus on pills, arrow nav, Enter to open popover, arrow nav
inside list popovers and calendar), and date+time keyboard entry via
extended DateField (withTime + flat props) and parseDate time suffix.

Includes interactive HTML mockup demonstrating the keyboard flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:47:57 +02:00
Roberto
faea5f0448 docs: add timeline batch-add implementation plan
9 tasks, manual verification per task (no automated test suite).
Covers parseDate utility, DateField primitive, EditEventDialog
migration, AddEventDialog rewrite with keyboard nav, edit-row mode,
batch submit with allSettled error handling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:59:43 +02:00
Roberto
c68e23b713 docs(spec): defer TaskFormDialog migration from timeline batch-add
TaskFormDialog uses TZDate + H/M selectors; DateField primitive does
not cover timezone or time-of-day. Track as follow-up after DateField
gains timezone/time support.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:56:35 +02:00
Roberto
aba0f38816 docs: add timeline batch-add design spec
Stage-then-commit batch flow for AddEventDialog. One project per
batch, typed date entry with smart parse, full keyboard operation.
Extracts DateField + parseDate as shared primitives, migrates
EditEventDialog and TaskFormDialog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:53:07 +02:00
Roberto
8dffbc714c feat: bump api + adiuvAI submodules for project folder integration
api/ (15 commits): migration + ORM model + tier matrix + quota helpers +
quota endpoint + folder_indexer (text/vision/PDF/DOCX) + WS index session
frames + scoped read tool + manifest formatter + manifest injection
into home/task-brief/multi-project brief.

adiuvAI/ (15 commits): drizzle schema + scanner + indexer + WS frame
senders + tRPC projectFolders router + drizzle-executor actions +
daily rescan + i18n (5 locales) + 5 React components (FolderChip,
FolderLinkCard, FolderFileList, FolderUnlinkDialog, FilesSection) +
ProjectTabBar/ProjectDetail wiring + pre-flight quota check + toasts.
2026-05-12 13:11:14 +02:00
Roberto
0abed9563b docs: add project-folder integration implementation plan
32 bite-sized tasks across 6 phases: API foundation, indexer (text +
vision + PDF/DOCX), agent wiring, Electron schema + scanner, renderer
UI, end-to-end smoke verification. Linked spec:
docs/superpowers/specs/2026-05-11-project-folder-integration-design.md
2026-05-11 22:24:34 +02:00
Roberto
361f89a29d docs: add project-folder integration design spec
Approved design for linking a local folder to a project: lightweight
manifest with per-file LLM summaries, WS-streamed indexing pipeline,
pre-injected manifest for Home/Brief/Task-Brief agents, tier-gated
token+file-count quota recorded backend-side.
2026-05-11 22:08:44 +02:00
Roberto
74e2152596 chore: bump adiuvAI submodule (remove kbd hint from TaskFormDialog) 2026-05-08 16:11:57 +02:00
Roberto
649e4f00a5 Update adiuvAI: fix popover auto-close in TaskDetailSheet 2026-05-08 16:08:48 +02:00
Roberto
1c8c7e2ddc chore: bump adiuvAI submodule (task UX polish fixes)
Pulls in fixes for: card context menu, change-status icon, sheet live render,
composer alignment, project link relocation to sheet header, no comment toast.
2026-05-08 16:03:26 +02:00
Roberto
b111c76661 Bump adiuvAI: TaskDetailSheet header X/menu alignment fix 2026-05-08 15:38:03 +02:00
Roberto
804a0a5af3 Bump adiuvAI: TaskFormDialog due-time picker + TaskDetailSheet header overlap fix 2026-05-08 15:24:11 +02:00
Roberto
314d5656ae Update adiuvAI: Drizzle migrator replaces hand-rolled DB migrations
Bumps adiuvAI submodule to dac1d50 — initDb() now uses
drizzle-orm/better-sqlite3/migrator against src/main/db/migrations/
instead of a hand-rolled CREATE TABLE IF NOT EXISTS string + try/catch
ALTER blob. Includes a one-time bootstrap that seeds __drizzle_migrations
on legacy DBs so existing data (incl. 51 user tasks) is preserved.

Fixes the empty-tasks-list regression caused by migration 0004
(estimate column + task_attachments table) not being mirrored into
db/index.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:12:35 +02:00
Roberto
e073f4f774 feat: Task UX evolution — paginated table, detail sheet, attachments
Bumps adiuvAI submodule from dd3f144 to e104ffc, completing 27 commits
across 7 phases of the Task UX Evolution plan (docs/2026-05-08-task-ux-evolution-plan.md):

- Phase A: schema + backend (estimate column, task_attachments table, tRPC sub-router, cascade delete)
- Phase B: building blocks (AssigneeStack, StatusBadge, TaskAttachmentChip + hook, ChatInputBox comment variant)
- Phase C: TaskDetailSheet (replaces TaskDetailDialog) — sticky header/body/composer, attachments, clickable chip popovers
- Phase D: TaskFormDialog quick-capture form with property pills (replaces vertical NewTask/EditTask layout)
- Phase E: TaskTable + TaskTableRow + TaskPager + TaskListView orchestrator
- Phase F: project page tasks tab uses TaskListView (KanbanBoard removed)
- Phase G: i18n keys for all 5 locales (en/it/es/fr/de) + attachment toast keys
2026-05-08 14:45:43 +02:00
Roberto
20240c5fea docs: add Task UX Evolution implementation plan
29 tasks across 7 phases (schema+backend, building blocks, detail sheet,
form dialog, table+pager+list view, project page, i18n). Each task is a
single commit, manual smoke verify in dev (no test suite).
2026-05-08 13:05:58 +02:00
Roberto
310410350f docs: add Task UX Evolution design spec
Validated design for task list refactor: shadcn Table view with shared
pagination, right-side detail Sheet with attachments, redesigned
quick-capture create/edit dialog, project detail page reusing the same
list view.
2026-05-08 12:26:28 +02:00
Roberto
5274f014b9 Update adiuvAI: sidebar alignment + timeline axis improvements
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:11:38 +02:00
Roberto
ba9c9a4702 Update submodules: task briefing carousel feature complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:10:28 +02:00
Roberto
064142d386 update project data 2026-05-03 22:52:26 +02:00
Roberto
0c3dfb3564 Update note management from db vector to index 2026-04-30 00:11:39 +02:00
Roberto
c16e68f0d0 timeline resize view 2026-04-29 23:13:49 +02:00
Roberto
75f6b0dca5 Added graphify 2026-04-29 14:42:44 +02:00
Roberto
27dbfdfa8d Update project state 2026-04-29 09:31:37 +02:00
Roberto
6a352075ec last state 2026-04-27 09:15:31 +02:00
Roberto
d80d4d6b9e update skills 2026-04-27 09:08:36 +02:00
Roberto
b2989e53eb Slide for adiuvAI 2026-04-19 14:49:36 +02:00
Roberto Musso
0ac2ce924d brief agent 2026-04-18 22:20:03 +02:00
Roberto Musso
3538050e75 memory 2026-04-17 22:48:19 +02:00
Roberto Musso
2ee3bb37db update skill config 2026-04-15 11:26:46 +02:00
Roberto Musso
25a5a6672e chore: update subproject commits for waitlist and website 2026-04-12 10:20:31 +02:00
Roberto Musso
8ce3ade8ce feat(onboarding): implement first-run user onboarding wizard for profile setup
- Added a new onboarding wizard that runs on the first app launch post-login.
- Collects user personalization data (job role, industry, primary use case, tone preference, language) and stores it in encrypted core memory.
- Auto-detects and saves formatting preferences (timezone, time format, date format) in local electron-store.
- Normalizes user free-text inputs via a backend LLM call before persisting.
- Introduced new backend routes for memory updates and normalization.
- Updated frontend components to support the onboarding flow with a chat-bubble aesthetic.
- Added settings section for profile editing and re-running the onboarding process.
- Ensured that the onboarding process is skippable and editable in the settings.
- Implemented verification steps to ensure proper functionality and data handling.

chore: update submodules for waitlist and website
2026-04-12 00:36:11 +02:00
Roberto Musso
54eb863c52 feat: Update local agent documentation to reflect changes in user_id and session_id handling; add marketing strategy document; update skills-lock.json with new Remotion best practices; update website subproject commit. 2026-04-11 02:15:59 +02:00
Roberto Musso
bc2c76d2bb docs: update CLAUDE.md with Google OAuth architecture and gotchas
Document non-obvious details from Steps 1-5 implementation:
- adiuvAI: deep-link protocol workaround, requestSingleInstanceLock,
  backup-key.ts device-bound key, loginWithOAuth fetch() vs get()
- api: _pending_states in-memory limitation, nullable password_hash,
  OAUTH_REDIRECT_URI pointing to API not website, 409 unverified-email
  guard, OAuth testing patterns with AsyncMock + monkeypatch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:39:20 +02:00
Roberto Musso
92648472d7 fix: handle unverified OAuth email conflict (api submodule)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:49:36 +02:00
Roberto Musso
2958961e75 feat: complete Step 5 Google OAuth — backup key + tests
- adiuvAI: add backup-key.ts (device-specific AES key via safeStorage),
  remove _cachedPassword from AuthManager
- api: add TestOAuth (6 tests) covering authorize, callback flows
- docs: mark Step 5 complete with lessons learned

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:42:29 +02:00
Roberto Musso
d068edc77e feat: Google OAuth Steps 2-4 — backend web-callback, Electron deep link, login UI, avatar
Backend (api @ feature/batch-agent-v2):
- GET /auth/oauth/{provider}/web-callback: bounces Google redirect to adiuvai://
- OAUTH_REDIRECT_URI default: http://localhost:8000/api/v1/.../web-callback

Electron (adiuvAI @ develop):
- adiuvai:// protocol registered in forge.config.ts and via setAsDefaultProtocolClient
- Single-instance lock + second-instance/open-url deep-link handlers
- AuthManager.loginWithOAuth() + handleOAuthCallback()
- auth.loginWithOAuth tRPC mutation
- LoginForm: Google button, divider, pending state
- AppShell + AccountSection: avatar photo with initials fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:04:14 +02:00
Roberto Musso
37d7e65b35 feat: implement Step 2 Google OAuth backend — provider abstraction, PKCE routes, user linking
Adds api/app/auth/oauth_providers.py with GoogleOAuthProvider (httpx-based,
no authlib needed) and generate_pkce_pair(). New routes:
GET /auth/oauth/{provider}/authorize and POST /auth/oauth/{provider}/callback
with state/PKCE validation and three-way user resolution (existing OAuth link,
email auto-link, new social-only user). Updates settings.py with
GOOGLE_AUTH_CLIENT_ID/SECRET and OAUTH_REDIRECT_URI.

Also includes Step 1 backend changes (already marked complete in plan):
oauth_accounts table migration, nullable password_hash, avatar_url on User.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:21:14 +02:00
Roberto
89bc761609 Merge branch 'main' of https://git.muticolturano.com/adiuvAI/workspace 2026-04-10 08:47:36 +02:00
Roberto
33fcd884e3 add implementation plan for Google OAuth login + avatar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 08:44:23 +02:00
50 changed files with 112922 additions and 167 deletions

View File

@@ -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
View 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

View File

@@ -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"
}
]
}
]
}
}

View File

@@ -1,3 +0,0 @@
{
"enableAllProjectMcpServers": true
}

View File

@@ -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
View 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
View File

@@ -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

View File

@@ -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
View File

@@ -1,9 +0,0 @@
{
"servers": {
"langfuse-docs": {
"url": "https://langfuse.com/api/mcp",
"type": "http"
}
},
"inputs": []
}

9
CLAUDE.md Normal file
View 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)

Submodule adiuvAI updated: 27bc9d90af...81fe6d29e2

2
api

Submodule api updated: 3cf067faea...cc0e258e8c

View 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 125 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.

Binary file not shown.

View 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 0023, 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` (`023` / `059`), 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.

File diff suppressed because it is too large Load Diff

View 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
View 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. 36 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.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
docs/adiuvAI-pitch.pptx Normal file

Binary file not shown.

BIN
docs/adiuvAI.pptx Normal file

Binary file not shown.

View 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 &nbsp;·&nbsp; 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 &nbsp;·&nbsp;
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>

View 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
View 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
View 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.*

View 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?

View File

@@ -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&agrave; Agentiche</h2> <h2>Funzionalit&agrave; 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">&#x2699;</div> <div class="feature-icon">&#x2699;</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">&#x1f6e0;</div>
<h3>Setup Agent</h3>
<p>Journey conversazionale interattiva per configurare un agente. L&rsquo;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&agrave; 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>&#x2699; Batch Agents</h3> <h3>&#x2699; Background Agents</h3>
<span class="desc">Tool Calling Robusto + Output Strutturato</span> <span class="desc">Tool Calling Multi-Turno &mdash; 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>&#x26a0; Nota architetturale:</strong> Il Batch API dei provider LLM <em>non &egrave; 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&agrave;/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&agrave; &egrave; priorit&agrave; 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&agrave; 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&agrave; elevata; qualit&agrave; 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>&#x1f6e0; Setup Agent</h3>
<span class="desc">Journey Conversazionale &mdash; Qualit&agrave; 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>&#x2139; Profilo diverso dai Background Agents:</strong> Il setup &egrave; un&rsquo;interazione <em>real-time con l&rsquo;utente</em> (3&ndash;15 turni, <code>temperature=0.4</code>). Il volume &egrave; basso (poche sessioni per utente nel tempo), quindi il costo &egrave; trascurabile anche con modelli premium. Priorit&agrave;: qualit&agrave; della conversazione e accuratezza nel produrre l&rsquo;<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&agrave;/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&agrave; 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&agrave; 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&agrave; 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&agrave; per il setup journey</td>
</tr>
<tr>
<td>Groq / Cerebras</td>
<td class="na-cell">&mdash;</td>
<td class="na-cell">&mdash;</td>
<td class="na-cell">&mdash;</td>
<td class="na-cell">Non consigliati: qualit&agrave; 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 &bull; Floating 500/300 &bull; Brief 1.5K/500 &bull; Batch 3K/2K &bull; Embed 500</span> <span class="desc">Token medi: Home 2K/1K &bull; Floating 500/300 &bull; Brief 1.5K/500 &bull; Background Agent 3K/2K &bull; Setup 4K/500 &bull; 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&agrave; massima per feature. <strong>Contro:</strong> 2 API key da gestire (Google + OpenAI).</p> <p class="strategy-pros"><strong>Pro:</strong> Costo ottimale, qualit&agrave; 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&agrave; record (394&ndash;840 TPS). <strong>Contro:</strong> Qualit&agrave; tool calling inferiore ai proprietari. Serve OpenAI per embeddings.</p> <p class="strategy-pros"><strong>Pro:</strong> Ultra economico, velocit&agrave; record (394&ndash;840 TPS). <strong>Contro:</strong> Qualit&agrave; 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&agrave; massima, 1 sola API key. <strong>Contro:</strong> Costo 2&ndash;6x superiore alle alternative.</p> <p class="strategy-pros"><strong>Pro:</strong> Ecosistema unificato, ZDR, affidabilit&agrave; massima, 1 sola API key. <strong>Contro:</strong> Costo 3&ndash;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>&#x2699; GPT-4.1 Batch per Agenti</h4> <h4>&#x2699; 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 &egrave; 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&rsquo;<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&ograve; generare pi&ugrave; chiamate LLM in sequenza).</p>
</div>
<div class="why-card reveal">
<h4>&#x1f6e0; GPT-4.1 per Setup Agent</h4>
<p>Il setup journey &egrave; fondamentalmente diverso dagli agenti di processing: &egrave; una <strong>conversazione interattiva real-time</strong> con l&rsquo;utente (3&ndash;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> &egrave; la scelta giusta: qualit&agrave; 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&rsquo;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
View 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 | |
| 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 | , WS stream |
| 3 | `LLM_MODEL_FLOATING_AGENT` | **Floating Agent** chat contestuale da task/project/note | Multi-turno (≤6 step) | Alta | Mediaalta | Alto | , 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 | |
| 7 | `LLM_MODEL_SETUP_AGENT` | **Setup Agent** journey conversazionale per costruire `AgentConfig` JSON | Multi-turno (≤15) | Media | Alta (UX critica) | Basso (una tantum) | , 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` | 67x 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 ~$45/mese → totale **~$78/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à (394840 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: **24 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: **13 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 | 35 giorni |
| Legal review interno contratti provider | 12 settimane |
| Negoziazione clausole sensibili (residency, audit, pricing) | 12 settimane |
| Firma DocuSign + attivazione ZDR su Org ID | 23 giorni |
| **Totale** | **47 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.*

View File

@@ -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
View 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.*

View 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 (12 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) (12 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.

View 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 = '&nbsp;&nbsp;';
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
View 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
View 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 12 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 12 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 24 sentences of prose.
LENGTH
24 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 13 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
48 sentences total across the 34 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 48 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.

190
docs/plan-google-auth.md Normal file
View File

@@ -0,0 +1,190 @@
# Piano: Integrazione Google Login + Avatar
## Contesto
L'app adiuvAI ha un sistema auth custom (email+password, bcrypt, JWT HS256) funzionante.
Obiettivo: aggiungere Google Login come primo social provider, con la prospettiva di aggiungerne 2-3 in futuro (Microsoft, GitHub).
**Decisione architetturale:** OAuth diretto nel sistema esistente (non Keycloak, non Auth0).
- Il backend continua a emettere i propri JWT
- Ogni provider e' un'implementazione concreta di un'astrazione condivisa
- Email+password resta il metodo primario di registrazione/login
- L'avatar utente viene preso da Google; per utenti email si usano iniziali o icona default
**Nota encryption:** La `encryption_key` Fernet e' generata random, NON derivata dalla password.
Il social login non rompe la crittografia server-side. L'unico fix necessario e' sganciare
il backup encryption locale (`_cachedPassword`) dalla password utente.
---
## Regole
- **Completamento step:** aggiornare questo documento marcando lo step come `[x]` e annotare eventuali lessons learned
- **Commit:** al termine di ogni step, eseguire commit del codice
---
## Step 1: Backend — DB + Model + Schema
**Status:** [x] Completato
**Cosa fare:**
- Migration Alembic: creare tabella `oauth_accounts` (`id`, `user_id FK`, `provider`, `provider_user_id`, `provider_email`, `created_at`; UNIQUE su `provider, provider_user_id`)
- Migration Alembic: rendere `users.password_hash` nullable
- Migration Alembic: aggiungere colonna `users.avatar_url` (VARCHAR, nullable)
- Model `OAuthAccount` in `api/app/models.py` con relationship a `User`
- Campo `avatar_url: str | None` sul model `User`
- Aggiornare `UserProfile` in `api/app/schemas.py` con `avatar_url`
**File coinvolti:**
- `api/alembic/versions/XXX_add_oauth_and_avatar.py` (nuovo)
- `api/app/models.py`
- `api/app/schemas.py`
**Lessons learned:**
- `get_current_user` in `middleware/auth.py` esegue una query separata per `name/surname` — aggiornare quella query per includere `avatar_url` (non solo il model). Altrimenti il campo viene ignorato anche se presente in DB.
- `update_profile` in `routes/auth.py` costruisce `UserProfile` manualmente: va aggiornato esplicitamente con `avatar_url=user.avatar_url`.
- La `OAuthAccount.user` relationship richiede forward reference: il model deve essere dichiarato _dopo_ `User` ma la relationship su `User` usa `OAuthAccount` — SQLAlchemy risolve automaticamente con le stringhe lazy evaluation, ma occorre che il model sia nello stesso modulo.
---
## Step 2: Backend — OAuth Provider + Route
**Status:** [x] Completato
**Cosa fare:**
- Creare astrazione provider OAuth riusabile in `api/app/auth/oauth_providers.py` (nuovo)
- Classe base con: `get_authorization_url()`, `exchange_code()`, `get_userinfo()`
- Implementazione concreta `GoogleOAuthProvider`
- Aggiungere settings in `api/app/config/settings.py`: `GOOGLE_AUTH_CLIENT_ID`, `GOOGLE_AUTH_CLIENT_SECRET`, `OAUTH_REDIRECT_URI`
- Separati da `GMAIL_CLIENT_ID/SECRET` (scope diversi: `openid email profile` vs `gmail.readonly`)
- Aggiungere route in `api/app/api/routes/auth.py`:
- `GET /auth/oauth/{provider}/authorize` — genera state + PKCE code_challenge, ritorna authorize URL
- `POST /auth/oauth/{provider}/callback` — valida state, scambia code, fetch userinfo, crea/linka utente, salva avatar_url da Google, emette JWT
- Logica utente nel callback:
- `oauth_accounts` match? -> login utente esistente
- Email match + `email_verified=true`? -> link account a utente esistente (aggiorna avatar se mancante)
- Nessun match? -> crea nuovo utente (Fernet key, password_hash=None, avatar da Google)
- Aggiungere dependency `authlib` in requirements
**Sicurezza:**
- PKCE obbligatorio (desktop app = public client)
- Auto-link email solo se `email_verified=true` da Google
- State param per prevenire CSRF
**File coinvolti:**
- `api/app/auth/__init__.py` (nuovo)
- `api/app/auth/oauth_providers.py` (nuovo)
- `api/app/api/routes/auth.py`
- `api/app/config/settings.py`
- `api/requirements.txt` (o pyproject.toml)
**Lessons learned:**
- **`authlib` non è necessaria**: il flow PKCE con Google si implementa direttamente con `httpx` (già in requirements). Aggiungere `authlib` sarebbe una dipendenza inutilizzata. Se si vorrà usare authlib in futuro (es. per provider con flow più complessi), aggiungerla allora.
- **State store in-memory**: `_pending_states` è un dict a livello modulo — funziona in dev con un solo processo, ma non sopravvive a restart e non scala su più worker. In produzione va sostituito con Redis (o un campo temporaneo su DB).
- **`_issue_refresh_token` helper**: la logica di emissione token è condivisa tra i tre branch del callback (link esistente, link email, nuovo utente) — fattorizzarla in un helper evita duplicazione.
- **`tuple_` import non usato**: l'import `sql_tuple` da sqlalchemy può essere rimosso (non serve per le query attuali).
- **Route `provider` tipizzata come `Literal["google"]`**: FastAPI valida automaticamente il parametro path e risponde 422 per provider sconosciuti, rendendo superfluo un check manuale. Il dict `_PROVIDERS` serve come fallback di sicurezza.
- **`await db.flush()` prima di creare `OAuthAccount`**: necessario per ottenere `new_user.id` prima del commit, altrimenti il FK fallisce.
---
## Step 3: Electron — Deep Link + Auth Manager
**Status:** [x] Completato
**Cosa fare:**
- Registrare protocollo custom `adiuvai://` in `adiuvAI/forge.config.ts` (packagerConfig.protocols)
- In `adiuvAI/src/main/index.ts`:
- `app.setAsDefaultProtocolClient('adiuvai')`
- Windows/Linux: gestire `second-instance` event, parsare argv per deep link
- macOS: gestire `open-url` event
- Forward code+state all'auth manager
- In `adiuvAI/src/main/auth/auth-manager.ts`:
- Nuovo metodo `loginWithOAuth(provider)`: chiama authorize, apre browser, attende callback, scambia code, salva token
- Nuovo metodo `handleOAuthCallback(code, state)`: risolve la promise pendente
- In `adiuvAI/src/main/router/index.ts`:
- Aggiungere procedura tRPC `auth.loginWithOAuth`
**File coinvolti:**
- `adiuvAI/forge.config.ts`
- `adiuvAI/src/main/index.ts`
- `adiuvAI/src/main/auth/auth-manager.ts`
- `adiuvAI/src/main/router/index.ts`
**Lessons learned:**
- **`adiuvai://` non è accettato da Google Console come redirect URI**: Google accetta solo `http://localhost` e `https://`. Soluzione: il backend espone `GET /auth/oauth/{provider}/web-callback` che riceve il redirect da Google e rimanda subito a `adiuvai://`. Il redirect_uri registrato su Google punta al backend, non direttamente all'Electron app.
- **`OAUTH_REDIRECT_URI` punta al backend, non al dominio website**: `adiuvai.com` è il sito statico. L'API starà su `api.adiuvai.com` (o simile). Il default in `settings.py` è `http://localhost:8000/api/v1/auth/oauth/google/web-callback` per sviluppo locale — sovrascrivere con la var d'ambiente in prod.
- **`app.requestSingleInstanceLock()` è necessario per `second-instance`**: senza il lock, l'evento non viene mai emesso su Windows/Linux. Se `requestSingleInstanceLock()` restituisce `false`, bisogna uscire subito (`app.quit()`).
- **`process.defaultApp` in dev**: in dev mode, Electron è lanciato come `electron .` — il nome del processo non corrisponde all'app. Occorre passare `[path.resolve(process.argv[1])]` come terzo argomento a `setAsDefaultProtocolClient` per includere lo script nella registrazione OS del protocollo.
- **`post()` in auth-manager serializza snake_case**: il backend si aspetta `{ code, state }` in snake_case. La conversione avviene automaticamente via `toSnakeCase()` dentro `post()`, quindi la chiamata da `handleOAuthCallback` è safe.
- **`loginWithOAuth` usa `fetch()` diretto** (non `this.get()`): la route authorize è pubblica (no JWT richiesto), ma `get()` lancia se non autenticati. Usare `fetch()` diretto evita la dipendenza dalla sessione attiva.
- **`avatarUrl` in `UserProfileSchema`**: il backend restituisce `avatar_url` (snake_case) che viene camelCased in `toCamelCase()` prima del parse Zod. Il campo deve stare nello schema Zod come `avatarUrl` (camelCase).
---
## Step 4: Electron — UI Login + Avatar
**Status:** [x] Completato
**Cosa fare:**
- In `adiuvAI/src/renderer/components/auth/LoginForm.tsx`:
- Email+password resta il form principale (prima opzione, in alto)
- Divider "oppure" sotto il form
- Bottone "Sign in with Google" sotto il divider
- Stato "In attesa del browser..." durante il flow OAuth
- Su successo: invalidare `auth.status`
- Mostrare avatar nel profilo utente:
- Avatar da `avatar_url` (immagine circolare) se disponibile
- Fallback: iniziali del nome o icona default per utenti senza avatar
- Aggiornare tipo `UserProfile` in `adiuvAI/src/shared/api-types.ts` con `avatar_url`
**File coinvolti:**
- `adiuvAI/src/renderer/components/auth/LoginForm.tsx`
- `adiuvAI/src/shared/api-types.ts`
- Componenti profilo utente (da identificare)
**Lessons learned:**
- **`oauthMutation.isPending` dura fino a 5 min**: la mutation rimane in `isPending` mentre Electron attende il deep-link. Il bottone mostra "Waiting for browser…" e tutti gli input vengono disabilitati tramite `isBusy = loginMutation.isPending || oauthMutation.isPending` per evitare azioni concorrenti.
- **Google icon inline SVG**: il progetto usa solo `lucide-react` per le icone. Il logo Google 'G' è stato inserito come SVG inline direttamente nel componente — non introdurre librerie di icone esterne.
- **`profile.avatarUrl` è snake_case sul backend ma camelCase dopo `toCamelCase()`**: il campo sul tipo `UserProfile` (Electron) si chiama `avatarUrl`. La proprietà è `nullable().optional()` nello schema Zod — verificare sempre il null prima di passarla a `AvatarImage`.
- **`AvatarImage` come fallback graceful**: Radix UI `AvatarImage` mostra `AvatarFallback` automaticamente se l'immagine non carica (CORS, URL scaduto, etc). Non serve gestire l'errore manualmente.
- **`AccountSection` usa IIFE per evitare variabili di blocco**: il profilo era condizionale — l'IIFE `(() => { ... })()` dentro JSX permette di dichiarare variabili locali (`displayName`, `initials`) senza inquinare il componente con useState o helper esterni.
- **`AppSidebarProps` type non usa `UserProfile` importato**: il tipo è definito inline per evitare dipendenze circolari tra renderer e shared. Se `UserProfile` dovesse cambiare, va aggiornato anche il tipo inline.
---
## Step 5: Fix Backup Encryption + Test
**Status:** [x] Completato
**Cosa fare:**
- Sganciare BackupManager da `_cachedPassword`:
- Generare chiave backup random 256-bit al primo login (qualsiasi metodo auth)
- Salvarla in electron-store via `safeStorage` (stessa pattern di token.ts)
- Usare questa chiave al posto di `_cachedPassword` per AES backup
- Per utenti esistenti: generare nuova chiave, usarla per backup futuri
- Test backend (`api/tests/test_auth.py`):
- Test authorize URL generation
- Test callback: creazione utente, linking account, duplicati
- Test callback con email match -> auto-link
- Test state mismatch -> 401
- Test manuale end-to-end:
- Click "Sign in with Google" -> browser -> consent -> redirect -> autenticato
- Account linking: stessa email via password e Google -> stesso utente
- Utente solo-social: nuovo utente senza password
- Backup con utente social -> chiave device-specific
**File coinvolti:**
- `adiuvAI/src/main/auth/auth-manager.ts`
- `adiuvAI/src/main/auth/backup-key.ts` (nuovo)
- `api/tests/test_auth.py`
**Lessons learned:**
- **`BackupManager` non esiste ancora nell'Electron app**: `_cachedPassword` era dichiarato ma non utilizzato da nessun consumer. La pulizia è stata semplice: rimuovere il campo, i setter nei metodi `login`/`register`/`logout`, e il getter `getCachedPassword()`. Se si implementerà un BackupManager in futuro, usare `getBackupKey()` da `backup-key.ts`.
- **`backup-key.ts` riusa `getToken/setToken` da `token.ts`**: la chiave backup è salvata nell'`encryptedTokens` dict sotto la chiave `backup_key`, esattamente come i JWT auth. Nessun nuovo meccanismo di storage necessario.
- **`deleteToken` importato dinamicamente in `deleteBackupKey()`**: non strettamente necessario (poteva essere importato staticamente), ma è un pattern difensivo per evitare dipendenze circolari in caso di refactor futuro.
- **I test OAuth mockano `exchange_code` e `get_userinfo` come `AsyncMock` direttamente sulla classe** (`patch.object(GoogleOAuthProvider, ...)`): funziona perché FastAPI crea una nuova istanza del provider per ogni request, quindi il mock intercetta l'istanza creata dentro la route.
- **`monkeypatch.setattr(settings, ...)` è sufficiente** per simulare credenziali Google configurate: non serve sovrascrivere variabili d'ambiente o ricreare l'app — Pydantic Settings legge gli attributi dall'oggetto singleton in runtime.
- **Il test `email_match` verifica che `sub` JWT sia identico** tra registrazione password e login OAuth: questo copre la logica di linking senza accedere direttamente al DB.
- **Edge case non testato**: se Google restituisce `email_verified=False` con un'email già registrata, il backend tenta di creare un nuovo utente con email duplicata → constraint violation → 500. Non è stato fixato in questo step (fuori scope), ma è stato identificato.

View 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, 36 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.

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View 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 510 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).

View File

@@ -0,0 +1 @@
C:\Users\PC-Roby\Documents\_adiuvai_workspace

View 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

View 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

File diff suppressed because one or more lines are too long

97154
graphify-out/graph.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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

Submodule waitlist added at df43f4783a

Submodule website updated: c60a9c2b1f...0006f36215