Same treatment as the BA operator console: the audio sits with the conversation it belongs to, so "did the AI capture what the customer actually said" is answerable by pressing play rather than by trusting the summary above it. Reads the new /api/agent-sessions/:id/call, which is scoped to the human who took the call — the recording lives on the voice session, owned by the telephony leg, and is unreachable through the ordinary session API. preload="none" so opening the screen doesn't pull a WAV of a whole phone call before anyone asked to hear one. A 404 is a normal answer and renders nothing: recordings are written at call END, so a live call shows its duration and end reason with a note instead of a dead player. tsc -b and vite build 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.