1262 lines
35 KiB
Markdown
1262 lines
35 KiB
Markdown
# Inou Intake System — Design Specification
|
|
|
|
## Vision
|
|
|
|
Transform how non-technical users capture health data. No forms. No configuration. Just conversation that crystallizes into structure.
|
|
|
|
The user talks, the system learns. Yesterday's input becomes today's smart prompt with previous values pre-filled.
|
|
|
|
---
|
|
|
|
## Core Principles
|
|
|
|
| Principle | Meaning |
|
|
|-----------|---------|
|
|
| **Intake AI, not advice AI** | We capture, structure, and ask smart follow-ups. We never diagnose or recommend treatment. |
|
|
| **Complexity on server, simplicity on client** | All intelligence lives server-side. Apps are dumb renderers. |
|
|
| **Templates emerge, not configured** | User describes leg trainer once → system learns the shape → next time shows fields with previous values. |
|
|
| **Conversation, not forms** | Onboarding isn't a moment, it's a relationship. Learn one thing at a time, naturally. |
|
|
| **Show before save** | Always confirm what was understood. Build trust. |
|
|
|
|
---
|
|
|
|
## Phase 1: Server + Ugly UI
|
|
|
|
### Goal
|
|
Fully functional intake system with web-based interface. Prove the model works before investing in pretty UI or mobile.
|
|
|
|
### Deliverables
|
|
|
|
#### 1. Data Model Extensions
|
|
|
|
**Prompt table:**
|
|
```sql
|
|
prompts (
|
|
prompt_id INTEGER PRIMARY KEY,
|
|
dossier_id INTEGER NOT NULL,
|
|
category TEXT, -- "medication", "vital", "exercise", "symptom"
|
|
type TEXT, -- "Lisinopril", "BP", "leg_trainer"
|
|
question TEXT, -- "Did you take your morning meds?"
|
|
|
|
-- Scheduling
|
|
frequency TEXT, -- "daily", "weekly:mon,wed,fri", "once", "until_resolved"
|
|
time_of_day TEXT, -- "08:00", "12:00", "21:00", NULL for anytime
|
|
next_ask INTEGER, -- epoch: next time to show
|
|
expires_at INTEGER, -- epoch: stop asking after this (0 = never)
|
|
|
|
-- Input definition
|
|
input_type TEXT, -- "checkbox", "checkbox_group", "number", "text", "scale", "custom"
|
|
input_config TEXT, -- JSON: field definitions, scale labels
|
|
|
|
-- Grouping
|
|
group_name TEXT, -- "morning_meds", "gym_session" — for collapsing
|
|
|
|
-- Source/linking
|
|
trigger_entry INTEGER, -- entry that spawned this (for follow-ups)
|
|
created_by INTEGER, -- dossier_id of who created (self, caregiver)
|
|
|
|
-- Last response (for pre-filling)
|
|
last_response TEXT, -- JSON: parsed/structured answer
|
|
last_response_raw TEXT, -- what they actually said/typed
|
|
last_response_at INTEGER, -- epoch
|
|
|
|
-- State
|
|
dismissed INTEGER DEFAULT 0, -- 1 = "don't ask again"
|
|
active INTEGER DEFAULT 1, -- 1 = active, 0 = paused
|
|
|
|
created_at INTEGER,
|
|
updated_at INTEGER
|
|
)
|
|
|
|
CREATE INDEX idx_prompts_dossier ON prompts(dossier_id);
|
|
CREATE INDEX idx_prompts_dossier_active ON prompts(dossier_id, active, next_ask);
|
|
```
|
|
|
|
### input_config Field Schema
|
|
|
|
The `input_config` column stores a JSON object defining form fields. The frontend renders these directly — no AI interpretation needed.
|
|
|
|
**Structure:**
|
|
```json
|
|
{
|
|
"fields": [
|
|
{
|
|
"key": "duration_min",
|
|
"label": "Duration",
|
|
"type": "number",
|
|
"datatype": "int",
|
|
"min": 1,
|
|
"max": 120,
|
|
"unit": "min",
|
|
"required": true
|
|
},
|
|
{
|
|
"key": "intensity",
|
|
"label": "How hard was it?",
|
|
"type": "select",
|
|
"datatype": "string",
|
|
"options": ["easy", "moderate", "hard"],
|
|
"required": true
|
|
},
|
|
{
|
|
"key": "exercises",
|
|
"label": "Exercises",
|
|
"type": "multiselect",
|
|
"datatype": "string[]",
|
|
"options": ["bench", "squat", "deadlift", "treadmill"],
|
|
"required": false
|
|
},
|
|
{
|
|
"key": "notes",
|
|
"label": "Notes",
|
|
"type": "text",
|
|
"datatype": "string",
|
|
"maxlength": 500,
|
|
"required": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Field types (what to render):**
|
|
|
|
| type | datatype | renders as |
|
|
|------|----------|------------|
|
|
| `text` | string | text input |
|
|
| `textarea` | string | multiline |
|
|
| `number` | int / float | number input with +/- |
|
|
| `checkbox` | bool | single checkbox |
|
|
| `select` | string | dropdown |
|
|
| `multiselect` | string[] | checkboxes or multi-picker |
|
|
| `scale` | int | 1-5 or 1-10 buttons |
|
|
| `time` | string | time picker (HH:MM) |
|
|
| `date` | string | date picker |
|
|
|
|
**Field properties:**
|
|
|
|
| property | applies to | meaning |
|
|
|----------|-----------|--------|
|
|
| `key` | all | JSON key in response |
|
|
| `label` | all | display label |
|
|
| `type` | all | widget type |
|
|
| `datatype` | all | validation type |
|
|
| `required` | all | must fill |
|
|
| `default` | all | pre-fill value (overridden by last_response) |
|
|
| `min` | number | minimum value |
|
|
| `max` | number | maximum value |
|
|
| `unit` | number | suffix to display ("°", "min", "lbs") |
|
|
| `step` | number | increment (0.1 for float) |
|
|
| `maxlength` | text | character limit |
|
|
| `options` | select, multiselect | choices |
|
|
| `scale_labels` | scale | e.g. ["terrible", "poor", "ok", "good", "great"] |
|
|
|
|
**Response handling:**
|
|
|
|
When rendering a prompt, the frontend:
|
|
1. Reads `input_config.fields`
|
|
2. For each field, checks `last_response[key]`
|
|
3. Pre-fills if exists
|
|
4. User submits → stored as new `last_response`
|
|
|
|
**Entry table additions:**
|
|
```sql
|
|
-- Existing entry table, ensure these fields:
|
|
entries (
|
|
...
|
|
parent_id INTEGER, -- for nested entries (gym session → exercises)
|
|
data TEXT, -- JSON blob for structured details + raw input
|
|
...
|
|
)
|
|
```
|
|
|
|
**Dossier table additions (now live):**
|
|
```sql
|
|
-- Added to dossiers table:
|
|
weight_unit TEXT, -- "kg" or "lbs"
|
|
height_unit TEXT, -- "cm" or "ft"
|
|
last_pull_at INTEGER, -- epoch: last time app fetched prompts
|
|
is_provider INTEGER DEFAULT 0, -- 1 if healthcare provider
|
|
provider_name TEXT, -- practice/clinic name
|
|
away_message TEXT, -- auto-responder text
|
|
away_enabled INTEGER DEFAULT 0 -- 1 if auto-responder active
|
|
-- (language already exists on dossier)
|
|
```
|
|
|
|
#### 2. API Endpoints
|
|
|
|
**GET /api/prompts?dossier=X**
|
|
|
|
Returns prompts due now for this dossier.
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"prompts": [
|
|
{
|
|
"id": "abc123",
|
|
"category": "medication",
|
|
"type": "morning_meds",
|
|
"question": "Did you take your morning medications?",
|
|
"input_type": "checkbox_group",
|
|
"input_config": {
|
|
"items": [
|
|
{ "name": "Lisinopril", "amount": "10mg" },
|
|
{ "name": "Metoprolol", "amount": "25mg" }
|
|
]
|
|
},
|
|
"group_name": "morning_meds",
|
|
"group_count": 2,
|
|
"actions": ["took_all", "missed_some", "skip"]
|
|
},
|
|
{
|
|
"id": "def456",
|
|
"category": "exercise",
|
|
"type": "leg_trainer",
|
|
"question": "Leg trainer (morning)",
|
|
"input_type": "custom",
|
|
"input_config": {
|
|
"fields": [
|
|
{ "name": "left_min", "label": "Left min", "type": "number", "previous": 20 },
|
|
{ "name": "left_angle", "label": "Left °", "type": "number", "previous": 30 },
|
|
{ "name": "left_speed", "label": "Left speed", "type": "number", "previous": 4 },
|
|
{ "name": "right_min", "label": "Right min", "type": "number", "previous": 20 },
|
|
{ "name": "right_angle", "label": "Right °", "type": "number", "previous": 42 },
|
|
{ "name": "right_speed", "label": "Right speed", "type": "number", "previous": 5 }
|
|
],
|
|
"notes_field": true
|
|
},
|
|
"actions": ["save", "skip"]
|
|
},
|
|
{
|
|
"id": "ghi789",
|
|
"category": "followup",
|
|
"type": "symptom_check",
|
|
"question": "Yesterday you had a headache. How are you today?",
|
|
"input_type": "scale",
|
|
"input_config": {
|
|
"options": ["Much better", "Same", "Worse"]
|
|
},
|
|
"actions": ["select", "skip"]
|
|
},
|
|
{
|
|
"id": "open",
|
|
"category": "general",
|
|
"type": "anything_else",
|
|
"question": "Anything else you'd like to mention?",
|
|
"input_type": "freeform",
|
|
"input_config": {
|
|
"placeholder": "Voice, text, or photo...",
|
|
"allow_photo": true,
|
|
"allow_voice": true
|
|
},
|
|
"actions": ["save", "nothing"]
|
|
}
|
|
],
|
|
"for_dossiers": [
|
|
{ "id": "xxx", "name": "Johan", "relation": "self" },
|
|
{ "id": "yyy", "name": "Sophia", "relation": "daughter" },
|
|
{ "id": "zzz", "name": "Mama", "relation": "mother-in-law" }
|
|
]
|
|
}
|
|
```
|
|
|
|
**POST /api/prompts/respond**
|
|
|
|
Submit response to a prompt.
|
|
|
|
Request:
|
|
```json
|
|
{
|
|
"prompt_id": "abc123",
|
|
"dossier_id": "xxx",
|
|
"action": "took_all",
|
|
"response": null
|
|
}
|
|
```
|
|
|
|
Or for custom input:
|
|
```json
|
|
{
|
|
"prompt_id": "def456",
|
|
"dossier_id": "yyy",
|
|
"action": "save",
|
|
"response": {
|
|
"left_min": 15,
|
|
"left_angle": 25,
|
|
"left_speed": 3,
|
|
"right_min": 15,
|
|
"right_angle": 35,
|
|
"right_speed": 4,
|
|
"notes": "She was tired today"
|
|
}
|
|
}
|
|
```
|
|
|
|
Returns:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"entries_created": ["entry_id_1", "entry_id_2"],
|
|
"prompt_updated": true
|
|
}
|
|
```
|
|
|
|
**POST /api/intake/parse**
|
|
|
|
Parse freeform input (text, voice transcript, photo).
|
|
|
|
Request:
|
|
```json
|
|
{
|
|
"dossier_id": "xxx",
|
|
"input_type": "text",
|
|
"content": "Sophia did leg trainer 20 min, left 30 degrees speed 4, right 40 degrees speed 5",
|
|
"attachments": []
|
|
}
|
|
```
|
|
|
|
Or with photo:
|
|
```json
|
|
{
|
|
"dossier_id": "xxx",
|
|
"input_type": "photo",
|
|
"content": null,
|
|
"attachments": [
|
|
{ "type": "image", "data": "base64..." }
|
|
],
|
|
"context": "supplement_bottle"
|
|
}
|
|
```
|
|
|
|
Returns parsed interpretation for confirmation:
|
|
```json
|
|
{
|
|
"interpretation": {
|
|
"target_dossier": { "id": "yyy", "name": "Sophia", "confidence": 0.95 },
|
|
"entries": [
|
|
{
|
|
"category": "exercise",
|
|
"type": "leg_trainer",
|
|
"display": "Leg trainer — 20 min",
|
|
"data": {
|
|
"duration_min": 20,
|
|
"left": { "angle": 30, "speed": 4 },
|
|
"right": { "angle": 40, "speed": 5 }
|
|
}
|
|
}
|
|
],
|
|
"follow_up_question": null,
|
|
"suggested_prompt": {
|
|
"question": "You log leg trainer regularly. Want me to remind you?",
|
|
"options": ["Yes, daily", "Yes, twice daily", "No thanks"]
|
|
}
|
|
},
|
|
"raw": "Sophia did leg trainer 20 min, left 30 degrees speed 4, right 40 degrees speed 5"
|
|
}
|
|
```
|
|
|
|
**POST /api/intake/confirm**
|
|
|
|
Confirm parsed interpretation, create entries.
|
|
|
|
Request:
|
|
```json
|
|
{
|
|
"interpretation_id": "temp_xxx",
|
|
"confirmed_dossier": "yyy",
|
|
"entries": [...],
|
|
"create_prompt": {
|
|
"answer": "Yes, twice daily"
|
|
}
|
|
}
|
|
```
|
|
|
|
**POST /api/intake/ask**
|
|
|
|
Handle intake AI asking follow-up questions.
|
|
|
|
Request:
|
|
```json
|
|
{
|
|
"dossier_id": "xxx",
|
|
"context": "unit_clarification",
|
|
"question_type": "weight_unit",
|
|
"user_input": "80"
|
|
}
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"question": "Is that kg or lbs?",
|
|
"options": ["kg", "lbs"],
|
|
"store_preference": true
|
|
}
|
|
```
|
|
|
|
**GET /api/summary?dossier=X&period=30d**
|
|
|
|
Generate summary for doctor visit.
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"dossier": { "name": "Sophia", "dob": "...", "sex": "female" },
|
|
"period": "last 30 days",
|
|
"sections": {
|
|
"medications": [],
|
|
"therapy": [
|
|
{
|
|
"name": "Leg trainer",
|
|
"frequency": "2x daily",
|
|
"compliance": "12/14 sessions",
|
|
"typical": "L 30° speed 4, R 40° speed 5",
|
|
"trend": "Right side improving (was 35° in December)"
|
|
}
|
|
],
|
|
"symptoms": [
|
|
{ "date": "Dec 28", "type": "headache", "resolution": "resolved after 2 days" }
|
|
],
|
|
"vitals": {
|
|
"weight": { "start": "32kg", "end": "32.5kg" }
|
|
},
|
|
"imaging": [...],
|
|
"labs": [...],
|
|
"notes": [
|
|
"Tired after school most days",
|
|
"Slept poorly Tuesday, better Wednesday"
|
|
]
|
|
},
|
|
"markdown": "...",
|
|
"shareable_link": "https://inou.com/s/xxxxx"
|
|
}
|
|
```
|
|
|
|
#### 3. Intake AI Logic (Server-side)
|
|
|
|
**Parsing engine:**
|
|
- Receives text/voice transcript/photo
|
|
- Identifies target dossier from context ("Sophia", "my son", "I")
|
|
- Extracts structured data
|
|
- Detects patterns that could become prompts
|
|
- Generates follow-up questions when needed
|
|
|
|
**Prompt generation:**
|
|
- From explicit statements: "I take vitamin D every morning" → daily prompt
|
|
- From repeated behavior: 5 similar entries → suggest prompt
|
|
- From concerning entries: "running stool" → follow-up tomorrow
|
|
|
|
**Template learning:**
|
|
- First entry with structure → store shape in prompt's input_config
|
|
- Next time → pre-fill with previous values
|
|
- Evolves as user input evolves
|
|
|
|
**Multi-dossier routing:**
|
|
- Parse input for person references
|
|
- Match against accessible dossiers
|
|
- Require confirmation if ambiguous
|
|
- Default to last-used dossier if truly unclear
|
|
|
|
#### 4. Onboarding Flows
|
|
|
|
Not a wizard. A series of prompts that appear naturally.
|
|
|
|
**Initial (account creation):**
|
|
- Name, DOB, sex — required
|
|
- That's it. Done.
|
|
|
|
**Day 1-7 (prompted when they return):**
|
|
- "What brings you to inou?" — capture chief concern
|
|
- "Do you take any daily medications?" — yes → capture list
|
|
- "Any supplements?" — yes → capture list
|
|
- "Anything you're tracking? Weight, blood pressure, exercise?"
|
|
|
|
**Week 2+ (contextual):**
|
|
- After first lab upload: "Any conditions we should know about?"
|
|
- After logging same thing 5x: "Want me to remind you?"
|
|
- After 2 weeks: "It helps to know family health history. Mind sharing?"
|
|
|
|
**Family history flow:**
|
|
- "Is your mother still living?" → [Yes] [No]
|
|
- If no: "How old? What happened?" → captures cause
|
|
- If yes: "Any health conditions?" → open field
|
|
- Same for father, siblings
|
|
- "Anything that runs in the family?" → open field
|
|
|
|
#### 5. Ugly Web UI
|
|
|
|
Minimal but functional. Prove the model.
|
|
|
|
**Dashboard:**
|
|
- List of prompts due today
|
|
- Grouped where applicable ("Morning meds (8)")
|
|
- Each renders based on input_type
|
|
- "Anything else" always at bottom
|
|
|
|
**Prompt rendering:**
|
|
- checkbox_group: checkboxes + "Took all" button
|
|
- number: input field with unit
|
|
- scale: buttons with labels
|
|
- custom: dynamic fields from input_config
|
|
- freeform: text area + photo upload + mic button (browser speech-to-text)
|
|
|
|
**Confirmation modal:**
|
|
- Shows parsed interpretation
|
|
- Allows dossier change if wrong
|
|
- Edit button for corrections
|
|
- Save / Cancel
|
|
|
|
**History view:**
|
|
- Today's entries
|
|
- Past 7 days
|
|
- Searchable
|
|
- Editable (tap to modify)
|
|
|
|
**Summary view:**
|
|
- Generate summary for selected period
|
|
- Copy as text
|
|
- Print
|
|
- Share link
|
|
|
|
---
|
|
|
|
## Phase 2: Pretty UI + Mobile
|
|
|
|
### Goal
|
|
Native mobile experience with Flutter. Beautiful UI. Local capabilities (speech-to-text, barcode, camera).
|
|
|
|
### Deliverables
|
|
|
|
#### 1. Flutter App
|
|
|
|
**Architecture:**
|
|
- Calls same API endpoints
|
|
- Local speech-to-text (send transcript to server)
|
|
- Local barcode scanning (send code to server for lookup)
|
|
- Local OCR first pass (send text + image if low confidence)
|
|
- Local notifications scheduled from prompt data
|
|
|
|
**Pull model:**
|
|
- On app open: GET /api/prompts
|
|
- Background refresh: once daily
|
|
- Store prompts locally for offline
|
|
- Queue responses if offline, sync when connected
|
|
|
|
**Push fallback:**
|
|
- Server tracks last_pull_at per dossier
|
|
- If no pull in 3+ days AND prompts due → push notification
|
|
- Push just says "Don't forget to log" — opens app → pull → render
|
|
|
|
**Screens:**
|
|
|
|
*Home (Prompts):*
|
|
- Cards for each due prompt
|
|
- Swipe to skip
|
|
- Tap to expand/respond
|
|
- Grouped cards for multi-item (morning meds)
|
|
- Always: "Anything else?" at bottom
|
|
|
|
*Input:*
|
|
- Big mic button for voice
|
|
- Camera for photos (bottle labels, wounds, readings)
|
|
- Text field
|
|
- Confirmation before save
|
|
|
|
*Log (History):*
|
|
- Timeline view
|
|
- Filter by category, dossier
|
|
- Tap to edit
|
|
|
|
*Summary:*
|
|
- Pick dossier, pick period
|
|
- Generate and display
|
|
- Share / Export
|
|
|
|
*Dossiers:*
|
|
- Switch between accessible dossiers
|
|
- Default for logging (remembers last used)
|
|
|
|
#### 2. Notification System
|
|
|
|
**Local notifications:**
|
|
- App schedules based on prompt times
|
|
- "Morning meds" at 8:00
|
|
- "Leg trainer (evening)" at 18:00
|
|
- Tapping opens app directly to that prompt
|
|
|
|
**Server push:**
|
|
- Firebase Cloud Messaging
|
|
- Only when app hasn't pulled in days
|
|
- Generic "Check in with inou"
|
|
|
|
#### 3. Offline Support
|
|
|
|
**Cached data:**
|
|
- Prompts (with input_config, previous values)
|
|
- Recent entries
|
|
- Dossier list
|
|
|
|
**Queued responses:**
|
|
- If offline, store locally
|
|
- Sync on reconnect
|
|
- Show "pending" indicator
|
|
|
|
#### 4. Voice Flow
|
|
|
|
**User taps mic:**
|
|
1. Device speech-to-text (on-device)
|
|
2. Transcript sent to server: POST /api/intake/parse
|
|
3. Server parses, returns interpretation
|
|
4. App shows confirmation
|
|
5. User confirms → entry created
|
|
|
|
**Natural language examples:**
|
|
- "Sophia did leg trainer, 20 minutes, left 30 degrees speed 4, right 40 degrees speed 5"
|
|
- "I took 2 Tylenol for a headache"
|
|
- "Mom took her pills"
|
|
- "Blood pressure 120 over 80"
|
|
- "Walked for an hour this morning"
|
|
|
|
#### 5. Photo Flow
|
|
|
|
**Supplement bottle:**
|
|
1. User taps camera on "Any supplements?" prompt
|
|
2. Takes photo of bottle
|
|
3. Device OCR extracts text (or barcode scan)
|
|
4. Server: looks up product, extracts dose
|
|
5. Returns: "Vitamin D3 5000 IU — when do you take this?"
|
|
6. User selects [Morning] [Evening] etc.
|
|
7. Prompt created
|
|
|
|
**Vitals (BP monitor, scale):**
|
|
1. Photo of display
|
|
2. OCR extracts numbers
|
|
3. Confirm values
|
|
4. Entry created
|
|
|
|
**Injury/symptom:**
|
|
1. Photo of knee scrape
|
|
2. User adds context: "My son fell"
|
|
3. Entry created with photo attached
|
|
|
|
#### 6. Pretty UI Design
|
|
|
|
Follow existing inou aesthetic:
|
|
- Warm, not clinical
|
|
- IBM Plex Sans
|
|
- Desert palette: warm beige (#F5EDE4), dark brown (#4A3728), terracotta (#C4704B)
|
|
|
|
Cards with generous padding. Large tap targets. Clear typography.
|
|
|
|
Accessible: works for someone with shaky hands or poor vision.
|
|
|
|
---
|
|
|
|
## Appendix A: Example Prompt Types
|
|
|
|
| Type | Input | Previous values? | Follow-up? |
|
|
|------|-------|------------------|------------|
|
|
| Medication (single) | checkbox | N/A | If missed 3+ days |
|
|
| Medications (group) | checkbox_group + "took all" | N/A | If missed |
|
|
| Vital (number) | number field + unit | Optional | If concerning |
|
|
| Supplement | checkbox | N/A | No |
|
|
| Exercise (simple) | number (duration) | Yes | If goal missed |
|
|
| Exercise (complex) | custom fields | Yes, all fields | No |
|
|
| Symptom follow-up | scale | N/A | If "worse" |
|
|
| Freeform | text + voice + photo | N/A | Based on content |
|
|
|
|
## Appendix B: Entry Structure Examples
|
|
|
|
**Simple medication:**
|
|
```json
|
|
{
|
|
"category": "medication",
|
|
"type": "Lisinopril",
|
|
"value": "taken",
|
|
"data": { "dose": "10mg" },
|
|
"timestamp": 1704369600
|
|
}
|
|
```
|
|
|
|
**Leg trainer session:**
|
|
```json
|
|
{
|
|
"category": "exercise",
|
|
"type": "leg_trainer",
|
|
"value": "20 min",
|
|
"data": {
|
|
"duration_min": 20,
|
|
"left": { "angle": 30, "speed": 4 },
|
|
"right": { "angle": 40, "speed": 5 },
|
|
"notes": "She was tired today",
|
|
"raw": "Sophia did leg trainer, 20 min..."
|
|
},
|
|
"timestamp": 1704369600
|
|
}
|
|
```
|
|
|
|
**Gym session with nested entries:**
|
|
```json
|
|
// Parent
|
|
{
|
|
"id": "gym_001",
|
|
"category": "exercise",
|
|
"type": "gym_session",
|
|
"value": null,
|
|
"timestamp": 1704369600
|
|
}
|
|
|
|
// Children
|
|
{
|
|
"parent_id": "gym_001",
|
|
"category": "exercise",
|
|
"type": "bench_press",
|
|
"value": "200",
|
|
"data": { "unit": "lbs" }
|
|
}
|
|
{
|
|
"parent_id": "gym_001",
|
|
"category": "exercise",
|
|
"type": "squats",
|
|
"value": "150",
|
|
"data": { "unit": "lbs" }
|
|
}
|
|
```
|
|
|
|
**Supplement from photo:**
|
|
```json
|
|
{
|
|
"category": "supplement",
|
|
"type": "Vitamin D3",
|
|
"value": "1",
|
|
"data": {
|
|
"dose": "5000 IU",
|
|
"brand": "Nature's Way",
|
|
"source": "photo_ocr",
|
|
"photo_id": "attach_xxx"
|
|
},
|
|
"timestamp": 1704369600
|
|
}
|
|
```
|
|
|
|
**Family history:**
|
|
```json
|
|
{
|
|
"category": "family_history",
|
|
"type": "mother",
|
|
"value": "deceased",
|
|
"data": {
|
|
"age_at_death": 78,
|
|
"cause": "heart attack",
|
|
"conditions": ["diabetes", "hypertension"],
|
|
"raw": "She had diabetes and high blood pressure, died of a heart attack at 78"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Appendix C: Intake AI Prompt Guidelines
|
|
|
|
**Role:**
|
|
You are an intake assistant for a health tracking system. Your job is to:
|
|
1. Parse natural language into structured health data
|
|
2. Ask clarifying questions when needed
|
|
3. Identify which person (dossier) an entry is about
|
|
4. Never give medical advice or interpret results
|
|
|
|
**Parsing rules:**
|
|
- Extract: category, type, value, relevant details
|
|
- Preserve raw input in data.raw
|
|
- Resolve aliases (D3 → Vitamin D3, BP → blood pressure)
|
|
- Detect time references ("yesterday", "this morning")
|
|
- Detect person references ("my son", "Sophia", "mom", "I")
|
|
|
|
**Multi-dossier:**
|
|
- If person mentioned matches an accessible dossier → route there
|
|
- If ambiguous ("blood pressure 120/80") → ask who
|
|
- If clearly about self ("I walked") → current user's dossier
|
|
|
|
**Follow-up questions:**
|
|
- Unknown unit: "Is that kg or lbs?"
|
|
- New supplement: "When do you take this?"
|
|
- Symptom logged: create follow-up prompt for tomorrow
|
|
- Repeated activity: "Want me to remind you about this?"
|
|
|
|
**What NOT to do:**
|
|
- Don't interpret if values are good/bad
|
|
- Don't recommend treatments
|
|
- Don't diagnose
|
|
- Don't say "you should see a doctor" (unless asked about resources)
|
|
|
|
---
|
|
|
|
## Appendix D: Implementation Order
|
|
|
|
### Phase 1 — Server + Ugly UI
|
|
|
|
1. **Database:** Add prompts table, extend entries
|
|
2. **API:** /api/prompts, /api/prompts/respond
|
|
3. **Web UI:** Render prompts, handle responses
|
|
4. **Intake parsing:** POST /api/intake/parse (start simple, text only)
|
|
5. **Confirmation flow:** Show interpretation, confirm, save
|
|
6. **Prompt generation:** From explicit statements ("I take X daily")
|
|
7. **Follow-up logic:** Symptoms trigger next-day prompts
|
|
8. **Template learning:** Remember structure from first entry
|
|
9. **Previous values:** Pre-fill from last entry of same type
|
|
10. **Grouping:** Collapse multiple meds into "took all"
|
|
11. **Multi-dossier:** Parse who, confirm, route
|
|
12. **Onboarding prompts:** Medications, supplements, goals
|
|
13. **Summary generation:** /api/summary
|
|
14. **Family history flow:** Guided questions
|
|
|
|
### Phase 2 — Pretty UI + Mobile
|
|
|
|
1. **Flutter scaffold:** Navigation, screens
|
|
2. **API integration:** Same endpoints
|
|
3. **Prompt rendering:** All input types
|
|
4. **Voice input:** On-device STT → server parse
|
|
5. **Photo input:** Camera, OCR, send to server
|
|
6. **Barcode:** On-device scan → server lookup
|
|
7. **Local notifications:** Schedule from prompt times
|
|
8. **Offline support:** Cache prompts, queue responses
|
|
9. **Push fallback:** Firebase integration
|
|
10. **Polish:** Design system, animations, accessibility
|
|
|
|
---
|
|
|
|
*Document version: 1.0*
|
|
*Last updated: January 5, 2025*
|
|
|
|
|
|
---
|
|
|
|
## Phase 1b: Provider Chat
|
|
|
|
### Goal
|
|
Secure messaging between patients/caregivers and healthcare providers. Replace Spruce for basic communication. FIPS 140-3 encrypted.
|
|
|
|
### Why Providers Switch
|
|
|
|
| Spruce | Inou |
|
|
|--------|------|
|
|
| $24-49/user/month | Free |
|
|
| Just messages | Messages + patient's complete dossier |
|
|
| HIPAA | HIPAA + FIPS 140-3 |
|
|
|
|
### Data Model (now live)
|
|
|
|
**Messages table:**
|
|
```sql
|
|
messages (
|
|
message_id INTEGER PRIMARY KEY,
|
|
conversation_id INTEGER NOT NULL,
|
|
dossier_id INTEGER NOT NULL, -- which patient dossier this relates to
|
|
sender_id INTEGER NOT NULL,
|
|
recipient_id INTEGER NOT NULL,
|
|
|
|
body TEXT, -- encrypted
|
|
attachments TEXT, -- encrypted JSON
|
|
|
|
sent_at INTEGER NOT NULL,
|
|
read_at INTEGER,
|
|
|
|
auto_reply INTEGER DEFAULT 0
|
|
)
|
|
|
|
CREATE INDEX idx_messages_conversation ON messages(conversation_id, sent_at);
|
|
CREATE INDEX idx_messages_recipient_unread ON messages(recipient_id, read_at);
|
|
```
|
|
|
|
**Conversations table:**
|
|
```sql
|
|
conversations (
|
|
conversation_id INTEGER PRIMARY KEY,
|
|
dossier_id INTEGER NOT NULL, -- the patient dossier
|
|
provider_id INTEGER NOT NULL, -- the provider's dossier
|
|
|
|
last_message_at INTEGER,
|
|
unread_count INTEGER DEFAULT 0,
|
|
|
|
created_at INTEGER
|
|
)
|
|
|
|
CREATE INDEX idx_conversations_dossier ON conversations(dossier_id);
|
|
CREATE INDEX idx_conversations_provider ON conversations(provider_id);
|
|
```
|
|
|
|
**Provider fields (added to dossiers table):**
|
|
```sql
|
|
-- See Phase 1 dossier additions for:
|
|
-- is_provider, provider_name, away_message, away_enabled
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
**GET /api/conversations?dossier=X**
|
|
|
|
List all conversations for a dossier (as patient or provider).
|
|
|
|
```json
|
|
{
|
|
"conversations": [
|
|
{
|
|
"id": "conv_123",
|
|
"patient": { "id": "xxx", "name": "Sophia" },
|
|
"provider": { "id": "yyy", "name": "Dr. Patel", "practice": "Kids First Pediatrics" },
|
|
"last_message": {
|
|
"body": "See you at the appointment tomorrow",
|
|
"sent_at": 1704369600,
|
|
"sender": "provider"
|
|
},
|
|
"unread_count": 0
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**GET /api/messages?conversation=X&limit=50&before=timestamp**
|
|
|
|
Get messages in a conversation (paginated).
|
|
|
|
```json
|
|
{
|
|
"messages": [
|
|
{
|
|
"id": "msg_456",
|
|
"sender": { "id": "xxx", "name": "Johan", "role": "caregiver" },
|
|
"body": "Sophia's leg trainer went well today. See attached notes.",
|
|
"attachments": [
|
|
{ "type": "entry", "id": "entry_789", "name": "Leg trainer session" }
|
|
],
|
|
"sent_at": 1704369600,
|
|
"read_at": 1704369700
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**POST /api/messages**
|
|
|
|
Send a message.
|
|
|
|
```json
|
|
{
|
|
"conversation_id": "conv_123",
|
|
"body": "Quick question about her medication dosage",
|
|
"attachments": []
|
|
}
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"id": "msg_789",
|
|
"sent_at": 1704369600,
|
|
"auto_reply": {
|
|
"id": "msg_790",
|
|
"body": "Thank you for your message. I'm currently unavailable but will respond within 24 hours. For urgent matters, please call 555-1234.",
|
|
"sent_at": 1704369601
|
|
}
|
|
}
|
|
```
|
|
|
|
**POST /api/messages/:id/read**
|
|
|
|
Mark message as read.
|
|
|
|
**PUT /api/provider/settings**
|
|
|
|
Update provider settings (away message, etc).
|
|
|
|
```json
|
|
{
|
|
"away_enabled": true,
|
|
"away_message": "Thank you for your message. I typically respond within 24 hours. For urgent matters, call 555-1234."
|
|
}
|
|
```
|
|
|
|
### Features
|
|
|
|
**Core:**
|
|
- Text messages (encrypted at rest, FIPS)
|
|
- Attach entries from dossier (labs, imaging, notes)
|
|
- Attach photos
|
|
- Read receipts
|
|
- Auto-responder when provider unavailable
|
|
|
|
**Notifications:**
|
|
- Push notification on new message
|
|
- Email notification if app not opened in 24h
|
|
|
|
**Provider View:**
|
|
- List of patient conversations
|
|
- Unread count badge
|
|
- Click patient → see conversation + dossier summary
|
|
- Quick access to recent entries, labs, imaging
|
|
|
|
### Provider Onboarding
|
|
|
|
**Invited by patient:**
|
|
1. Patient sends invite to provider email
|
|
2. Provider receives: "Johan invited you to view Sophia's health dossier"
|
|
3. Provider signs up (free): name, email, practice name
|
|
4. Provider linked to patient's dossier with role = medical
|
|
5. Provider can message, view dossier
|
|
|
|
**Provider invites patients:**
|
|
1. Provider creates account (free)
|
|
2. Provider uploads patient list (email CSV) or invites one by one
|
|
3. Patient receives: "Dr. Patel invited you to connect on inou"
|
|
4. Patient signs up → linked to provider
|
|
5. Patient can message, optionally upgrade to full tracking
|
|
|
|
### Patient Tiers
|
|
|
|
| Tier | Cost | Features |
|
|
|------|------|----------|
|
|
| Passive | Free | Receive messages, basic profile, view shared docs |
|
|
| Active | Paid | Full dossier: intake AI, tracking, prompts, summaries, imaging, labs, genome |
|
|
|
|
Provider brings patients in free. Some upgrade. That's the revenue.
|
|
|
|
### Auto-Responder Logic
|
|
|
|
```go
|
|
func sendMessage(msg Message) {
|
|
save(msg)
|
|
notify(recipient)
|
|
|
|
if recipient.is_provider && recipient.away_enabled {
|
|
autoReply := Message{
|
|
sender: recipient,
|
|
recipient: msg.sender,
|
|
body: recipient.away_message,
|
|
auto_reply: true,
|
|
}
|
|
save(autoReply)
|
|
notify(msg.sender)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security
|
|
|
|
- All messages encrypted with FIPS 140-3 (AES-256-GCM)
|
|
- TLS 1.3 in transit
|
|
- Messages only visible to conversation participants
|
|
- Providers can only see dossiers they're invited to
|
|
- Audit log for all access
|
|
|
|
### Implementation Order
|
|
|
|
1. **Database:** messages, conversations tables; provider fields on dossier
|
|
2. **API:** conversations, messages, send, read
|
|
3. **Web UI:** conversation list, message thread, compose
|
|
4. **Auto-responder:** provider settings, auto-reply logic
|
|
5. **Notifications:** push (Firebase), email fallback
|
|
6. **Provider invite flow:** patient invites provider
|
|
7. **Bulk invite:** provider imports patient list
|
|
8. **Provider dashboard:** patient list, unread counts, dossier access
|
|
|
|
---
|
|
|
|
*Document version: 1.1*
|
|
*Last updated: January 5, 2025*
|
|
|
|
|
|
---
|
|
|
|
## Appendix E: Entry Categories
|
|
|
|
Complete list of entry categories supported by the system.
|
|
|
|
### Core (Daily Logging)
|
|
|
|
| Category | Description | Example Types |
|
|
|----------|-------------|---------------|
|
|
| `medication` | Medications taken or prescribed | Lisinopril, Metoprolol, anti-seizure |
|
|
| `vital` | Vital signs and measurements | BP, weight, temperature, SpO2 |
|
|
| `exercise` | Physical activity and therapy sessions | leg_trainer, walking, gym_session |
|
|
| `symptom` | Symptoms and complaints | headache, seizure, fatigue |
|
|
| `supplement` | Supplements and vitamins | Vitamin D3, magnesium, iron |
|
|
| `note` | Free-form notes | daily observations |
|
|
|
|
### Events
|
|
|
|
| Category | Description | Example Types | Nesting |
|
|
|----------|-------------|---------------|--------|
|
|
| `surgery` | Surgical procedures | shunt_placement, shunt_revision, ETV | Parent with child `procedure` entries |
|
|
| `hospitalization` | Hospital stays | NICU, inpatient, ER | Parent with child entries for daily events |
|
|
| `consultation` | Doctor visits and consults | video_call, office_visit, second_opinion | Parent with child `finding` or `recommendation` entries |
|
|
|
|
### Conditions
|
|
|
|
| Category | Description | Example Types | Notes |
|
|
|----------|-------------|---------------|-------|
|
|
| `diagnosis` | Medical diagnoses | IVH, hydrocephalus, epilepsy, ROP | Can have status: active, resolved, suspected |
|
|
| `device` | Implanted or external devices | VP_shunt, Ommaya_reservoir, cochlear_implant | Track settings changes over time as new entries |
|
|
|
|
### Development
|
|
|
|
| Category | Description | Example Types | Notes |
|
|
|----------|-------------|---------------|-------|
|
|
| `therapy` | Therapeutic interventions | Vojta, Bobath, Feldenkrais, ABM, speech_therapy | Include outcome: effective, ineffective, ongoing |
|
|
| `assessment` | Developmental evaluations | motor, speech, cognitive, vision | Qualitative findings, milestones |
|
|
|
|
### Foundational
|
|
|
|
| Category | Description | Example Types | Notes |
|
|
|----------|-------------|---------------|-------|
|
|
| `birth` | Birth and prenatal history | delivery, Apgar, gestational_age, complications | One-time entries, foundational context |
|
|
| `history` | Past medical events without full documentation | surgery, illness, injury, allergy | Brief recollections: "appendectomy at age 12" |
|
|
| `family_history` | Family medical history | mother, father, sibling, maternal_grandmother | Include condition, age at diagnosis, outcome |
|
|
|
|
### Findings (Interpretations)
|
|
|
|
| Category | Description | Example Types | Notes |
|
|
|----------|-------------|---------------|-------|
|
|
| `imaging_finding` | Radiologist or AI interpretation | MRI_finding, CT_finding, XR_finding | Links to DICOM study via data.study_guid |
|
|
| `eeg_finding` | EEG report findings | epileptiform_activity, sleep_pattern | From EEG monitoring reports |
|
|
|
|
### Meta
|
|
|
|
| Category | Description | Example Types | Notes |
|
|
|----------|-------------|---------------|-------|
|
|
| `provider` | Healthcare providers | neurosurgeon, neurologist, therapist | Institution, contact, relationship period |
|
|
| `question` | Open medical questions | surgical, diagnostic, treatment | Answer via child entry when resolved |
|
|
| `upload` | Uploaded files | imaging, genetics, document, lab_report | Existing category |
|
|
|
|
### Nesting Examples
|
|
|
|
**Complex surgery with sub-procedures:**
|
|
```
|
|
Parent:
|
|
category: surgery
|
|
type: shunt_revision
|
|
value: "Complex Shunt Revision"
|
|
timestamp: 2021-12-10
|
|
data: {
|
|
location: "University Clinic Ulm",
|
|
surgeon: "Dr. Aurelia Peraud",
|
|
indication: "Isolated 4th ventricle with brainstem compression"
|
|
}
|
|
|
|
Children (parent_id → parent):
|
|
category: procedure, type: shunt_replacement, data: {old: "Codman", new: "Miethke Blu"}
|
|
category: procedure, type: catheter_placement, data: {target: "4th ventricle"}
|
|
category: procedure, type: ETV, data: {location: "3rd ventricle floor"}
|
|
category: procedure, type: catheter_placement, data: {from: "3rd ventricle", to: "anterior brain stem"}
|
|
```
|
|
|
|
**Device with settings history:**
|
|
```
|
|
-- Initial installation --
|
|
category: device
|
|
type: miethke_blu_valve
|
|
value: "installed"
|
|
timestamp: 2021-12-10
|
|
data: {setting: "20 cmH2O", component: "M. blue plus", surgeon: "Dr. Peraud"}
|
|
|
|
-- Setting adjustment --
|
|
category: device
|
|
type: miethke_blu_valve
|
|
value: "adjusted"
|
|
timestamp: 2022-08-15
|
|
data: {setting: "30 cmH2O", adjusted_by: "Ulm team"}
|
|
|
|
-- Suspected malfunction --
|
|
category: device
|
|
type: miethke_blu_valve
|
|
value: "malfunction"
|
|
timestamp: 2024-10-21
|
|
data: {expected_setting: "30 cmH2O", actual_setting: "24 cmH2O", status: "suspected dysfunction"}
|
|
```
|
|
|
|
**Open question with answer:**
|
|
```
|
|
Parent:
|
|
category: question
|
|
type: surgical
|
|
value: "Is there opportunity for endoscopy to rearrange natural CSF flow?"
|
|
timestamp: 2025-01-01
|
|
data: {status: "unanswered", raised_by: "parent"}
|
|
|
|
Child (when answered):
|
|
parent_id: [question entry]
|
|
category: answer
|
|
type: specialist_opinion
|
|
value: "Dr. Peraud recommends shunt revision over endoscopy due to..."
|
|
timestamp: 2025-02-15
|
|
data: {source: "video consultation", provider: "Dr. Peraud"}
|
|
```
|
|
|
|
**Consultation with findings:**
|
|
```
|
|
Parent:
|
|
category: consultation
|
|
type: video_call
|
|
value: "Ulm Neurosurgery Follow-up"
|
|
timestamp: 2024-10-21
|
|
data: {
|
|
institution: "University Clinic Ulm",
|
|
provider: "Prof. Dr. Aurelia Peraud",
|
|
interpreter: "Frau Natalya Kazmina"
|
|
}
|
|
|
|
Children:
|
|
category: finding, type: imaging, value: "Ventricular enlargement progression"
|
|
category: finding, type: device, value: "Suspected valve dysfunction"
|
|
category: recommendation, type: surgical, value: "Shunt and valve revision recommended"
|
|
category: recommendation, type: screening, value: "MRSA/MRGN testing required pre-op"
|
|
```
|
|
|
|
**Personal medical history (undocumented past events):**
|
|
```
|
|
category: history
|
|
type: surgery
|
|
value: "Appendectomy"
|
|
data: {age: 12, approximate_year: 2005, notes: "no complications"}
|
|
|
|
category: history
|
|
type: illness
|
|
value: "Chickenpox"
|
|
data: {age: 6, severity: "normal"}
|
|
|
|
category: history
|
|
type: injury
|
|
value: "Broke left arm"
|
|
data: {year: 2015, treatment: "cast 6 weeks"}
|
|
|
|
category: history
|
|
type: allergy
|
|
value: "Penicillin"
|
|
data: {reaction: "rash", severity: "moderate", discovered: "childhood"}
|
|
```
|
|
|
|
**Family history:**
|
|
```
|
|
category: family_history
|
|
type: mother
|
|
value: "Breast cancer"
|
|
data: {age_at_diagnosis: 58, outcome: "survived", treatment: "mastectomy + chemo"}
|
|
|
|
category: family_history
|
|
type: father
|
|
value: "Type 2 diabetes"
|
|
data: {age_at_diagnosis: 52, status: "ongoing", management: "metformin"}
|
|
|
|
category: family_history
|
|
type: maternal_grandmother
|
|
value: "Alzheimer's disease"
|
|
data: {age_at_onset: 72, age_at_death: 81}
|
|
|
|
category: family_history
|
|
type: brother
|
|
value: "Healthy"
|
|
data: {age: 35, notes: "no known conditions"}
|
|
|
|
category: family_history
|
|
type: family_pattern
|
|
value: "Heart disease"
|
|
data: {notes: "Multiple paternal relatives with early heart attacks"}
|
|
```
|