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
This commit is contained in:
2026-09-19 13:42:35 +05:30
parent 2835252bb3
commit 51b9cb9743
15 changed files with 82 additions and 47 deletions

View File

@@ -98,9 +98,6 @@ export default function App() {
))}
</nav>
<EngineBox />
<button className="helpbtn" onClick={() => setHelping(h => !h)} aria-pressed={helping}>
<img src={logo} alt="" /><span>Ask Behavision</span><Icon.Chevron size={14} />
</button>
<div className="who">
{session.standalone
? <>
@@ -125,7 +122,16 @@ export default function App() {
</>}
</div>
</aside>
<main className="main"><Current session={session} /></main>
<main className="main">
<Current session={session} />
{/* Loya's door, top right of every screen. A buddy you have to find in
a sidebar is not around; one in the corner is. */}
{!helping && (
<button className="loya-fab" onClick={() => setHelping(true)} aria-label="Ask Loya" title="Ask Loya">
<img src={logo} alt="" /><span>Loya</span>
</button>
)}
</main>
{helping && <Assistant session={session} onClose={() => setHelping(false)} />}
</div>
)

View File

@@ -65,10 +65,12 @@ if (scenario) {
const q = history[history.length - 1]?.text ?? ''
if (!st.claimed) return Promise.reject(new Error('The assistant is not switched on for this server.'))
const text = /camera/i.test(q)
? 'Add the camera from the Cameras screen: its address is on a label on the camera, pick the make so the stream path fills in, type its password and press Test. Once it connects, run Check placement and walk past it - only a "good" verdict means it can recognise faces.'
? 'Go to Cameras and press Add camera. The address is on a sticker on the camera itself; pick the make and I fill in the stream path. Test it, save it, then walk past it once so I can tell you whether the placement works.'
: /code|install/i.test(q)
? 'The installation code comes from head office: the owner opens the shop there, presses "Set up a shop PC" and reads the code out. It works once.'
: 'Chennai is online with one camera connected, but nobody has proved that camera yet - run Check placement and walk past it. Until then a quiet screen might mean the camera is aimed badly, not that nobody came.'
? 'Whoever runs head office makes one: open the shop there, press Set up a shop PC, and read the code out. It works once.'
: /who|morning|came/i.test(q)
? 'Three people so far: Priya at 13:12 (her sixth visit), a new face at 13:18 I have called Visitor 7, and Visitor 3 just now.'
: 'Chennai is online and the door camera is connected, but nobody has proved it yet. Walk past it once with Check placement running and I will tell you if it can actually see faces - until then a quiet screen might just be a badly aimed camera.'
return delay({text, used: /camera|code/i.test(q) ? [] : ['site_status', 'cameras']}, 1400)
},
}

View File

@@ -147,7 +147,7 @@ input, textarea { user-select: text; }
.side .who .id b { display: block; font-size: 12.5px; font-weight: 550; }
.side .who .id span { display: block; font-size: 11px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main { min-width: 0; min-height: 0; overflow: auto; }
.main { min-width: 0; min-height: 0; overflow: auto; position: relative; }
/* ================================================================= page == */
@@ -590,3 +590,11 @@ tbody tr[role="button"]:hover, tbody tr.clickable:hover { background: var(--s2);
.formsection.dangerzone { margin-top: var(--sp-6); padding: var(--sp-4); border: 1px solid color-mix(in srgb, var(--bad) 30%, transparent); border-radius: var(--r); background: var(--bad-2); }
.formsection.dangerzone h4 { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 30%, transparent); }
tr.click { cursor: pointer; } tr.click:hover td { background: var(--s2); }
/* Loya's door: fixed to the top-right of the content area, on every screen. */
.loya-fab { position: fixed; top: var(--sp-4); right: var(--sp-5); z-index: 20;
display: flex; align-items: center; gap: 8px; padding: 8px 14px 8px 10px; border-radius: 99px;
border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); background: var(--s1); color: var(--ink);
box-shadow: var(--shadow-lg); cursor: pointer; font-size: 13px; font-weight: 600; }
.loya-fab img { width: 20px; height: 20px; object-fit: contain; }
.loya-fab:hover { background: var(--accent-3); border-color: var(--accent); }

View File

@@ -11,10 +11,10 @@ import logo from '../assets/loyaly-mark.png'
// 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?',
'Is everything working right now?',
'Why has nobody been recognised today?',
'Where do I get an installation code?',
'Who came in this morning?',
]
export default function Assistant({ session, onClose }) {
@@ -43,12 +43,12 @@ export default function Assistant({ session, onClose }) {
}
return (
<aside className="helper" role="dialog" aria-label="Ask Behavision">
<aside className="helper" role="dialog" aria-label="Loya">
<header className="helperhead">
<img src={logo} alt="" />
<div>
<b>Ask Behavision</b>
<span>Setup help, and what your shop is doing right now.</span>
<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>
@@ -59,13 +59,15 @@ export default function Assistant({ session, onClose }) {
<Icon.CloudOff size={16} />
<div>
<b>This PC runs on its own</b>
<span>The assistant lives at head office. Link this PC to a shop to ask it questions; the basics are below.</span>
<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">
<p>Ask in plain words. It reads your shop’s own cameras and arrivals, and it knows how the setup works.</p>
<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>
@@ -84,13 +86,20 @@ export default function Assistant({ session, onClose }) {
<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 ask' : 'Ask anything about this shop…'} />
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() {