Transform repetitive multi-item logging into one-tap shortcuts. When a user consistently logs the same combination of items together (e.g., "morning supplements", "breakfast shake", "post-workout stack"), the system learns these patterns and offers to log the entire template with a single confirmation.
Core Principle: Move users from active data entry → passive confirmation.
┌─────────────────────────────────────────────────────────────────────────────┐
│ MEAL TEMPLATE LEARNING SYSTEM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ Event Save │────▶│ Pattern Detection│────▶│ Template Suggestion │ │
│ │ (confirm.jsx)│ │ (mealPatterns.js)│ │ (home.jsx) │ │
│ └──────────────┘ └──────────────────┘ └───────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────────┐ │
│ │voice_events │ │user_meal_templates│ │ Template Confirmation │ │
│ │(source of │ │(confirmed only) │ │ (templateConfirm.jsx) │ │
│ │ truth) │ └──────────────────┘ └───────────────────────┘ │
│ └──────────────┘ │
│ │ │
│ └───────────────────────────────────────────────────────────────── │
│ Pattern detection queries voice_events directly │
│ No intermediate storage needed │
└─────────────────────────────────────────────────────────────────────────────┘
The voice_events table is the source of truth for what users have logged. Rather than duplicating this data into a separate pattern tracking table, we should:
- Query
voice_eventson-demand to detect patterns - Only persist to
user_meal_templateswhen user explicitly confirms a template - Calculate occurrence counts from actual event data, not stored counters
Benefits:
- No data duplication or sync issues
- Always accurate (reflects actual user behavior)
- Simpler schema (one less table)
- Patterns are computed, not stored
| Rejected Approach | Why Not |
|---|---|
meal_pattern_history table |
Duplicates data already in voice_events |
detection_count column |
Can be calculated from voice_events query |
| Storing unconfirmed patterns | Query on-demand instead |
Located in: supabase/migrations/20250128_create_product_catalog.sql (lines 141-162)
CREATE TABLE user_meal_templates (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
template_name TEXT NOT NULL, -- "Morning Supplements", "Breakfast Shake"
template_key TEXT NOT NULL, -- "morning supplements", "breakfast shake"
items JSONB NOT NULL, -- [{product_id, quantity, name, event_type, ...}]
typical_time_range TEXT, -- "07:00-09:00"
times_logged INTEGER DEFAULT 1, -- How many times user USED this template
first_logged_at TIMESTAMPTZ,
last_logged_at TIMESTAMPTZ,
UNIQUE(user_id, template_key)
);Enhancement Needed: Add columns for auto-generation tracking:
-- Migration: 20250129_enhance_meal_templates.sql
ALTER TABLE user_meal_templates ADD COLUMN IF NOT EXISTS
auto_generated BOOLEAN DEFAULT false; -- True if system-detected vs manually created
ALTER TABLE user_meal_templates ADD COLUMN IF NOT EXISTS
fingerprint TEXT; -- Normalized item fingerprint for matching (e.g., "magtein|omega-3|vitamin-d")
-- Index for fingerprint-based lookups
CREATE INDEX IF NOT EXISTS idx_meal_templates_fingerprint
ON user_meal_templates(user_id, fingerprint);
-- Index for time-based suggestions
CREATE INDEX IF NOT EXISTS idx_meal_templates_time_user
ON user_meal_templates(user_id, typical_time_range);| Column | Purpose |
|---|---|
times_logged |
How many times user has used this template after creation |
auto_generated |
true if system detected pattern, false if user manually created |
fingerprint |
Normalized sorted item IDs for quick pattern matching |
Note: We intentionally do NOT have a detection_count column. The number of times a pattern was detected before template creation is computed from voice_events at query time.
Purpose: Detect meal patterns by querying existing voice_events data.
Query voice_events to find recurring item combinations.
/**
* Analyze voice_events to find recurring meal patterns
*
* @param {string} userId - User ID
* @param {Object} options
* @param {number} options.timeWindowMinutes - Window for grouping items (default: 30)
* @param {number} options.minOccurrences - Minimum times pattern must occur (default: 2)
* @param {number} options.lookbackDays - How far back to analyze (default: 30)
* @returns {Promise<Array<{fingerprint, items, occurrences, typicalHour}>>}
*/
export async function detectMealPatterns(userId, options = {}) {
const {
timeWindowMinutes = 30,
minOccurrences = 2,
lookbackDays = 30
} = options;
// 1. Fetch recent events
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - lookbackDays);
const { data: events } = await supabase
.from('voice_events')
.select('id, event_type, event_data, event_time, product_catalog_id')
.eq('user_id', userId)
.gte('event_time', cutoffDate.toISOString())
.in('event_type', ['food', 'supplement', 'medication'])
.order('event_time', { ascending: true });
// 2. Group events into "sessions" (items logged within timeWindowMinutes of each other)
const sessions = groupEventsIntoSessions(events, timeWindowMinutes);
// 3. Generate fingerprints for each session
const sessionFingerprints = sessions.map(session => ({
fingerprint: generateMealFingerprint(session.items),
items: session.items,
hour: new Date(session.startTime).getHours()
}));
// 4. Count fingerprint occurrences
const fingerprintCounts = countFingerprints(sessionFingerprints);
// 5. Filter to patterns that meet minimum occurrence threshold
const patterns = Object.entries(fingerprintCounts)
.filter(([_, data]) => data.count >= minOccurrences)
.map(([fingerprint, data]) => ({
fingerprint,
items: data.items,
occurrences: data.count,
typicalHour: Math.round(data.totalHours / data.count)
}));
// 6. Exclude patterns that already exist as templates
const { data: existingTemplates } = await supabase
.from('user_meal_templates')
.select('fingerprint')
.eq('user_id', userId);
const existingFingerprints = new Set(existingTemplates?.map(t => t.fingerprint) || []);
return patterns.filter(p => !existingFingerprints.has(p.fingerprint));
}Group consecutive events into "meal sessions".
/**
* Group events that were logged within windowMinutes of each other
*
* @param {Array} events - Sorted events from voice_events
* @param {number} windowMinutes - Time window for grouping
* @returns {Array<{items: Array, startTime: Date, endTime: Date}>}
*/
function groupEventsIntoSessions(events, windowMinutes) {
if (!events?.length) return [];
const sessions = [];
let currentSession = {
items: [extractItemFromEvent(events[0])],
startTime: events[0].event_time,
endTime: events[0].event_time
};
for (let i = 1; i < events.length; i++) {
const event = events[i];
const prevTime = new Date(currentSession.endTime);
const currTime = new Date(event.event_time);
const diffMinutes = (currTime - prevTime) / (1000 * 60);
if (diffMinutes <= windowMinutes) {
// Same session - add to current
currentSession.items.push(extractItemFromEvent(event));
currentSession.endTime = event.event_time;
} else {
// New session - save current and start new
if (currentSession.items.length >= 2) {
sessions.push(currentSession);
}
currentSession = {
items: [extractItemFromEvent(event)],
startTime: event.event_time,
endTime: event.event_time
};
}
}
// Don't forget last session
if (currentSession.items.length >= 2) {
sessions.push(currentSession);
}
return sessions;
}Create a normalized identifier for a set of items.
/**
* Create a sortable, comparable fingerprint from items
* Uses product_catalog_id when available, falls back to normalized name
*
* @param {Array} items - Array of {product_id, name, event_type}
* @returns {string} - Normalized fingerprint like "uuid-123|uuid-456|vitamin-d"
*/
export function generateMealFingerprint(items) {
return items
.map(item => item.product_id || normalizeProductKey(item.name))
.sort()
.join('|');
}Jaccard similarity for partial template matching.
/**
* Calculate how similar two meal patterns are
* Used for suggesting existing templates when user logs partial match
*
* @returns {number} - Similarity score 0-1
*/
export function calculatePatternSimilarity(fingerprint1, fingerprint2) {
const set1 = new Set(fingerprint1.split('|'));
const set2 = new Set(fingerprint2.split('|'));
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}Check if items being logged match an existing template OR a detected pattern.
/**
* Check if current items match a template or emerging pattern
* Called during event logging to offer shortcuts
*
* @param {string} userId
* @param {Array} currentItems - Items user is about to log
* @returns {Promise<{type: 'template'|'pattern'|null, data?: Object, similarity: number}>}
*/
export async function checkForPatternMatch(userId, currentItems) {
const currentFingerprint = generateMealFingerprint(currentItems);
// 1. Check existing templates first (confirmed patterns)
const { data: templates } = await supabase
.from('user_meal_templates')
.select('*')
.eq('user_id', userId);
for (const template of templates || []) {
const similarity = calculatePatternSimilarity(currentFingerprint, template.fingerprint);
if (similarity >= 0.7) { // 70% match threshold
return {
type: 'template',
data: template,
similarity
};
}
}
// 2. Check for emerging patterns (not yet templates)
const patterns = await detectMealPatterns(userId, { minOccurrences: 2 });
for (const pattern of patterns) {
const similarity = calculatePatternSimilarity(currentFingerprint, pattern.fingerprint);
if (similarity >= 0.7) {
return {
type: 'pattern',
data: pattern,
similarity
};
}
}
return { type: null, similarity: 0 };
}Templates are ONLY created when user explicitly confirms. No "draft" or "pending" templates.
/**
* Create a confirmed template from a detected pattern
* Called when user accepts a pattern suggestion
*
* @param {string} userId
* @param {Object} pattern - Pattern data from detectMealPatterns
* @param {string} templateName - User-provided name
* @returns {Promise<Object>} - Created template
*/
export async function createTemplateFromPattern(userId, pattern, templateName) {
// Generate time range from typical hour (±1 hour window)
const startHour = Math.max(0, pattern.typicalHour - 1);
const endHour = Math.min(23, pattern.typicalHour + 1);
const timeRange = `${String(startHour).padStart(2, '0')}:00-${String(endHour).padStart(2, '0')}:59`;
const { data: template, error } = await supabase
.from('user_meal_templates')
.insert({
user_id: userId,
template_name: templateName,
template_key: normalizeProductKey(templateName),
fingerprint: pattern.fingerprint,
items: pattern.items,
typical_time_range: timeRange,
times_logged: 0, // User hasn't used it via template yet
first_logged_at: new Date().toISOString(),
auto_generated: true
})
.select()
.single();
if (error) throw error;
return template;
}Add proactive template suggestions based on time of day.
// In home.jsx, add after user check
const [suggestedTemplate, setSuggestedTemplate] = useState(null);
useEffect(() => {
if (user?.id) {
checkForTemplateSuggestion();
}
}, [user?.id]);
const checkForTemplateSuggestion = async () => {
const { getSuggestedTemplate } = require('@/utils/mealPatterns');
const template = await getSuggestedTemplate(user.id);
if (template) {
setSuggestedTemplate(template);
}
};/**
* Get a template to suggest based on current time
* Only suggests templates that have been used at least once
*/
export async function getSuggestedTemplate(userId) {
const now = new Date();
const currentHour = now.getHours();
const currentTime = `${String(currentHour).padStart(2, '0')}:00`;
// Find templates matching current time window
// Only suggest templates that exist (all templates in this table are confirmed)
const { data: templates } = await supabase
.from('user_meal_templates')
.select('*')
.eq('user_id', userId)
.order('times_logged', { ascending: false });
// Filter by time range
const matching = templates?.filter(t => {
if (!t.typical_time_range) return false;
const [start, end] = t.typical_time_range.split('-');
return currentTime >= start && currentTime <= end;
});
// Don't suggest if already logged today
if (matching?.length > 0) {
const template = matching[0];
const alreadyLoggedToday = await checkIfLoggedToday(userId, template.id);
return alreadyLoggedToday ? null : template;
}
return null;
}
/**
* Check if user already logged this template today
*/
async function checkIfLoggedToday(userId, templateId) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const { data } = await supabase
.from('voice_events')
.select('id')
.eq('user_id', userId)
.eq('template_id', templateId)
.gte('event_time', today.toISOString())
.limit(1);
return data?.length > 0;
}New component: mobile/src/components/TemplateSuggestionCard.jsx
/**
* Proactive template suggestion card shown on home screen
*/
export default function TemplateSuggestionCard({ template, onAccept, onDismiss }) {
const colors = useColors();
// Calculate total nutrition from items
const totals = useMemo(() => {
return template.items.reduce((acc, item) => ({
calories: acc.calories + (item.calories || 0),
protein: acc.protein + (item.protein || 0),
items: acc.items + 1
}), { calories: 0, protein: 0, items: 0 });
}, [template.items]);
return (
<TouchableOpacity
style={styles.card}
onPress={onAccept}
>
<View style={styles.header}>
<Ionicons name="flash" size={20} color={colors.primary} />
<Text style={styles.title}>Log your {template.template_name}?</Text>
</View>
<Text style={styles.subtitle}>
{totals.items} items • {totals.calories} cal
</Text>
<View style={styles.itemsPreview}>
{template.items.slice(0, 3).map((item, i) => (
<Text key={i} style={styles.itemName}>{item.name}</Text>
))}
{template.items.length > 3 && (
<Text style={styles.more}>+{template.items.length - 3} more</Text>
)}
</View>
<View style={styles.actions}>
<TouchableOpacity onPress={onAccept} style={styles.acceptButton}>
<Text style={styles.acceptText}>Log Now</Text>
</TouchableOpacity>
<TouchableOpacity onPress={onDismiss} style={styles.dismissButton}>
<Ionicons name="close" size={20} color={colors.textSecondary} />
</TouchableOpacity>
</View>
</TouchableOpacity>
);
}New screen: mobile/src/app/template-confirm.jsx
When user accepts a template suggestion, show a quick confirmation screen:
export default function TemplateConfirmScreen() {
const params = useLocalSearchParams();
const template = JSON.parse(params.template);
const [itemQuantities, setItemQuantities] = useState(
template.items.reduce((acc, item, i) => {
acc[i] = item.default_quantity || 1;
return acc;
}, {})
);
const handleConfirm = async () => {
// Create events for all items
const events = template.items.map((item, i) => ({
event_type: item.event_type,
event_data: buildEventData(item, itemQuantities[i]),
template_id: template.id
}));
await createMultipleEvents(events, user.id);
await incrementTemplateUsage(template.id);
// Success feedback
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
router.back();
};
return (
<View>
<Header title={template.template_name} />
<ScrollView>
{template.items.map((item, i) => (
<TemplateItemRow
key={i}
item={item}
quantity={itemQuantities[i]}
onQuantityChange={(q) => setItemQuantities({...itemQuantities, [i]: q})}
/>
))}
</ScrollView>
<TouchableOpacity onPress={handleConfirm}>
<Text>Log {template.items.length} Items</Text>
</TouchableOpacity>
</View>
);
}Detect when user says something like "my morning vitamins" or "usual breakfast".
Add to voiceEventParser.js:
/**
* Check if user input matches a meal template name
*/
export async function matchTemplateByVoice(transcription, userId) {
const normalized = transcription.toLowerCase().trim();
// Common template trigger phrases
const triggers = [
/^(my |the |usual |regular |normal )?(.+?)( routine| stack| combo| meal)?$/i,
/^log (my )?(.+)$/i,
/^(took |had |ate )(my )?(.+)$/i
];
let potentialName = null;
for (const trigger of triggers) {
const match = normalized.match(trigger);
if (match) {
potentialName = (match[2] || match[3] || '').trim();
break;
}
}
if (!potentialName) return null;
// Search templates (all templates in this table are confirmed by design)
const { data: templates } = await supabase
.from('user_meal_templates')
.select('*')
.eq('user_id', userId);
// Fuzzy match template names
const matches = templates?.filter(t => {
const templateKey = t.template_key.toLowerCase();
const searchKey = normalizeProductKey(potentialName);
return templateKey.includes(searchKey) ||
searchKey.includes(templateKey) ||
calculateSimilarity(templateKey, searchKey) > 0.7;
});
if (matches?.length > 0) {
// Return best match by times_logged
return matches.sort((a, b) => b.times_logged - a.times_logged)[0];
}
return null;
}Update home.jsx handleVoicePress:
// After transcription, before AI parsing
const templateMatch = await matchTemplateByVoice(parsed.transcription, userId);
if (templateMatch) {
console.log(`Matched template: ${templateMatch.template_name}`);
// Navigate to template confirmation
router.push({
pathname: '/template-confirm',
params: { template: JSON.stringify(templateMatch) }
});
return;
}
// Continue with normal parsing if no template match...After events are successfully saved, check if we've now hit the threshold for a new pattern:
// In handleConfirm, after createVoiceEvent succeeds:
const handleConfirm = async () => {
// ... existing event creation code ...
// After successful save, check for emerging patterns
const { checkForNewPatterns } = require('@/utils/mealPatterns');
const newPatterns = await checkForNewPatterns(user.id, { minOccurrences: 2 });
if (newPatterns.length > 0) {
// Show pattern detection prompt for the most recent pattern
showPatternDetectedModal(newPatterns[0]);
}
};const showPatternDetectedModal = (pattern) => {
Alert.alert(
'Save as Quick Log?',
`You've logged these ${pattern.items.length} items together ${pattern.occurrences} times. Save as a template for one-tap logging?`,
[
{ text: 'Not Now', style: 'cancel' },
{
text: 'Save Template',
onPress: () => promptForTemplateName(pattern)
}
]
);
};
const promptForTemplateName = (pattern) => {
// Navigate to template naming screen
router.push({
pathname: '/create-template',
params: { pattern: JSON.stringify(pattern) }
});
};/**
* Check if any new patterns have reached the threshold since last check
* Uses voice_events as source of truth - no separate storage needed
*
* @param {string} userId
* @param {Object} options
* @returns {Promise<Array>} - Patterns that just reached threshold
*/
export async function checkForNewPatterns(userId, options = {}) {
// Query voice_events to detect patterns
const patterns = await detectMealPatterns(userId, options);
// Filter to patterns not already saved as templates
// (detectMealPatterns already excludes existing templates)
return patterns;
}Allow users to view, edit, and delete their meal templates.
export default function TemplatesScreen() {
const { data: user } = useUser();
const [templates, setTemplates] = useState([]);
useEffect(() => {
loadTemplates();
}, [user?.id]);
const loadTemplates = async () => {
const { data } = await supabase
.from('user_meal_templates')
.select('*')
.eq('user_id', user.id)
.order('times_logged', { ascending: false });
setTemplates(data || []);
};
return (
<View>
<Header title="Meal Templates" />
<FlatList
data={templates}
renderItem={({ item }) => (
<TemplateCard
template={item}
onPress={() => router.push({
pathname: '/template-confirm',
params: { template: JSON.stringify(item) }
})}
onEdit={() => router.push({
pathname: '/edit-template',
params: { templateId: item.id }
})}
onDelete={() => deleteTemplate(item.id)}
/>
)}
/>
<TouchableOpacity
onPress={() => router.push('/create-template')}
style={styles.createButton}
>
<Text>Create New Template</Text>
</TouchableOpacity>
</View>
);
}/**
* Increment template usage and update last_logged_at
*/
export async function incrementTemplateUsage(templateId) {
const { data: template } = await supabase
.from('user_meal_templates')
.select('times_logged')
.eq('id', templateId)
.single();
await supabase
.from('user_meal_templates')
.update({
times_logged: template.times_logged + 1,
last_logged_at: new Date().toISOString()
})
.eq('id', templateId);
}When user modifies template quantities, learn the new defaults:
/**
* Update template with new default quantities based on user selections
*/
export async function learnTemplateQuantities(templateId, newQuantities) {
const { data: template } = await supabase
.from('user_meal_templates')
.select('items')
.eq('id', templateId)
.single();
// Weighted average of old and new quantities
const updatedItems = template.items.map((item, i) => {
const oldQty = item.default_quantity || 1;
const newQty = newQuantities[i] || oldQty;
// 70% weight to historical, 30% to new
const learned = Math.round((oldQty * 0.7 + newQty * 0.3) * 10) / 10;
return { ...item, default_quantity: learned };
});
await supabase
.from('user_meal_templates')
.update({ items: updatedItems })
.eq('id', templateId);
}- Create
mealPatterns.jswith core pattern detection - Add migration for
user_meal_templatesenhancements (fingerprint, auto_generated columns) - Implement
generateMealFingerprint()andcalculatePatternSimilarity() - Implement
groupEventsIntoSessions()anddetectMealPatterns()
- Implement
checkForNewPatterns() - Integrate pattern detection into confirm.jsx (after event save)
- Create pattern detected modal UI
- Create template naming screen (
create-template.jsx)
- Create
TemplateSuggestionCardcomponent - Implement
getSuggestedTemplate()time-based logic - Integrate suggestion card into home.jsx
- Create template confirmation screen (
template-confirm.jsx)
- Implement
matchTemplateByVoice() - Integrate with handleVoicePress in home.jsx
- Add template trigger phrase detection
- Test voice-based template logging
- Create templates management screen (
templates.jsx) - Implement template editing (
edit-template.jsx) - Add quantity learning feedback loop
- Polish UI and add haptics
Beyond same-session meal templates, users may have sequential patterns that span time:
- "I always take my evening supplements 30 minutes after dinner"
- "I take Vitamin D with breakfast, then Magnesium before bed"
- "After my morning coffee, I take my thyroid medication"
/**
* A sequence template represents items that are always logged in order,
* but not necessarily at the same time.
*/
const sequenceTemplate = {
id: 'uuid',
user_id: 'uuid',
sequence_name: 'Daily Vitamin Routine',
// Ordered steps with relative timing
steps: [
{
item: { product_id: 'vitamin-d-uuid', name: 'Vitamin D3' },
timing: 'morning', // Approximate time of day
trigger: 'with_meal', // When to suggest
typical_hour: 8
},
{
item: { product_id: 'magnesium-uuid', name: 'Magnesium' },
timing: 'evening',
trigger: 'before_bed',
typical_hour: 21,
delay_from_previous: null // Not directly after previous
}
],
// Tracking
times_completed: 15, // Full sequence completions
completion_rate: 0.85 // 85% of days user completes full sequence
};/**
* Detect cross-day sequential patterns
* Looks for items that are ALWAYS logged on the same days, in consistent order
*/
export async function detectSequencePatterns(userId, options = {}) {
const { lookbackDays = 30, minCompletionRate = 0.7 } = options;
// 1. Get all events grouped by day
const eventsByDay = await getEventsByDay(userId, lookbackDays);
// 2. For each unique product, find what OTHER products appear on the same days
const coOccurrenceMatrix = buildCoOccurrenceMatrix(eventsByDay);
// 3. Find pairs/groups with high co-occurrence rate
const frequentPairs = findFrequentPairs(coOccurrenceMatrix, minCompletionRate);
// 4. Determine typical ordering within each day
const orderedSequences = determineOrdering(frequentPairs, eventsByDay);
return orderedSequences;
}Unlike meal templates (instant logging), sequence templates become reminders:
- User logs "Vitamin D" in morning
- System tracks: "Step 1 of Daily Vitamin Routine complete"
- In evening, shows reminder: "Don't forget your Magnesium (part of Daily Vitamin Routine)"
| Meal Templates | Sequence Templates |
|---|---|
| Items logged within 30 min | Items logged across hours/day |
| One-tap logs ALL items | Reminders for each step |
| Same-time grouping | Same-day ordering |
| "Log your Breakfast?" | "Step 2: Take Magnesium" |
This is a future enhancement that builds on the meal template foundation:
- First implement meal templates (same-session)
- Collect data on cross-day patterns
- Add sequence detection as Phase 8
- Consider push notifications for step reminders
mealPatterns.test.js: Pattern fingerprinting, similarity calculation, session groupingtemplateMatching.test.js: Voice-to-template matching
- Pattern detection after event saves
- Template creation flow
- Time-based suggestion accuracy
- Partial match suggestions (4 of 5 items)
- Full flow: Log items → Pattern detected → Create template → Use template
- Adoption Rate: % of users with at least one template
- Template Usage: Templates logged per user per week
- Time Savings: Average seconds saved per template use vs. manual logging
- Pattern Detection Accuracy: % of suggested patterns users accept
- Learning Improvement: Quantity accuracy over time
mobile/src/utils/mealPatterns.js- Pattern detection engine (queries voice_events)mobile/src/components/TemplateSuggestionCard.jsx- Suggestion UImobile/src/app/template-confirm.jsx- Template logging confirmationmobile/src/app/templates.jsx- Template management screenmobile/src/app/create-template.jsx- Manual template creationmobile/src/app/edit-template.jsx- Template editingmobile/__tests__/utils/mealPatterns.test.js- Pattern testssupabase/migrations/20250129_enhance_meal_templates.sql- Add fingerprint, auto_generated columns
mobile/src/app/(tabs)/home.jsx- Template suggestions, voice matchingmobile/src/app/confirm.jsx- Pattern detection after savemobile/src/utils/voiceEventParser.js- Template voice matchingmobile/src/utils/photoEventParser.js- Pattern detection for multi-item
| Question | Decision | Rationale |
|---|---|---|
| Time Window | 30 minutes | Reasonable for meal/supplement sessions |
| Occurrence Threshold | 2 times | Lower friction to create templates |
| Partial Matches | Yes (70%+ similarity) | Suggest template if user logs most items |
| Intermediate Storage | None - query voice_events | Source of truth, no sync issues |
The Meal Template Learning System transforms DashMobileApp from a logging tool into an intelligent health companion that learns user habits and offers proactive shortcuts.
Key Design Principles:
- voice_events is the source of truth - patterns are computed, not stored separately
- Only confirmed templates are persisted - no draft/pending state
- 2 occurrences = suggestion threshold - low friction to create templates
- Partial matching enabled - 4 of 5 items still suggests the template
By detecting repeated patterns and offering one-tap logging, we dramatically reduce friction for consistent health routines while maintaining the flexibility for ad-hoc logging.