const URGENT_KEYS = ['blood', 'blackOrDarkRed', 'severePain', 'vomiting', 'fever', 'cannotPassGas']; const URGENT_MESSAGE = 'These reported symptoms can need prompt medical care. Contact a clinician or urgent service now; call emergency services for heavy or nonstop bleeding, fainting, or severe worsening symptoms.'; const URGENT_TEXT_PATTERNS = Object.freeze([ ['blood', /\b(?:rectal bleeding|bleeding from (?:the )?(?:rectum|bottom)|blood(?:y)? (?:in|on|with) (?:my |the )?(?:stool|poop|bowel movement)|(?:stool|poop) (?:has|contains|with) blood)\b/i], ['blackOrDarkRed', /\b(?:(?:black|dark[- ]?red) (?:stool|poop|bowel movement)|(?:stool|poop|bowel movement) (?:is|looks?) (?:black|dark[- ]?red))s?\b/i], ['severePain', /\b(?:severe|constant|unrelenting) (?:abdominal|stomach|belly) pain\b/i], ['vomiting', /\b(?:vomit(?:ing|ed|s)?|throw(?:ing|s)? up|threw up|thrown up|puk(?:e|ed|ing|es)|barf(?:ed|ing|s)?|upchuck(?:ed|ing|s)?|toss(?:ed|ing|es)? (?:my|your|his|her|our|their|the) cookies|los(?:e|t|ing|es) (?:my|your|his|her|our|their|the) lunch|(?:i|we|you|he|she|they|someone) (?:(?:have|had|just|already|recently|am|are|was|were|kept) )?(?:hurl(?:s|ed|ing)?|spew(?:s|ed|ing)?)(?=\s*(?:[.!?]|$|again\b|twice\b|all night\b))|emesis)\b/i], ['fever', /\bfever(?:ish)?\b/i], ['cannotPassGas', /\b(?:cannot|can['’]?t|cant|unable to|not able to) pass gas\b/i], ]); export function bucketForBristolType(type) { const value = Number(type); if (value === 1 || value === 2) return 'constipation'; if (value === 3 || value === 4) return 'typical'; if (value >= 5 && value <= 7) return 'loose'; return 'unknown'; } export function detectUrgentFlags(symptoms = {}) { const flags = URGENT_KEYS.filter((key) => symptoms[key] === true); return { urgent: flags.length > 0, flags, message: flags.length ? URGENT_MESSAGE : 'No urgent symptom was selected. This tracker is not a diagnosis; seek care whenever you are worried or symptoms persist.', }; } export function detectUrgentText(text = '') { const flags = URGENT_TEXT_PATTERNS.filter(([, pattern]) => pattern.test(String(text))).map(([key]) => key); return { urgent: flags.length > 0, flags, message: flags.length ? URGENT_MESSAGE : '' }; } export function hasUrgentLedgerContext(entries = []) { return Array.isArray(entries) && entries.some(entry => detectUrgentFlags(entry?.symptoms).urgent || detectUrgentText(entry?.note).urgent); } export const urgentChatMessage = `Pause and get medical help. ${URGENT_MESSAGE}`; export function buildTimmySummary(entries = []) { if (!entries.length) return 'No logs yet. Add one when you are ready and I’ll summarize the pattern—not diagnose it.'; const counts = entries.reduce((acc, entry) => { const bucket = bucketForBristolType(entry.bristolType); acc[bucket] = (acc[bucket] || 0) + 1; return acc; }, {}); const pieces = [`${entries.length} ${entries.length === 1 ? 'log' : 'logs'}`]; if (counts.typical) pieces.push(`${counts.typical} typical`); if (counts.constipation) pieces.push(`${counts.constipation} on the firm side`); if (counts.loose) pieces.push(`${counts.loose} on the loose side`); return `${pieces.join(' · ')}. Patterns matter more than one entry. You choose what to eat; I only help you notice changes.`; } export function sanitizeEntry(input = {}) { const symptoms = {}; for (const key of URGENT_KEYS) symptoms[key] = input.symptoms?.[key] === true; const bristolType = Math.min(7, Math.max(1, Number(input.bristolType) || 4)); return { id: String(input.id || globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`), occurredAt: new Date(input.occurredAt || Date.now()).toISOString(), bristolType, color: ['brown', 'green', 'yellow', 'pale', 'red', 'black'].includes(input.color) ? input.color : 'brown', urgency: Math.min(4, Math.max(0, Number(input.urgency) || 0)), discomfort: Math.min(4, Math.max(0, Number(input.discomfort) || 0)), note: String(input.note || '').trim().slice(0, 500), photoDataUrl: typeof input.photoDataUrl === 'string' && input.photoDataUrl.startsWith('data:image/') ? input.photoDataUrl : '', symptoms, }; } export function photoQualityMessage({ width = 0, height = 0, brightness = 0.5 } = {}) { if (width < 640 || height < 480) return 'Move a little closer or use a higher-resolution photo. The image stays on this device.'; if (brightness < 0.12) return 'Add more light before saving. Timmy only checks whether the photo is usable.'; if (brightness > 0.95) return 'Reduce glare before saving. Timmy only checks whether the photo is usable.'; return 'Ready for your review. Choose the matching Bristol form yourself; Timmy does not interpret the picture.'; } export function exportLedger(entries, exportedAt = new Date().toISOString()) { return JSON.stringify({ product: 'Timmy the Talking Turd', schemaVersion: 1, exportedAt, entries: Array.isArray(entries) ? entries : [], }, null, 2); } export function importLedger(text) { const parsed = JSON.parse(text); if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.entries)) throw new Error('This is not a supported Timmy export.'); return parsed.entries.map(sanitizeEntry); } export const urgentSymptomKeys = Object.freeze([...URGENT_KEYS]);