Demo is for Mahindra, so the console carries their branding and their cars: BE 6, XEV 9e and XEV 9S replace the placeholder models, in step with the voice agent's own brief — a model offered in the dropdown that the agent has never heard of is a call where she has to improvise. Brand, persona (Meera) and the demo credentials move into config.ts rather than being scattered through the pages. Accent colour follows. Login is simplified: credentials are prefilled and the button takes focus, so signing in during a demo is one keystroke. Both fields stay editable — this is a dev demo tenant, not a real account. useState is explicitly typed <string>: DEMO_LOGIN is `as const`, so inferring from it narrowed the state to a literal type and setEmail/setPassword refused any other value. tsc -b and vite build both clean. |
||
|---|---|---|
| src | ||
| .env.example | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.js | ||
| README.md | ||
| tailwind.config.js | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
Mahindra EV Sales Desk Console
Two-screen operator console for the live call-transfer demo: place an outbound Mahindra EV sales call, and watch it transfer to a human colleague mid-call.
Backs onto Zino app 583 (org 900000104, dev). Line-up: BE 6, XEV 9e, XEV 9S. Built on the frontgen React
scaffold — Vite + Tailwind + React Router + TanStack Query + src/zino-sdk.
The two screens
Place a call — lead name, model, and the two numbers: the customer Meera
dials, and the colleague the transfer bridges to. Submitting starts a
Lead Outreach workflow instance; its trigger places the call server-side.
Live monitor — polls the leads record view every 3s. Transferred to human
is the transfer having fired.
Running locally
npm install
echo 'VITE_ZINO_API_URL=https://dev-apps.getzino.in' >> .env
npm run dev
Sign in with an app user of app 583 (e.g. salesops@mahindra-ev.demo / Mahindra@2026 (prefilled on the login screen)).
VITE_ZINO_API_URL is only read from .env in dev. A production build
requires the config.js the server writes at placement time, and throws if it
is absent — a promoted artifact must never fall back to the build machine's
value and quietly call the wrong backend.
API notes worth keeping
The base URL is the root (https://dev-apps.getzino.in), not an app-scoped
path, because login and the app APIs live at different depths:
| Call | Path |
|---|---|
| Login | POST /usr/login |
| Start a lead | POST /app/583/start |
| Leads list | POST /app/583/view/recordview |
src/api.ts bypasses two SDK methods on purpose — both are SDK bugs, not
preferences:
workflows.startWorkflow()postsworkflow_id, but core'sStartRequestreadsworkflow_uuidand rejects the call outright.views.getTabularView()issuesGET /view/recordview— the legacy unscoped route. The app-scoped one is POST/app/{id}/view/recordviewwith the paging/filters in the body.
Auth still goes through the SDK, so there is one token and one session.
Why the browser holds no API key
Screen 1 submits a workflow activity as the signed-in user. The workflow's own
trigger then places the call using the agent key held in its server-side config.
The zvk_ key never reaches the browser.
The per-call transfer number
handover_number rides on the lead into the call's context, and agents-backend
prefers it over anything configured on the agent — the agent's numbers are the
fallback for calls that don't name one. Requires an agents-backend carrying the
per-call override; without it the field is sent, silently ignored, and the call
transfers to the agent's default number instead.
Known gap
The live transcript and the co-pilot's suggestions live in agents-backend, which the app-user token does not reach — the SDK talks to the renderer's workflow/view services. So the monitor shows call status live, but not the turn-by-turn feed. Closing that needs either a new endpoint or the trigger writing transcript lines onto the instance.