Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mock/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"openAIBaseURL": "http://localhost:8080/v1",
"capabilities": [
"text:markdown",
"text",
"vision",
"thinking"
],
Expand Down
11 changes: 3 additions & 8 deletions specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ If the model name cannot be retrieved from the /models endpoint of the OpenAI AP
## Configuration
The web application should consume static configurations of the following parameters:
- openAIBaseURL(string): OpenAI base URL
- capabilities(string array): Model capabilities (text, vision, audio, etc).
- capabilities(string array): Model capabilities (text, vision, audio, thinking, etc)
- instanceName(string): used to construct the UI title
- engineName(string): for display in the UI
- chatFormat(string): the markup used by the model, e.g. "markdown" or "plaintext"
Expand Down Expand Up @@ -97,16 +97,11 @@ curl http://localhost:8000/v1/chat/completions \

### Chat format

The capabilities field can contain a special value `text:markdown`.
This field indicates that the markup used by the respective model is Markdown.
Always assume the markup used by the model is Markdown.
Format the prompts, reasoning and responses following Markdown.

Always sanitize the input and output to prevent XSS vulnerabilities.

If this key is not set, assume plain text is used.
The prompts, reasoning and responses should be rendered directly, and not be interpreted as any markup.
Therefore, any characters that could be interpreted as HTML should be escaped, and line breaks should be preserved.

If the `text:markdown` capability is present, format the prompts, reasoning and responses following Markdown.
As far as practically possible, use standard Vanilla Framework [typography](https://vanillaframework.io/docs/base/typography) to format the Markdown styles.

Use `markdown-it` as the Markdown parser, and apply syntax highlighting using `highlight.js` for code blocks in the responses.
Expand Down
32 changes: 6 additions & 26 deletions src/components/MessageBubble.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,11 @@ const emit = defineEmits<{

const store = useChatStore()

function formatPlain(text: string | null | undefined): string {
if (!text) return ''
return DOMPurify.sanitize(
String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/\n/g, '<br>'),
)
}

function formatMarkdown(text: string | null | undefined): string {
if (!text) return ''
return DOMPurify.sanitize(md.render(String(text)))
}

function formatText(text: string | null | undefined): string {
return store.isMarkdown ? formatMarkdown(text) : formatPlain(text)
}

function getUserText(): string {
const c = props.message.content
if (Array.isArray(c)) {
Expand Down Expand Up @@ -114,9 +97,8 @@ watch(
<div class="chat-message__avatar" aria-label="You">You</div>
<div class="chat-message__bubble">
<div
class="chat-message__text"
:class="{ 'chat-message__text--markdown': store.isMarkdown }"
v-html="formatText(getUserText())"
class="chat-message__text chat-message__text--markdown"
v-html="formatMarkdown(getUserText())"
></div>
<div v-if="message.images && message.images.length" class="chat-message__images">
<img
Expand Down Expand Up @@ -153,11 +135,10 @@ watch(
<div
v-show="message.reasoningOpen"
ref="reasoningContainer"
class="reasoning-content"
:class="{ 'reasoning-content--markdown': store.isMarkdown }"
class="reasoning-content reasoning-content--markdown"
@scroll="onReasoningScroll"
>
<div v-html="formatText(message.reasoningContent)"></div>
<div v-html="formatMarkdown(message.reasoningContent)"></div>
</div>
</div>

Expand All @@ -170,11 +151,10 @@ watch(
<!-- Response text -->
<div
v-else
class="chat-message__text"
:class="{ 'chat-message__text--markdown': store.isMarkdown }"
class="chat-message__text chat-message__text--markdown"
>
<span
v-html="formatText(typeof message.content === 'string' ? message.content : '')"
v-html="formatMarkdown(typeof message.content === 'string' ? message.content : '')"
></span>
</div>

Expand Down
3 changes: 0 additions & 3 deletions src/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ export const useChatStore = defineStore('chat', () => {
() => config.value.capabilities?.includes('thinking') ?? false,
)

const isMarkdown = computed(() => config.value.capabilities?.includes('text:markdown') ?? false)

// ── Actions ───────────────────────────────────────────────────────────────
async function fetchConfig(): Promise<void> {
const response = await fetch(import.meta.env.VITE_CONFIG_URL)
Expand Down Expand Up @@ -144,7 +142,6 @@ export const useChatStore = defineStore('chat', () => {
uiTitle,
supportsVision,
showThinkingToggle,
isMarkdown,
fetchConfig,
fetchModelName,
openLightbox,
Expand Down