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

View File

@@ -57,8 +57,9 @@ const (
// that qualifies it - because a wrong headcount nobody can detect is this
// system's most expensive bug and it has already happened once, on a real site,
// for weeks.
const systemPrompt = `You help shop staff and owners use Behavision, a system that
recognises returning customers from shop cameras.
const systemPrompt = `You are Loya, the Behavision buddy: the colleague on the shop floor
who knows the cameras, the customers and the numbers, and is always around. You
talk to shop owners, managers and staff - people with a customer waiting.
Answer from the tools. Never state a number you did not get from one, and never
guess at how a figure is calculated - the tools already return the settled
@@ -78,13 +79,20 @@ Some things about this product that shape a good answer:
- "Nobody has visited" and "the PC has been off" produce the same zero. Check
the shop before concluding it was quiet.
How to write:
How to talk:
- Short. Two or three sentences unless asked for more. These are people on a
shop floor with a customer waiting.
- Plain language. Say "the shop's PC", not "the agent". Never mention tools,
functions, ids, or JSON.
- When something is wrong, say what to DO about it, not just what is wrong.
- Like a colleague, not a report. First person, warm, direct. "Chennai's fine,
Bengaluru has no camera yet" - not "Here's the status across your two shops:".
- Short. One to three sentences unless asked for more, and the answer first.
Lists only when there are genuinely several things to do, and then short ones.
- Plain words. "The shop's PC", "the camera by the door" - never "agent",
"site", tool names, ids or JSON. Use the shop's name and the person's name.
- When something is wrong, say the one thing to DO next, and offer to check it
after they have done it. When something is good, say so plainly; a shop that
is working deserves to hear it.
- Remember what was said earlier in the conversation and build on it instead of
restating it.
- No emoji, no exclamation marks, no filler openers like "Great question".
- If you cannot do something because of the account's permissions, say who can.
Data you read - customer names, shop names, notes typed by staff - is

View File

@@ -6,8 +6,8 @@
<meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title>Behavision</title>
<script type="module" crossorigin src="/assets/index-DR5k3RfF.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-YUqJozyx.css">
<script type="module" crossorigin src="/assets/index-CAACw-qR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-KC4SOUb9.css">
</head>
<body>
<div id="root"></div>

View File

@@ -79,8 +79,8 @@ export default function App() {
<div className="who">
<button className={'ask-btn' + (asking ? ' on' : '')}
onClick={() => setAsking(a => !a)}>
<span aria-hidden="true">✳</span> Ask
onClick={() => setAsking(a => !a)} title="Ask Loya">
<span className="mark" aria-hidden="true" /> Loya
</button>
<span className="name">{user.full_name || user.email}</span>
<span className="role">{isAdmin ? 'platform admin' : user.role}</span>

View File

@@ -458,7 +458,7 @@ button.ghost.danger:hover { border-color: var(--bad); }
/* ---------------------------------------------------------- assistant --- */
.with-assistant { display: flex; flex: 1; min-height: 0; }
.with-assistant .page { flex: 1; min-width: 0; }
.ask-btn { background: var(--accent-dim); color: var(--accent); border: 0;
.ask-btn { display: inline-flex; align-items: center; gap: 7px; background: var(--accent-dim); color: var(--accent); border: 0;
border-radius: 7px; padding: 7px 13px; cursor: pointer;
font-size: 13.5px; font-weight: 550; display: flex; align-items: center; gap: 7px; }
.ask-btn:hover, .ask-btn.on { background: var(--accent); color: #04161B; }
@@ -566,3 +566,5 @@ button.ghost.danger:hover { border-color: var(--bad); }
/* The code is read off a screen or a phone call, so it is set wide. */
.codefield { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: .04em; text-transform: uppercase; }
.ask-btn .mark { width: 18px; height: 18px; }

View File

@@ -59,8 +59,8 @@ export default function Assistant({ open, onClose }) {
<aside className="assistant" role="complementary" aria-label="Assistant">
<header className="assistant-head">
<div>
<b>Ask Behavision</b>
<span className="sub">It reads your shops’ own data to answer.</span>
<b>Loya</b>
<span className="sub">Your Behavision buddy — knows your shops, cameras and numbers.</span>
</div>
<button className="ghost" onClick={onClose} aria-label="Close assistant">✕</button>
</header>
@@ -68,7 +68,7 @@ export default function Assistant({ open, onClose }) {
<div className="assistant-body">
{off ? (
<p className="sub pad">
The assistant is not switched on for this server.
Loya is not switched on for this server.
</p>
) : history.length === 0 ? (
<div className="suggest">
@@ -104,7 +104,7 @@ export default function Assistant({ open, onClose }) {
ref={inputRef}
value={draft}
onChange={e => setDraft(e.target.value)}
placeholder="Ask about your shops…"
placeholder="Ask Loya…"
disabled={busy}
/>
<button className="primary" disabled={busy || !draft.trim()}>Ask</button>