feat(renderer): recipe & event card renderers (M10.4, M10.5)
- RecipeCard: ingredients checklist, numbered steps, scale slider - EventCard: date chip, countdown timer, ICS download, Google Calendar - Extract <recipe_ext> and <event_ext> tags from AI responses - Both render inline in chat messages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
aec87a347c
commit
c070e76515
@@ -1063,3 +1063,83 @@ export function stripMarkdownLinks(text: string): string {
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
// ─── Recipe extraction (M10.4) ─────────────────────────────────
|
||||
|
||||
export interface RecipeData {
|
||||
title: string
|
||||
servings?: string
|
||||
time?: string
|
||||
calories?: string
|
||||
ingredients: string[]
|
||||
steps: string[]
|
||||
}
|
||||
|
||||
const RECIPE_EXT_RE = /<recipe_ext\s+([^>]+)>([\s\S]*?)<\/recipe_ext>/gi
|
||||
|
||||
export function extractRecipes(text: string): RecipeData[] {
|
||||
const results: RecipeData[] = []
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = RECIPE_EXT_RE.exec(text)) !== null) {
|
||||
const attrs = m[1]
|
||||
const body = m[2]
|
||||
|
||||
const title = attrs.match(/title="([^"]*)"/)?.[1] || 'Recipe'
|
||||
const servings = attrs.match(/servings="([^"]*)"/)?.[1]
|
||||
const time = attrs.match(/time="([^"]*)"/)?.[1]
|
||||
const calories = attrs.match(/calories="([^"]*)"/)?.[1]
|
||||
|
||||
// Parse body: lines starting with - are ingredients, numbered lines are steps
|
||||
const lines = body.split('\n').map(l => l.trim()).filter(Boolean)
|
||||
const ingredients: string[] = []
|
||||
const steps: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^[-*]\s+/.test(line)) {
|
||||
ingredients.push(line.replace(/^[-*]\s+/, ''))
|
||||
} else if (/^\d+[.)]\s+/.test(line)) {
|
||||
steps.push(line.replace(/^\d+[.)]\s+/, ''))
|
||||
}
|
||||
}
|
||||
|
||||
results.push({ title, servings, time, calories, ingredients, steps })
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export function stripRecipeTags(text: string): string {
|
||||
return text.replace(RECIPE_EXT_RE, '').replace(/\n{3,}/g, '\n\n').trim()
|
||||
}
|
||||
|
||||
// ─── Event extraction (M10.5) ──────────────────────────────────
|
||||
|
||||
export interface EventData {
|
||||
title: string
|
||||
date: string
|
||||
location?: string
|
||||
url?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
const EVENT_EXT_RE = /<event_ext\s+([^>]*?)(?:\/>|>([\s\S]*?)<\/event_ext>)/gi
|
||||
|
||||
export function extractEvents(text: string): EventData[] {
|
||||
const results: EventData[] = []
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = EVENT_EXT_RE.exec(text)) !== null) {
|
||||
const attrs = m[1]
|
||||
const body = m[2]?.trim()
|
||||
|
||||
const title = attrs.match(/title="([^"]*)"/)?.[1] || 'Event'
|
||||
const date = attrs.match(/date="([^"]*)"/)?.[1] || ''
|
||||
const location = attrs.match(/location="([^"]*)"/)?.[1]
|
||||
const url = attrs.match(/url="([^"]*)"/)?.[1]
|
||||
|
||||
results.push({ title, date, location, url, description: body })
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export function stripEventTags(text: string): string {
|
||||
return text.replace(EVENT_EXT_RE, '').replace(/\n{3,}/g, '\n\n').trim()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user