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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Behavision</title>
<script type="module" crossorigin src="./assets/index-BUi-8yXQ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Di_2m1ih.css">
<script type="module" crossorigin src="./assets/index-upywadx9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Z_jL3Bie.css">
</head>
<body>
<div id="root"></div>

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() {