From cd9e029788b5f47a3f749dac9e8c551b5f8da5bc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:54:55 +0000 Subject: [PATCH 1/8] feat: implement bulk submission functionality - Add `~` prefixed backlogged date schema validation and parsing support. - Refactor `LetterComposer` to manage an array of draft inputs. - Include a "More" checkbox toggle for activating bulk mode during the review phase. - Pass backdated date strings gracefully without breaking gamification points/streaks. - Adjust internal IndexedDB array `.between()` fetches across the app to include `~` prefixed keys alongside normal `dateKey` queries. Co-authored-by: kiyarose <75678535+kiyarose@users.noreply.github.com> --- patch_app.py | 8 + patch_app_imports.py | 9 + patch_between.py | 75 + patch_composer.py | 444 ++++++ patch_composer_bug.py | 11 + patch_composer_draft.py | 8 + patch_composer_types.py | 7 + patch_dates.py | 14 + patch_others.py | 42 + patch_projector.py | 7 + patch_timeline.py | 6 + patch_timeline_days.py | 31 + src/App.tsx | 1259 +++++++++-------- src/db/validation.ts | 108 +- .../compilation/weeklyReportExport.ts | 384 ++--- src/features/compilation/weeklyStats.ts | 221 +-- src/features/compose/LetterComposer.tsx | 695 +++++---- src/features/packages/packageGenerator.ts | 89 +- src/features/packages/packageService.ts | 151 +- src/features/safety/riskAssessment.ts | 303 ++-- src/features/score/scoreService.ts | 809 +++++------ src/features/shop/weekTimeline.ts | 121 +- src/shared/dates.ts | 16 +- 23 files changed, 2836 insertions(+), 1982 deletions(-) create mode 100644 patch_app.py create mode 100644 patch_app_imports.py create mode 100644 patch_between.py create mode 100644 patch_composer.py create mode 100644 patch_composer_bug.py create mode 100644 patch_composer_draft.py create mode 100644 patch_composer_types.py create mode 100644 patch_dates.py create mode 100644 patch_others.py create mode 100644 patch_projector.py create mode 100644 patch_timeline.py create mode 100644 patch_timeline_days.py diff --git a/patch_app.py b/patch_app.py new file mode 100644 index 0000000..9b94fb4 --- /dev/null +++ b/patch_app.py @@ -0,0 +1,8 @@ +with open('src/App.tsx', 'r') as f: + content = f.read() + +# We need to type the parameter `drafts` correctly if it's an array, +# wait, `onSubmit` in LetterComposer gives an array of `Draft`. +# Then we map each `draft` to `upsertEntryFromDraft`. +# TypeScript complains that we don't pass all properties, which means `draft` is maybe considered as a string? +# Let's see the error again. diff --git a/patch_app_imports.py b/patch_app_imports.py new file mode 100644 index 0000000..4fc753c --- /dev/null +++ b/patch_app_imports.py @@ -0,0 +1,9 @@ +with open('src/App.tsx', 'r') as f: + content = f.read() + +# Make sure App.tsx knows the type of `drafts` +# Actually `LetterComposer` infers it correctly from the component prop type. +# Let's inspect the exact error: +# src/App.tsx(575,40): error TS2345: Argument of type '{ dateKey: string; scoreDelta: number; streakAtSubmit: number; length: number; toString(): string; ... }' is not assignable to parameter of type 'EntryDraft'. +# Why is `length` and `toString` there? Because `drafts` might be a string if we didn't update the `onSubmit` parameter properly in `LetterComposer.tsx`. Wait, `Draft[]` doesn't have `length` and `toString` as properties of the element! But an array does. +# Oh! `draft` in the loop is iterating over `drafts`, but maybe TypeScript infers `drafts` as something else? diff --git a/patch_between.py b/patch_between.py new file mode 100644 index 0000000..1e08a66 --- /dev/null +++ b/patch_between.py @@ -0,0 +1,75 @@ +# weeklyReportExport.ts +with open('src/features/compilation/weeklyReportExport.ts', 'r') as f: + content = f.read() +# "return getDb().entries.where('dateKey').between(startKey, endKey, true, true).toArray()" +# Replace it with fetching both and combining. +old1 = "return getDb().entries.where('dateKey').between(startKey, endKey, true, true).toArray()" +new1 = """const entriesNorm = await getDb().entries.where('dateKey').between(startKey, endKey, true, true).toArray() + const entriesBulk = await getDb().entries.where('dateKey').between('~' + startKey, '~' + endKey, true, true).toArray() + return [...entriesNorm, ...entriesBulk]""" +content = content.replace(old1, new1) + +old2 = """ return getDb().entries + .where('dateKey') + .between(toDateKey(weekStart), endKey, true, true) + .toArray()""" +new2 = """ const entriesNorm = await getDb().entries.where('dateKey').between(toDateKey(weekStart), endKey, true, true).toArray() + const entriesBulk = await getDb().entries.where('dateKey').between('~' + toDateKey(weekStart), '~' + endKey, true, true).toArray() + return [...entriesNorm, ...entriesBulk]""" +content = content.replace(old2, new2) +with open('src/features/compilation/weeklyReportExport.ts', 'w') as f: + f.write(content) + +# weeklyStats.ts +with open('src/features/compilation/weeklyStats.ts', 'r') as f: + content = f.read() +old3 = """ const entries = ( + await getDb().entries.where('dateKey').between(startKey, endKey, true, true).toArray() + ).sort((a, b) => b.createdAt - a.createdAt)""" +new3 = """ const entriesNorm = await getDb().entries.where('dateKey').between(startKey, endKey, true, true).toArray() + const entriesBulk = await getDb().entries.where('dateKey').between('~' + startKey, '~' + endKey, true, true).toArray() + const entries = [...entriesNorm, ...entriesBulk].sort((a, b) => b.createdAt - a.createdAt)""" +content = content.replace(old3, new3) +with open('src/features/compilation/weeklyStats.ts', 'w') as f: + f.write(content) + +# weekTimeline.ts +with open('src/features/shop/weekTimeline.ts', 'r') as f: + content = f.read() +old4 = """ const entries = await getDb() + .entries.where('dateKey') + .between(toDateKey(weekStart), weekEndKey, true, true) + .toArray()""" +new4 = """ const entriesNorm = await getDb().entries.where('dateKey').between(toDateKey(weekStart), weekEndKey, true, true).toArray() + const entriesBulk = await getDb().entries.where('dateKey').between('~' + toDateKey(weekStart), '~' + weekEndKey, true, true).toArray() + const entries = [...entriesNorm, ...entriesBulk]""" +content = content.replace(old4, new4) +with open('src/features/shop/weekTimeline.ts', 'w') as f: + f.write(content) + +# riskAssessment.ts +with open('src/features/safety/riskAssessment.ts', 'r') as f: + content = f.read() +old5 = "return getDb().entries.where('dateKey').between(startKey, dateKey, true, true).toArray()" +new5 = """const entriesNorm = await getDb().entries.where('dateKey').between(startKey, dateKey, true, true).toArray() + const entriesBulk = await getDb().entries.where('dateKey').between('~' + startKey, '~' + dateKey, true, true).toArray() + return [...entriesNorm, ...entriesBulk]""" +content = content.replace(old5, new5) +old6 = "const rows = await getDb().entries.where('dateKey').between(startKey, dateKey, true, true).toArray()" +new6 = """const rowsNorm = await getDb().entries.where('dateKey').between(startKey, dateKey, true, true).toArray() + const rowsBulk = await getDb().entries.where('dateKey').between('~' + startKey, '~' + dateKey, true, true).toArray() + const rows = [...rowsNorm, ...rowsBulk]""" +content = content.replace(old6, new6) +with open('src/features/safety/riskAssessment.ts', 'w') as f: + f.write(content) + +# packageGenerator.ts +with open('src/features/packages/packageGenerator.ts', 'r') as f: + content = f.read() +old7 = "const count = await getDb().entries.where('dateKey').between(startKey, endKey, true, true).count()" +new7 = """const countNorm = await getDb().entries.where('dateKey').between(startKey, endKey, true, true).count() + const countBulk = await getDb().entries.where('dateKey').between('~' + startKey, '~' + endKey, true, true).count() + const count = countNorm + countBulk""" +content = content.replace(old7, new7) +with open('src/features/packages/packageGenerator.ts', 'w') as f: + f.write(content) diff --git a/patch_composer.py b/patch_composer.py new file mode 100644 index 0000000..1e4c8e8 --- /dev/null +++ b/patch_composer.py @@ -0,0 +1,444 @@ +import re + +with open('src/features/compose/LetterComposer.tsx', 'r') as f: + content = f.read() + +# We need to change LetterComposer to manage an array of drafts instead of one. +# But `draftRisk` and `submittedRisk` must also be handled. + +new_component = """ +export type Timeframe = 'just now' | 'an hour ago' | 'yesterday' + +export type DraftInputState = { + id: string + sentiment: SentimentValue + emotion: EntryEmotion + emotionNote: string + situation: string + details: string + timeframe: Timeframe +} + +export function LetterComposer({ + onSubmit, + disabled = false, +}: { + onSubmit: (drafts: Draft[]) => Promise | void + disabled?: boolean +}) { + const [step, setStep] = useState<'write' | 'review'>('write') + + const createDraftInput = (): DraftInputState => ({ + id: Math.random().toString(36).slice(2), + sentiment: '+', + emotion: 'happy', + emotionNote: '', + situation: '', + details: '', + timeframe: 'just now', + }) + + const [draftInputs, setDraftInputs] = useState([createDraftInput()]) + const [isBulkMode, setIsBulkMode] = useState(false) + + const [isSubmitting, setIsSubmitting] = useState(false) + const [submitState, setSubmitState] = useState<'idle' | 'done' | 'error'>('idle') + const [draftRisk, setDraftRisk] = useState | null>(null) + const [submittedRisk, setSubmittedRisk] = useState(null) + const [dateKey, setDateKey] = useState(() => 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) + return () => { + window.clearInterval(intervalId) + 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') + setDraftInputs([ + { + id: Math.random().toString(36).slice(2), + sentiment: detail.sentiment, + emotion: detail.emotion, + emotionNote: detail.emotionNote, + situation: detail.situation, + details: detail.details, + timeframe: 'just now', + }, + ]) + setIsBulkMode(false) + setDraftRisk(null) + setSubmitState('idle') + } + window.addEventListener('mentell:debug-ai-test-fill', fillDebugAiTest) + return () => window.removeEventListener('mentell:debug-ai-test-fill', fillDebugAiTest) + }, []) + + // Assess risk for the first draft (or all of them? just the first is fine for UI indicator, + // but we assess all on submit anyway. Let's just do the first one for the live light) + const primaryRiskInput = useMemo( + () => ({ + dateKey, + sentiment: draftInputs[0].sentiment, + emotion: draftInputs[0].emotion, + emotionNote: draftInputs[0].emotionNote, + situation: draftInputs[0].situation, + details: draftInputs[0].details, + }), + [dateKey, draftInputs], + ) + + useEffect(() => { + let active = true + const id = window.setTimeout(() => { + const next = assessDraftRisk(primaryRiskInput) + if (active) setDraftRisk(next) + }, 250) + return () => { + active = false + window.clearTimeout(id) + } + }, [primaryRiskInput]) + + const progressState: ProgressState = + draftRisk?.warningLevel === 'warn' ? 'warn' : step === 'review' ? 'review' : 'write' + + const updateDraft = (id: string, updates: Partial) => { + setDraftInputs((prev) => + prev.map((draft) => (draft.id === id ? { ...draft, ...updates } : draft)) + ) + } + + const addDraft = () => { + setDraftInputs((prev) => [...prev, createDraftInput()]) + } + + async function handleSubmit() { + if (disabled || isSubmitting) return + setIsSubmitting(true) + setSubmitState('idle') + setSubmittedRisk(null) + try { + const finalDrafts: Draft[] = [] + let highestRisk: RiskAssessment | null = null + + for (const draft of draftInputs) { + let draftDateKey = dateKey + if (draft.timeframe === 'yesterday') { + // We need a helper to get yesterday. We can import subDays from date-fns + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + draftDateKey = '~' + dateKeyForLocalDay(yesterday) + } else if (draft.timeframe === 'an hour ago') { + draftDateKey = dateKey + } + + const riskInputForDraft = { + dateKey: draftDateKey, + sentiment: draft.sentiment, + emotion: draft.emotion, + emotionNote: draft.emotionNote, + situation: draft.situation, + details: draft.details, + } + + const finalRisk = await assessRisk(riskInputForDraft) + + if (!highestRisk || finalRisk.riskScore > highestRisk.riskScore) { + highestRisk = finalRisk + } + + finalDrafts.push({ + dateKey: draftDateKey, + sentiment: draft.sentiment, + emotion: draft.emotion, + emotionNote: draft.emotion === 'other' ? draft.emotionNote.trim() : '', + situation: draft.situation.trim(), + details: draft.details.trim(), + flaggedTerms: finalRisk.flaggedTerms, + warningLevel: finalRisk.warningLevel, + riskScore: finalRisk.riskScore, + interventionScore: finalRisk.interventionScore, + riskLevel: finalRisk.riskLevel, + }) + } + + await onSubmit(finalDrafts) + + setStep('write') + setDraftInputs([createDraftInput()]) + setIsBulkMode(false) + setDraftRisk(null) + setSubmitState('done') + if ( + highestRisk && + (highestRisk.responseKind === 'crisis' || + (highestRisk.responseKind !== 'none' && highestRisk.supportiveMessage?.trim())) + ) { + setSubmittedRisk(highestRisk) + } + window.setTimeout(() => setSubmitState('idle'), 2200) + } catch { + setSubmitState('error') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+ + +
+
+
+
{isBulkMode ? 'Bulk Submission' : 'Today’s letter'}
+
Date: {dateKey}
+
+ disabled ? {} : updateDraft(draftInputs[0].id, { sentiment: val })} /> +
+ +
+ + updateDraft(draftInputs[0].id, { situation: e.target.value })} + placeholder="What happened?" + /> + + + +