fix: restore complete source code and fix launchers
- Copy complete source code packages from original CodeNomad project - Add root package.json with npm workspace configuration - Include electron-app, server, ui, tauri-app, and opencode-config packages - Fix Launch-Windows.bat and Launch-Dev-Windows.bat to work with correct npm scripts - Fix Launch-Unix.sh to work with correct npm scripts - Launchers now correctly call npm run dev:electron which launches Electron app
This commit is contained in:
165
packages/ui/src/components/markdown.tsx
Normal file
165
packages/ui/src/components/markdown.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
||||
import { renderMarkdown, onLanguagesLoaded, initMarkdown, decodeHtmlEntities } from "../lib/markdown"
|
||||
import type { TextPart, RenderCache } from "../types/message"
|
||||
import { getLogger } from "../lib/logger"
|
||||
const log = getLogger("session")
|
||||
|
||||
const markdownRenderCache = new Map<string, RenderCache>()
|
||||
|
||||
function makeMarkdownCacheKey(partId: string, themeKey: string, highlightEnabled: boolean) {
|
||||
return `${partId}:${themeKey}:${highlightEnabled ? 1 : 0}`
|
||||
}
|
||||
|
||||
interface MarkdownProps {
|
||||
part: TextPart
|
||||
isDark?: boolean
|
||||
size?: "base" | "sm" | "tight"
|
||||
disableHighlight?: boolean
|
||||
onRendered?: () => void
|
||||
}
|
||||
|
||||
export function Markdown(props: MarkdownProps) {
|
||||
const [html, setHtml] = createSignal("")
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let latestRequestedText = ""
|
||||
|
||||
const notifyRendered = () => {
|
||||
Promise.resolve().then(() => props.onRendered?.())
|
||||
}
|
||||
|
||||
createEffect(async () => {
|
||||
const part = props.part
|
||||
const rawText = typeof part.text === "string" ? part.text : ""
|
||||
const text = decodeHtmlEntities(rawText)
|
||||
const dark = Boolean(props.isDark)
|
||||
const themeKey = dark ? "dark" : "light"
|
||||
const highlightEnabled = !props.disableHighlight
|
||||
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : "__anonymous__"
|
||||
const cacheKey = makeMarkdownCacheKey(partId, themeKey, highlightEnabled)
|
||||
|
||||
latestRequestedText = text
|
||||
|
||||
const localCache = part.renderCache
|
||||
if (localCache && localCache.text === text && localCache.theme === themeKey) {
|
||||
setHtml(localCache.html)
|
||||
notifyRendered()
|
||||
return
|
||||
}
|
||||
|
||||
const globalCache = markdownRenderCache.get(cacheKey)
|
||||
if (globalCache && globalCache.text === text) {
|
||||
setHtml(globalCache.html)
|
||||
part.renderCache = globalCache
|
||||
notifyRendered()
|
||||
return
|
||||
}
|
||||
|
||||
if (!highlightEnabled) {
|
||||
part.renderCache = undefined
|
||||
|
||||
try {
|
||||
const rendered = await renderMarkdown(text, { suppressHighlight: true })
|
||||
|
||||
if (latestRequestedText === text) {
|
||||
const cacheEntry: RenderCache = { text, html: rendered, theme: themeKey }
|
||||
setHtml(rendered)
|
||||
part.renderCache = cacheEntry
|
||||
markdownRenderCache.set(cacheKey, cacheEntry)
|
||||
notifyRendered()
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to render markdown:", error)
|
||||
if (latestRequestedText === text) {
|
||||
const cacheEntry: RenderCache = { text, html: text, theme: themeKey }
|
||||
setHtml(text)
|
||||
part.renderCache = cacheEntry
|
||||
markdownRenderCache.set(cacheKey, cacheEntry)
|
||||
notifyRendered()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const rendered = await renderMarkdown(text)
|
||||
|
||||
if (latestRequestedText === text) {
|
||||
const cacheEntry: RenderCache = { text, html: rendered, theme: themeKey }
|
||||
setHtml(rendered)
|
||||
part.renderCache = cacheEntry
|
||||
markdownRenderCache.set(cacheKey, cacheEntry)
|
||||
notifyRendered()
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to render markdown:", error)
|
||||
if (latestRequestedText === text) {
|
||||
const cacheEntry: RenderCache = { text, html: text, theme: themeKey }
|
||||
setHtml(text)
|
||||
part.renderCache = cacheEntry
|
||||
markdownRenderCache.set(cacheKey, cacheEntry)
|
||||
notifyRendered()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const handleClick = async (e: Event) => {
|
||||
const target = e.target as HTMLElement
|
||||
const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
|
||||
|
||||
if (copyButton) {
|
||||
e.preventDefault()
|
||||
const code = copyButton.getAttribute("data-code")
|
||||
if (code) {
|
||||
const decodedCode = decodeURIComponent(code)
|
||||
await navigator.clipboard.writeText(decodedCode)
|
||||
const copyText = copyButton.querySelector(".copy-text")
|
||||
if (copyText) {
|
||||
copyText.textContent = "Copied!"
|
||||
setTimeout(() => {
|
||||
copyText.textContent = "Copy"
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
containerRef?.addEventListener("click", handleClick)
|
||||
|
||||
// Register listener for language loading completion
|
||||
const cleanupLanguageListener = onLanguagesLoaded(async () => {
|
||||
if (props.disableHighlight) {
|
||||
return
|
||||
}
|
||||
|
||||
const part = props.part
|
||||
const rawText = typeof part.text === "string" ? part.text : ""
|
||||
const text = decodeHtmlEntities(rawText)
|
||||
|
||||
if (latestRequestedText !== text) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const rendered = await renderMarkdown(text)
|
||||
if (latestRequestedText === text) {
|
||||
setHtml(rendered)
|
||||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||
part.renderCache = { text, html: rendered, theme: themeKey }
|
||||
notifyRendered()
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to re-render markdown after language load:", error)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
containerRef?.removeEventListener("click", handleClick)
|
||||
cleanupLanguageListener()
|
||||
})
|
||||
})
|
||||
|
||||
const proseClass = () => "markdown-body"
|
||||
|
||||
return <div ref={containerRef} class={proseClass()} innerHTML={html()} />
|
||||
}
|
||||
Reference in New Issue
Block a user