entari-plugin-hyw 3.5.0rc1__py3-none-any.whl → 3.5.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of entari-plugin-hyw might be problematic. Click here for more details.
- entari_plugin_hyw/__init__.py +77 -82
- entari_plugin_hyw/assets/card-dist/index.html +360 -99
- entari_plugin_hyw/card-ui/src/App.vue +246 -52
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +122 -67
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +46 -26
- entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
- entari_plugin_hyw/card-ui/src/types.ts +1 -0
- entari_plugin_hyw/{core/history.py → history.py} +25 -1
- entari_plugin_hyw/image_cache.py +283 -0
- entari_plugin_hyw/{core/pipeline.py → pipeline.py} +102 -27
- entari_plugin_hyw/{utils/prompts.py → prompts.py} +7 -24
- entari_plugin_hyw/render_vue.py +314 -0
- entari_plugin_hyw/{utils/search.py → search.py} +227 -10
- {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/METADATA +1 -1
- {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/RECORD +18 -29
- entari_plugin_hyw/core/__init__.py +0 -0
- entari_plugin_hyw/core/config.py +0 -35
- entari_plugin_hyw/core/hyw.py +0 -48
- entari_plugin_hyw/core/render_vue.py +0 -255
- entari_plugin_hyw/test_output/render_0.jpg +0 -0
- entari_plugin_hyw/test_output/render_1.jpg +0 -0
- entari_plugin_hyw/test_output/render_2.jpg +0 -0
- entari_plugin_hyw/test_output/render_3.jpg +0 -0
- entari_plugin_hyw/test_output/render_4.jpg +0 -0
- entari_plugin_hyw/tests/ui_test_output.jpg +0 -0
- entari_plugin_hyw/tests/verify_ui.py +0 -139
- entari_plugin_hyw/utils/__init__.py +0 -2
- entari_plugin_hyw/utils/browser.py +0 -40
- entari_plugin_hyw/utils/playwright_tool.py +0 -36
- /entari_plugin_hyw/{utils/misc.py → misc.py} +0 -0
- {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/WHEEL +0 -0
- {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
|
|
3
4
|
import { marked, type Tokens } from 'marked'
|
|
5
|
+
import katex from 'katex'
|
|
6
|
+
import 'katex/dist/katex.min.css'
|
|
4
7
|
import hljs from 'highlight.js/lib/core'
|
|
5
8
|
// Import only common languages to reduce bundle size
|
|
6
9
|
import python from 'highlight.js/lib/languages/python'
|
|
@@ -77,45 +80,29 @@ renderer.code = ({ text, lang }: Tokens.Code): string => {
|
|
|
77
80
|
highlighted = hljs.highlightAuto(text).value
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
// Add line numbers to code
|
|
84
|
+
const addLineNumbers = (code: string): string => {
|
|
85
|
+
const lines = code.split('\n')
|
|
86
|
+
return lines.map((line, i) =>
|
|
87
|
+
`<span class="code-line"><span class="line-number">${i + 1}</span><span class="line-content">${line}</span></span>`
|
|
88
|
+
).join('')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const highlightedWithLines = addLineNumbers(highlighted)
|
|
92
|
+
|
|
80
93
|
// Bare mode: just the code, no window decoration
|
|
81
94
|
if (props.bare) {
|
|
82
|
-
return `<pre class="!mt-0 !mb-0 !rounded-none !bg-gray-50 !p-
|
|
95
|
+
return `<pre class="!mt-0 !mb-0 !rounded-none !bg-gray-50 !p-0 border-b border-gray-100 code-with-lines"><code class="hljs language-${language} text-[17.5px] leading-snug font-mono">${highlightedWithLines}</code></pre>`
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
// Dynamic Icon mapping
|
|
86
|
-
|
|
87
|
-
const map: Record<string, { icon: string, color: string }> = {
|
|
88
|
-
'python': { icon: 'mdi:language-python', color: 'text-blue-500' },
|
|
89
|
-
'javascript': { icon: 'mdi:language-javascript', color: 'text-yellow-500' },
|
|
90
|
-
'js': { icon: 'mdi:language-javascript', color: 'text-yellow-500' },
|
|
91
|
-
'typescript': { icon: 'mdi:language-typescript', color: 'text-blue-600' },
|
|
92
|
-
'ts': { icon: 'mdi:language-typescript', color: 'text-blue-600' },
|
|
93
|
-
'bash': { icon: 'mdi:terminal', color: 'text-green-500' },
|
|
94
|
-
'sh': { icon: 'mdi:terminal', color: 'text-green-500' },
|
|
95
|
-
'shell': { icon: 'mdi:terminal', color: 'text-green-500' },
|
|
96
|
-
'json': { icon: 'mdi:code-json', color: 'text-yellow-600' },
|
|
97
|
-
'html': { icon: 'mdi:language-html5', color: 'text-orange-500' },
|
|
98
|
-
'css': { icon: 'mdi:language-css3', color: 'text-blue-500' },
|
|
99
|
-
'yaml': { icon: 'mdi:file-cog', color: 'text-purple-500' },
|
|
100
|
-
'sql': { icon: 'mdi:database', color: 'text-red-500' }
|
|
101
|
-
}
|
|
102
|
-
return map[l] || { icon: 'mdi:code-braces', color: 'text-red-500' }
|
|
103
|
-
}
|
|
104
|
-
const langInfo = getLangIcon(language)
|
|
99
|
+
|
|
105
100
|
|
|
106
101
|
return `
|
|
107
102
|
<div class="my-6 space-y-1 group">
|
|
108
|
-
<div class="
|
|
109
|
-
<div class="flex items-center gap-2">
|
|
110
|
-
<Icon icon="${langInfo.icon}" class="${langInfo.color} text-sm" />
|
|
111
|
-
<span class="text-[10px] font-black text-gray-700 uppercase tracking-widest">${language}</span>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="text-gray-500 text-[9px] font-mono tracking-tighter tabular-nums">
|
|
114
|
-
Source Code
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
103
|
+
<div class="h-4 w-24 ml-auto" style="background-color: var(--theme-color);"></div>
|
|
117
104
|
<div class="">
|
|
118
|
-
<pre class="!mt-0 !mb-0 !rounded-none !bg-white !p-
|
|
105
|
+
<pre class="!mt-0 !mb-0 !rounded-none !bg-white !p-0 code-with-lines"><code class="hljs language-${language} text-[17.5px] leading-snug font-mono">${highlightedWithLines}</code></pre>
|
|
119
106
|
</div>
|
|
120
107
|
</div>
|
|
121
108
|
`
|
|
@@ -123,6 +110,19 @@ renderer.code = ({ text, lang }: Tokens.Code): string => {
|
|
|
123
110
|
|
|
124
111
|
marked.use({ renderer })
|
|
125
112
|
|
|
113
|
+
// Render LaTeX math with KaTeX
|
|
114
|
+
function renderMath(tex: string, displayMode: boolean): string {
|
|
115
|
+
try {
|
|
116
|
+
return katex.renderToString(tex, {
|
|
117
|
+
displayMode,
|
|
118
|
+
throwOnError: false,
|
|
119
|
+
strict: false,
|
|
120
|
+
})
|
|
121
|
+
} catch {
|
|
122
|
+
return `<code>${tex}</code>`
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
126
|
// Process markdown and convert citations to badges
|
|
127
127
|
const processedHtml = computed(() => {
|
|
128
128
|
let md = props.markdown || ''
|
|
@@ -130,23 +130,40 @@ const processedHtml = computed(() => {
|
|
|
130
130
|
// Remove References section at end
|
|
131
131
|
md = md.replace(/(?:^|\n)\s*(?:#{1,3}|\*\*)\s*(?:References|Citations|Sources)[\s\S]*$/i, '')
|
|
132
132
|
|
|
133
|
+
// Protect math blocks from markdown parsing by replacing with placeholders
|
|
134
|
+
const mathBlocks: { placeholder: string; html: string }[] = []
|
|
135
|
+
let mathIndex = 0
|
|
136
|
+
|
|
137
|
+
// Block math: $$...$$ or \[...\]
|
|
138
|
+
md = md.replace(/\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]/g, (_, tex1, tex2) => {
|
|
139
|
+
const tex = tex1 || tex2
|
|
140
|
+
const placeholder = `%%MATH_BLOCK_${mathIndex++}%%`
|
|
141
|
+
mathBlocks.push({ placeholder, html: `<div class="my-4 overflow-x-auto">${renderMath(tex.trim(), true)}</div>` })
|
|
142
|
+
return placeholder
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Inline math: $...$ or \(...\) (but not $$)
|
|
146
|
+
md = md.replace(/\$([^\$\n]+?)\$|\\\((.+?)\\\)/g, (_, tex1, tex2) => {
|
|
147
|
+
const tex = tex1 || tex2
|
|
148
|
+
const placeholder = `%%MATH_INLINE_${mathIndex++}%%`
|
|
149
|
+
mathBlocks.push({ placeholder, html: renderMath(tex.trim(), false) })
|
|
150
|
+
return placeholder
|
|
151
|
+
})
|
|
152
|
+
|
|
133
153
|
// Convert markdown to HTML
|
|
134
154
|
let html = marked.parse(md) as string
|
|
135
155
|
|
|
156
|
+
// Restore math blocks
|
|
157
|
+
for (const { placeholder, html: mathHtml } of mathBlocks) {
|
|
158
|
+
html = html.replace(placeholder, mathHtml)
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
// Render <summary> tags as technical highlight blocks
|
|
137
162
|
html = html.replace(/<summary>([\s\S]*?)<\/summary>/g, (_, content) => {
|
|
138
163
|
return `
|
|
139
|
-
|
|
140
|
-
<div class="
|
|
141
|
-
|
|
142
|
-
<Icon icon="mdi:lightning-bolt" class="text-red-500 text-sm" />
|
|
143
|
-
<span class="text-[10px] font-black text-gray-700 uppercase tracking-widest">Summary</span>
|
|
144
|
-
</div>
|
|
145
|
-
<div class="text-gray-500 text-[9px] font-mono tracking-tighter tabular-nums">
|
|
146
|
-
Insight
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
<div class="p-5 text-[15px] leading-relaxed text-gray-800 font-medium bg-white ">
|
|
164
|
+
<div class="my-8 group shadow-sm shadow-black/10">
|
|
165
|
+
<div class="h-4 w-full" style="background-color: var(--theme-color);"></div>
|
|
166
|
+
<div class="p-6 text-[19px] leading-relaxed text-gray-700 font-medium bg-white">
|
|
150
167
|
${content}
|
|
151
168
|
</div>
|
|
152
169
|
</div>
|
|
@@ -177,7 +194,7 @@ const processedHtml = computed(() => {
|
|
|
177
194
|
})
|
|
178
195
|
})
|
|
179
196
|
|
|
180
|
-
const containerClass = "w-full bg-white text-[
|
|
197
|
+
const containerClass = "w-full bg-white text-[16.5px] select-text";
|
|
181
198
|
|
|
182
199
|
let gridHtml = `<div class="${containerClass}">`
|
|
183
200
|
|
|
@@ -186,7 +203,7 @@ const processedHtml = computed(() => {
|
|
|
186
203
|
allRows.forEach((row: any[], rowIndex: number) => {
|
|
187
204
|
const isHeader = rowIndex === 0;
|
|
188
205
|
const rowBg = isHeader
|
|
189
|
-
? 'bg-white text-gray-
|
|
206
|
+
? 'bg-white text-gray-800 font-black uppercase tracking-tight'
|
|
190
207
|
: (rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50/30');
|
|
191
208
|
const borderB = rowIndex < allRows.length - 1 ? 'border-b border-gray-200' : '';
|
|
192
209
|
|
|
@@ -196,7 +213,7 @@ const processedHtml = computed(() => {
|
|
|
196
213
|
const justify = cell.align === 'center' ? 'justify-center text-center' : (cell.align === 'right' ? 'justify-end text-right' : 'justify-start');
|
|
197
214
|
const borderClass = colIndex === row.length - 1 ? '' : 'border-r border-gray-100';
|
|
198
215
|
|
|
199
|
-
gridHtml += `<div class="flex-1 py-
|
|
216
|
+
gridHtml += `<div class="flex-1 py-3.5 px-4 min-w-0 break-words flex items-center leading-tight ${justify} ${borderClass}">
|
|
200
217
|
<span>${cell.html}</span>
|
|
201
218
|
</div>`;
|
|
202
219
|
});
|
|
@@ -205,22 +222,27 @@ const processedHtml = computed(() => {
|
|
|
205
222
|
gridHtml += `</div>`;
|
|
206
223
|
|
|
207
224
|
if (props.bare) {
|
|
208
|
-
return `<div class="
|
|
225
|
+
return `<div class="border-b border-gray-200">${gridHtml}</div>`
|
|
209
226
|
}
|
|
210
227
|
|
|
211
228
|
return `
|
|
212
229
|
<div class="my-6 group">
|
|
213
|
-
<div class="
|
|
230
|
+
<div class="bg-white p-0 border-t border-gray-100">
|
|
214
231
|
${gridHtml}
|
|
215
232
|
</div>
|
|
216
233
|
</div>
|
|
217
234
|
`
|
|
218
235
|
})
|
|
219
236
|
|
|
220
|
-
// Convert [N] citations to
|
|
237
|
+
// Convert [N] citations to small square badges with shadow
|
|
221
238
|
html = html.replace(/\[(\d+)\]/g, (_, n) => {
|
|
222
239
|
const num = parseInt(n)
|
|
223
|
-
return `<
|
|
240
|
+
return `<sup class="inline-flex items-center justify-center w-[15px] h-[15px] text-[10px] font-bold cursor-default select-none ml-1 align-middle" style="background-color: var(--theme-color); color: var(--header-text-color); box-shadow: 0 1px 2px 0 rgba(0,0,0,0.15)">${num}</sup>`
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// Style <u> underline tags with theme-colored solid underline
|
|
244
|
+
html = html.replace(/<u>([^<]*)<\/u>/g, (_, content) => {
|
|
245
|
+
return `<span class="underline decoration-[3px] underline-offset-[6px]" style="text-decoration-color: var(--theme-color)">${content}</span>`
|
|
224
246
|
})
|
|
225
247
|
|
|
226
248
|
return html
|
|
@@ -229,14 +251,15 @@ const processedHtml = computed(() => {
|
|
|
229
251
|
|
|
230
252
|
<template>
|
|
231
253
|
<div ref="contentRef"
|
|
232
|
-
class="prose prose-slate max-w-none
|
|
233
|
-
prose-headings:text-gray-
|
|
234
|
-
prose-p:text-gray-800 prose-p:leading-
|
|
254
|
+
class="prose prose-slate max-w-none prose-lg
|
|
255
|
+
prose-headings:text-gray-800 prose-headings:font-bold prose-headings:mb-3 prose-headings:mt-8 prose-headings:tracking-tight
|
|
256
|
+
prose-p:text-gray-800 prose-p:leading-7 prose-p:my-4 prose-p:text-[20px] prose-li:text-[20px] prose-li:text-gray-800
|
|
235
257
|
prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
|
|
236
|
-
prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded-none prose-code:text-[0.
|
|
258
|
+
prose-code:bg-gray-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-none prose-code:text-[0.85em] prose-code:font-mono prose-code:text-gray-800
|
|
237
259
|
prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-200 prose-pre:rounded-none prose-pre:p-0
|
|
238
|
-
prose-img:rounded-none prose-img:my-6 prose-img:max-h-[400px] prose-img:w-auto prose-img:object-contain prose-img:border prose-img:border-gray-200
|
|
239
|
-
prose-ol:list-decimal prose-ol:pl-
|
|
260
|
+
prose-img:rounded-none prose-img:my-6 prose-img:max-h-[400px] prose-img:w-auto prose-img:object-contain prose-img:border prose-img:border-gray-200
|
|
261
|
+
prose-ol:list-decimal prose-ol:pl-7 prose-ol:list-outside prose-ol:my-5
|
|
262
|
+
prose-li:my-2.5 prose-li:leading-7
|
|
240
263
|
[&>*:first-child]:!mt-0"
|
|
241
264
|
v-html="processedHtml">
|
|
242
265
|
</div>
|
|
@@ -253,15 +276,15 @@ const processedHtml = computed(() => {
|
|
|
253
276
|
.prose ul {
|
|
254
277
|
list-style: none !important;
|
|
255
278
|
padding-left: 0.25rem !important;
|
|
256
|
-
margin-top:
|
|
257
|
-
margin-bottom:
|
|
279
|
+
margin-top: 1rem !important;
|
|
280
|
+
margin-bottom: 1rem !important;
|
|
258
281
|
}
|
|
259
282
|
|
|
260
283
|
.prose ul > li {
|
|
261
284
|
position: relative !important;
|
|
262
285
|
padding-left: 1.5rem !important;
|
|
263
|
-
margin-top: 0.
|
|
264
|
-
margin-bottom: 0.
|
|
286
|
+
margin-top: 0.75rem !important;
|
|
287
|
+
margin-bottom: 0.75rem !important;
|
|
265
288
|
line-height: 1.6 !important;
|
|
266
289
|
}
|
|
267
290
|
|
|
@@ -270,9 +293,9 @@ const processedHtml = computed(() => {
|
|
|
270
293
|
position: absolute !important;
|
|
271
294
|
left: 0 !important;
|
|
272
295
|
top: 0.6em !important;
|
|
273
|
-
width:
|
|
274
|
-
height:
|
|
275
|
-
background-color: #ef4444 !important; /*
|
|
296
|
+
width: 8px !important;
|
|
297
|
+
height: 8px !important;
|
|
298
|
+
background-color: var(--theme-color, #ef4444) !important; /* Theme color */
|
|
276
299
|
border-radius: 0 !important;
|
|
277
300
|
}
|
|
278
301
|
|
|
@@ -290,9 +313,9 @@ const processedHtml = computed(() => {
|
|
|
290
313
|
}
|
|
291
314
|
|
|
292
315
|
.prose ul ul > li::before {
|
|
293
|
-
width:
|
|
294
|
-
height:
|
|
295
|
-
background-color: #ef4444 !important; /*
|
|
316
|
+
width: 6px !important;
|
|
317
|
+
height: 6px !important;
|
|
318
|
+
background-color: var(--theme-color, #ef4444) !important; /* Theme color - same as parent, slightly smaller */
|
|
296
319
|
top: 0.65em !important;
|
|
297
320
|
}
|
|
298
321
|
|
|
@@ -312,8 +335,8 @@ const processedHtml = computed(() => {
|
|
|
312
335
|
left: 0 !important;
|
|
313
336
|
top: 0 !important;
|
|
314
337
|
bottom: 0 !important;
|
|
315
|
-
width:
|
|
316
|
-
background-color: #ef4444 !important; /*
|
|
338
|
+
width: 5px !important;
|
|
339
|
+
background-color: var(--theme-color, #ef4444) !important; /* Theme color - thick line */
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
|
|
@@ -321,10 +344,42 @@ const processedHtml = computed(() => {
|
|
|
321
344
|
/* Ensure images don't have artifacts */
|
|
322
345
|
.prose img {
|
|
323
346
|
display: block;
|
|
324
|
-
margin-left:
|
|
347
|
+
margin-left: 0;
|
|
325
348
|
margin-right: auto;
|
|
326
349
|
}
|
|
327
350
|
.prose pre {
|
|
328
351
|
border: none !important;
|
|
329
352
|
}
|
|
353
|
+
|
|
354
|
+
/* Code line numbers - Modern minimalist style */
|
|
355
|
+
.code-with-lines code {
|
|
356
|
+
display: block;
|
|
357
|
+
padding: 1.25em 0;
|
|
358
|
+
background: white;
|
|
359
|
+
}
|
|
360
|
+
.code-with-lines .code-line {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: stretch;
|
|
363
|
+
}
|
|
364
|
+
.code-with-lines .line-number {
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
width: 36px;
|
|
367
|
+
padding: 0.1em 8px 0.1em 4px;
|
|
368
|
+
text-align: right;
|
|
369
|
+
color: var(--text-muted, #9ca3af);
|
|
370
|
+
background: white;
|
|
371
|
+
border-right: 1px solid #e5e7eb;
|
|
372
|
+
user-select: none;
|
|
373
|
+
font-size: 11px;
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: flex-start;
|
|
376
|
+
justify-content: flex-end;
|
|
377
|
+
}
|
|
378
|
+
.code-with-lines .line-content {
|
|
379
|
+
flex: 1;
|
|
380
|
+
padding: 0.1em 1.25em 0.1em 1em;
|
|
381
|
+
white-space: pre-wrap;
|
|
382
|
+
word-break: break-all;
|
|
383
|
+
background: white;
|
|
384
|
+
}
|
|
330
385
|
</style>
|
|
@@ -14,11 +14,15 @@ const props = defineProps<{
|
|
|
14
14
|
isFirst?: boolean
|
|
15
15
|
isLast?: boolean
|
|
16
16
|
prevStageName?: string
|
|
17
|
+
refOffset?: number
|
|
17
18
|
}>()
|
|
18
19
|
|
|
19
20
|
function getDomain(url: string): string {
|
|
20
21
|
try {
|
|
21
|
-
|
|
22
|
+
const urlObj = new URL(url)
|
|
23
|
+
const hostname = urlObj.hostname.replace('www.', '')
|
|
24
|
+
const pathname = urlObj.pathname === '/' ? '' : urlObj.pathname
|
|
25
|
+
return hostname + pathname
|
|
22
26
|
} catch {
|
|
23
27
|
return url
|
|
24
28
|
}
|
|
@@ -56,12 +60,12 @@ function getStageTheme(name?: string) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
const themes: Record<string, any> = {
|
|
59
|
-
'search': { color: 'text-blue-600', bg: 'bg-blue-50',
|
|
60
|
-
'crawler': { color: 'text-orange-600', bg: 'bg-orange-50',
|
|
61
|
-
'agent': { color: 'text-purple-600', bg: 'bg-purple-50',
|
|
62
|
-
'instruct': { color: 'text-red-600', bg: 'bg-red-50',
|
|
63
|
-
'vision': { color: 'text-
|
|
64
|
-
'default': { color: 'text-gray-600', bg: 'bg-gray-50',
|
|
63
|
+
'search': { color: 'text-blue-600', bg: 'bg-blue-50', iconBg: 'bg-blue-100/50', icon: 'mdi:magnify' },
|
|
64
|
+
'crawler': { color: 'text-orange-600', bg: 'bg-orange-50', iconBg: 'bg-orange-100/50', icon: 'mdi:web' },
|
|
65
|
+
'agent': { color: 'text-purple-600', bg: 'bg-purple-50', iconBg: 'bg-white/80', icon: 'mdi:robot' },
|
|
66
|
+
'instruct': { color: 'text-red-600', bg: 'bg-red-50', iconBg: 'bg-white/80', icon: 'mdi:lightning-bolt' },
|
|
67
|
+
'vision': { color: 'text-purple-600', bg: 'bg-purple-50', iconBg: 'bg-white/80', icon: 'mdi:eye' },
|
|
68
|
+
'default': { color: 'text-gray-600', bg: 'bg-gray-50', iconBg: 'bg-gray-100/50', icon: 'mdi:circle' }
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
function getIcon(name: string): string {
|
|
@@ -104,18 +108,18 @@ function getModelLogo(model: string): string | undefined {
|
|
|
104
108
|
<div class="rounded-none overflow-hidden bg-white">
|
|
105
109
|
|
|
106
110
|
<!-- Header -->
|
|
107
|
-
<div :class="['bg-white px-
|
|
108
|
-
<div class="flex items-center gap-
|
|
109
|
-
<div :class="['w-
|
|
110
|
-
<img v-if="getModelLogo(stage.model)" :src="getModelLogo(stage.model)" class="w-
|
|
111
|
-
<Icon v-else :icon="getIcon(stage.name)" class="text-
|
|
111
|
+
<div :class="['bg-white px-4 py-2.5 flex items-center justify-between gap-3']">
|
|
112
|
+
<div class="flex items-center gap-3">
|
|
113
|
+
<div :class="['w-8 h-8 flex items-center justify-center shrink-0 overflow-hidden border border-gray-100', getStageTheme(stage.name).iconBg, getStageTheme(stage.name).color]">
|
|
114
|
+
<img v-if="getModelLogo(stage.model)" :src="getModelLogo(stage.model)" class="w-5 h-5 object-contain" />
|
|
115
|
+
<Icon v-else :icon="getIcon(stage.name)" class="text-lg" />
|
|
112
116
|
</div>
|
|
113
117
|
<div class="flex flex-col">
|
|
114
|
-
<span class="font-black text-
|
|
115
|
-
<span class="text-[
|
|
118
|
+
<span class="font-black text-[18px] text-gray-800 uppercase tracking-tight">{{ stage.name }}</span>
|
|
119
|
+
<span class="text-[15.5px] font-mono tabular-nums tracking-tighter" style="color: var(--text-muted)">{{ getModelShort(stage.model) }}</span>
|
|
116
120
|
</div>
|
|
117
121
|
</div>
|
|
118
|
-
<div v-if="stage.time > 0 || stage.cost > 0" class="text-[
|
|
122
|
+
<div v-if="stage.time > 0 || stage.cost > 0" class="text-[15.5px] font-mono flex items-center justify-end gap-2 leading-tight min-w-[120px]" style="color: var(--text-muted)">
|
|
119
123
|
<span v-if="stage.cost > 0">{{ formatCost(stage.cost) }}</span>
|
|
120
124
|
<span v-if="stage.time > 0 && stage.cost > 0" class="text-gray-300">·</span>
|
|
121
125
|
<span v-if="stage.time > 0">{{ formatTime(stage.time) }}</span>
|
|
@@ -123,39 +127,55 @@ function getModelLogo(model: string): string | undefined {
|
|
|
123
127
|
</div>
|
|
124
128
|
|
|
125
129
|
|
|
126
|
-
<div v-if="stage.references?.length || stage.image_references?.length" class="bg-white pl-
|
|
130
|
+
<div v-if="stage.references?.length || stage.image_references?.length || stage.crawled_pages?.length" class="bg-white pl-11 relative">
|
|
127
131
|
<div v-if="stage.references?.length" class="divide-y divide-gray-50 relative z-10">
|
|
128
132
|
<a v-for="(ref, idx) in stage.references" :key="idx"
|
|
129
133
|
:href="ref.url" target="_blank"
|
|
130
|
-
class="flex items-start gap-
|
|
134
|
+
class="flex items-start gap-3 pr-3 py-3 hover:bg-gray-50 transition-colors group">
|
|
131
135
|
<!-- Favicon - Aligned with Title -->
|
|
132
|
-
<img :src="getFavicon(ref.url)" class="w-
|
|
136
|
+
<img :src="getFavicon(ref.url)" class="w-4 h-4 rounded-none shrink-0 object-contain mt-[4px]">
|
|
133
137
|
|
|
134
138
|
<!-- Content: Title and Domain -->
|
|
135
139
|
<div class="flex-1 min-w-0 flex flex-col">
|
|
136
140
|
<div class="flex items-center gap-2">
|
|
137
|
-
<span class="flex-1 text-
|
|
138
|
-
<!--
|
|
139
|
-
<span class="shrink-0
|
|
141
|
+
<span class="flex-1 text-[18px] font-bold text-gray-700 truncate leading-tight tracking-tight">{{ ref.title }}</span>
|
|
142
|
+
<!-- Square Badge with Shadow -->
|
|
143
|
+
<span class="shrink-0 w-[18px] h-[18px] text-[11px] font-bold flex items-center justify-center" style="background-color: var(--theme-color); color: var(--header-text-color); box-shadow: 0 1px 3px 0 rgba(0,0,0,0.15)">{{ (refOffset || 0) + idx + 1 }}</span>
|
|
140
144
|
</div>
|
|
141
|
-
<div class="text-[
|
|
145
|
+
<div class="text-[15.5px] font-mono truncate mt-0.5 tracking-tighter" style="color: var(--text-muted)">{{ getDomain(ref.url) }}</div>
|
|
142
146
|
</div>
|
|
143
147
|
</a>
|
|
144
148
|
</div>
|
|
145
149
|
|
|
146
150
|
<!-- Image Search Results -->
|
|
147
|
-
<div v-if="stage.image_references?.length" class="pr-
|
|
148
|
-
<div class="grid grid-cols-
|
|
151
|
+
<div v-if="stage.image_references?.length" class="pr-3 py-3 relative z-10">
|
|
152
|
+
<div class="grid grid-cols-2 gap-2 items-start">
|
|
149
153
|
<a v-for="(img, idx) in stage.image_references" :key="idx"
|
|
150
154
|
v-show="!failedImages[img.url]"
|
|
151
155
|
:href="img.url" target="_blank"
|
|
152
|
-
class="relative
|
|
156
|
+
class="relative overflow-hidden transition-all hover:opacity-90 group">
|
|
153
157
|
<img :src="img.thumbnail || img.url"
|
|
154
158
|
@error="handleImageError(img.url)"
|
|
155
|
-
class="w-full h-
|
|
159
|
+
class="w-full h-auto block group-hover:scale-[1.02] transition-transform">
|
|
156
160
|
</a>
|
|
157
161
|
</div>
|
|
158
162
|
</div>
|
|
163
|
+
|
|
164
|
+
<div v-if="stage.crawled_pages?.length" class="divide-y divide-gray-50 relative z-10">
|
|
165
|
+
<a v-for="(page, idx) in stage.crawled_pages" :key="idx"
|
|
166
|
+
:href="page.url" target="_blank"
|
|
167
|
+
class="flex items-start gap-3 pr-3 py-3 hover:bg-gray-50 transition-colors group">
|
|
168
|
+
<img :src="getFavicon(page.url)" class="w-4 h-4 rounded-none shrink-0 object-contain mt-[4px]">
|
|
169
|
+
<div class="flex-1 min-w-0 flex flex-col">
|
|
170
|
+
<div class="flex items-center gap-2">
|
|
171
|
+
<span class="flex-1 text-[18px] font-bold text-gray-700 truncate leading-tight tracking-tight">{{ page.title }}</span>
|
|
172
|
+
<!-- Square Badge with Shadow -->
|
|
173
|
+
<span class="shrink-0 w-[18px] h-[18px] text-[11px] font-bold flex items-center justify-center" style="background-color: var(--theme-color); color: var(--header-text-color); box-shadow: 0 1px 3px 0 rgba(0,0,0,0.15)">{{ (refOffset || 0) + (stage.references?.length || 0) + idx + 1 }}</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="text-[15.5px] font-mono truncate mt-0.5 tracking-tighter" style="color: var(--text-muted)">{{ getDomain(page.url) }}</div>
|
|
176
|
+
</div>
|
|
177
|
+
</a>
|
|
178
|
+
</div>
|
|
159
179
|
</div>
|
|
160
180
|
</div>
|
|
161
181
|
</div>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
const stripPrefixBeforeH1 = (text) => {
|
|
3
|
+
const h1Match = text.match(/^#\s+/m)
|
|
4
|
+
if (h1Match && h1Match.index !== undefined) {
|
|
5
|
+
return text.substring(h1Match.index)
|
|
6
|
+
}
|
|
7
|
+
return text
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const dedent = (text) => {
|
|
11
|
+
const lines = text.split('\n')
|
|
12
|
+
// Find minimum indentation of non-empty lines
|
|
13
|
+
let minIndent = Infinity
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (line.trim().length === 0) continue
|
|
16
|
+
const leadingSpace = line.match(/^\s*/)?.[0].length || 0
|
|
17
|
+
if (leadingSpace < minIndent) minIndent = leadingSpace
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (minIndent === Infinity || minIndent === 0) return text
|
|
21
|
+
|
|
22
|
+
return lines.map(line => {
|
|
23
|
+
if (line.trim().length === 0) return ''
|
|
24
|
+
return line.substring(minIndent)
|
|
25
|
+
}).join('\n')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parse = (rawMd) => {
|
|
29
|
+
if (!rawMd) return []
|
|
30
|
+
|
|
31
|
+
const md = stripPrefixBeforeH1(rawMd)
|
|
32
|
+
|
|
33
|
+
let content = md.replace(/^#\s+.+$/m, '')
|
|
34
|
+
content = content.replace(/(?:^|\n)\s*(?:#{1,3}|\*\*)\s*(?:References|Citations|Sources)[\s\S]*$/i, '')
|
|
35
|
+
content = content.trim()
|
|
36
|
+
|
|
37
|
+
const sections = []
|
|
38
|
+
|
|
39
|
+
const combinedRegex = /(```[\s\S]*?```|((?:^|\n)\|[^\n]*\|(?:\n\|[^\n]*\|)*)|<summary>[\s\S]*?<\/summary>)/
|
|
40
|
+
|
|
41
|
+
let remaining = content
|
|
42
|
+
|
|
43
|
+
while (remaining) {
|
|
44
|
+
const match = remaining.match(combinedRegex)
|
|
45
|
+
if (!match) {
|
|
46
|
+
if (remaining.trim()) {
|
|
47
|
+
sections.push({ type: 'markdown', content: remaining.trim() })
|
|
48
|
+
}
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const index = match.index
|
|
53
|
+
const matchedStr = match[0]
|
|
54
|
+
const preText = remaining.substring(0, index)
|
|
55
|
+
|
|
56
|
+
if (preText.trim()) {
|
|
57
|
+
sections.push({ type: 'markdown', content: preText.trim() })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isCode = matchedStr.startsWith('```')
|
|
61
|
+
const isSummary = matchedStr.startsWith('<summary>')
|
|
62
|
+
const isTable = !isCode && !isSummary && matchedStr.trim().startsWith('|')
|
|
63
|
+
|
|
64
|
+
if (isCode || isTable || isSummary) {
|
|
65
|
+
let language = ''
|
|
66
|
+
let content = matchedStr.trim()
|
|
67
|
+
|
|
68
|
+
if (isCode) {
|
|
69
|
+
const match = matchedStr.match(/^```(\w+)/)
|
|
70
|
+
if (match && match[1]) language = match[1]
|
|
71
|
+
} else if (isSummary) {
|
|
72
|
+
content = content.replace(/^<summary>/, '').replace(/<\/summary>$/, '')
|
|
73
|
+
content = dedent(content)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sections.push({
|
|
77
|
+
type: 'card',
|
|
78
|
+
title: isCode ? 'Code' : (isSummary ? 'Summary' : 'Table'),
|
|
79
|
+
content: content,
|
|
80
|
+
contentType: isCode ? 'code' : (isSummary ? 'summary' : 'table'),
|
|
81
|
+
language: language
|
|
82
|
+
})
|
|
83
|
+
} else {
|
|
84
|
+
sections.push({ type: 'markdown', content: matchedStr })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
remaining = remaining.substring(index + matchedStr.length)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return sections
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const test1 = `
|
|
94
|
+
# Title
|
|
95
|
+
|
|
96
|
+
<summary>
|
|
97
|
+
Indented text.
|
|
98
|
+
It might become code block.
|
|
99
|
+
</summary>
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
console.log("\n--- Test 2 (After Fix) ---")
|
|
103
|
+
console.log(JSON.stringify(parse(test1), null, 2))
|
|
@@ -79,13 +79,37 @@ class HistoryManager:
|
|
|
79
79
|
"""Save conversation history to disk"""
|
|
80
80
|
import os
|
|
81
81
|
import time
|
|
82
|
+
import re
|
|
82
83
|
|
|
83
84
|
if key not in self._history:
|
|
84
85
|
return
|
|
85
86
|
|
|
86
87
|
try:
|
|
87
88
|
os.makedirs(save_dir, exist_ok=True)
|
|
88
|
-
|
|
89
|
+
|
|
90
|
+
# Extract user's first message (question) for filename
|
|
91
|
+
user_question = ""
|
|
92
|
+
for msg in self._history[key]:
|
|
93
|
+
if msg.get("role") == "user":
|
|
94
|
+
content = msg.get("content", "")
|
|
95
|
+
# Handle content that might be a list (multimodal)
|
|
96
|
+
if isinstance(content, list):
|
|
97
|
+
for item in content:
|
|
98
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
99
|
+
user_question = item.get("text", "")
|
|
100
|
+
break
|
|
101
|
+
else:
|
|
102
|
+
user_question = str(content)
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
# Clean and truncate question for filename (10 chars)
|
|
106
|
+
question_part = re.sub(r'[\\/:*?"<>|\n\r\t]', '', user_question)[:10].strip()
|
|
107
|
+
if not question_part:
|
|
108
|
+
question_part = "conversation"
|
|
109
|
+
|
|
110
|
+
# Format: YYYYMMDD_HHMMSS_question.md
|
|
111
|
+
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime())
|
|
112
|
+
filename = f"{save_dir}/{time_str}_{question_part}.md"
|
|
89
113
|
|
|
90
114
|
# Formatter
|
|
91
115
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|