From ea9b4a7ce22b7bba429862f2abe7f828bb72f360 Mon Sep 17 00:00:00 2001 From: krystalenby <32627918+krystalenby@users.noreply.github.com> Date: Wed, 10 Jun 2026 04:03:27 -0400 Subject: [PATCH] HotFeat: Repair Crisis Scoring system A hotfix that introduces fixes to the crisis scoring system, preventing it from working as intended, also is added an enhanced system for responding to user message types --- VERSION | 2 +- package.json | 2 +- src/db/schema.ts | 37 +- src/db/validation.ts | 7 +- src/features/compose/LetterComposer.tsx | 179 ++++--- src/features/debug/DebugPanel.tsx | 190 ++++++- src/features/entries/entryService.ts | 14 +- src/features/safety/riskAssessment.ts | 634 ++++++++++++++++-------- src/shared/sync/syncService.ts | 12 +- worker/src/riskAssessment.ts | 176 ++++--- 10 files changed, 854 insertions(+), 399 deletions(-) diff --git a/VERSION b/VERSION index b0f139e..15b989e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.13.7 +1.16.0 diff --git a/package.json b/package.json index 6a1d82c..756d703 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mentell", "private": true, - "version": "1.13.7", + "version": "1.16.0", "type": "module", "scripts": { "sync:assets": "node scripts/sync-assets.mjs", diff --git a/src/db/schema.ts b/src/db/schema.ts index 68f7cd9..2777cde 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -17,9 +17,10 @@ export type EntryRow = { situation: string details: string flaggedTerms: string[] - warningLevel: WarningLevel - riskScore: number - riskLevel: RiskLevel + warningLevel: WarningLevel + riskScore: number + interventionScore: number + riskLevel: RiskLevel scoreDelta: number streakAtSubmit: number } @@ -144,9 +145,9 @@ export class MentellDB extends Dexie { characterAppearance: '&id, updatedAt', }) - this.version(6) - .stores({ - entries: '&id, dateKey, createdAt, updatedAt, sentiment, warningLevel, riskLevel', + this.version(6) + .stores({ + entries: '&id, dateKey, createdAt, updatedAt, sentiment, warningLevel, riskLevel', notes: '&id, createdAt, updatedAt, tag', stickies: '&id, createdAt, updatedAt, zIndex', packages: '&id, kind, periodKey, createdAt, updatedAt, openedAt', @@ -155,11 +156,25 @@ export class MentellDB extends Dexie { .upgrade(async (tx) => { await tx.table('entries').toCollection().modify((row: Partial) => { if (typeof row.riskScore !== 'number') row.riskScore = row.warningLevel === 'warn' ? 0.5 : 0 - if (!row.riskLevel) row.riskLevel = row.warningLevel === 'warn' ? 'elevated' : 'none' - }) - }) - } -} + if (!row.riskLevel) row.riskLevel = row.warningLevel === 'warn' ? 'elevated' : 'none' + }) + }) + + this.version(7) + .stores({ + entries: '&id, dateKey, createdAt, updatedAt, sentiment, warningLevel, riskLevel', + notes: '&id, createdAt, updatedAt, tag', + stickies: '&id, createdAt, updatedAt, zIndex', + packages: '&id, kind, periodKey, createdAt, updatedAt, openedAt', + characterAppearance: '&id, updatedAt', + }) + .upgrade(async (tx) => { + await tx.table('entries').toCollection().modify((row: Partial) => { + if (typeof row.interventionScore !== 'number') row.interventionScore = 0 + }) + }) + } +} let dbInstance: MentellDB | null = null let dbInstanceName: string | null = null diff --git a/src/db/validation.ts b/src/db/validation.ts index bc5558c..58e2938 100644 --- a/src/db/validation.ts +++ b/src/db/validation.ts @@ -16,9 +16,10 @@ export const EntryRowSchema = z.object({ situation: z.string(), details: z.string(), flaggedTerms: z.array(z.string()), - warningLevel: WarningLevelSchema, - riskScore: z.number().min(0).max(1), - riskLevel: RiskLevelSchema, + warningLevel: WarningLevelSchema, + riskScore: z.number().min(0).max(1), + interventionScore: z.number(), + riskLevel: RiskLevelSchema, scoreDelta: z.number().int(), streakAtSubmit: z.number().int().nonnegative(), }) diff --git a/src/features/compose/LetterComposer.tsx b/src/features/compose/LetterComposer.tsx index 4a64bb1..6518ffe 100644 --- a/src/features/compose/LetterComposer.tsx +++ b/src/features/compose/LetterComposer.tsx @@ -1,10 +1,13 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { ProgressLight, type ProgressState } from '../../components/ProgressLight' -import { SentimentPills, type SentimentValue } from '../../components/SentimentPills' -import { dateKeyForLocalDay } from '../../shared/dates' -import { assessLocalRisk, assessRisk, type RiskAssessment } from '../safety/riskAssessment' -import { CrisisResourcePanel } from '../safety/CrisisResourcePanel' -import type { EntryEmotion, RiskLevel } from '../../db/schema' +import React, { useEffect, useMemo, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' +import { ProgressLight, type ProgressState } from '../../components/ProgressLight' +import { SentimentPills, type SentimentValue } from '../../components/SentimentPills' +import { dateKeyForLocalDay } from '../../shared/dates' +import { isDebugMode } from '../../shared/debug/debugFlags' +import { motionDuration, shouldReduceMotion } from '../../shared/motion/useMotionPrefs' +import { assessLocalRisk, assessRisk, type RiskAssessment } from '../safety/riskAssessment' +import { CrisisResourcePanel } from '../safety/CrisisResourcePanel' +import type { EntryEmotion, RiskLevel } from '../../db/schema' type Draft = { dateKey: string @@ -14,19 +17,28 @@ type Draft = { situation: string details: string flaggedTerms: string[] - warningLevel: 'none' | 'warn' - riskScore: number - riskLevel: RiskLevel + warningLevel: 'none' | 'warn' + riskScore: number + interventionScore: number + riskLevel: RiskLevel } -const EMOTION_OPTIONS: Array<{ value: EntryEmotion; label: string }> = [ +const EMOTION_OPTIONS: Array<{ value: EntryEmotion; label: string }> = [ { value: 'happy', label: 'πŸ™‚ Happy' }, { value: 'calm', label: '😌 Calm' }, { value: 'anxious', label: '😟 Anxious' }, { value: 'sad', label: 'πŸ˜” Sad' }, { value: 'angry', label: '😠 Angry' }, - { value: 'other', label: 'πŸ€” None of these fit' }, -] + { value: 'other', label: 'πŸ€” None of these fit' }, +] + +type DebugAiTestFill = { + sentiment: SentimentValue + emotion: EntryEmotion + emotionNote: string + situation: string + details: string +} export function LetterComposer({ onSubmit, @@ -47,8 +59,8 @@ export function LetterComposer({ const [submittedRisk, setSubmittedRisk] = useState(null) const [dateKey, setDateKey] = useState(() => dateKeyForLocalDay(new Date())) - useEffect(() => { - const refreshDateKey = () => setDateKey(dateKeyForLocalDay(new Date())) + useEffect(() => { + const refreshDateKey = () => setDateKey(dateKeyForLocalDay(new Date())) const intervalId = window.setInterval(refreshDateKey, 60_000) window.addEventListener('focus', refreshDateKey) document.addEventListener('visibilitychange', refreshDateKey) @@ -57,7 +69,25 @@ export function LetterComposer({ window.removeEventListener('focus', refreshDateKey) document.removeEventListener('visibilitychange', refreshDateKey) } - }, []) + }, []) + + useEffect(() => { + if (!isDebugMode()) return + const fillDebugAiTest = (event: Event) => { + const detail = (event as CustomEvent).detail + if (!detail) return + setStep('write') + setSentiment(detail.sentiment) + setEmotion(detail.emotion) + setEmotionNote(detail.emotionNote) + setSituation(detail.situation) + setDetails(detail.details) + setDraftRisk(null) + setSubmitState('idle') + } + window.addEventListener('mentell:debug-ai-test-fill', fillDebugAiTest) + return () => window.removeEventListener('mentell:debug-ai-test-fill', fillDebugAiTest) + }, []) const riskInput = useMemo( () => ({ @@ -100,11 +130,12 @@ export function LetterComposer({ emotionNote: emotion === 'other' ? emotionNote.trim() : '', situation: situation.trim(), details: details.trim(), - flaggedTerms: finalRisk.flaggedTerms, - warningLevel: finalRisk.warningLevel, - riskScore: finalRisk.riskScore, - riskLevel: finalRisk.riskLevel, - }) + flaggedTerms: finalRisk.flaggedTerms, + warningLevel: finalRisk.warningLevel, + riskScore: finalRisk.riskScore, + interventionScore: finalRisk.interventionScore, + riskLevel: finalRisk.riskLevel, + }) // Clear inputs after successful submit to make completion obvious. setStep('write') @@ -113,10 +144,11 @@ export function LetterComposer({ setEmotionNote('') setSituation('') setDetails('') + setDraftRisk(null) setSubmitState('done') - if (finalRisk.warningLevel === 'warn' || finalRisk.responseKind) { - setSubmittedRisk(finalRisk) - } + if (finalRisk.responseKind !== 'none') { + setSubmittedRisk(finalRisk) + } window.setTimeout(() => setSubmitState('idle'), 2200) } catch { setSubmitState('error') @@ -244,9 +276,11 @@ export function LetterComposer({ - {submittedRisk ? ( - setSubmittedRisk(null)} /> - ) : null} + + {submittedRisk ? ( + setSubmittedRisk(null)} /> + ) : null} + ) } @@ -267,23 +301,28 @@ function DraftRiskNotice({ risk }: { risk: RiskAssessment }) { ) } -function SupportNotice({ risk }: { risk: RiskAssessment }) { - const celebration = risk.responseKind === 'celebration' - return ( +function SupportNotice({ risk }: { risk: RiskAssessment }) { + const celebration = risk.responseKind === 'positive' + const message = + risk.supportiveMessage ?? + (celebration + ? 'That sounds like something worth noticing. Keep going.' + : 'This sounds like a moment that deserves gentleness.') + return (
{celebration ? 'This deserves a little confetti' : 'A small note for this moment'}
-
{risk.supportiveMessage}
+
{message}
- Risk {risk.riskScore.toFixed(2)} Β· {risk.source} + Intervention {risk.interventionScore.toFixed(1)} Β· {risk.source}
) } function RiskNotice({ risk }: { risk: RiskAssessment }) { - const crisis = risk.riskLevel === 'crisis' + const crisis = risk.responseKind === 'crisis' return (
@@ -292,43 +331,55 @@ function RiskNotice({ risk }: { risk: RiskAssessment }) { : 'I noticed this feels heavy. You are cared about.'}
- {risk.supportiveMessage ?? - (crisis - ? 'If you might be in immediate danger, please contact emergency services or a crisis line now.' - : 'Consider sharing these feelings with someone you trust or a mental health professional.')} + {risk.supportiveMessage ?? + (crisis + ? 'If you might be in immediate danger, please contact emergency services, 988, or someone you trust now. Take one slow breath and stay near another person if you can. This moment can pass.' + : 'Consider sharing these feelings with someone you trust or a mental health professional.')}
- Risk {risk.riskScore.toFixed(2)} Β· {risk.source} -
- {risk.reasons.length ?
Signals: {risk.reasons.join(', ')}
: null} - + Intervention {risk.interventionScore.toFixed(1)} Β· {risk.source} +
+ {risk.reasons.length ?
Signals: {risk.reasons.join(', ')}
: null} + {crisis ? : null} ) } -function RiskResultModal({ risk, onClose }: { risk: RiskAssessment; onClose: () => void }) { - const elevated = risk.warningLevel === 'warn' - const celebration = risk.responseKind === 'celebration' - const titleId = 'risk-result-modal-title' - return ( -
-
+function RiskResultModal({ risk, onClose }: { risk: RiskAssessment; onClose: () => void }) { + const crisis = risk.responseKind === 'crisis' + const support = risk.responseKind === 'support' + const celebration = risk.responseKind === 'positive' + const titleId = 'risk-result-modal-title' + const reduced = shouldReduceMotion() + return ( + +
- {elevated ? 'You are not alone' : celebration ? 'Look at you go' : 'A note for you'} + {crisis ? 'You are not alone' : celebration ? 'Look at you go' : 'A note for you'}
- {elevated - ? 'Mentell noticed this entry may need extra care.' - : celebration - ? 'Mentell noticed a bright patch worth celebrating.' - : 'Mentell found a supportive note after reading your entry.'} + {crisis + ? 'Mentell noticed this entry may need extra care.' + : celebration + ? 'Mentell noticed a bright patch worth celebrating.' + : 'Mentell found a supportive note after reading your entry.'}
- {elevated ? : } + {support || crisis ? : }
-
-
- ) -} + + + ) +} function Field({ label, children }: { label: string; children: React.ReactNode }) { return ( diff --git a/src/features/debug/DebugPanel.tsx b/src/features/debug/DebugPanel.tsx index 39d6bde..f1cf0b7 100644 --- a/src/features/debug/DebugPanel.tsx +++ b/src/features/debug/DebugPanel.tsx @@ -33,10 +33,34 @@ import { } from './debugNotifications' import { dexieDatabaseName, scopedStorageKey } from '../../shared/storage/storageScope' import { normalizeEndpointUrl } from '../compilation/weeklyAiSummary' -import { dateKeyForLocalDay } from '../../shared/dates' -import { notifyLocalDataChanged } from '../../shared/sync/localDataEvents' - -export function DebugPanel() { +import { dateKeyForLocalDay } from '../../shared/dates' +import { notifyLocalDataChanged } from '../../shared/sync/localDataEvents' + +const DEBUG_AI_TESTS = [ + { + id: 'CRISIS', + label: 'SENT_TRIGGER_CRISIS', + reason: 'I am afraid I might hurt myself tonight', + sentiment: '-' as const, + emotion: 'sad' as const, + }, + { + id: 'SUPPORT', + label: 'SENT_TRIGGER_SUPPORT', + reason: 'a negative interaction made me feel overwhelmed and alone', + sentiment: '-' as const, + emotion: 'anxious' as const, + }, + { + id: 'EXEC', + label: 'SENT_TRIGGER_EXEC', + reason: 'I nailed something important and feel proud of myself', + sentiment: '+' as const, + emotion: 'happy' as const, + }, +] + +export function DebugPanel() { const enabled = useMemo(() => isDebugMode(), []) const [open, setOpen] = useState(false) const [busy, setBusy] = useState(false) @@ -108,7 +132,7 @@ export function DebugPanel() { await ensurePackage('weekly', periodKey) } - async function checkAiEndpoint() { + async function checkAiEndpoint() { const endpoint = import.meta.env.VITE_WEEKLY_AI_ENDPOINT if (typeof endpoint !== 'string' || !endpoint.trim()) { setAiEndpointResult('AI endpoint: VITE_WEEKLY_AI_ENDPOINT is not set.') @@ -135,7 +159,22 @@ export function DebugPanel() { } catch (e) { setAiEndpointResult(`AI endpoint check failed: ${e instanceof Error ? e.message : String(e)}`) } - } + } + + function fillAiTest(test: (typeof DEBUG_AI_TESTS)[number]) { + window.dispatchEvent( + new CustomEvent('mentell:debug-ai-test-fill', { + detail: { + sentiment: test.sentiment, + emotion: test.emotion, + emotionNote: '', + situation: `${test.label}(${test.reason})`, + details: 'Debug AI intervention trigger. Submit this letter to exercise the worker classifier.', + }, + }), + ) + setOpen(false) + } useEffect(() => { if (!enabled || !open) return @@ -285,10 +324,31 @@ export function DebugPanel() { ) : null} - - -
-
+
+ +
+
AI response tests
+
+ Fills the composer with debug-only sentinel input. Parentheses become the generated emotion reason. +
+
+ {DEBUG_AI_TESTS.map((test) => ( + + ))} +
+
+ +
+
notifications
- + +