Files
Behavision/desktop/frontend/src/views/Setup.jsx
Suriyakumarvijayanayagam 81e2c605b9 The first five minutes, as the product and not as a developer's first run
The first launch was a code box with a link under it, then an empty
Live screen with 'No cameras' in amber in a far corner, then a form
asking for an IP address, and for the first few minutes of all of it
the engine silently downloading 275 MB with nothing on screen but a
stopped-looking status. Walked in a browser with the new mock; nobody
who was not an installer would have got through it.

Now: a welcome that asks the one question a shop owner can answer -
managed from a head office, or on this PC only - with each path in a
sentence; a Getting Started checklist on Live that reads its three steps
from the engine and ticks them itself (recognition ready, camera added
and connected, camera proven by a walk-past), with the one button for
the next step, and that disappears the moment somebody is recognised;
and the model download reported as a percentage in the tray, the
sidebar and the checklist, parsed by the supervisor from the engine's
own progress lines.

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

133 lines
5.4 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 { useState } from 'react'
import { api, message } from '../bridge.js'
import * as Icon from '../ui/icons.jsx'
import logo from '../assets/loyaly-mark.png'
// Linking this PC to a shop — the first thing that happens on a new install,
// and until now the one thing the app could not do.
//
// It comes BEFORE sign-in on purpose. The installer standing at a new counter
// has an installation code and, quite often, no account of their own yet; the
// PC's identity is not a person's identity. The endpoint behind this is
// deliberately unauthenticated for the same reason — requiring a login first
// would mean shipping a password to every shop that installs the software.
export default function Setup({ onDone, onCancel }) {
const [code, setCode] = useState('')
const [busy, setBusy] = useState(null)
const [error, setError] = useState(null)
async function submit(e) {
e.preventDefault()
setBusy('claim'); setError(null)
try {
onDone(await api.claim(code))
} catch (err) {
setError(message(err))
} finally {
setBusy(null)
}
}
async function standalone() {
setBusy('alone'); setError(null)
try {
onDone(await api.runStandalone())
} catch (err) {
setError(message(err))
} finally {
setBusy(null)
}
}
// First launch: a choice, not a code box. Two thirds of the people who
// open this have no idea what an installation code is; the other third has
// one in their hand. Both must see their own path in the first second.
const [path, setPath] = useState(onCancel ? 'code' : null)
if (path === null) {
return (
<div className="login">
<div className="box wide">
<span className="mark"><img src={logo} alt="" /></span>
<h1>Welcome to Behavision</h1>
<p className="lead">
This PC will watch your shop’s cameras and recognise returning customers.
First, one question: is this shop managed from a head office?
</p>
<div className="choices">
<button type="button" className="choice" onClick={() => setPath('code')}>
<Icon.Cloud size={22} />
<b>Yes — I have an installation code</b>
<span>Head office gave you a code like <em>ABCDEF-123456-…</em>. This PC joins that shop and gets its cameras from there.</span>
</button>
<button type="button" className="choice" onClick={() => setPath('alone')}>
<Icon.Shield size={22} />
<b>No — set up on this PC only</b>
<span>Cameras, customers and recognition stay on this PC. Nothing is sent anywhere. You can link to a head office later.</span>
</button>
</div>
</div>
</div>
)
}
if (path === 'alone') {
return (
<div className="login">
<div className="box">
<span className="mark"><img src={logo} alt="" /></span>
<h1>On this PC only</h1>
<p className="lead">
Behavision will run entirely here. Next you’ll add your camera — it can find it on the network for you — and walk past it once so it can prove it works.
</p>
{error && <div className="err"><Icon.Warning size={15} />{error}</div>}
<button type="button" className="btn primary" disabled={!!busy} onClick={standalone}>
{busy === 'alone' ? 'Setting up…' : 'Continue'}
</button>
<div className="alt">
<button type="button" className="linkbtn" onClick={() => setPath(null)}><Icon.Back size={14} /> Back</button>
</div>
</div>
</div>
)
}
return (
<div className="login">
<div className="box">
<span className="mark"><img src={logo} alt="" /></span>
<h1>{onCancel ? 'Link to head office' : 'Join your shop'}</h1>
<p className="lead">
Type the installation code head office gave you. It works once, and this PC becomes that shop.
</p>
<form onSubmit={submit}>
{error && <div className="err"><Icon.Warning size={15} />{error}</div>}
<label className="field">
<span>Installation code</span>
{/* Uppercase and letter-spaced because the code arrives read aloud
down a phone or photographed off a screen. Spaces, dashes and
case are stripped on the server, so what is typed here can be
as untidy as it needs to be. */}
<input value={code} autoFocus required
placeholder="ABCDEF-123456-GHIJKL-789012"
autoComplete="off" spellCheck="false"
style={{ textTransform: 'uppercase', letterSpacing: '.06em' }}
onChange={e => setCode(e.target.value)} />
</label>
<button className="btn primary" disabled={!!busy || code.trim().length < 6}>
{busy === 'claim' ? 'Linking…' : 'Link this PC'}
</button>
</form>
<p className="foot">
Don’t have one? Whoever runs head office creates it under the shop: <em>Shops → the shop → Set up a shop PC</em>.
</p>
<div className="alt">
{onCancel
? <button type="button" className="linkbtn" onClick={onCancel}>Not now — go back</button>
: <button type="button" className="linkbtn" onClick={() => setPath(null)}><Icon.Back size={14} /> Back</button>}
</div>
</div>
</div>
)
}