The shop app gets its help panel, and its last two old screens catch up
Ask Behavision: a panel beside any screen that talks to the head-office assistant as the signed-in user - setup questions and 'is my shop working' answered by the same thing, without leaving the app. The assistant's prompt now knows how the product is set up (installation codes, adding a camera, what a placement verdict means, the model download on first run), so it is the help and not only the analyst. A PC running on its own has nobody to ask and gets the essentials as text. Cameras and Customers were still on the pre-redesign markup - the add camera drawer ran off the right edge of the window because it used a class the new stylesheet never sized. Both are rebuilt: cameras as picture-led cards with connection and 'proven' as two separate claims and a placement check laid out as the two steps it is; the customer record as a proper sheet. mock.js renders the app in a browser with fake bindings (?mock=fresh|standalone|claimed, dev server only), so a screen can be put in front of somebody without a Windows build. It is how these were reviewed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
76
desktop/frontend/src/mock.js
Normal file
76
desktop/frontend/src/mock.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// A stand-in for the Go bindings, for looking at screens in a browser.
|
||||
//
|
||||
// The app only exists inside Wails, so until this there was no way to put a
|
||||
// screen in front of somebody without a Windows build - which is how the
|
||||
// first-run experience went unreviewed. Loaded ONLY by the dev server and only
|
||||
// with ?mock=<scenario>; a production bundle never contains it.
|
||||
//
|
||||
// ?mock=fresh first launch after install: unclaimed, engine starting
|
||||
// ?mock=standalone chose "run on this PC only", no cameras yet
|
||||
// ?mock=claimed claimed, signed in, one camera, arrivals flowing
|
||||
//
|
||||
// State is in memory and advances as you click, so the flow can be walked.
|
||||
const scenario = new URLSearchParams(location.search).get('mock')
|
||||
|
||||
if (scenario) {
|
||||
const st = {
|
||||
claimed: scenario === 'claimed',
|
||||
standalone: scenario === 'standalone',
|
||||
logged_in: scenario === 'claimed',
|
||||
cameras: scenario === 'claimed' ? [{id: 'entrance', label: 'Entrance', host: '192.168.1.122', port: 554, path: '/ch0_1.264', username: 'admin', has_password: true, enabled: true}] : [],
|
||||
engine: scenario === 'fresh' ? 'starting' : 'running',
|
||||
startedAt: Date.now(),
|
||||
}
|
||||
const user = {id: 'u1', email: 'suriya@tenext.in', full_name: 'Suriya', role: 'owner', client_id: 'c1', client_name: 'TeNext Retail'}
|
||||
const session = () => ({logged_in: st.logged_in, user: st.logged_in ? user : {}, site_name: st.claimed ? 'TeNext Chennai' : '', claimed: st.claimed, standalone: st.standalone && !st.claimed})
|
||||
const now = () => new Date().toISOString()
|
||||
const arrivals = () => st.cameras.length === 0 ? [] : [
|
||||
{type: 'person.seen', ts: now(), camera_id: 'entrance', data: {label: 'Visitor 3', identity_id: 3, similarity: 0.61, gender: 'Male', age: 34, emotion: 'neutral'}},
|
||||
{type: 'person.new', ts: new Date(Date.now() - 95e3).toISOString(), camera_id: 'entrance', data: {label: 'Visitor 7', identity_id: 7, gender: 'Female', age: 28}},
|
||||
{type: 'person.seen', ts: new Date(Date.now() - 410e3).toISOString(), camera_id: 'entrance', data: {label: 'Priya', identity_id: 2, similarity: 0.72, gender: 'Female', age: 41}},
|
||||
]
|
||||
const delay = (v, ms = 120) => new Promise(r => setTimeout(() => r(v), ms))
|
||||
const App = {
|
||||
Session: () => delay(session()),
|
||||
Login: (email) => { st.logged_in = true; user.email = email || user.email; return delay(session()) },
|
||||
Logout: () => { st.logged_in = false; return delay(session()) },
|
||||
Claim: (code) => code.replace(/[^A-Z0-9]/gi, '').length >= 20
|
||||
? (st.claimed = true, st.standalone = false, delay(session(), 900))
|
||||
: Promise.reject(new Error('That installation code is not valid. Ask for a new one.')),
|
||||
RunStandalone: () => { st.standalone = true; return delay(session()) },
|
||||
EngineStatus: () => {
|
||||
// The engine takes a minute or two on first run (models download).
|
||||
if (st.engine === 'starting' && Date.now() - st.startedAt > 20000) st.engine = 'running'
|
||||
const cams = Object.fromEntries(st.cameras.map(c => [c.id, true]))
|
||||
return delay({state: st.engine === 'starting' ? 'running' : 'running', reachable: st.engine !== 'starting', recognition_model: st.engine === 'starting' ? '' : 'w600k_r50', cameras: cams, restarts: 0})
|
||||
},
|
||||
StartEngine: () => delay({state: 'running', reachable: true}),
|
||||
StopEngine: () => delay({state: 'stopped', reachable: false}),
|
||||
Cameras: () => delay(st.cameras.map(c => ({...c, connected: true, frames: 1200, faces: 9}))),
|
||||
TestCamera: (cam) => delay({ok: Boolean(cam.host), width: 800, height: 448, codec: 'hevc', error: cam.host ? '' : 'no host'}, 1500),
|
||||
SaveCamera: (id, cam) => { const c = {id: id || cam.id || 'cam' + (st.cameras.length + 1), ...cam, has_password: Boolean(cam.password)}; delete c.password; st.cameras = [...st.cameras.filter(x => x.id !== c.id), c]; return delay(c) },
|
||||
DeleteCamera: (id) => { st.cameras = st.cameras.filter(c => c.id !== id); return delay(null) },
|
||||
StartPlacementCheck: () => delay({state: 'running'}),
|
||||
PlacementResult: () => delay({state: 'finished', verdict: 'good', headline: 'faces recognised on a walk-past', advice: []}),
|
||||
StreamURL: () => '',
|
||||
Live: () => delay({stats: {cameras: st.cameras.map(c => ({camera_id: c.id, connected: true, pipeline: {best_quality: {n: 40, fraction_below_gate: 0.18}}})), gallery: {identities: st.cameras.length ? 7 : 0, sightings: st.cameras.length ? 44 : 0}}, events: arrivals()}),
|
||||
PipelineStatus: () => delay({webhook_url: 'http://127.0.0.1:53658/events', queued: 0, dropped: 0, claimed: st.claimed, standalone: st.standalone && !st.claimed, broker_up: st.claimed, accepted: st.claimed ? 12 : 0}),
|
||||
LocalIdentities: () => delay([]), LocalSightings: () => delay([]),
|
||||
Footfall: () => delay({total: 0, buckets: []}), Sites: () => delay([]),
|
||||
VisitorHistory: () => delay([]), VisitorPhoto: () => delay({available: false, reason: 'This system is not storing images.'}),
|
||||
ForgetCustomer: () => delay(null), Sales: () => delay([]),
|
||||
Customers: () => delay([{id: 'v1', label: 'Priya', number: 2, first_seen_at: now(), last_seen_at: now(), visits: 6}]),
|
||||
SaveProfile: () => delay(null), RecordPurchase: () => delay(null),
|
||||
Ask: (history) => {
|
||||
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.'
|
||||
: /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.'
|
||||
return delay({text, used: /camera|code/i.test(q) ? [] : ['site_status', 'cameras']}, 1400)
|
||||
},
|
||||
}
|
||||
window.go = {main: {App}}
|
||||
}
|
||||
Reference in New Issue
Block a user