improve markdown rendering (#19)
This commit is contained in:
parent
5b59ee04e7
commit
f47b965c48
10 changed files with 98 additions and 30 deletions
|
|
@ -41,6 +41,7 @@
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"isomorphic-dompurify": "^2.25.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"melt": "https://pkg.vc/-/@melt-ui/melt@42e572f",
|
"melt": "https://pkg.vc/-/@melt-ui/melt@42e572f",
|
||||||
"mode-watcher": "^1.0.8",
|
"mode-watcher": "^1.0.8",
|
||||||
|
|
|
||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
|
|
@ -123,6 +123,9 @@ importers:
|
||||||
globals:
|
globals:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.2.0
|
version: 16.2.0
|
||||||
|
isomorphic-dompurify:
|
||||||
|
specifier: ^2.25.0
|
||||||
|
version: 2.25.0
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^26.0.0
|
specifier: ^26.0.0
|
||||||
version: 26.1.0
|
version: 26.1.0
|
||||||
|
|
@ -1065,6 +1068,9 @@ packages:
|
||||||
'@types/node@24.0.1':
|
'@types/node@24.0.1':
|
||||||
resolution: {integrity: sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==}
|
resolution: {integrity: sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
|
|
@ -1440,6 +1446,9 @@ packages:
|
||||||
dom-accessibility-api@0.6.3:
|
dom-accessibility-api@0.6.3:
|
||||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||||
|
|
||||||
|
dompurify@3.2.6:
|
||||||
|
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
|
||||||
|
|
||||||
dotenv@16.5.0:
|
dotenv@16.5.0:
|
||||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -1714,6 +1723,10 @@ packages:
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
isomorphic-dompurify@2.25.0:
|
||||||
|
resolution: {integrity: sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
jest-axe@9.0.0:
|
jest-axe@9.0.0:
|
||||||
resolution: {integrity: sha512-Xt7O0+wIpW31lv0SO1wQZUTyJE7DEmnDEZeTt9/S9L5WUywxrv8BrgvTuQEqujtfaQOcJ70p4wg7UUgK1E2F5g==}
|
resolution: {integrity: sha512-Xt7O0+wIpW31lv0SO1wQZUTyJE7DEmnDEZeTt9/S9L5WUywxrv8BrgvTuQEqujtfaQOcJ70p4wg7UUgK1E2F5g==}
|
||||||
engines: {node: '>= 16.0.0'}
|
engines: {node: '>= 16.0.0'}
|
||||||
|
|
@ -3477,6 +3490,9 @@ snapshots:
|
||||||
undici-types: 7.8.0
|
undici-types: 7.8.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
|
|
@ -3847,6 +3863,10 @@ snapshots:
|
||||||
|
|
||||||
dom-accessibility-api@0.6.3: {}
|
dom-accessibility-api@0.6.3: {}
|
||||||
|
|
||||||
|
dompurify@3.2.6:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
dotenv@16.5.0: {}
|
dotenv@16.5.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
@ -4172,6 +4192,16 @@ snapshots:
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
isomorphic-dompurify@2.25.0:
|
||||||
|
dependencies:
|
||||||
|
dompurify: 3.2.6
|
||||||
|
jsdom: 26.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- canvas
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
jest-axe@9.0.0:
|
jest-axe@9.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
axe-core: 4.9.1
|
axe-core: 4.9.1
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ export const create = mutation({
|
||||||
export const createAndAddMessage = mutation({
|
export const createAndAddMessage = mutation({
|
||||||
args: {
|
args: {
|
||||||
content: v.string(),
|
content: v.string(),
|
||||||
|
content_html: v.optional(v.string()),
|
||||||
role: messageRoleValidator,
|
role: messageRoleValidator,
|
||||||
session_token: v.string(),
|
session_token: v.string(),
|
||||||
images: v.optional(v.array(v.object({
|
images: v.optional(v.array(v.object({
|
||||||
|
|
@ -120,6 +121,7 @@ export const createAndAddMessage = mutation({
|
||||||
|
|
||||||
const messageId = await ctx.runMutation(api.messages.create, {
|
const messageId = await ctx.runMutation(api.messages.create, {
|
||||||
content: args.content,
|
content: args.content,
|
||||||
|
content_html: args.content_html,
|
||||||
role: args.role,
|
role: args.role,
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
session_token: args.session_token,
|
session_token: args.session_token,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export const create = mutation({
|
||||||
args: {
|
args: {
|
||||||
conversation_id: v.string(),
|
conversation_id: v.string(),
|
||||||
content: v.string(),
|
content: v.string(),
|
||||||
|
content_html: v.optional(v.string()),
|
||||||
role: messageRoleValidator,
|
role: messageRoleValidator,
|
||||||
session_token: v.string(),
|
session_token: v.string(),
|
||||||
|
|
||||||
|
|
@ -73,6 +74,7 @@ export const create = mutation({
|
||||||
ctx.db.insert('messages', {
|
ctx.db.insert('messages', {
|
||||||
conversation_id: args.conversation_id,
|
conversation_id: args.conversation_id,
|
||||||
content: args.content,
|
content: args.content,
|
||||||
|
content_html: args.content_html,
|
||||||
role: args.role,
|
role: args.role,
|
||||||
// Optional, coming from SK API route
|
// Optional, coming from SK API route
|
||||||
model_id: args.model_id,
|
model_id: args.model_id,
|
||||||
|
|
@ -96,6 +98,7 @@ export const updateContent = mutation({
|
||||||
session_token: v.string(),
|
session_token: v.string(),
|
||||||
message_id: v.string(),
|
message_id: v.string(),
|
||||||
content: v.string(),
|
content: v.string(),
|
||||||
|
content_html: v.optional(v.string()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const session = await ctx.runQuery(api.betterAuth.publicGetSession, {
|
const session = await ctx.runQuery(api.betterAuth.publicGetSession, {
|
||||||
|
|
@ -114,6 +117,7 @@ export const updateContent = mutation({
|
||||||
|
|
||||||
await ctx.db.patch(message._id, {
|
await ctx.db.patch(message._id, {
|
||||||
content: args.content,
|
content: args.content,
|
||||||
|
content_html: args.content_html,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -125,6 +129,7 @@ export const updateMessage = mutation({
|
||||||
token_count: v.optional(v.number()),
|
token_count: v.optional(v.number()),
|
||||||
cost_usd: v.optional(v.number()),
|
cost_usd: v.optional(v.number()),
|
||||||
generation_id: v.optional(v.string()),
|
generation_id: v.optional(v.string()),
|
||||||
|
content_html: v.optional(v.string()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const session = await ctx.runQuery(api.betterAuth.publicGetSession, {
|
const session = await ctx.runQuery(api.betterAuth.publicGetSession, {
|
||||||
|
|
@ -145,6 +150,7 @@ export const updateMessage = mutation({
|
||||||
token_count: args.token_count,
|
token_count: args.token_count,
|
||||||
cost_usd: args.cost_usd,
|
cost_usd: args.cost_usd,
|
||||||
generation_id: args.generation_id,
|
generation_id: args.generation_id,
|
||||||
|
content_html: args.content_html,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export default defineSchema({
|
||||||
conversation_id: v.string(),
|
conversation_id: v.string(),
|
||||||
role: v.union(v.literal('user'), v.literal('assistant'), v.literal('system')),
|
role: v.union(v.literal('user'), v.literal('assistant'), v.literal('system')),
|
||||||
content: v.string(),
|
content: v.string(),
|
||||||
|
content_html: v.optional(v.string()),
|
||||||
// Optional, coming from SK API route
|
// Optional, coming from SK API route
|
||||||
model_id: v.optional(v.string()),
|
model_id: v.optional(v.string()),
|
||||||
provider: v.optional(providerValidator),
|
provider: v.optional(providerValidator),
|
||||||
|
|
|
||||||
25
src/lib/utils/markdown-it.ts
Normal file
25
src/lib/utils/markdown-it.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { fromAsyncCodeToHtml } from '@shikijs/markdown-it/async';
|
||||||
|
import MarkdownItAsync from 'markdown-it-async';
|
||||||
|
import { codeToHtml } from 'shiki';
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
||||||
|
const md = MarkdownItAsync();
|
||||||
|
|
||||||
|
md.use(
|
||||||
|
fromAsyncCodeToHtml(
|
||||||
|
// Pass the codeToHtml function
|
||||||
|
codeToHtml,
|
||||||
|
{
|
||||||
|
themes: {
|
||||||
|
light: 'github-light-default',
|
||||||
|
dark: 'github-dark-default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
function sanitizeHtml(html: string) {
|
||||||
|
return DOMPurify.sanitize(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { md, sanitizeHtml }
|
||||||
|
|
@ -1,22 +1,5 @@
|
||||||
import { fromAsyncCodeToHtml } from '@shikijs/markdown-it/async';
|
|
||||||
import MarkdownItAsync from 'markdown-it-async';
|
|
||||||
import type { Getter } from 'runed';
|
import type { Getter } from 'runed';
|
||||||
import { codeToHtml } from 'shiki';
|
import { md } from './markdown-it';
|
||||||
|
|
||||||
const md = MarkdownItAsync();
|
|
||||||
|
|
||||||
md.use(
|
|
||||||
fromAsyncCodeToHtml(
|
|
||||||
// Pass the codeToHtml function
|
|
||||||
codeToHtml,
|
|
||||||
{
|
|
||||||
themes: {
|
|
||||||
light: 'github-light-default',
|
|
||||||
dark: 'github-dark-default',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export class Markdown {
|
export class Markdown {
|
||||||
highlighted = $state<string | null>(null);
|
highlighted = $state<string | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { err, ok, Result, ResultAsync } from 'neverthrow';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import { generationAbortControllers } from './cache.js';
|
import { generationAbortControllers } from './cache.js';
|
||||||
|
import { md } from '$lib/utils/markdown-it.js';
|
||||||
|
|
||||||
// Set to true to enable debug logging
|
// Set to true to enable debug logging
|
||||||
const ENABLE_LOGGING = true;
|
const ENABLE_LOGGING = true;
|
||||||
|
|
@ -379,6 +380,11 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentHtmlResultPromise = ResultAsync.fromPromise(
|
||||||
|
md.renderAsync(content),
|
||||||
|
(e) => `Failed to render HTML: ${e}`
|
||||||
|
);
|
||||||
|
|
||||||
const generationStatsResult = await retryResult(() => getGenerationStats(generationId!, key), {
|
const generationStatsResult = await retryResult(() => getGenerationStats(generationId!, key), {
|
||||||
delay: 500,
|
delay: 500,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
|
|
@ -398,6 +404,12 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`,
|
||||||
|
|
||||||
log('Background: Got generation stats', startTime);
|
log('Background: Got generation stats', startTime);
|
||||||
|
|
||||||
|
const contentHtmlResult = await contentHtmlResultPromise;
|
||||||
|
|
||||||
|
if (contentHtmlResult.isErr()) {
|
||||||
|
log(`Background: Failed to render HTML: ${contentHtmlResult.error}`, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
const [updateMessageResult, updateGeneratingResult, updateCostUsdResult] = await Promise.all([
|
const [updateMessageResult, updateGeneratingResult, updateCostUsdResult] = await Promise.all([
|
||||||
ResultAsync.fromPromise(
|
ResultAsync.fromPromise(
|
||||||
client.mutation(api.messages.updateMessage, {
|
client.mutation(api.messages.updateMessage, {
|
||||||
|
|
@ -406,6 +418,7 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`,
|
||||||
cost_usd: generationStats.total_cost,
|
cost_usd: generationStats.total_cost,
|
||||||
generation_id: generationId,
|
generation_id: generationId,
|
||||||
session_token: sessionToken,
|
session_token: sessionToken,
|
||||||
|
content_html: contentHtmlResult.unwrapOr(undefined),
|
||||||
}),
|
}),
|
||||||
(e) => `Failed to update message: ${e}`
|
(e) => `Failed to update message: ${e}`
|
||||||
),
|
),
|
||||||
|
|
@ -521,6 +534,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const convMessageResult = await ResultAsync.fromPromise(
|
const convMessageResult = await ResultAsync.fromPromise(
|
||||||
client.mutation(api.conversations.createAndAddMessage, {
|
client.mutation(api.conversations.createAndAddMessage, {
|
||||||
content: args.message,
|
content: args.message,
|
||||||
|
content_html: '',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
images: args.images,
|
images: args.images,
|
||||||
session_token: sessionToken,
|
session_token: sessionToken,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { sanitizeHtml } from '$lib/utils/markdown-it';
|
||||||
import { Markdown } from '$lib/utils/markdown.svelte';
|
import { Markdown } from '$lib/utils/markdown.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -10,4 +11,4 @@
|
||||||
const markdown = new Markdown(() => content);
|
const markdown = new Markdown(() => content);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@html markdown.current}
|
{@html sanitizeHtml(markdown.current ?? '')}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import '../../../markdown.css';
|
import '../../../markdown.css';
|
||||||
import MarkdownRenderer from './markdown-renderer.svelte';
|
import MarkdownRenderer from './markdown-renderer.svelte';
|
||||||
import { ImageModal } from '$lib/components/ui/image-modal';
|
import { ImageModal } from '$lib/components/ui/image-modal';
|
||||||
|
import { sanitizeHtml } from '$lib/utils/markdown-it';
|
||||||
|
|
||||||
const style = tv({
|
const style = tv({
|
||||||
base: 'prose rounded-xl p-2',
|
base: 'prose rounded-xl p-2',
|
||||||
|
|
@ -58,6 +59,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class={style({ role: message.role })}>
|
<div class={style({ role: message.role })}>
|
||||||
|
{#if message.content_html}
|
||||||
|
{@html sanitizeHtml(message.content_html)}
|
||||||
|
{:else}
|
||||||
<svelte:boundary>
|
<svelte:boundary>
|
||||||
<MarkdownRenderer content={message.content} />
|
<MarkdownRenderer content={message.content} />
|
||||||
|
|
||||||
|
|
@ -70,6 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</svelte:boundary>
|
</svelte:boundary>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue