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:
File diff suppressed because one or more lines are too long
1
desktop/frontend/dist/assets/index-Z_jL3Bie.css
vendored
Normal file
1
desktop/frontend/dist/assets/index-Z_jL3Bie.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
desktop/frontend/dist/index.html
vendored
4
desktop/frontend/dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
server/internal/web/dist/index.html
vendored
4
server/internal/web/dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user