Skip to content

Commit 53bfdec

Browse files
committed
UI
1 parent 43b90b1 commit 53bfdec

4 files changed

Lines changed: 192 additions & 13 deletions

File tree

MyApp/wwwroot/lib/mjs/markdown.mjs

Lines changed: 0 additions & 12 deletions
This file was deleted.

MyApp/wwwroot/pages/Generation.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import ArtifactMenu from "./components/ArtifactMenu.mjs"
99
import ArtifactReactions from "./components/ArtifactReactions.mjs"
1010
import RatingsBadge from "./components/RatingsBadge.mjs"
1111
import AudioPlayer from "./components/AudioPlayer.mjs"
12-
import { renderMarkdown } from "../lib/mjs/markdown.mjs"
12+
import { renderMarkdown } from "./lib/markdown.mjs"
1313

1414
export default {
1515
components: {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { Marked } from "../../lib/mjs/marked.min.mjs"
2+
import hljs from "../../lib/mjs/highlight.min.mjs"
3+
4+
export const marked = (() => {
5+
const ret = new Marked(
6+
markedHighlight({
7+
langPrefix: 'hljs language-',
8+
highlight(code, lang, info) {
9+
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
10+
return hljs.highlight(code, { language }).value
11+
}
12+
})
13+
)
14+
ret.use({ extensions:[thinkTag()] })
15+
//ret.use({ extensions: [divExtension()] })
16+
return ret
17+
})();
18+
19+
export function renderMarkdown(content) {
20+
if (content) {
21+
content = content
22+
.replaceAll(`\\[ \\boxed{`,'\n<span class="inline-block text-xl text-blue-500 bg-blue-50 px-3 py-1 rounded">')
23+
.replaceAll('} \\]','</span>\n')
24+
}
25+
return marked.parse(content)
26+
}
27+
28+
// export async function renderMarkdown(body) {
29+
// const rawHtml = marked.parse(body)
30+
// return <main dangerouslySetInnerHTML={{ __html: rawHtml }} />
31+
// }
32+
33+
export function markedHighlight(options) {
34+
if (typeof options === 'function') {
35+
options = {
36+
highlight: options
37+
}
38+
}
39+
40+
if (!options || typeof options.highlight !== 'function') {
41+
throw new Error('Must provide highlight function')
42+
}
43+
44+
if (typeof options.langPrefix !== 'string') {
45+
options.langPrefix = 'language-'
46+
}
47+
48+
return {
49+
async: !!options.async,
50+
walkTokens(token) {
51+
if (token.type !== 'code') {
52+
return
53+
}
54+
55+
const lang = getLang(token.lang)
56+
57+
if (options.async) {
58+
return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(updateToken(token))
59+
}
60+
61+
const code = options.highlight(token.text, lang, token.lang || '')
62+
if (code instanceof Promise) {
63+
throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.')
64+
}
65+
updateToken(token)(code)
66+
},
67+
renderer: {
68+
code(code, infoString) {
69+
const lang = getLang(infoString)
70+
let text = code.text
71+
const classAttr = lang
72+
? ` class="${options.langPrefix}${escape(lang)}"`
73+
: ' class="hljs"';
74+
text = text.replace(/\n$/, '')
75+
return `<pre><code${classAttr}>${code.escaped ? text : escape(text, true)}\n</code></pre>`
76+
}
77+
}
78+
}
79+
}
80+
81+
function getLang(lang) {
82+
return (lang || '').match(/\S*/)[0]
83+
}
84+
85+
function updateToken(token) {
86+
return code => {
87+
if (typeof code === 'string' && code !== token.text) {
88+
token.escaped = true
89+
token.text = code
90+
}
91+
}
92+
}
93+
94+
// copied from marked helpers
95+
const escapeTest = /[&<>"']/
96+
const escapeReplace = new RegExp(escapeTest.source, 'g')
97+
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/
98+
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g')
99+
const escapeReplacements = {
100+
'&': '&amp;',
101+
'<': '&lt;',
102+
'>': '&gt;',
103+
'"': '&quot;',
104+
"'": '&#39;'
105+
}
106+
const getEscapeReplacement = ch => escapeReplacements[ch]
107+
function escape(html, encode) {
108+
if (encode) {
109+
if (escapeTest.test(html)) {
110+
return html.replace(escapeReplace, getEscapeReplacement)
111+
}
112+
} else {
113+
if (escapeTestNoEncode.test(html)) {
114+
return html.replace(escapeReplaceNoEncode, getEscapeReplacement)
115+
}
116+
}
117+
118+
return html
119+
}
120+
121+
/**
122+
* Marked.js extension for rendering <think> tags as expandable, scrollable components
123+
* using Tailwind CSS
124+
*/
125+
126+
// Extension for Marked.js to handle <think> tags
127+
function thinkTag() {
128+
globalThis.toggleThink = toggleThink
129+
return ({
130+
name: 'thinkTag',
131+
level: 'block',
132+
start(src) {
133+
return src.match(/^<think>/)?.index;
134+
},
135+
tokenizer(src) {
136+
const rule = /^<think>([\s\S]*?)<\/think>/
137+
const match = rule.exec(src)
138+
if (match) {
139+
return {
140+
type: 'thinkTag',
141+
raw: match[0],
142+
content: match[1].trim(),
143+
}
144+
}
145+
return undefined
146+
},
147+
renderer(token) {
148+
// Parse the markdown content inside the think tag
149+
const parsedContent = marked.parse(token.content)
150+
151+
// Generate a unique ID for this think component
152+
const uniqueId = 'think-' + Math.random().toString(36).substring(2, 10)
153+
154+
// Create the expandable, scrollable component with Tailwind CSS
155+
return `
156+
<div class="my-4 border border-gray-200 rounded-lg shadow-sm">
157+
<button
158+
id="${uniqueId}-toggle"
159+
class="flex justify-between items-center w-full py-2 px-4 text-left text-gray-700 font-medium hover:bg-gray-50 focus:outline-none"
160+
onclick="toggleThink('${uniqueId}')">
161+
<span>Thinking</span>
162+
<svg id="${uniqueId}-icon" class="h-5 w-5 text-gray-500 transform transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
163+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
164+
</svg>
165+
</button>
166+
<div
167+
id="${uniqueId}-content"
168+
class="hidden overflow-auto max-h-64 px-4 border-t border-gray-200 bg-gray-50"
169+
style="max-height:16rem;">
170+
${parsedContent}
171+
</div>
172+
</div>
173+
`
174+
}
175+
})
176+
}
177+
178+
// JavaScript function to toggle the visibility of the think content
179+
function toggleThink(id) {
180+
const content = document.getElementById(`${id}-content`)
181+
const icon = document.getElementById(`${id}-icon`)
182+
183+
if (content.classList.contains('hidden')) {
184+
content.classList.remove('hidden')
185+
icon.classList.add('rotate-180')
186+
} else {
187+
content.classList.add('hidden')
188+
icon.classList.remove('rotate-180')
189+
}
190+
}

MyApp/wwwroot/pages/lib/store.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useAuth, useFormatters } from "@servicestack/vue"
33
import { JsonServiceClient, ApiResult, combinePaths, rightPart, lastRightPart } from "@servicestack/client"
44
import { openDB, deleteDB, wrap, unwrap } from '/lib/mjs/idb.mjs'
55
import { toJsonObject, sortByCreatedDesc, getRatingDisplay, getHDClass, toJsonArray, storageArray } from "./utils.mjs"
6+
67
import {
78
QueryWorkflows,
89
GetWorkflowInfo,

0 commit comments

Comments
 (0)