- /flow renders the dialog engine's current step as a mock screen (consent,
age, onboarding and scales with progress, mood 0–10, free text, dose
reminder, crisis with hotlines and breathing, safety plan), so every
engine scenario gets the design without duplicating its logic
- today / meds / doctor / profile / messages tabs; navy sidebar and
emergency rail on desktop, emergency block and tab bar on phones
- API: GET /me/{token}/today (today's doses, week marks, next visit, demo
clock time); demo ЕМИАС sign-in seeds the persona's synthetic profile and
prescription from the dashboard fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
2.2 KiB
TypeScript
83 lines
2.2 KiB
TypeScript
import type { AuthMode, Reply } from "../api/types";
|
|
|
|
/*
|
|
* Session persistence behind one small interface. Web uses localStorage; the mobile shell swaps the backend for
|
|
* secure device storage (e.g. Capacitor Preferences / Keychain) without touching screens.
|
|
*/
|
|
|
|
export interface Session {
|
|
token: string;
|
|
mode: AuthMode;
|
|
displayName: string | null;
|
|
}
|
|
|
|
export type ChatEntry =
|
|
| { id: string; from: "assistant"; reply: Reply }
|
|
| { id: string; from: "patient"; text: string };
|
|
|
|
/** Dialog snapshot per token: full history + the latest unanswered batch of engine replies. */
|
|
export interface DialogSnapshot {
|
|
entries: ChatEntry[];
|
|
pending: Reply[];
|
|
unread: number;
|
|
}
|
|
|
|
interface KeyValue {
|
|
get(key: string): string | null;
|
|
set(key: string, value: string): void;
|
|
remove(key: string): void;
|
|
}
|
|
|
|
const browserStorage: KeyValue = {
|
|
get: (k) => {
|
|
try {
|
|
return window.localStorage.getItem(k);
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
set: (k, v) => {
|
|
try {
|
|
window.localStorage.setItem(k, v);
|
|
} catch {
|
|
/* private mode / quota: the session simply is not remembered */
|
|
}
|
|
},
|
|
remove: (k) => {
|
|
try {
|
|
window.localStorage.removeItem(k);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
},
|
|
};
|
|
|
|
const SESSION_KEY = "ptsr.session.v1";
|
|
const dialogKey = (token: string) => `ptsr.dialog.v2.${token}`;
|
|
const EMPTY: DialogSnapshot = { entries: [], pending: [], unread: 0 };
|
|
|
|
function read<T>(storage: KeyValue, key: string): T | null {
|
|
const raw = storage.get(key);
|
|
if (!raw) return null;
|
|
try {
|
|
return JSON.parse(raw) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function createSessionStore(storage: KeyValue = browserStorage) {
|
|
return {
|
|
load: () => read<Session>(storage, SESSION_KEY),
|
|
save: (s: Session) => storage.set(SESSION_KEY, JSON.stringify(s)),
|
|
clear: (s: Session | null, wipeDialog = true) => {
|
|
storage.remove(SESSION_KEY);
|
|
if (s && wipeDialog) storage.remove(dialogKey(s.token));
|
|
},
|
|
loadDialog: (token: string): DialogSnapshot => ({ ...EMPTY, ...read<DialogSnapshot>(storage, dialogKey(token)) }),
|
|
saveDialog: (token: string, snap: DialogSnapshot) => storage.set(dialogKey(token), JSON.stringify(snap)),
|
|
};
|
|
}
|
|
|
|
export const sessionStore = createSessionStore();
|