Multi-tenant SaaS frontend for interacting with Odoo ERP through an AI agent
A modern, responsive interface that allows users to query and manage data from their Odoo instance (inventory, invoices, sales, employees) using natural language through an AI-powered chat. Key features:
Core Chat:
- Real-time chat with SSE streaming
- Rich Markdown-formatted responses
- Image upload with inline preview (vision-based AI interactions)
- Rotating suggestion carousel: 4 random cards from a pool of 11 active suggestions, auto-rotates every 7s, pauses on hover
- Conversation history grouped by date (today, yesterday, last 7 days)
Action Management:
- AI-proposed CRUD actions with confirm/cancel flow (from text and vision sources)
- Field editor modal with per-field validation (422 error handling)
- Visual feedback for write operations (create, update, method calls)
- Success cards with record links to Odoo
- Validation error prompts with missing field indicators
- Ambiguity resolution with interactive selection cards
- Entity autocomplete for Odoo model search
- Audit history popover for action execution trail
- Auto-sequencing:
queue_nexttriggers follow-up actions automatically
Analytics & Export:
- Interactive charts (bar, line, pie) powered by Recharts
- Automatic Excel export button on chart cards
- Standalone Excel download cards for explicit export requests
- PDF report download cards
Pinned Insights Dashboard:
- Pin charts, files, and exports to a collapsible right sidebar
- Sidebar splits pins into Live (variable/refreshable charts, 2-col grid) and Saved (static/point-in-time charts + files + Excel, grouped below)
- Refresh button shown only for live (
volatility: "variable") charts — static charts are point-in-time, refresh is suppressed - Static charts use Bookmark icons (save/saved) instead of Pin icons to convey point-in-time semantics
- Flying pin animation with spring physics
- Max 20 pins per user with optimistic UI updates
- Global load: all pins are fetched once on login (
GET /me/pins) and merged across conversations; per-chat loads are skipped when already loaded - User-scoped clear:
clearAllcallsDELETE /me/pins(removes all pins across every conversation for the user) - Defensive validation: malformed pins (e.g., chart pin missing
chartpayload) are filtered out before rendering to prevent runtime crashes
Notification System:
- Proactive alerts from Odoo (sales, stock, invoices) with severity levels
- Notification feed in right sidebar (Alerts tab) with unread badge
- Mark as read (individual and bulk), dismiss
- Deep link: click notification to inject prompt into chat
- Configurable settings modal (toggle alerts by category, daily summary)
- Auto-polling every 30 seconds
Authentication & Multi-Tenancy:
- Supabase email/password authentication (DEV MODE bypass when unset)
- Demo mode: unauthenticated access when backend sets
demo_available(banner in chat + "Try Demo" button on login) - Organization management (name, slug, type)
- Role-based access control (SuperAdmin, Admin, Client User)
- Subscription tiers (Free, Starter, Implementor S/M/L/XL/XXL) with slot limits
- Team management: invite users by email, toggle free/paid slots
- 2-step onboarding wizard (org creation + Odoo connection)
- 402 payment limit modal (graceful degradation, no crash)
Configuration:
- Admin settings panel with 4 tabs (Org, Instances, Users, Feedback), each section rendered as a
CollapsibleCard(animated accordion) - Credential UI is role-aware:
CLIENT_USERsees a single-instance block with a status row (configured/not-configured + username inline) and an inline edit form that expands via a Pencil button (animated withAnimatePresence);ADMINsees an accordion per config - Odoo connection configuration, validation, and instance inspection
- Multi-language support (Spanish, English, French, German, Portuguese, Italian, Hindi, Gujarati, Tamil, Kannada, Marathi)
- Light / dark mode with preference persisted in
localStorage(no flash on reload) - Collapsible and responsive sidebar (mobile-friendly)
- Accessibility:
aria-labelon all interactive icon buttons,role="switch"on toggles
Builder vs Client (dual-voice UI):
- Audience-aware copy: ADMIN / SUPERADMIN see technical strings (e.g.
create · sale.order,#42, raw Odoo model names); CLIENT_USER and anonymous visitors see natural concierge copy (e.g.Confirmar pedido,Tu factura #42 quedó emitida.). Mapping from Odoo model → document type lives inlib/odoo-model-to-doctype.ts; translation keys inBuilder.*andClient.*namespaces. - Audience-aware density: icon sizes and button/input/card heights and radii scale up for Client (more spacious, larger tap targets) and stay compact for Builder, driven by CSS
--btn-h-*,--input-h,--*-radiusdensity tokens. - Custom brand mark (
AgentMark) replaces the genericBoticon across the sidebar, chat avatar and empty-chat hero.Wordmarkin the sidebar header. Discreet "Powered by TheOdooAgent" lockup (PoweredBy) shown only to Client (white-label-friendly: implementer's brand wins, TheOdooAgent stays as credit). - LangGraph trace panel (
LangGraphTracePanel): collapsible right-side dev panel showing node-level execution events, visible only to Builder. Currently a stub that emits syntheticstream:start/stream:endentries — wired to be replaced by the backend's SSEtraceevent when available.
Other:
- Plans and pricing page (Free, Starter $50/mo, Implementor from $100/mo) with Stripe checkout and billing portal integration; current plan highlighted; Implementor detail modal with tier comparison table. The "Plans" link in the sidebar is currently hidden (commented out)
Core:
- Next.js 16 - React framework with App Router
- React 19 - UI library
- TypeScript 5 - Static typing
Auth:
- Supabase - Email/password authentication + session management
Styling & UI:
- Tailwind CSS v4 - CSS utilities (configured via
@theme) - Framer Motion - Smooth animations and transitions
- Lucide React - Icon library
Charts:
- Recharts - Composable charting library (bar, area, pie)
Internationalization:
- next-intl - Locale-based routing, 11 supported languages
Rendering:
- react-markdown - Markdown rendering for agent responses
app/
[locale]/
layout.tsx # Root layout with provider stack (9 nested contexts)
page.tsx # Auth-based redirector (no user→/chat, no org→onboarding, SUPERADMIN→/superadmin, else→chat)
login/page.tsx # Supabase email/password login (+ DEV MODE bypass)
register/page.tsx # Account signup → onboarding flow
invite/page.tsx # Accept team invitation by token
superadmin/page.tsx # Superadmin panel (standalone, no AppShell)
(app)/
layout.tsx # AppShell wrapper (ChatContext + RightPanelContext); only wraps app routes
chat/page.tsx # New query (rotating carousel: 4 random suggestions from pool of 11 + input)
chat/[id]/page.tsx # Conversation with SSE streaming
onboarding/page.tsx # Odoo connection form (inside AppShell; no full-screen wrapper)
settings/page.tsx # Admin panel: org, Odoo configs, users, invitations
pricing/page.tsx # Subscription plans
globals.css # Theme variables (light/dark) + markdown styles
components/
app-shell.tsx # Wrapper with ChatContext + RightPanelContext; mounts LangGraphTracePanel for builder roles
AgentMark.tsx # Brand mark primitives: MarkB, MarkI, Wordmark, Lockup
theme-initializer.tsx # Client component: applies .dark class from localStorage on every route change
auth/
auth-guard.tsx # Login redirect HOC (checks auth, shows spinner)
chat/
sidebar.tsx # Collapsible sidebar + history (paginated); delegates bottom nav to UserMenu
user-menu.tsx # Popover menu (bottom of sidebar): avatar/initials, settings, superadmin link, theme toggle, language sub-menu, instance sub-menu, login/logout, PoweredBy (Client only)
chat-messages.tsx # Message bubbles with metadata + charts + image handling + feedback button (shown when allow_feedback)
feedback-modal.tsx # Modal to report an AI message (category + comment + expected response)
chat-input.tsx # Auto-resizing input with image upload + send/stop
demo-banner.tsx # Banner shown in demo mode (unauthenticated or no org)
odoo-config-selector.tsx # Dropdown to switch active Odoo config + credential status indicator
success-card.tsx # Green card for successful actions
validation-prompt.tsx # Orange card for missing fields
odoo-action-button.tsx # Purple action confirmation button
action-proposal-button.tsx # AI-proposed CRUD action confirm/cancel with field editor
selection-card.tsx # Multi-option selector for ambiguity resolution
odoo-file-card.tsx # PDF report download card
odoo-chart-card.tsx # Interactive charts (bar/line/pie) + Excel export + pin
excel-export-card.tsx # Standalone Excel download card
audit-history-popover.tsx # Action execution history timeline
entity-autocomplete.tsx # Odoo model search with debounced autocomplete
langgraph-trace-panel.tsx # Builder-only collapsible right-side trace panel (stub; replace with SSE `trace` event when available)
pinned/
pinned-sidebar.tsx # Collapsible right sidebar (pins + alerts tabs)
pinned-insight-mini-card.tsx # Compact card with refresh (charts) + unpin
pin-toggle-button.tsx # Reusable pin/unpin toggle on chart/file cards
flying-pin-animation.tsx # Spring-animated flying pin portal
notifications/
notification-feed.tsx # Notification list with mark-all-read
notification-card.tsx # Individual alert card with time-ago
notification-settings-modal.tsx # Toggle alerts by category
settings/
user-credentials-section.tsx # Section for users to save their own Odoo credentials; CLIENT_USER: single-instance block; ADMIN: accordion per config
admin-user-credentials-modal.tsx # Admin modal to manage credentials for any org user; CLIENT_USER: single block with instance switcher + pencil edit; ADMIN: accordion per config
admin-invitation-credentials-modal.tsx # Admin modal to pre-load credentials for a pending invitation; CLIENT_USER: single block (ClientPendingCredentialBlock); ADMIN: accordion (PendingConfigPanel)
odoo/
connection-form.tsx # Odoo connection form (saves via POST /admin/orgs/{id}/configs, not localStorage)
instance-inspector.tsx # View installed Odoo modules
pricing/pricing-cards.tsx # Plan cards (Free, Starter, Implementor); accepts currentTier prop; Stripe checkout/portal CTAs; Implementor detail modal
ui/
error-toast.tsx # Toast notification provider + display
limit-reached-modal.tsx # 402 payment limit upgrade modal
password-input.tsx # Password field with show/hide toggle (Eye/EyeOff)
doc-num.tsx # Document number: `.docnum` pill for Client (only mono surface they see), `font-technical` plain for Builder
powered-by.tsx # Discreet "Powered by TheOdooAgent" lockup (Client-only footer)
hooks/
use-auth.tsx # Supabase auth context (login/register/logout, DEV MODE stub)
use-session.tsx # /me endpoint context (user/org/subscription/odoo_configs bootstrap; always loads, even unauthenticated)
use-chat.ts # Chat state + SSE + image upload + action execution + clearChats (resets all chat state on logout)
use-odoo-config.tsx # Odoo config context (configs from backend; activeConfigId persisted in localStorage; isDemoMode flag)
use-pinned-insights.tsx # Pinned insights context (pin/unpin/refresh/clear/loadAllPins); defensive payload validation
use-notifications.tsx # Notification context (polling/read/dismiss/settings)
use-limit-reached-modal.tsx # 402 limit modal context (listens to auth:limit_reached event)
use-audience-translations.ts # `useAudienceT(ns)` → translator scoped to `Builder.<ns>` or `Client.<ns>` based on role
use-icon-size.ts # `useIconSize(slot)` → role-aware icon size (builder: 16/20/24, client: 18/22/28)
lib/
api.ts # Centralized API client (28+ endpoints, authFetch with 401/402)
types.ts # TypeScript interfaces (Message, Metadata, Action, Charts, Multi-tenant)
supabase.ts # Supabase client singleton + IS_AUTH_ENABLED + getAccessToken
pin-animation-events.ts # Pub-sub system for flying pin animations
odoo-model-to-doctype.ts # Maps Odoo model (`account.move`, `sale.order`, …) → user-facing docType (`invoice`, `order`, …) for Client copy
i18n/ # Routing, request config, navigation (Link/Router wrappers)
messages/ # Translations (es, en, fr, de, pt, it, hi, gu, ta, kn, mr)
proxy.ts # Locale detection middleware
The root layout nests 9 context providers in this order:
NextIntlClientProvider
→ AuthProvider (Supabase user)
→ SessionProvider (/me bootstrap: org, subscription, slots)
→ OdooConfigProvider (localStorage)
→ ToastProvider (error notifications)
→ LimitReachedModalProvider (402 modal)
→ NotificationProvider (30s polling)
→ PinnedInsightsProvider (pin state)
→ [root layout ends here]
→ AppShell (ChatContext + RightPanelContext) ← only inside (app)/ route group
The interface uses specialized cards to handle different response types from the AI agent:
Displayed when the agent successfully performs a write operation:
- Green card with CheckCircle icon
- Shows record ID and name
- Dual-voice: Builder sees technical headline + raw record name +
font-technical#id+ "View in Odoo" link. Client sees natural copy keyed by action × docType (e.g.Tu factura #42 quedó emitida.) viaClient.ActionSuccess.<action>.<docType>translations; record id is wrapped in<DocNum>(warm-raised pill); Odoo link is hidden.
Displayed when required fields are missing:
- Orange card with AlertCircle icon
- Lists missing fields as bullet points
- Guides user to provide complete information
AI-proposed CRUD action with confirm/cancel flow:
- Purple button using Odoo brand color (#714B67)
- Shows action summary (model, operation, data)
- Field editor modal with inline editing and validation
- Handles 422 per-field validation errors from backend
- User-edited field indicators (badge showing "Modified")
- Confirm executes the action; cancel shows a translated cancellation message
- Loading and completed states with visual feedback
- Dual-voice: Builder header shows uppercase
action · modeland uses the backend-providedaction_btnlabel verbatim. Client header shows neutral "Confirmar acción" and the CTA label comes fromClient.ActionProposal.verb.<action>.<docType>(e.g. "Confirmar pedido", "Descargar") with a.genericfallback per action.
Interactive button for confirmable method calls:
- Purple button using Odoo brand color
- Loading state with spinner during execution
- Completed state with checkmark
- Example: "Confirm Quotation", "Approve Purchase Order"
Displayed when the agent needs to resolve ambiguity:
- Lists matching records as selectable options
- Clicking an option sends the selection back as a chat message
PDF report download card:
- Red-themed icon for PDF files
- Shows filename and download button
- Links to backend-served static file
Interactive analytics visualization:
- Supports bar, line (area), and pie charts via Recharts
- Responsive layout with horizontal bars on narrow containers
- Custom tooltip with formatted values (currency, integer, decimal)
- Axis tick values auto-compacted (K / M / B / T) for large numbers;
no_decimalsflag suppresses decimal places - Purple color palette matching Odoo branding
- Footer with global total and group-by info
- Excel export button appears when
export_urlis present (ghost style, top-right) - Pin button to save chart to pinned insights sidebar
Standalone Excel download card for explicit export requests:
- Green Excel icon (#1D6F42) matching Microsoft Excel branding
- Shows filename and "export ready" message
- Download button with
downloadattribute to force browser download
Compact card displayed in the pinned insights sidebar:
- Chart cards: Icon by chart type (bar/pie/line) + live dot (variable) or "Histórico" badge (static), title, formatted total (
K/Mabbreviation for large currency values), refresh + unpin buttons in top-right hover area - File cards: Red PDF icon, filename, download link, unpin button
- Excel cards: Green Excel icon, filename, download link, unpin button
- Refresh button appears only on live (
volatility: "variable") chart cards and only when not in demo mode and an active Odoo config exists — static charts never refresh - Buttons revealed on hover with smooth opacity transition
Individual alert displayed in the notification feed:
- Severity-based color coding (critical, warning, info, success)
- Title, body, and relative timestamp ("5 min ago")
- Read/unread visual state
- Click to dismiss or deep-link into chat with prompt injection
Auto-resizing textarea with image upload support:
- Paperclip button opens native file picker (
accept="image/*") - Selected image shows as 64px thumbnail preview with X to remove
- Supports sending text only, image only, or both together
- Enter to send, Shift+Enter for newline
Action execution history timeline:
- Shows all actions executed in the current conversation
- Displays action type, model, record IDs, status
- Highlights user-edited fields vs system values
- Empty state when no actions have been executed
Odoo model search with autocomplete:
- Debounced search against backend (
/chat/{id}/search) - Dropdown with matching records (id + name)
- Used within ActionProposalButton field editor
Brand-mark primitives used across the app instead of the generic Bot icon:
MarkB— block mark (used in sidebar header, chat AI avatar, empty-state hero)MarkI— alternate mark (used in the typing indicator)Wordmark— "TheOdooAgent" text mark used in the expanded sidebar headerLockup— mark + wordmark lockup used byPoweredBy
Builder-only collapsible right-side trace panel for visualising LangGraph node execution:
- Shown only when
meData?.user?.roleisADMINorSUPERADMIN - Collapsed by default as a floating pill with event count; expands to a fixed 320px aside
- Each entry: timestamp, level (
ok/info/err/warn), node, message - Currently a stub — entries are synthesized in
AppShellon eachisStreamingtransition. Replace with the backend's SSEtraceevent when available.
Renders a document number with audience-aware styling:
- Client (CLIENT_USER + anonymous):
.docnumpill — Roboto Mono on a warm-raised background; the only mono surface the Client sees - Builder (ADMIN/SUPERADMIN): plain
.font-technical— mono is already pervasive for them, no pill
Discreet "Powered by TheOdooAgent" lockup intended for the Client sidebar footer only — supports partial white-labelling (the implementer's brand stays visually dominant; TheOdooAgent stays as a credit).
App loads → GET /me (no auth token)
→ redirected to /chat
→ demo_available: false → /chat (no demo, login link visible in sidebar)
→ demo_available: true → /chat (Demo Mode)
activeConfigId = "demo"
banner shown in chat
"Try Demo" button on login page
Demo Mode lets visitors interact with the AI using a read-only Odoo demo instance — no account required. Unauthenticated users always land on
/chatfirst; a "Sign in" link is visible in the sidebar bottom nav.
App loads → AuthProvider restores Supabase session
→ SessionProvider calls GET /me
→ SUPERADMIN → /superadmin (standalone panel, no app shell)
→ No org yet → /onboarding (Odoo connection form, inside AppShell)
→ Org exists → /chat
→ 401 from any API call → check active Supabase session → if session exists: clear session → /login; if no session: ignore (unauthenticated user hitting protected endpoint)
→ 402 from any API call → show LimitReachedModal (no crash)
Admins have access to /settings (4-tab panel, each section a CollapsibleCard) with full control over:
Settings
├── Org tab
│ └── Organization → edit name, slug, org type
├── Instances tab
│ ├── Add Connection → form to create a new Odoo config
│ ├── Saved Configs → list of existing connections (test, delete)
│ └── Inspector → inspect installed Odoo modules
├── Users tab
│ ├── (PARTNER org) Users → list members, change role, toggle free/paid slot, remove
│ │ seats widget (paid X/limit · free X/limit) shown in section subheader
│ │ Invite → send invite by email (role fixed as CLIENT_USER); optional instance + credential pre-load
│ │ Sent Invitations → view status (pending / accepted / expired), filter tabs
│ │ pending invitations: "Show link" button + copy URL
│ │ pending invitations can be cancelled (X button with inline confirmation; frees seat immediately)
│ └── (SOLITARY org) → upgrade banner with contact CTA (no team management UI)
└── Feedback tab
└── Feedback → list of feedback reports submitted by org users; expandable rows with 3 tabs:
Data (category, comment, expected response, admin_notes), Messages (conversation snapshot),
Note (tenant_notes: internal note editable by admin)
Role comparison:
| Capability | CLIENT_USER | ADMIN | SUPERADMIN |
|---|---|---|---|
| Chat & query Odoo | ✓ | ✓ | ✓ |
| Submit feedback on AI messages | per allow_feedback flag |
per allow_feedback flag |
per allow_feedback flag |
| View plans/pricing link | — | ✓ | ✓ |
| View settings | — | ✓ | ✓ |
| Manage Odoo connections | — | ✓ | ✓ |
| Manage users & invitations | — | ✓ | ✓ |
| View org feedback reports | — | ✓ | ✓ |
| Edit organization | — | ✓ | ✓ |
| Cross-org administration | — | — | ✓ |
| Feedback dashboard + full triage | — | — | ✓ |
Admin sends invite (email) → POST /admin/orgs/{id}/invitations
→ backend emails token link: /invite?token=...
Invitee opens link → GET /admin/invitations/{token}/preview (no auth)
→ renders without app shell (no sidebar, no chat context)
→ shows registration form
email (pre-filled, read-only)
org name + role badge
password field (with show/hide toggle)
→ submit:
1. POST Supabase signUp → gets accessToken
2. POST /admin/invitations/accept (Bearer accessToken)
3. reload /me → redirect /chat
Error states:
token missing / invalid → "Invitation not found"
token expired (410) → "Invitation expired"
already accepted (409) → "Already used" + go to chat button
The invitation page handles registration inline — the invitee never needs to visit
/loginor/registerseparately.
When NEXT_PUBLIC_SUPABASE_URL is unset:
IS_AUTH_ENABLED = falseuseAuth()returns stub userdev@localhost- Login page shows "Continue without login" bypass button
- No token sent to backend (backend must also be in DEV MODE)
| Concept | Values | Description |
|---|---|---|
| Roles | SUPERADMIN, ADMIN, CLIENT_USER |
Per-user permission level |
| Org Types | PARTNER, SOLITARY |
Multi-client vs single company |
| Subscriptions | FREE, STARTER, IMPLEMENTOR_S, IMPLEMENTOR_M, IMPLEMENTOR_L, IMPLEMENTOR_XL, IMPLEMENTOR_XXL |
Tier with slot limits |
| Slots | paid_slots_limit, free_slots_limit |
Max users per org by type |
| Odoo Configs | OdooConfigSummaryWithCreds[] |
Multiple Odoo connections per org; active one selected via activeConfigId; enriched with per-user credentials (hasCredentials, odoo_username) |
| Demo Mode | demo_available: boolean |
Backend flag enabling unauthenticated access; activeConfigId = "demo" |
| allow_feedback | boolean (per user, on MeUser) |
When true, a "Report" button appears on hover over the last AI message only. Users submit reports with optional category (wrong_answer, crash, misunderstood, other), comment, and expected response. Managed via PATCH /admin/orgs/{id}/users/{id}. |
Settings page (/settings) provides admin controls for:
- Organization name/slug/type editing
- Odoo connections: create (PARTNER only), update, delete (multiple per org)
- Users: list, change role, toggle free/paid, remove (PARTNER); upgrade banner (SOLITARY)
- Invitations: send by email, view status (pending/accepted/expired) — PARTNER only
┌─────────────┐ POST /chat/{id}/stream ┌─────────────────┐
│ │ ──────────────────────────────► │ │
│ Frontend │ { message, odoo_config } │ Backend │
│ (Next.js) │ │ (FastAPI/SSE) │
│ │ ◄────────────────────────────── │ │
└─────────────┘ text/event-stream (SSE) └─────────────────┘
chunks with optional metadata
┌─────────────┐ POST /chat/{id}/upload ┌─────────────────┐
│ │ ──────────────────────────────► │ │
│ Frontend │ multipart/form-data (image) │ Backend │
│ (Next.js) │ │ (FastAPI/OCR) │
│ │ ◄────────────────────────────── │ │
└─────────────┘ JSON (action_proposal) └─────────────────┘
Consumed endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/me |
Current user + org + subscription |
POST |
/me/onboarding |
Setup org + Odoo connection (409 on slug conflict) |
GET |
/me/conversations |
Chat history (paginated with limit/offset) |
POST |
/chat/{id}/stream |
Send message + receive SSE response with metadata (body: { message, config_id, language }) |
POST |
/chat/{id}/upload |
Upload image (multipart) + receive JSON with action proposal (field: config_id) |
POST |
/chat/{id}/action |
Execute confirmable action (body: { config_id, action, context, language }) |
GET |
/chat/{id}/history |
Load full message history for a conversation (query param: config_id) |
GET |
/chat/{id}/audit |
Action execution history (audit trail) |
POST |
/chat/{id}/search |
Odoo entity name_search (body: { model, query, config_id }) |
GET |
/me/pins |
Fetch all pinned insights for the current user across every conversation |
DELETE |
/me/pins |
Clear all pins for the current user (user-scoped) |
GET |
/chat/{id}/pins |
Fetch all pinned insights for a chat |
POST |
/chat/{id}/pin |
Create a new pin (chart, file, or excel) |
DELETE |
/chat/{id}/pin/{pinId} |
Delete a specific pin |
POST |
/chat/{id}/pin/{pinId}/refresh |
Refresh a pinned chart with updated data |
DELETE |
/chat/{id}/pins |
Clear all pins for a chat |
GET |
/chat/{id}/notifications |
Fetch notification list (filterable) |
PATCH |
/chat/{id}/notifications/{id}/read |
Mark notification as read |
PATCH |
/chat/{id}/notifications/read-all |
Mark all notifications as read |
POST |
/test-connection |
Validate Odoo credentials |
POST |
/inspect-instance |
Fetch installed Odoo modules |
POST |
/admin/orgs |
Create organization |
PATCH |
/admin/orgs/{id} |
Update organization (name, slug, type) |
PATCH |
/admin/orgs/{id}/type |
Change org type (PARTNER ↔ SOLITARY) — superadmin only |
POST |
/admin/superadmin/users/{id}/promote |
Create a new org for a user with no org (legacy accounts without auto-provisioning) — superadmin only |
GET |
/admin/orgs/{id}/configs |
List Odoo connections |
POST |
/admin/orgs/{id}/configs |
Create Odoo connection |
PATCH |
/admin/orgs/{id}/configs/{id} |
Update Odoo connection |
DELETE |
/admin/orgs/{id}/configs/{id} |
Delete Odoo connection |
GET |
/admin/orgs/{id}/users |
List organization users |
PATCH |
/admin/orgs/{id}/users/{id} |
Update user (role, is_free_license, allow_feedback) |
DELETE |
/admin/orgs/{id}/users/{id} |
Remove user from organization |
POST |
/admin/orgs/{id}/invitations |
Send invitation by email |
GET |
/admin/orgs/{id}/invitations |
List invitations |
DELETE |
/admin/orgs/{id}/invitations/{invId} |
Cancel a pending invitation (frees seat immediately) |
POST |
/admin/invitations/accept |
Accept invitation by token |
GET |
/me/odoo-credentials |
List current user's saved credentials (one per config) |
PUT |
/me/odoo-credentials/{configId} |
Save/update current user's credentials for a config |
GET |
/admin/orgs/{id}/users/{userId}/odoo-credentials |
Admin: list all credentials for a user (returns AdminUserCredential[]) |
GET |
/admin/orgs/{id}/users/{userId}/odoo-credentials/{configId} |
Admin: get a user's credentials for a specific config |
PUT |
/admin/orgs/{id}/users/{userId}/odoo-credentials/{configId} |
Admin: save/update a user's credentials for a config (empty strings = assign instance without credentials) |
DELETE |
/admin/orgs/{id}/users/{userId}/odoo-credentials/{configId} |
Admin: delete a user's credentials for a config |
POST |
/billing/checkout |
Create Stripe checkout session for a given tier |
POST |
/billing/portal |
Create Stripe billing portal session |
POST |
/chat/{id}/feedback |
Submit user feedback for a message (body: { config_id, message_id?, user_comment?, category?, expected_response? }) |
PATCH |
/chat/{id}/feedback/{feedbackId} |
Update tenant notes on a feedback report (body: { tenant_notes }) |
GET |
/admin/feedback |
List feedback reports (filterable by status, category, org_id; paginated) |
GET |
/admin/feedback/stats |
Feedback statistics (total, 24h, 7d, by_status, by_category, top_orgs) |
GET |
/admin/feedback/{id} |
Fetch single feedback report detail |
PATCH |
/admin/feedback/{id} |
Update report (status, admin_notes, is_hidden) |
DELETE |
/admin/feedback/{id} |
Delete feedback report |
The backend sends typed events in the SSE stream. Each event has an explicit type field:
Text Chunk (streaming response):
{
"type": "text",
"content": "I found 5 contacts in your database..."
}Action Proposal (CRUD confirmation):
{
"type": "action_proposal",
"action": {
"action": "create",
"model": "res.partner",
"vals": { "name": "Juan Perez", "email": "juan@example.com" },
"target_ids": [],
"status": "pending_confirmation"
},
"labels": {
"action_btn": "Create Contact",
"confirm_btn": "Confirm",
"cancel_btn": "Cancel",
"cancelled_msg": "Action cancelled. How else can I help you?"
}
}Selection Prompt (ambiguity resolution):
{
"type": "selection_prompt",
"field": "partner_id",
"searchValue": "Juan",
"options": [
{ "index": 0, "id": 42, "name": "Juan Perez" },
{ "index": 1, "id": 43, "name": "Juan Garcia" }
]
}Chart (analytics visualization):
{
"type": "chart",
"chart_type": "bar",
"title": "Sales by Product",
"data": [
{ "label": "Product A", "value": 15000 },
{ "label": "Product B", "value": 8500 }
],
"meta": {
"value_label": "Revenue",
"value_format": "currency",
"currency_symbol": "$",
"currency_iso": "USD",
"no_decimals": false,
"group_by": "product",
"model": "sale.order",
"period": "2026-02",
"total": 23500
},
"export_url": "/static/reports/sales_by_product_abc123.xlsx"
}Export (explicit Excel export request):
{
"type": "export",
"export_url": "/static/reports/export_abc123.xlsx",
"filename": "sales_report_2026_02.xlsx"
}Watermark (subscription-based):
{
"type": "watermark",
"show": true
}The labels field in action proposals contains translated UI text based on the language sent in the request. The frontend uses these labels directly for button text and cancellation messages.
The /chat/{id}/upload endpoint accepts multipart/form-data with file, odoo_config (JSON string), and language fields. It returns a regular JSON response (not SSE):
{
"message": "I found an invoice in the image. Here's what I extracted:",
"type": "action_proposal",
"action": {
"action": "create",
"model": "account.move",
"vals": { "partner_id": 42, "amount_total": 1500.00 },
"target_ids": [],
"status": "pending_confirmation"
},
"labels": {
"action_btn": "Create Invoice",
"confirm_btn": "Confirm",
"cancel_btn": "Cancel",
"cancelled_msg": "Action cancelled."
}
}The frontend renders the uploaded image in the user's message bubble and displays the action proposal below the assistant's response using the same ActionProposalButton component used for SSE-based proposals.
The POST /chat/{id}/pin/{pinId}/refresh endpoint re-queries Odoo and returns the updated chart data:
{
"status": "ok",
"new_payload": {
"type": "chart",
"chart_type": "bar",
"title": "Sales by Product",
"data": [{ "label": "Product A", "value": 16200 }],
"meta": { "value_label": "Revenue", "value_format": "currency", "currency_symbol": "$", "group_by": "product", "model": "sale.order", "period": "2026-02", "total": 16200 }
},
"refreshed_at": "2026-02-24T15:30:00Z"
}The /chat/{id}/action endpoint returns:
Success (201 - Create):
{
"status": "ok",
"message": "Contact created successfully (ID: 42)",
"result": { "action": "create", "model": "res.partner", "id": 42 }
}Success (200 - Update):
{
"status": "ok",
"message": "Contact updated successfully (IDs: [42])",
"result": { "action": "update", "model": "res.partner", "ids": [42], "success": true }
}Success (200 - Report):
{
"status": "ok",
"message": "Report generated successfully",
"result": { "action": "report", "model": "account.move", "ids": [1], "file_url": "/static/reports/invoice.pdf", "filename": "INV-2026-001.pdf" }
}Error Responses:
- 400 - Validation error (missing fields, invalid data)
- 401 - Odoo authentication failed
- 402 - Payment limit reached (triggers LimitReachedModal)
- 422 - Odoo business error (constraint violation, per-field errors)
- 500 - Odoo execution error
Auto-sequencing: The response may include a queue_next field with { text: string } to automatically trigger a follow-up action after a short delay.
Backward Compatibility:
The parser still supports the old format without type field for gradual migration:
{"content": "..."}- Node.js 18+
- Backend running at
http://localhost:8000(odoo-agent-back) - Supabase project (optional — leave unset for DEV MODE)
# Supabase Auth (leave empty for DEV MODE — no auth, no token)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_...
# Backend API base URL (default: http://localhost:8000)
NEXT_PUBLIC_API_BASE=http://localhost:8000# Install dependencies
npm install
# Start development server
npm run devOpen http://localhost:3000 — it automatically redirects based on auth state.
| Command | Description |
|---|---|
npm run dev |
Development server (hot reload) |
npm run build |
Production build |
npm run start |
Production server |
npm run lint |
Linting with ESLint |
The color system supports light and dark mode with CSS variables defined in app/globals.css under @theme. Components use semantic utility tokens — never raw hex values.
Theme preference is persisted in localStorage under the key theme ("dark" | "light"). The ThemeInitializer component (mounted in <body> in app/[locale]/layout.tsx) applies the .dark class on every route change via usePathname, ensuring the correct theme is always active across navigations.
| Token | Role | Example usage |
|---|---|---|
bg-base |
Page background | <div className="bg-base"> |
bg-surface |
Card/panel background | <div className="bg-surface"> |
bg-raised |
Elevated element (hover, input bg) | hover:bg-raised |
text-foreground |
Primary text | <p className="text-foreground"> |
text-text-secondary |
Secondary/label text | <label className="text-text-secondary"> |
text-text-muted |
Placeholder / de-emphasized text | placeholder:text-text-muted |
bg-accent / text-accent |
Interactive primary (replaces primary) |
buttons, active states |
bg-accent-hover |
Hover state for accent buttons | hover:bg-accent-hover |
bg-accent-subtle / text-accent |
Accent tint (icon backgrounds) | icon wrappers |
bg-error / text-error |
Destructive actions | delete buttons, error messages |
bg-error-subtle |
Error tint | hover on delete, inline errors |
text-success-solid |
Success color | success icons |
text-warning-solid |
Warning color | warning icons, badges |
text-info |
Info color | info icons |
border-border |
Default border | all card/input borders |
| Token | Usage |
|---|---|
text-heading |
Section headings (h1/h2) |
text-subheading |
Sub-section headings |
text-body |
Default body text (replaces text-sm) |
text-small |
Secondary labels (replaces text-xs) |
text-micro |
Captions, badges, timestamps |
font-technical |
Monospaced/code values (slugs, URLs, IDs) |
- Success cards use
text-success-solid/bg-success-subtle - Validation prompts use
text-warning-solid/bg-warning-subtle - Action buttons use
--color-odoo-purple(#714B67) — Odoo brand color - PDF file cards use
text-errorred accent - Excel export cards use
#1D6F42(Excel green) - Charts use Odoo purple palette
- Notification severity: critical (
text-error), warning (text-warning-solid), info (text-info), success (text-success-solid)
Beyond color, the design system exposes density tokens in app/globals.css that scale button/input/card heights and radii. They map to Tailwind v4 utilities via --spacing-* and --radius-*:
| CSS variable | Tailwind utility | Role |
|---|---|---|
--btn-h-sm |
h-btn-sm / w-btn-sm |
Small button (default 40px) |
--btn-h-md |
h-btn-md / w-btn-md |
Default button height (44px) |
--btn-h-lg |
h-btn-lg / w-btn-lg |
Large CTA (48px) |
--input-h |
min-h-input / h-input |
Inputs / textareas (44px) |
--btn-radius |
rounded-btn |
Button / input corner radius |
--input-radius |
rounded-input |
Input corner radius |
--card-radius |
rounded-card |
Card / modal corner radius |
--layout-gap |
gap-layout-gap |
Standard layout gap |
The defaults baked into :root correspond to the Client density (larger, more spacious — appropriate for anonymous / demo visitors before /me resolves). Builder density is applied by swapping the variable values on a .builder / .client class scope.
Pair these tokens with useIconSize(slot) for icons (slot inline | button | heading) so a single component stays visually consistent across audiences.
- Cards / modals:
rounded-cardtoken (literal fallback:rounded-lg) - Buttons / inputs / small elements:
rounded-btntoken (literal fallback:rounded-md) - Button height:
h-btn-md(useh-btn-sm/h-btn-lgfor variants) - Icons: size via
useIconSize(...)(or 20px literal in static surfaces),strokeWidth={1.5}throughout - Animations:
duration-0.15+ease: "easeOut"(replaced spring physics)
User-facing copy and icon sizes split by audience (role):
- Builder =
ADMINorSUPERADMIN— execution-oriented, mono-friendly, exposes Odoo internals (e.g. "EJECUTANDO · fetch_records", "ValidationError",sale.order). - Client =
CLIENT_USER+ anonymous — concierge style, no jargon, document numbers only (e.g. "Lista", "No pude conectarme con tu sistema").
Read strings via useAudienceT("<namespace>") which resolves to Builder.<namespace> or Client.<namespace> automatically. Keys must exist under both roots in every messages/*.json — keep them in lockstep across all eleven locales.
Read icon sizes via useIconSize(slot):
| Slot | Builder | Client |
|---|---|---|
inline |
16 | 18 |
button |
20 | 22 |
heading |
24 | 28 |
| Code | Language |
|---|---|
es |
Spanish (default) |
en |
English |
fr |
French |
de |
German |
pt |
Portuguese |
it |
Italian |
hi |
Hindi |
gu |
Gujarati |
ta |
Tamil |
kn |
Kannada |
mr |
Marathi |
Translations are located in messages/[locale].json.
| Namespace | Description |
|---|---|
Metadata |
Page title and description |
Sidebar |
Collapse/expand labels, empty state |
UserMenu |
Bottom sidebar popover: avatar, settings, theme, language, instance, login/logout |
ChatGroups |
Date-based grouping labels |
NewChat |
Welcome screen and suggestions |
ChatInput |
Input placeholder, disclaimer, image attach/remove, send/stop aria labels |
ChatMessages |
Chat UI: typing, success, validation, selection, file, chart, export, action proposal, audit, feedback button |
Feedback |
Feedback modal: title, categories, comment, submit/cancel, success toast |
ChatHistory |
Loading states |
Pricing |
Plans, features, and CTAs |
Settings |
Connection form, inspector, security, admin panel (org, configs, users, invitations, feedback reports) |
PinnedInsights |
Pin/unpin tooltips, empty state, error messages |
Notifications |
Alert feed, settings, time labels |
Auth |
Login, register, DEV MODE bypass |
Onboarding |
2-step wizard (org + Odoo connection) |
LimitReachedModal |
402 payment limit message |
Invite |
Invitation acceptance (loading, success, error, expired) |
LocaleSwitcher |
Language names |
Builder.* |
Builder-voice strings — read via useAudienceT("<ns>") when role is ADMIN/SUPERADMIN. Includes Builder.Trace (LangGraph panel) and Builder.ChatMessages (e.g. typing indicator: "Ejecutando · query"). |
Client.* |
Client-voice strings — read via useAudienceT("<ns>") when role is CLIENT_USER or anonymous. Includes Client.ChatMessages, Client.ActionSuccess.<action>.<docType> (success card headlines) and Client.ActionProposal.verb.<action>.<docType> (confirm-button labels). Every entry must have a .generic fallback. |