Files
Behavision/desktop/frontend/src/views/Assistant.jsx
Suriyakumarvijayanayagam 51b9cb9743 The assistant is Loya, and she lives in the top-right corner
A name, a voice and a door. The prompt now asks for a colleague on the
shop floor - answer first, one to three sentences, the shop's name and
the person's name, the one thing to do next - instead of a report with
headings. Both apps put her behind the Loyaly mark in the top-right
corner of every screen, because a buddy you have to find in a sidebar
is not around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 13:42:35 +05:30

119 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react'
import { api, message } from '../bridge.js'
import * as Icon from '../ui/icons.jsx'
import logo from '../assets/loyaly-mark.png'
// The help. A conversation with the head-office assistant, which answers from
// the shop's own data and knows how the product is set up - so "why is nobody
// being recognised" and "how do I add my camera" are both answered here, by
// the same thing, without leaving the app.
//
// The history lives in this component and is resent whole; nothing is stored
// anywhere. A PC running on its own has no head office to ask and is told so.
const SUGGESTED = [
'Is my shop working right now?',
'How do I add my camera?',
'Why has nobody been recognised today?',
'Who came in this morning?',
]
export default function Assistant({ session, onClose }) {
const [turns, setTurns] = useState([])
const [draft, setDraft] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState(null)
const scroller = useRef(null)
const standalone = session?.standalone
useEffect(() => { scroller.current?.scrollTo({ top: 1e9, behavior: 'smooth' }) }, [turns, busy])
async function ask(text) {
const q = (text ?? draft).trim()
if (!q || busy) return
const history = [...turns, { role: 'user', text: q }]
setTurns(history); setDraft(''); setBusy(true); setError(null)
try {
const a = await api.ask(history.map(t => ({ role: t.role, text: t.text })))
setTurns([...history, { role: 'assistant', text: a.text, used: a.used ?? [] }])
} catch (e) {
setError(message(e))
setTurns(turns) // the question stays in the box, not in the transcript
setDraft(q)
} finally { setBusy(false) }
}
return (
<aside className="helper" role="dialog" aria-label="Loya">
<header className="helperhead">
<img src={logo} alt="" />
<div>
<b>Loya</b>
<span>Your Behavision buddy — knows your cameras, your customers and your numbers.</span>
</div>
<button className="close" onClick={onClose} aria-label="Close"><Icon.Close size={16} /></button>
</header>
<div className="helperbody" ref={scroller}>
{standalone && (
<div className="helpernote">
<Icon.CloudOff size={16} />
<div>
<b>This PC runs on its own</b>
<span>Loya lives at head office. Link this PC to a shop to talk to her; the basics are below.</span>
</div>
</div>
)}
{turns.length === 0 && (
<div className="helperintro">
<div className="turn assistant"><div className="bubble">
{greeting(session)} Ask me anything about {session?.site_name || 'this shop'} — what’s happening now, or how to set something up.
</div></div>
<div className="chips">
{SUGGESTED.map(s => <button key={s} className="chip" onClick={() => ask(s)} disabled={busy || standalone}>{s}</button>)}
</div>
{standalone && <QuickHelp />}
</div>
)}
{turns.map((t, i) => (
<div key={i} className={`turn ${t.role}`}>
<div className="bubble">{t.text}</div>
{t.used?.length > 0 && <span className="used">Looked at: {t.used.join(', ')}</span>}
</div>
))}
{busy && <div className="turn assistant"><div className="bubble thinking"><span /><span /><span /></div></div>}
{error && <div className="err"><Icon.Warning size={15} />{error}</div>}
</div>
<form className="helperask" onSubmit={e => { e.preventDefault(); ask() }}>
<input value={draft} onChange={e => setDraft(e.target.value)} disabled={busy || standalone}
placeholder={standalone ? 'Link this PC to head office to talk to Loya' : 'Ask Loya…'} />
<button className="btn primary" disabled={busy || standalone || !draft.trim()} aria-label="Ask"><Icon.Chevron size={16} /></button>
</form>
</aside>
)
}
function greeting(session) {
const h = new Date().getHours()
const part = h < 12 ? 'Morning' : h < 17 ? 'Afternoon' : 'Evening'
const name = session?.user?.full_name?.split(' ')[0]
return name ? `${part}, ${name}.` : `${part}.`
}
// What a PC with no head office can still be told. Static on purpose: there is
// nobody to ask, and a chat box that always fails is worse than a short list.
function QuickHelp() {
return (
<dl className="quickhelp">
<dt>Adding a camera</dt>
<dd>Cameras → Add camera. The address is on a label on the camera; pick the make and the stream path fills itself in. Test, then save.</dd>
<dt>Proving it works</dt>
<dd>Press Check placement and walk past the camera like a customer for 25 seconds. Only “good” means it can recognise faces — otherwise move it to head height, facing the way people approach.</dd>
<dt>Nothing showing on Live</dt>
<dd>Check the camera is Connected and Proven. A camera aimed from above or the side streams fine and recognises nobody.</dd>
<dt>Linking to head office later</dt>
<dd>Use the link button at the bottom of the sidebar and type an installation code from head office. Nothing recorded here is lost.</dd>
</dl>
)
}