From 04a175171b396e7403d56706ead357bc2e39ae3d Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 00:00:08 +0000 Subject: [PATCH] feat(renderer): interactive table with sort, filter & CSV export (M10.9) - InteractiveTable.vue: sortable columns, row filter, CSV export - Extracts markdown tables from AI responses into structured data - Click column header to sort (asc/desc), numeric-aware - Filter input for live row filtering - Renders alongside regular chat markdown Co-Authored-By: Claude Opus 4.6 --- .../app/src/components/chat/ChatMessage.vue | 16 +++ .../components/renderers/InteractiveTable.vue | 134 ++++++++++++++++++ .../app/src/composables/useTableExtractor.ts | 49 +++++++ 3 files changed, 199 insertions(+) create mode 100644 packages/app/src/components/renderers/InteractiveTable.vue create mode 100644 packages/app/src/composables/useTableExtractor.ts diff --git a/packages/app/src/components/chat/ChatMessage.vue b/packages/app/src/components/chat/ChatMessage.vue index af8f8b39..55d9d757 100644 --- a/packages/app/src/components/chat/ChatMessage.vue +++ b/packages/app/src/components/chat/ChatMessage.vue @@ -200,6 +200,15 @@ /> +
+ +
+
{ return extractRecipes(props.message.content) }) +const inlineTables = computed(() => { + if (isUser.value) return [] + return extractTables(props.message.content) +}) + const inlineEvents = computed(() => { if (isUser.value) return [] return extractEvents(props.message.content) diff --git a/packages/app/src/components/renderers/InteractiveTable.vue b/packages/app/src/components/renderers/InteractiveTable.vue new file mode 100644 index 00000000..5ff44d03 --- /dev/null +++ b/packages/app/src/components/renderers/InteractiveTable.vue @@ -0,0 +1,134 @@ + + + diff --git a/packages/app/src/composables/useTableExtractor.ts b/packages/app/src/composables/useTableExtractor.ts new file mode 100644 index 00000000..9c8de362 --- /dev/null +++ b/packages/app/src/composables/useTableExtractor.ts @@ -0,0 +1,49 @@ +/** + * Extract markdown tables from text into structured data + * for interactive table rendering. + */ + +export interface TableData { + headers: string[] + rows: string[][] + raw: string +} + +// Matches a markdown table: header row, separator row, data rows +const TABLE_RE = /^(\|[^\n]+\|)\n(\|[\s:|-]+\|)\n((?:\|[^\n]+\|\n?)+)/gm + +export function extractTables(text: string): TableData[] { + const results: TableData[] = [] + let m: RegExpExecArray | null + const re = new RegExp(TABLE_RE.source, TABLE_RE.flags) + + while ((m = re.exec(text)) !== null) { + const headerLine = m[1] + const dataBlock = m[3] + + const headers = parseRow(headerLine) + const rows = dataBlock + .trim() + .split('\n') + .map(parseRow) + .filter((r) => r.length > 0) + + if (headers.length > 0 && rows.length > 0) { + results.push({ headers, rows, raw: m[0] }) + } + } + + return results +} + +function parseRow(line: string): string[] { + return line + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((cell) => cell.trim()) +} + +export function hasTables(text: string): boolean { + return TABLE_RE.test(text) +}