Skip to content

Commit

Permalink
pref: use web worker to count token and render markdown
Browse files Browse the repository at this point in the history
  • Loading branch information
ourongxing committed Jun 14, 2023
1 parent 8ff3811 commit d168a6d
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 91 deletions.
24 changes: 11 additions & 13 deletions src/components/Chat/MessageContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
type Accessor,
For,
Show,
createEffect,
createSignal,
createMemo
} from "solid-js"
import { type Accessor, For, Show, createEffect, createMemo } from "solid-js"
import { RootStore, defaultMessage } from "~/store"
import { scrollToBottom } from "~/utils"
import MessageItem from "./MessageItem"
Expand Down Expand Up @@ -45,6 +38,11 @@ export default function ({
return true
})

const shownTokens = (token: number) => {
if (token > 1000) return (token / 1000).toFixed(1) + "k"
else return token
}

return (
<div
class="px-1em"
Expand Down Expand Up @@ -87,22 +85,22 @@ export default function ({
when={store.inputContentToken}
fallback={
<span class="mx-1 text-slate/40">
{`有效上下文 Tokens : ${
{`有效上下文 Tokens : ${shownTokens(
store.contextToken
}/$${store.contextToken$.toFixed(4)}`}
)}/$${store.contextToken$.toFixed(4)}`}
</span>
}
>
<span class="mx-1 text-slate/40">
{`有效上下文+提问 Tokens : ${
{`有效上下文+提问 Tokens : ${shownTokens(
store.contextToken + store.inputContentToken
}(`}
)}(`}
<span
classList={{
"text-red-500": store.remainingToken < 0
}}
>
{store.remainingToken}
{shownTokens(store.remainingToken)}
</span>
{`)/$${(store.contextToken$ + store.inputContentToken$).toFixed(
4
Expand Down
95 changes: 57 additions & 38 deletions src/components/Chat/MessageItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Show } from "solid-js"
import { Show, createEffect, createSignal } from "solid-js"
import { useCopyCode } from "~/hooks"
import { RootStore } from "~/store"
import type { ChatMessage } from "~/types"
import { copyToClipboard } from "~/utils"
import { copyToClipboard, throttle250 } from "~/utils"
import MessageAction from "./MessageAction"
import openai from "/assets/openai.svg?raw"
import vercel from "/assets/vercel.svg?raw"
import type { FakeRoleUnion } from "./SettingAction"
import { renderMarkdown } from "~/wokers"

interface Props {
message: ChatMessage
Expand All @@ -18,6 +19,7 @@ interface Props {
export default (props: Props) => {
useCopyCode()
const { store, setStore } = RootStore
const [renderedMarkdown, setRenderedMarkdown] = createSignal("")
const roleClass = {
error: "bg-gradient-to-r from-red-400 to-red-700",
system: "bg-gradient-to-r from-gray-300 via-gray-200 to-gray-300",
Expand Down Expand Up @@ -105,47 +107,64 @@ export default (props: Props) => {
}
}

createEffect(() => {
props.message.content

if (props.message.type === "temporary") {
throttle250(() => {
renderMarkdown(props.message.content).then(html => {
setRenderedMarkdown(html)
})
})
} else {
renderMarkdown(props.message.content).then(html => {
setRenderedMarkdown(html)
})
}
})

return (
<div
class="group flex gap-3 px-4 mx--4 rounded-lg transition-colors sm:hover:bg-slate/6 dark:sm:hover:bg-slate/5 relative message-item"
classList={{
temporary: props.message.type === "temporary"
}}
>
<Show when={renderedMarkdown()}>
<div
class={`shadow-slate-5 shadow-sm dark:shadow-none shrink-0 w-7 h-7 mt-4 rounded-full op-80 flex items-center justify-center cursor-pointer ${
roleClass[props.message.role]
}`}
class="group flex gap-3 px-4 mx--4 rounded-lg transition-colors sm:hover:bg-slate/6 dark:sm:hover:bg-slate/5 relative message-item"
classList={{
"animate-spin": props.message.type === "temporary"
temporary: props.message.type === "temporary"
}}
onClick={lockMessage}
>
<Show when={props.message.type === "locked"}>
<div class="i-carbon:locked text-white" />
</Show>
<div
class={`shadow-slate-5 shadow-sm dark:shadow-none shrink-0 w-7 h-7 mt-4 rounded-full op-80 flex items-center justify-center cursor-pointer ${
roleClass[props.message.role]
}`}
classList={{
"animate-spin": props.message.type === "temporary"
}}
onClick={lockMessage}
>
<Show when={props.message.type === "locked"}>
<div class="i-carbon:locked text-white" />
</Show>
</div>
<div
class="message prose prose-slate dark:prose-invert dark:text-slate break-words overflow-hidden"
innerHTML={renderedMarkdown()
.replace(
/\s*Vercel\s*/g,
`<a href="http://vercel.com/?utm_source=busiyi&utm_campaign=oss" style="border-bottom:0;margin-left: 6px">${vercel}</a>`
)
.replace(
/\s*OpenAI\s*/g,
`<a href="https://www.openai.com" style="border-bottom:0;margin-left: 6px">${openai}</a>`
)}
/>
<MessageAction
del={del}
copy={copy}
edit={edit}
reAnswer={reAnswer}
role={props.message.role}
hidden={props.hiddenAction}
/>
</div>
<div
class="message prose prose-slate dark:prose-invert dark:text-slate break-words overflow-hidden"
innerHTML={store.md
?.render(props.message.content)
.replace(
/\s*Vercel\s*/g,
`<a href="http://vercel.com/?utm_source=busiyi&utm_campaign=oss" style="border-bottom:0;margin-left: 6px">${vercel}</a>`
)
.replace(
/\s*OpenAI\s*/g,
`<a href="https://www.openai.com" style="border-bottom:0;margin-left: 6px">${openai}</a>`
)}
/>
<MessageAction
del={del}
copy={copy}
edit={edit}
reAnswer={reAnswer}
role={props.message.role}
hidden={props.hiddenAction}
/>
</div>
</Show>
)
}
7 changes: 2 additions & 5 deletions src/components/Chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ export default function () {
createResizeObserver(containerRef, ({ width }, el) => {
if (el === containerRef) setContainerWidth(`${width}px`)
})
import("~/markdown-it").then(({ mdFactory }) => {
mdFactory().then(md => {
if (!store.md) setStore("md", md)
document.querySelector("#root")?.classList.remove("before")
})
window.setTimeout(() => {
document.querySelector("#root")?.classList.remove("before")
})
document.querySelector("#root")?.classList.add("after")
loadSession(store.sessionId)
Expand Down
26 changes: 12 additions & 14 deletions src/markdown-it/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import MarkdownIt from "markdown-it"
// @ts-ignore
import mdKatex from "markdown-it-katex"
import mdHighlight from "markdown-it-highlightjs"
import mdKbd from "markdown-it-kbd"
import preWrapperPlugin from "./preWrapper"

export async function mdFactory() {
// @ts-ignore
const { default: mdKatex } = await import("markdown-it-katex")
const { default: mdHighlight } = await import("markdown-it-highlightjs")
return MarkdownIt({
linkify: true,
breaks: true
export const md = MarkdownIt({
linkify: true,
breaks: true
})
.use(mdKatex)
.use(mdHighlight, {
inline: true
})
.use(mdKatex)
.use(mdHighlight, {
inline: true
})
.use(mdKbd)
.use(preWrapperPlugin)
}
.use(mdKbd)
.use(preWrapperPlugin)
52 changes: 32 additions & 20 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createStore } from "solid-js/store"
import { defaultEnv } from "./env"
import { type ChatMessage, LocalStorageKey } from "./types"
import { batch, createMemo, createRoot } from "solid-js"
import { countTokens, fetchAllSessions, getSession } from "./utils"
import { batch, createEffect, createMemo, createRoot } from "solid-js"
import { fetchAllSessions, getSession, throttle250 } from "./utils"
import { Fzf } from "fzf"
import type { Model, Option, SimpleModel } from "~/types"
import type MarkdownIt from "markdown-it"
import { countTokensInWorker } from "~/wokers"

let globalSettings = { ...defaultEnv.CLIENT_GLOBAL_SETTINGS }
let _ = import.meta.env.CLIENT_GLOBAL_SETTINGS
Expand Down Expand Up @@ -102,27 +102,20 @@ function Store() {
inputContent: "",
messageList: [] as ChatMessage[],
currentAssistantMessage: "",
contextToken: 0,
currentMessageToken: 0,
inputContentToken: 0,
loading: false,
inputRef: null as HTMLTextAreaElement | null,
md: null as MarkdownIt | null,
get validContext() {
return validContext()
},
get contextToken() {
return contextToken()
},
get contextToken$() {
return contextToken$()
},
get currentMessageToken() {
return currentMessageToken()
},
get currentMessageToken$() {
return currentMessageToken$()
},
get inputContentToken() {
return inputContentToken()
},
get inputContentToken$() {
return inputContentToken$()
},
Expand All @@ -144,15 +137,34 @@ function Store() {
: store.messageList.filter(k => k.type === "locked")
)

const contextToken = createMemo(() =>
store.validContext.reduce((acc, cur) => acc + countTokens(cur.content), 0)
)
createEffect(() => {
store.inputContent
throttle250(() => {
countTokensInWorker(store.inputContent).then(res => {
setStore("inputContentToken", res)
})
})
})

const currentMessageToken = createMemo(() =>
countTokens(store.currentAssistantMessage)
)
createEffect(() => {
store.messageList
throttle250(() => {
countTokensInWorker(
store.messageList.map(k => k.content).join("\n")
).then(res => {
setStore("contextToken", res)
})
})
})

const inputContentToken = createMemo(() => countTokens(store.inputContent))
createEffect(() => {
store.currentAssistantMessage
throttle250(() => {
countTokensInWorker(store.currentAssistantMessage).then(res => {
setStore("currentMessageToken", res)
})
})
})

const remainingToken = createMemo(
() =>
Expand Down
12 changes: 11 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import throttle from "just-throttle"
export * from "./tokens"
export * from "./storage"

export async function copyToClipboard(text: string) {
Expand Down Expand Up @@ -127,3 +126,14 @@ export function isEmoji(character: string) {
)
return regex.test(character)
}

export const throttle250 = throttle(
f => {
f()
},
300,
{
leading: false,
trailing: true
}
)
3 changes: 3 additions & 0 deletions src/wokers/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
35 changes: 35 additions & 0 deletions src/wokers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import TokenWorker from "./tokens.worker?worker"
import MarkdownWorker from "./markdown.worker?worker"
import { generateId } from "~/utils"

const tokenWorker = new TokenWorker()
const markdownWorker = new MarkdownWorker()
export function countTokensInWorker(content: string): Promise<number> {
if (!content) return Promise.resolve(0)
const id = generateId()
tokenWorker.postMessage({ type: "token", id, payload: content })
return new Promise(resolve => {
function handler(e: MessageEvent) {
if (e.data.type === "token-return" && e.data.id === id) {
tokenWorker.removeEventListener("message", handler)
resolve(e.data.payload as number)
}
}
tokenWorker.addEventListener("message", handler)
})
}

export function renderMarkdown(content: string): Promise<string> {
if (!content) return Promise.resolve("")
const id = generateId()
markdownWorker.postMessage({ type: "markdown", id, payload: content })
return new Promise(resolve => {
function handler(e: MessageEvent) {
if (e.data.type === "markdown-return" && e.data.id === id) {
markdownWorker.removeEventListener("message", handler)
resolve(e.data.payload as string)
}
}
markdownWorker.addEventListener("message", handler)
})
}
14 changes: 14 additions & 0 deletions src/wokers/markdown.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { md } from "~/markdown-it"

const sw = self as unknown as ServiceWorkerGlobalScope & typeof globalThis

sw.addEventListener("message", event => {
if (event.data.type === "markdown" && event.data.payload) {
const renderd = md.render(event.data.payload)
sw.postMessage({
type: "markdown-return",
payload: renderd,
id: event.data.id
})
}
})
Loading

0 comments on commit d168a6d

Please sign in to comment.