mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-19 15:25:19 +00:00
The AI chat drawer was rendering assistant responses as plain text, so code blocks, lists, tables and inline formatting came through as literal asterisks and backticks — noisy and hard to scan. Adds a shared renderMarkdown() helper in resources/scripts/utils/ markdown.ts that parses GFM markdown via marked and sanitizes the result with DOMPurify before handing it to Vue's v-html. AiChatMessage uses the helper for assistant messages only; user messages stay as plain text since markdown syntax in their own typed input would be surprising. Assistant bubbles get the Tailwind `prose prose-sm` classes from the already-enabled @tailwindcss/typography plugin so headings, lists and code blocks inherit sensible defaults without per-element styling. Security: DOMPurify runs in its default browser profile, which strips <script>, event handlers, javascript: URLs and every other XSS vector. The AI provider isn't a trusted source — it can echo arbitrary user input and tool-call results from the database — so sanitization is non-negotiable even though the immediate source is our own backend.
47 lines
1.4 KiB
Vue
47 lines
1.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { AiChatMessage } from '@/scripts/types/ai-config'
|
|
import { renderMarkdown } from '@/scripts/utils/markdown'
|
|
|
|
const props = defineProps<{
|
|
message: AiChatMessage
|
|
}>()
|
|
|
|
const isUser = computed(() => props.message.role === 'user')
|
|
|
|
// Assistant messages get rendered as markdown → sanitized HTML so GFM
|
|
// features (code blocks, lists, tables, inline formatting) display as
|
|
// the model intended. User messages stay as plain text because the
|
|
// user typed them verbatim and markdown syntax would be surprising.
|
|
const renderedHtml = computed(() =>
|
|
isUser.value ? '' : renderMarkdown(props.message.content ?? ''),
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="flex"
|
|
:class="isUser ? 'justify-end' : 'justify-start'"
|
|
>
|
|
<div
|
|
class="max-w-[85%] rounded-lg px-4 py-2 text-sm"
|
|
:class="
|
|
isUser
|
|
? 'bg-primary-500 text-white'
|
|
: 'bg-surface-tertiary text-body'
|
|
"
|
|
>
|
|
<p v-if="isUser" class="whitespace-pre-wrap break-words">
|
|
{{ message.content ?? '' }}
|
|
</p>
|
|
<!-- Assistant output is sanitized via DOMPurify in renderMarkdown
|
|
before it reaches v-html — see resources/scripts/utils/markdown.ts. -->
|
|
<div
|
|
v-else
|
|
class="prose prose-sm max-w-none break-words"
|
|
v-html="renderedHtml"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|