Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support rendering partial markdown and improve performance #73

Open
wants to merge 6 commits into
base: promplate-demo
Choose a base branch
from
Open
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
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
"highlight.js": "^11.10.0",
"katex": "^0.16.11",
"lenis": "^1.1.13",
"markdown-it": "^14.1.0",
"markdown-it-highlightjs": "^4.2.0",
"markdown-it-katex": "^2.0.3",
"partial-json": "^0.1.7",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"reveal.js": "^5.1.0",
"solid-js": "^1.9.1",
"solid-markdown": "https://pkg.pr.new/promplate/solid-markdown@4387fb3",
"solid-toast": "^0.5.0",
"solidjs-use": "^2.3.0",
"svelte": "^4.2.19",
Expand Down
1,863 changes: 982 additions & 881 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useClipboard } from 'solidjs-use'
import hljs from 'highlight.js'
import { Show, createMemo, createSignal } from 'solid-js'
import type { SolidMarkdownComponents } from 'solid-markdown'

const availableLanguages = hljs.listLanguages()

const CodeBlock: SolidMarkdownComponents['code'] = (props) => {
const { children, inline } = props
if (inline)
return <code>{children}</code>

const [lang, setLang] = createSignal<string>()

const { copy, copied } = useClipboard({ copiedDuring: 1000 })

const code = createMemo<string>(() => {
setLang(props.class?.replace(/^language-/, ''))
// @ts-expect-error `children` is a function
return children()[0]()()
})

return (
<code class="group hljs block w-full overflow-x-scroll !px-20px !py-18px">
<Show when={code()}>
<button onClick={() => copy(code())} class="gap-1 text-sm gpt-copy-btn">
{copied() && <div class="text-sm font-sans">Copied!</div>}
<div class={copied() ? 'i-mingcute-copy-2-fill' : 'i-mingcute-copy-2-line'} />
</button>
<div class="contents" innerHTML={lang() && availableLanguages.includes(lang()!) ? hljs.highlight(code(), { language: lang()! }).value : hljs.highlightAuto(code()).value} />
</Show>
</code>
)
}

export default CodeBlock
71 changes: 20 additions & 51 deletions src/components/MessageItem.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createSignal } from 'solid-js'
import MarkdownIt from 'markdown-it'
import mdKatex from 'markdown-it-katex'
import mdHighlight from 'markdown-it-highlightjs'
import { useClipboard, useEventListener } from 'solidjs-use'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import remarkBreaks from 'remark-breaks'
import { SolidMarkdown } from 'solid-markdown'
import IconRefresh from './icons/Refresh'
import CodeBlock from './CodeBlock'
import type { Accessor } from 'solid-js'
import type { ChatMessage } from '@/types'

Expand All @@ -22,22 +23,6 @@ export default ({ role, message, showRetry, onRetry }: Props) => {
user: 'bg-$c-fg-30',
assistant: 'bg-emerald-600/50 dark:bg-emerald-300 sm:(bg-gradient-to-br from-cyan-200 to-green-200)',
}
const [source] = createSignal('')
const { copy, copied } = useClipboard({ source, copiedDuring: 1000 })

useEventListener('click', (e) => {
const el = e.target as HTMLElement
let code = null

if (el.matches('[data-code]')) {
code = decodeURIComponent(el.dataset.code!)
copy(code)
}
if (el.matches('[data-code] > div')) {
code = decodeURIComponent(el.parentElement!.dataset.code!)
copy(code)
}
})

function heuristicPatch(markdown: string) {
const pattern = /(^|\n)```\S*$/
Expand All @@ -48,41 +33,25 @@ export default ({ role, message, showRetry, onRetry }: Props) => {
: markdown
}

const htmlString = () => {
const md = MarkdownIt({
linkify: true,
breaks: true,
}).use(mdKatex).use(mdHighlight)
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args
const token = tokens[idx]
const rawCode = fence(...args)

return `<div class="relative group">
<div data-code=${encodeURIComponent(token.content)} class="gpt-copy-btn">
${copied() ? '<div mr-1 text-sm display-inline-block>Copied!</div><div i-mingcute-copy-2-fill></div>' : '<div i-mingcute-copy-2-line></div>'}
</div>
${rawCode}
</div>`
}

switch (typeof message) {
case 'function':
return md.render(heuristicPatch(message()))
case 'string':
return md.render(heuristicPatch(message))
default:
return ''
}
}

return (
<div class="px-2rem transition-colors -mx-2rem hover:bg-$c-fg-2 2xl:(px-2rem -mx-2rem) md:(px-5 transition-background-color -mx-5)">
<div class="py-0.5 transition-padding 2xl:py-2 md:py-1">
<div class="flex gap-3.5 rounded-lg" class:op-75={role === 'user'} class:reverse-self-msg={role === 'user' && alignRightMine}>
<div class={`shrink-0 w-7 h-7 my-4 rounded-full op-80 ${roleClass[role]} <sm:w-1 <sm:h-auto <md:transition-background-color`} />
<div class="relative max-w-full overflow-hidden break-words prose <sm:text-3.6 message" innerHTML={htmlString()} />
<SolidMarkdown
renderingStrategy="reconcile"
remarkPlugins={[remarkGfm, remarkBreaks, remarkMath]}
rehypePlugins={[rehypeKatex]}
class="relative max-w-full overflow-hidden break-words prose <sm:text-3.6 message"
components={{
code: CodeBlock,
pre({ children }) {
return <pre class="group overflow-hidden">{children}</pre>
},
}}
>
{heuristicPatch(typeof message === 'function' ? message() : message)}
</SolidMarkdown>
</div>
{showRetry?.() && onRetry && (
<div class="mb-2 fie px-3">
Expand Down
3 changes: 2 additions & 1 deletion src/message.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.message pre {
background-color: #24242d;
font-size: 0.8rem;
padding: 0.4rem 0.6rem;
padding: 0;
position: relative;
}

.dark .message pre {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class API {

const res = await fetch(`${promplateBaseUrl}/single/suggest`, {
method: 'PUT',
body: JSON.stringify({ messages, model: 'llama3-70b-8192', prefill: true }),
body: JSON.stringify({ messages, model: 'llama-3.2-90b-text-preview', prefill: true }),
headers: { 'content-type': 'application/json' },
})

Expand Down
2 changes: 1 addition & 1 deletion uno.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default defineConfig({
'b-slate-link': 'border-b border-($c-fg-70 none) hover:border-dashed',
'gpt-title': 'text-lg font-extrabold md:text-2xl sm:text-xl',
'gpt-subtitle': 'bg-gradient-(from-emerald-400 to-sky-200 to-r) bg-clip-text text-lg text-transparent font-extrabold md:text-2xl sm:text-xl',
'gpt-copy-btn': 'absolute top-12px right-12px z-3 fcc border b-transparent w-fit min-w-8 h-8 p-2 bg-white/4 hover:bg-white/8 text-white/85 dark:(bg-$c-fg-5 hover:bg-$c-fg-10 text-$c-fg-90) cursor-pointer transition-all duration-150 active:scale-90 op-0 group-hover:op-100 rounded-1.1 backdrop-blur-10',
'gpt-copy-btn': 'absolute top-12px right-12px z-3 fcc border b-transparent h-8 p-2 bg-white/4 hover:bg-white/8 text-white/85 dark:(bg-$c-fg-5 hover:bg-$c-fg-10 text-$c-fg-90) transition-all duration-150 active:scale-90 op-0 group-hover:op-100 rounded-1.1 backdrop-blur-10',
'gpt-retry-btn': 'fi gap-1 px-2 py-0.5 op-70 ring-1.2 ring-$c-fg-50 rounded-md text-sm <sm:text-xs <sm:ring-$c-fg-30 cursor-pointer hover:bg-$c-fg-5 transition-background-color',
'gpt-back-top-btn': 'fcc text-base fixed bottom-17px right-17px sm:(bottom-20px right-20px) z-10 cursor-pointer',
'gpt-password-input': 'px-4 py-3 h-12 rounded-sm bg-(slate op-15) base-focus',
Expand Down