diff --git a/packages/app/src/components/chat/ChatMessage.vue b/packages/app/src/components/chat/ChatMessage.vue index e2e347cc..7e865174 100644 --- a/packages/app/src/components/chat/ChatMessage.vue +++ b/packages/app/src/components/chat/ChatMessage.vue @@ -184,6 +184,22 @@ /> +
+ +
+ +
+ +
+
{ return extractCashuTokens(props.message.content) }) +const inlineRecipes = computed(() => { + if (isUser.value) return [] + return extractRecipes(props.message.content) +}) + +const inlineEvents = computed(() => { + if (isUser.value) return [] + return extractEvents(props.message.content) +}) + const isCodeResponse = computed(() => !isUser.value && props.triggeringQuery.trim().toLowerCase() === '/code' ) diff --git a/packages/app/src/components/renderers/EventCard.vue b/packages/app/src/components/renderers/EventCard.vue new file mode 100644 index 00000000..ea93dac1 --- /dev/null +++ b/packages/app/src/components/renderers/EventCard.vue @@ -0,0 +1,142 @@ + + + diff --git a/packages/app/src/components/renderers/RecipeCard.vue b/packages/app/src/components/renderers/RecipeCard.vue new file mode 100644 index 00000000..40550d1d --- /dev/null +++ b/packages/app/src/components/renderers/RecipeCard.vue @@ -0,0 +1,111 @@ + + + diff --git a/packages/app/src/composables/contentExtraction.ts b/packages/app/src/composables/contentExtraction.ts index 36187a27..a69004fe 100644 --- a/packages/app/src/composables/contentExtraction.ts +++ b/packages/app/src/composables/contentExtraction.ts @@ -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 = /]+)>([\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 = /]*?)(?:\/>|>([\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() +}