feat(web): screens from docs/mock — focus flow, today, meds, doctor, profile

- /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>
This commit is contained in:
2026-10-04 08:11:45 +03:00
co-authored by Claude Opus 5.5
parent b8a93ce9d4
commit b4b1bc51c8
28 changed files with 3795 additions and 1770 deletions
+4 -3
View File
@@ -6,7 +6,8 @@ import { sessionStore, type Session } from "./store";
interface SessionApi {
session: Session | null;
signIn: (auth: AuthResult) => void;
signOut: () => void;
/** Leaves the account. Anonymous history is always wiped: it cannot be restored anyway. */
signOut: (opts?: { wipe?: boolean }) => void;
}
const Ctx = createContext<SessionApi | null>(null);
@@ -20,9 +21,9 @@ export function SessionProvider({ children }: { children: ReactNode }) {
setSession(next);
}, []);
const signOut = useCallback(() => {
const signOut = useCallback((opts?: { wipe?: boolean }) => {
setSession((current) => {
sessionStore.clear(current);
sessionStore.clear(current, opts?.wipe ?? current?.mode === "anonymous");
return null;
});
}, []);
+14 -6
View File
@@ -12,9 +12,16 @@ export interface Session {
}
export type ChatEntry =
| { id: string; from: "assistant"; reply: Reply; answered: boolean }
| { 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;
@@ -46,7 +53,8 @@ const browserStorage: KeyValue = {
};
const SESSION_KEY = "ptsr.session.v1";
const chatKey = (token: string) => `ptsr.chat.v1.${token}`;
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);
@@ -62,12 +70,12 @@ 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) => {
clear: (s: Session | null, wipeDialog = true) => {
storage.remove(SESSION_KEY);
if (s) storage.remove(chatKey(s.token));
if (s && wipeDialog) storage.remove(dialogKey(s.token));
},
loadChat: (token: string) => read<ChatEntry[]>(storage, chatKey(token)) ?? [],
saveChat: (token: string, entries: ChatEntry[]) => storage.set(chatKey(token), JSON.stringify(entries)),
loadDialog: (token: string): DialogSnapshot => ({ ...EMPTY, ...read<DialogSnapshot>(storage, dialogKey(token)) }),
saveDialog: (token: string, snap: DialogSnapshot) => storage.set(dialogKey(token), JSON.stringify(snap)),
};
}