Spaces:
Paused
Paused
Download web/src/App.jsx from secutorpro/dispatch-console: direct link, hf CLI and curl.
- Browser
- Download file 4.98 kB
-
https://huggingface.co/spaces/secutorpro/dispatch-console/resolve/main/web/src/App.jsx
- Command line
-
hf download hf://spaces/secutorpro/dispatch-console/web/src/App.jsx
-
curl -L -o App.jsx https://huggingface.co/spaces/secutorpro/dispatch-console/resolve/main/web/src/App.jsx
4.98 kB
| import { useCallback, useEffect, useRef, useState } from 'react'; | |
| import { api, getToken, setToken } from './api.js'; | |
| import { DispatchSocket } from './ws.js'; | |
| import { PttPlayer, startCapture, sosBeep } from './audio.js'; | |
| import MapView from './components/MapView.jsx'; | |
| import SosPanel from './components/SosPanel.jsx'; | |
| import MembersPanel from './components/MembersPanel.jsx'; | |
| import PttPanel from './components/PttPanel.jsx'; | |
| import Login from './components/Login.jsx'; | |
| export default function App() { | |
| const [user, setUser] = useState(null); | |
| const [members, setMembers] = useState([]); | |
| const [groups, setGroups] = useState([]); | |
| const [sos, setSos] = useState([]); | |
| const [floors, setFloors] = useState({}); | |
| const [connected, setConnected] = useState(false); | |
| const [tab, setTab] = useState('members'); | |
| const [selectedGroup, setSelectedGroup] = useState(null); | |
| const [talking, setTalking] = useState(false); | |
| const [muted, setMuted] = useState(false); | |
| const sockRef = useRef(null); | |
| const playerRef = useRef(null); | |
| const captureRef = useRef(null); | |
| const mutedRef = useRef(false); | |
| mutedRef.current = muted; | |
| // restore session on reload | |
| useEffect(() => { | |
| if (!getToken()) return; | |
| api.get('/me').then(setUser).catch(() => setToken('')); | |
| }, []); | |
| const startSocket = useCallback(() => { | |
| sockRef.current?.close(); | |
| sockRef.current = new DispatchSocket({ | |
| onStatus: setConnected, | |
| onWelcome: (d) => { | |
| setGroups(d.groups); | |
| setMembers(d.members); | |
| setSos(d.sos); | |
| setFloors(Object.fromEntries(d.floors.map((f) => [f.groupId, f]))); | |
| }, | |
| onMembers: setMembers, | |
| onGroups: setGroups, | |
| onSosNew: (alert) => { | |
| setSos((prev) => [alert, ...prev.filter((a) => a.id !== alert.id)]); | |
| if (!mutedRef.current) sosBeep(4); | |
| }, | |
| onSosUpdate: (alert) => setSos((prev) => prev.map((a) => (a.id === alert.id ? alert : a))), | |
| onFloor: (f) => | |
| setFloors((prev) => { | |
| if (f.memberId == null) { | |
| const { [f.groupId]: _drop, ...rest } = prev; | |
| return rest; | |
| } | |
| return { ...prev, [f.groupId]: f }; | |
| }), | |
| onAudio: ({ memberId, pcm }) => { | |
| if (mutedRef.current || memberId === 0) return; | |
| if (!playerRef.current) playerRef.current = new PttPlayer(); | |
| playerRef.current.play(pcm); | |
| }, | |
| }); | |
| }, []); | |
| useEffect(() => { | |
| if (user && !sockRef.current) startSocket(); | |
| }, [user, startSocket]); | |
| const handleLogin = async (username, password) => { | |
| const { token, user: u } = await api.post('/login', { username, password }); | |
| setToken(token); | |
| setUser(u); | |
| }; | |
| const logout = () => { | |
| sockRef.current?.close(); | |
| sockRef.current = null; | |
| captureRef.current?.stop(); | |
| captureRef.current = null; | |
| setToken(''); | |
| setUser(null); | |
| }; | |
| const beginTalk = async () => { | |
| if (!selectedGroup || talking) return; | |
| try { | |
| captureRef.current = await startCapture((pcm) => sockRef.current?.sendAudio(selectedGroup, 0, pcm)); | |
| sockRef.current?.send({ type: 'ptt_start', groupId: selectedGroup }); | |
| setTalking(true); | |
| } catch (err) { | |
| console.error('could not start microphone:', err); | |
| } | |
| }; | |
| const endTalk = () => { | |
| sockRef.current?.send({ type: 'ptt_stop' }); | |
| captureRef.current?.stop(); | |
| captureRef.current = null; | |
| setTalking(false); | |
| }; | |
| const ackSos = (id) => api.post(`/sos/${id}/ack`).catch((e) => console.error(e)); | |
| const resolveSos = (id) => api.post(`/sos/${id}/resolve`).catch((e) => console.error(e)); | |
| if (!user) return <Login onLogin={handleLogin} />; | |
| const openCount = sos.filter((a) => a.state === 'open').length; | |
| return ( | |
| <div className="app"> | |
| <header> | |
| <div className="brand">Dispatch Console</div> | |
| <div className={`conn ${connected ? 'on' : 'off'}`}>{connected ? 'live' : 'offline'}</div> | |
| <span className="muted">{user.username}</span> | |
| <button className="secondary" onClick={logout}>Log out</button> | |
| </header> | |
| <aside> | |
| <SosPanel sos={sos} openCount={openCount} onAck={ackSos} onResolve={resolveSos} /> | |
| <div className="tabs"> | |
| <button className={tab === 'members' ? 'active' : ''} onClick={() => setTab('members')}>Members</button> | |
| <button className={tab === 'ptt' ? 'active' : ''} onClick={() => setTab('ptt')}>Intercom</button> | |
| </div> | |
| {tab === 'members' ? ( | |
| <MembersPanel members={members} groups={groups} /> | |
| ) : ( | |
| <PttPanel | |
| groups={groups} | |
| floors={floors} | |
| selected={selectedGroup} | |
| setSelected={setSelectedGroup} | |
| onTalkStart={beginTalk} | |
| onTalkStop={endTalk} | |
| talking={talking} | |
| muted={muted} | |
| setMuted={setMuted} | |
| /> | |
| )} | |
| </aside> | |
| <MapView members={members} groups={groups} sos={sos} /> | |
| </div> | |
| ); | |
| } |