entari-plugin-hyw 2.2.5__py3-none-any.whl → 3.5.0rc6__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.
Files changed (91) hide show
  1. entari_plugin_hyw/__init__.py +371 -315
  2. entari_plugin_hyw/assets/card-dist/index.html +396 -0
  3. entari_plugin_hyw/assets/card-dist/logos/anthropic.svg +1 -0
  4. entari_plugin_hyw/assets/card-dist/logos/cerebras.svg +9 -0
  5. entari_plugin_hyw/assets/card-dist/logos/deepseek.png +0 -0
  6. entari_plugin_hyw/assets/card-dist/logos/gemini.svg +1 -0
  7. entari_plugin_hyw/assets/card-dist/logos/google.svg +1 -0
  8. entari_plugin_hyw/assets/card-dist/logos/grok.png +0 -0
  9. entari_plugin_hyw/assets/card-dist/logos/huggingface.png +0 -0
  10. entari_plugin_hyw/assets/card-dist/logos/microsoft.svg +15 -0
  11. entari_plugin_hyw/assets/card-dist/logos/minimax.png +0 -0
  12. entari_plugin_hyw/assets/card-dist/logos/mistral.png +0 -0
  13. entari_plugin_hyw/assets/card-dist/logos/nvida.png +0 -0
  14. entari_plugin_hyw/assets/card-dist/logos/openai.svg +1 -0
  15. entari_plugin_hyw/assets/card-dist/logos/openrouter.png +0 -0
  16. entari_plugin_hyw/assets/card-dist/logos/perplexity.svg +24 -0
  17. entari_plugin_hyw/assets/card-dist/logos/qwen.png +0 -0
  18. entari_plugin_hyw/assets/card-dist/logos/xai.png +0 -0
  19. entari_plugin_hyw/assets/card-dist/logos/xiaomi.png +0 -0
  20. entari_plugin_hyw/assets/card-dist/logos/zai.png +0 -0
  21. entari_plugin_hyw/assets/card-dist/vite.svg +1 -0
  22. entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
  23. entari_plugin_hyw/assets/icon/cerebras.svg +9 -0
  24. entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  25. entari_plugin_hyw/assets/icon/gemini.svg +1 -0
  26. entari_plugin_hyw/assets/icon/google.svg +1 -0
  27. entari_plugin_hyw/assets/icon/grok.png +0 -0
  28. entari_plugin_hyw/assets/icon/huggingface.png +0 -0
  29. entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
  30. entari_plugin_hyw/assets/icon/minimax.png +0 -0
  31. entari_plugin_hyw/assets/icon/mistral.png +0 -0
  32. entari_plugin_hyw/assets/icon/nvida.png +0 -0
  33. entari_plugin_hyw/assets/icon/openai.svg +1 -0
  34. entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  35. entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
  36. entari_plugin_hyw/assets/icon/qwen.png +0 -0
  37. entari_plugin_hyw/assets/icon/xai.png +0 -0
  38. entari_plugin_hyw/assets/icon/xiaomi.png +0 -0
  39. entari_plugin_hyw/assets/icon/zai.png +0 -0
  40. entari_plugin_hyw/card-ui/.gitignore +24 -0
  41. entari_plugin_hyw/card-ui/README.md +5 -0
  42. entari_plugin_hyw/card-ui/index.html +16 -0
  43. entari_plugin_hyw/card-ui/package-lock.json +2342 -0
  44. entari_plugin_hyw/card-ui/package.json +31 -0
  45. entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
  46. entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
  47. entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
  48. entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
  49. entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
  50. entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
  51. entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
  52. entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
  53. entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
  54. entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
  55. entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
  56. entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
  57. entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
  58. entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
  59. entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
  60. entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
  61. entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
  62. entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
  63. entari_plugin_hyw/card-ui/public/vite.svg +1 -0
  64. entari_plugin_hyw/card-ui/src/App.vue +412 -0
  65. entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
  66. entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
  67. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +386 -0
  68. entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
  69. entari_plugin_hyw/card-ui/src/components/StageCard.vue +237 -0
  70. entari_plugin_hyw/card-ui/src/main.ts +5 -0
  71. entari_plugin_hyw/card-ui/src/style.css +29 -0
  72. entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
  73. entari_plugin_hyw/card-ui/src/types.ts +52 -0
  74. entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
  75. entari_plugin_hyw/card-ui/tsconfig.json +7 -0
  76. entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
  77. entari_plugin_hyw/card-ui/vite.config.ts +16 -0
  78. entari_plugin_hyw/history.py +170 -0
  79. entari_plugin_hyw/image_cache.py +274 -0
  80. entari_plugin_hyw/misc.py +128 -0
  81. entari_plugin_hyw/pipeline.py +1338 -0
  82. entari_plugin_hyw/prompts.py +108 -0
  83. entari_plugin_hyw/render_vue.py +314 -0
  84. entari_plugin_hyw/search.py +696 -0
  85. entari_plugin_hyw-3.5.0rc6.dist-info/METADATA +116 -0
  86. entari_plugin_hyw-3.5.0rc6.dist-info/RECORD +88 -0
  87. entari_plugin_hyw/hyw_core.py +0 -555
  88. entari_plugin_hyw-2.2.5.dist-info/METADATA +0 -135
  89. entari_plugin_hyw-2.2.5.dist-info/RECORD +0 -6
  90. {entari_plugin_hyw-2.2.5.dist-info → entari_plugin_hyw-3.5.0rc6.dist-info}/WHEEL +0 -0
  91. {entari_plugin_hyw-2.2.5.dist-info → entari_plugin_hyw-3.5.0rc6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,386 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ import { marked, type Tokens } from 'marked'
5
+ import katex from 'katex'
6
+ import 'katex/dist/katex.min.css'
7
+ import hljs from 'highlight.js/lib/core'
8
+ // Import only common languages to reduce bundle size
9
+ import python from 'highlight.js/lib/languages/python'
10
+ import javascript from 'highlight.js/lib/languages/javascript'
11
+ import typescript from 'highlight.js/lib/languages/typescript'
12
+ import json from 'highlight.js/lib/languages/json'
13
+ import bash from 'highlight.js/lib/languages/bash'
14
+ import css from 'highlight.js/lib/languages/css'
15
+ import xml from 'highlight.js/lib/languages/xml'
16
+ import java from 'highlight.js/lib/languages/java'
17
+ import cpp from 'highlight.js/lib/languages/cpp'
18
+ import go from 'highlight.js/lib/languages/go'
19
+ import rust from 'highlight.js/lib/languages/rust'
20
+ import sql from 'highlight.js/lib/languages/sql'
21
+ import markdown from 'highlight.js/lib/languages/markdown'
22
+ import shell from 'highlight.js/lib/languages/shell'
23
+ import yaml from 'highlight.js/lib/languages/yaml'
24
+ import properties from 'highlight.js/lib/languages/properties'
25
+
26
+ hljs.registerLanguage('python', python)
27
+ hljs.registerLanguage('javascript', javascript)
28
+ hljs.registerLanguage('js', javascript)
29
+ hljs.registerLanguage('typescript', typescript)
30
+ hljs.registerLanguage('ts', typescript)
31
+ hljs.registerLanguage('json', json)
32
+ hljs.registerLanguage('bash', bash)
33
+ hljs.registerLanguage('sh', bash)
34
+ hljs.registerLanguage('shell', shell)
35
+ hljs.registerLanguage('zsh', bash)
36
+ hljs.registerLanguage('css', css)
37
+ hljs.registerLanguage('html', xml)
38
+ hljs.registerLanguage('xml', xml)
39
+ hljs.registerLanguage('java', java)
40
+ hljs.registerLanguage('cpp', cpp)
41
+ hljs.registerLanguage('c', cpp)
42
+ hljs.registerLanguage('go', go)
43
+ hljs.registerLanguage('rust', rust)
44
+ hljs.registerLanguage('sql', sql)
45
+ hljs.registerLanguage('markdown', markdown)
46
+ hljs.registerLanguage('md', markdown)
47
+ hljs.registerLanguage('yaml', yaml)
48
+ hljs.registerLanguage('yml', yaml)
49
+ hljs.registerLanguage('properties', properties)
50
+ hljs.registerLanguage('ini', properties)
51
+ hljs.registerLanguage('conf', properties)
52
+
53
+ import 'highlight.js/styles/github.css'
54
+
55
+ const props = defineProps<{
56
+ markdown: string
57
+ numSearchRefs?: number
58
+ numPageRefs?: number
59
+ bare?: boolean // When true, tables and code blocks render without window decoration
60
+ }>()
61
+
62
+ // Configure marked with syntax highlighting
63
+ marked.setOptions({
64
+ breaks: true,
65
+ gfm: true,
66
+ })
67
+
68
+ // Custom renderer for code blocks with technical layout
69
+ const renderer = new marked.Renderer()
70
+ renderer.code = ({ text, lang }: Tokens.Code): string => {
71
+ const language = lang || 'text'
72
+ let highlighted = ''
73
+ if (lang && hljs.getLanguage(lang)) {
74
+ try {
75
+ highlighted = hljs.highlight(text, { language: lang }).value
76
+ } catch {
77
+ highlighted = hljs.highlightAuto(text).value
78
+ }
79
+ } else {
80
+ highlighted = hljs.highlightAuto(text).value
81
+ }
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
+
93
+ // Bare mode: just the code, no window decoration
94
+ if (props.bare) {
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>`
96
+ }
97
+
98
+ // Dynamic Icon mapping
99
+
100
+
101
+ return `
102
+ <div class="my-6 space-y-1 group">
103
+ <div class="h-4 w-24 ml-auto" style="background-color: var(--theme-color);"></div>
104
+ <div class="">
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>
106
+ </div>
107
+ </div>
108
+ `
109
+ }
110
+
111
+ marked.use({ renderer })
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
+ // Process markdown and convert citations to badges
127
+ const processedHtml = computed(() => {
128
+ let md = props.markdown || ''
129
+
130
+ // Remove References section at end
131
+ md = md.replace(/(?:^|\n)\s*(?:#{1,3}|\*\*)\s*(?:References|Citations|Sources)[\s\S]*$/i, '')
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
+
153
+ // Convert markdown to HTML
154
+ let html = marked.parse(md) as string
155
+
156
+ // Restore math blocks
157
+ for (const { placeholder, html: mathHtml } of mathBlocks) {
158
+ html = html.replace(placeholder, mathHtml)
159
+ }
160
+
161
+ // Render <summary> tags as technical highlight blocks
162
+ html = html.replace(/<summary>([\s\S]*?)<\/summary>/g, (_, content) => {
163
+ return `
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 font-medium bg-white" style="color: var(--text-body)">
167
+ ${content}
168
+ </div>
169
+ </div>
170
+ `
171
+ })
172
+
173
+ // Wrap tables in crisp technical borders
174
+ html = html.replace(/<table[^>]*>([\s\S]*?)<\/table>/g, (_, content) => {
175
+ // Parse table content to simple structure
176
+ const rows = content.match(/<tr[^>]*>[\s\S]*?<\/tr>/g) || []
177
+
178
+ // Extract headers
179
+ const headerRow = rows[0] || ''
180
+ const headers = (headerRow.match(/<th[^>]*>([\s\S]*?)<\/th>/g) || []).map((h: string) => {
181
+ const alignMatch = h.match(/align="([^"]*)"/)
182
+ const align = alignMatch ? alignMatch[1] : 'left'
183
+ const text = h.replace(/<[^>]+>/g, '')
184
+ return { text, align }
185
+ })
186
+
187
+ // Extract body rows
188
+ const bodyRows = rows.slice(1).map((row: string) => {
189
+ return (row.match(/<td[^>]*>([\s\S]*?)<\/td>/g) || []).map((c: string, i: number) => {
190
+ const alignMatch = c.match(/align="([^"]*)"/)
191
+ const align = alignMatch ? alignMatch[1] : (headers[i]?.align || 'left')
192
+ const innerHtml = c.replace(/^<td[^>]*>|<\/td>$/g, '')
193
+ return { html: innerHtml, align }
194
+ })
195
+ })
196
+
197
+ const containerClass = "w-full bg-white text-[16.5px] select-text";
198
+
199
+ let gridHtml = `<div class="${containerClass}">`
200
+
201
+ const allRows: any[] = [headers.map((h: any) => ({ html: h.text, align: h.align })), ...bodyRows];
202
+
203
+ allRows.forEach((row: any[], rowIndex: number) => {
204
+ const isHeader = rowIndex === 0;
205
+ const rowBg = isHeader
206
+ ? 'bg-white text-gray-800 font-black uppercase tracking-tight'
207
+ : (rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50/30');
208
+ const borderB = rowIndex < allRows.length - 1 ? 'border-b border-gray-200' : '';
209
+
210
+ gridHtml += `<div class="flex w-full ${rowBg} ${borderB}">`;
211
+
212
+ row.forEach((cell: any, colIndex: number) => {
213
+ const justify = cell.align === 'center' ? 'justify-center text-center' : (cell.align === 'right' ? 'justify-end text-right' : 'justify-start');
214
+ const borderClass = colIndex === row.length - 1 ? '' : 'border-r border-gray-100';
215
+
216
+ gridHtml += `<div class="flex-1 py-3.5 px-4 min-w-0 break-words flex items-center leading-tight ${justify} ${borderClass}">
217
+ <span>${cell.html}</span>
218
+ </div>`;
219
+ });
220
+ gridHtml += `</div>`;
221
+ });
222
+ gridHtml += `</div>`;
223
+
224
+ if (props.bare) {
225
+ return `<div class="border-b border-gray-200">${gridHtml}</div>`
226
+ }
227
+
228
+ return `
229
+ <div class="my-6 group">
230
+ <div class="bg-white p-0 border-t border-gray-100">
231
+ ${gridHtml}
232
+ </div>
233
+ </div>
234
+ `
235
+ })
236
+
237
+ // Convert [N] citations to small square badges with shadow
238
+ html = html.replace(/(\s*)\[(\d+)\]/g, (_, _space, n) => {
239
+ const num = parseInt(n)
240
+ return `<sup class="inline-flex items-center justify-center w-[15px] h-[15px] text-[10px] font-bold cursor-default select-none ml-0.5 mr-0 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>`
246
+ })
247
+
248
+ return html
249
+ })
250
+ </script>
251
+
252
+ <template>
253
+ <div ref="contentRef"
254
+ class="prose prose-slate max-w-none prose-lg
255
+ prose-headings:font-bold prose-headings:mb-3 prose-headings:mt-8 prose-headings:tracking-tight
256
+ prose-p:leading-7 prose-p:my-4 prose-p:text-[20px] prose-li:text-[20px]
257
+ prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
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
259
+ prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-200 prose-pre:rounded-none prose-pre:p-0
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
263
+ [&>*:first-child]:!mt-0"
264
+ style="--prose-headings: var(--text-primary); --prose-body: var(--text-body); --prose-bold: var(--text-primary); --prose-code: var(--text-body)"
265
+ v-html="processedHtml">
266
+ </div>
267
+ </template>
268
+
269
+ <style>
270
+ /* Highlight.js theme - minimal */
271
+ .hljs {
272
+ background: transparent !important;
273
+ padding: 0 !important;
274
+ }
275
+
276
+ /* Custom List Styling - Premium technical bullet */
277
+ .prose ul {
278
+ list-style: none !important;
279
+ padding-left: 0.25rem !important;
280
+ margin-top: 1rem !important;
281
+ margin-bottom: 1rem !important;
282
+ }
283
+
284
+ .prose ul > li {
285
+ position: relative !important;
286
+ padding-left: 1.5rem !important;
287
+ margin-top: 0.75rem !important;
288
+ margin-bottom: 0.75rem !important;
289
+ line-height: 1.6 !important;
290
+ }
291
+
292
+ .prose ul > li::before {
293
+ content: "" !important;
294
+ position: absolute !important;
295
+ left: 0 !important;
296
+ top: 0.6em !important;
297
+ width: 8px !important;
298
+ height: 8px !important;
299
+ background-color: var(--theme-color, #ef4444) !important; /* Theme color */
300
+ border-radius: 0 !important;
301
+ }
302
+
303
+ /* Nested list styling */
304
+ .prose ul ul {
305
+ margin-top: 0.25rem !important;
306
+ margin-bottom: 0.25rem !important;
307
+ padding-left: 1rem !important;
308
+ }
309
+
310
+ .prose ul ul > li {
311
+ padding-left: 1.25rem !important;
312
+ margin-top: 0.25rem !important;
313
+ margin-bottom: 0.25rem !important;
314
+ }
315
+
316
+ .prose ul ul > li::before {
317
+ width: 6px !important;
318
+ height: 6px !important;
319
+ background-color: var(--theme-color, #ef4444) !important; /* Theme color - same as parent, slightly smaller */
320
+ top: 0.65em !important;
321
+ }
322
+
323
+ /* Custom Blockquote Styling - Dual Red Lines */
324
+ .prose blockquote {
325
+ border-left: none !important;
326
+ padding-left: 1rem !important;
327
+ margin-left: 0 !important;
328
+ position: relative !important;
329
+ font-style: italic !important;
330
+ color: var(--text-body, #3a3a3c) !important; /* Premium reading color */
331
+ }
332
+
333
+ .prose blockquote::before {
334
+ content: "" !important;
335
+ position: absolute !important;
336
+ left: 0 !important;
337
+ top: 0 !important;
338
+ bottom: 0 !important;
339
+ width: 5px !important;
340
+ background-color: var(--theme-color, #ef4444) !important; /* Theme color - thick line */
341
+ }
342
+
343
+
344
+
345
+ /* Ensure images don't have artifacts */
346
+ .prose img {
347
+ display: block;
348
+ margin-left: 0;
349
+ margin-right: auto;
350
+ }
351
+ .prose pre {
352
+ border: none !important;
353
+ }
354
+
355
+ /* Code line numbers - Modern minimalist style */
356
+ .code-with-lines code {
357
+ display: block;
358
+ padding: 1.25em 0;
359
+ background: white;
360
+ }
361
+ .code-with-lines .code-line {
362
+ display: flex;
363
+ align-items: stretch;
364
+ }
365
+ .code-with-lines .line-number {
366
+ flex-shrink: 0;
367
+ width: 36px;
368
+ padding: 0.1em 8px 0.1em 4px;
369
+ text-align: right;
370
+ color: var(--text-muted, #9ca3af);
371
+ background: white;
372
+ border-right: 1px solid #e5e7eb;
373
+ user-select: none;
374
+ font-size: 11px;
375
+ display: flex;
376
+ align-items: flex-start;
377
+ justify-content: flex-end;
378
+ }
379
+ .code-with-lines .line-content {
380
+ flex: 1;
381
+ padding: 0.1em 1.25em 0.1em 1em;
382
+ white-space: pre-wrap;
383
+ word-break: break-all;
384
+ background: white;
385
+ }
386
+ </style>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import MarkdownContent from './MarkdownContent.vue'
3
+
4
+ const props = defineProps<{
5
+ title: string
6
+ content: string
7
+ level: number
8
+ numSearchRefs?: number
9
+ numPageRefs?: number
10
+ index: number
11
+ }>()
12
+ </script>
13
+
14
+ <template>
15
+ <div class="mb-5">
16
+ <!-- Normal Text Section: macOS Window Style -->
17
+ <div class="rounded-none overflow-hidden border border-gray-300 bg-white/70 backdrop-blur-xl">
18
+ <!-- Section Window Header with traffic lights + title on right -->
19
+ <div class="flex items-center justify-between px-3 py-2 bg-gray-100/80 backdrop-blur-lg border-b border-gray-200/40">
20
+ <div class="flex items-center gap-1.5">
21
+ <!-- macOS Traffic Lights -->
22
+ <div class="flex gap-1.5 mr-2">
23
+ <div class="w-2.5 h-2.5 rounded-full bg-[#ff5f56] shadow-md"></div>
24
+ <div class="w-2.5 h-2.5 rounded-full bg-[#ffbd2e] shadow-md"></div>
25
+ <div class="w-2.5 h-2.5 rounded-full bg-[#27c93f] shadow-md"></div>
26
+ </div>
27
+ </div>
28
+ <span class="text-[11px] font-mono text-gray-700/70 uppercase font-bold tracking-wider">{{ title }}</span>
29
+ </div>
30
+
31
+ <!-- Section Content -->
32
+ <div class="p-5 bg-white/40 backdrop-blur-sm">
33
+ <MarkdownContent
34
+ :markdown="content"
35
+ :num-search-refs="numSearchRefs"
36
+ :num-page-refs="numPageRefs"
37
+ />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
@@ -0,0 +1,237 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { Icon } from '@iconify/vue'
4
+ import type { Stage } from '../types'
5
+
6
+ const props = defineProps<{
7
+ stage: Stage
8
+ isFirst?: boolean
9
+ isLast?: boolean
10
+ prevStageName?: string
11
+ refOffset?: number
12
+ }>()
13
+
14
+ const failedImages = ref<Record<string, boolean>>({})
15
+ const imageHeights = ref<Record<string, number>>({})
16
+
17
+ function handleImageError(url: string) {
18
+ failedImages.value[url] = true
19
+ }
20
+
21
+ function handleImageLoad(url: string, event: Event) {
22
+ const img = event.target as HTMLImageElement
23
+ if (img.naturalWidth && img.naturalHeight) {
24
+ // Store aspect ratio as height per unit width
25
+ imageHeights.value[url] = img.naturalHeight / img.naturalWidth
26
+ }
27
+ }
28
+
29
+ // Compute two columns for masonry layout
30
+ const imageColumns = computed(() => {
31
+ const images = props.stage.image_references || []
32
+ const leftColumn: typeof images = []
33
+ const rightColumn: typeof images = []
34
+ let leftHeight = 0
35
+ let rightHeight = 0
36
+
37
+ for (const img of images) {
38
+ if (failedImages.value[img.url]) continue
39
+
40
+ // Get aspect ratio (default to 1 if not loaded yet)
41
+ const aspectRatio = imageHeights.value[img.url] || 1
42
+
43
+ // Add to shorter column
44
+ if (leftHeight <= rightHeight) {
45
+ leftColumn.push(img)
46
+ leftHeight += aspectRatio
47
+ } else {
48
+ rightColumn.push(img)
49
+ rightHeight += aspectRatio
50
+ }
51
+ }
52
+
53
+ return { leftColumn, rightColumn }
54
+ })
55
+
56
+
57
+
58
+
59
+
60
+ function getDomain(url: string): string {
61
+ try {
62
+ const urlObj = new URL(url)
63
+ const hostname = urlObj.hostname.replace('www.', '')
64
+ const pathname = urlObj.pathname === '/' ? '' : urlObj.pathname
65
+ return hostname + pathname
66
+ } catch {
67
+ return url
68
+ }
69
+ }
70
+
71
+ function getFavicon(url: string): string {
72
+ const domain = getDomain(url)
73
+ return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`
74
+ }
75
+
76
+ function formatTime(seconds: number): string {
77
+ return `${seconds.toFixed(2)}s`
78
+ }
79
+
80
+ function formatCost(dollars: number): string {
81
+ return dollars > 0 ? `$${dollars.toFixed(6)}` : '$0'
82
+ }
83
+
84
+ function getModelShort(model: string): string {
85
+ const short = model.includes('/') ? model.split('/').pop() || model : model
86
+ return short.length > 25 ? short.slice(0, 23) + '…' : short
87
+ }
88
+
89
+ function getStageTheme(name?: string) {
90
+ if (!name) return themes['default']
91
+ const key = name.toLowerCase()
92
+
93
+ if (key.includes('search')) return themes['search']
94
+ if (key.includes('crawl') || key.includes('page')) return themes['crawler']
95
+ if (key.includes('agent')) return themes['agent']
96
+ if (key.includes('instruct')) return themes['instruct']
97
+ if (key.includes('vision')) return themes['vision']
98
+
99
+ return themes['default']
100
+ }
101
+
102
+ const themes: Record<string, any> = {
103
+ 'search': { color: 'text-blue-600', bg: 'bg-blue-50', iconBg: 'bg-blue-100/50', icon: 'mdi:magnify' },
104
+ 'crawler': { color: 'text-orange-600', bg: 'bg-orange-50', iconBg: 'bg-orange-100/50', icon: 'mdi:web' },
105
+ 'agent': { color: 'text-purple-600', bg: 'bg-purple-50', iconBg: 'bg-white/80', icon: 'mdi:robot' },
106
+ 'instruct': { color: 'text-red-600', bg: 'bg-red-50', iconBg: 'bg-white/80', icon: 'mdi:lightning-bolt' },
107
+ 'vision': { color: 'text-purple-600', bg: 'bg-purple-50', iconBg: 'bg-white/80', icon: 'mdi:eye' },
108
+ 'default': { color: 'text-gray-600', bg: 'bg-gray-50', iconBg: 'bg-gray-100/50', icon: 'mdi:circle' }
109
+ }
110
+
111
+ function getIcon(name: string): string {
112
+ const key = name.toLowerCase()
113
+ if (key.includes('search')) return 'mdi:magnify'
114
+ if (key.includes('crawl') || key.includes('page')) return 'mdi:web'
115
+ if (key.includes('agent')) return 'mdi:robot'
116
+ if (key.includes('instruct')) return 'mdi:lightning-bolt'
117
+ if (key.includes('vision')) return 'mdi:eye'
118
+ return 'mdi:circle'
119
+ }
120
+
121
+ function getModelLogo(model: string): string | undefined {
122
+ if (!model) return undefined
123
+ const m = model.toLowerCase()
124
+ if (m.includes('openai') || m.includes('gpt')) return 'logos/openai.svg'
125
+ if (m.includes('claude') || m.includes('anthropic')) return 'logos/anthropic.svg'
126
+ if (m.includes('gemini') || m.includes('google')) return 'logos/google.svg'
127
+ if (m.includes('deepseek')) return 'logos/deepseek.png'
128
+ if (m.includes('huggingface')) return 'logos/huggingface.png'
129
+ if (m.includes('mistral')) return 'logos/mistral.png'
130
+ if (m.includes('perplexity')) return 'logos/perplexity.svg'
131
+ if (m.includes('cerebras')) return 'logos/cerebras.svg'
132
+ if (m.includes('grok')) return 'logos/grok.png'
133
+ if (m.includes('qwen')) return 'logos/qwen.png'
134
+ if (m.includes('minimax')) return 'logos/minimax.png'
135
+ if (m.includes('nvidia') || m.includes('nvida')) return 'logos/nvida.png'
136
+ if (m.includes('azure') || m.includes('microsoft')) return 'logos/microsoft.svg'
137
+ if (m.includes('xai')) return 'logos/xai.png'
138
+ if (m.includes('xiaomi')) return 'logos/xiaomi.png'
139
+ if (m.includes('zai')) return 'logos/zai.png'
140
+ return undefined
141
+ }
142
+ </script>
143
+
144
+ <template>
145
+ <div class="relative">
146
+ <!-- Content -->
147
+ <div class="flex-1 min-w-0 pl-2">
148
+ <div class="rounded-none overflow-hidden bg-white">
149
+
150
+ <!-- Header -->
151
+ <div :class="['bg-white px-4 py-2.5 flex items-center justify-between gap-3']">
152
+ <div class="flex items-center gap-3">
153
+ <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]">
154
+ <img v-if="getModelLogo(stage.model)" :src="getModelLogo(stage.model)" class="w-5 h-5 object-contain" />
155
+ <Icon v-else :icon="getIcon(stage.name)" class="text-lg" />
156
+ </div>
157
+ <div class="flex flex-col">
158
+ <span class="font-black text-[18px] text-gray-800 uppercase tracking-tight">{{ stage.name }}</span>
159
+ <span class="text-[15.5px] font-mono tabular-nums tracking-tighter" style="color: var(--text-muted)">{{ getModelShort(stage.model) }}</span>
160
+ </div>
161
+ </div>
162
+ <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)">
163
+ <span v-if="stage.cost > 0">{{ formatCost(stage.cost) }}</span>
164
+ <span v-if="stage.time > 0 && stage.cost > 0" class="text-gray-300">·</span>
165
+ <span v-if="stage.time > 0">{{ formatTime(stage.time) }}</span>
166
+ </div>
167
+ </div>
168
+
169
+
170
+ <div v-if="stage.references?.length || stage.image_references?.length || stage.crawled_pages?.length" class="bg-white pl-11 relative">
171
+ <div v-if="stage.references?.length" class="divide-y divide-gray-50 relative z-10">
172
+ <a v-for="(ref, idx) in stage.references" :key="idx"
173
+ :href="ref.url" target="_blank"
174
+ class="flex items-start gap-3 pr-3 py-3 hover:bg-gray-50 transition-colors group">
175
+ <!-- Favicon - Aligned with Title -->
176
+ <img :src="getFavicon(ref.url)" class="w-4 h-4 rounded-none shrink-0 object-contain mt-[4px]">
177
+
178
+ <!-- Content: Title and Domain -->
179
+ <div class="flex-1 min-w-0 flex flex-col">
180
+ <div class="flex items-center gap-2">
181
+ <span class="flex-1 text-[18px] font-bold text-gray-700 truncate leading-tight tracking-tight">{{ ref.title }}</span>
182
+ <!-- Square Badge with Shadow -->
183
+ <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>
184
+ </div>
185
+ <div class="text-[15.5px] font-mono truncate mt-0.5 tracking-tighter" style="color: var(--text-muted)">{{ getDomain(ref.url) }}</div>
186
+ </div>
187
+ </a>
188
+ </div>
189
+
190
+ <!-- Image Search Results - True Masonry Layout -->
191
+ <div v-if="stage.image_references?.length" class="pr-3 py-3 relative z-10">
192
+ <div class="flex gap-2">
193
+ <!-- Left Column -->
194
+ <div class="flex-1 flex flex-col gap-2">
195
+ <a v-for="(img, idx) in imageColumns.leftColumn" :key="`left-${img.url}-${idx}`"
196
+ :href="img.url" target="_blank"
197
+ class="relative overflow-hidden transition-all hover:opacity-90 group block">
198
+ <img :src="img.thumbnail || img.url"
199
+ @load="handleImageLoad(img.url, $event)"
200
+ @error="handleImageError(img.url)"
201
+ class="w-full h-auto block group-hover:scale-[1.02] transition-transform">
202
+ </a>
203
+ </div>
204
+ <!-- Right Column -->
205
+ <div class="flex-1 flex flex-col gap-2">
206
+ <a v-for="(img, idx) in imageColumns.rightColumn" :key="`right-${img.url}-${idx}`"
207
+ :href="img.url" target="_blank"
208
+ class="relative overflow-hidden transition-all hover:opacity-90 group block">
209
+ <img :src="img.thumbnail || img.url"
210
+ @load="handleImageLoad(img.url, $event)"
211
+ @error="handleImageError(img.url)"
212
+ class="w-full h-auto block group-hover:scale-[1.02] transition-transform">
213
+ </a>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <div v-if="stage.crawled_pages?.length" class="divide-y divide-gray-50 relative z-10">
219
+ <a v-for="(page, idx) in stage.crawled_pages" :key="idx"
220
+ :href="page.url" target="_blank"
221
+ class="flex items-start gap-3 pr-3 py-3 hover:bg-gray-50 transition-colors group">
222
+ <img :src="getFavicon(page.url)" class="w-4 h-4 rounded-none shrink-0 object-contain mt-[4px]">
223
+ <div class="flex-1 min-w-0 flex flex-col">
224
+ <div class="flex items-center gap-2">
225
+ <span class="flex-1 text-[18px] font-bold text-gray-700 truncate leading-tight tracking-tight">{{ page.title }}</span>
226
+ <!-- Square Badge with Shadow -->
227
+ <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>
228
+ </div>
229
+ <div class="text-[15.5px] font-mono truncate mt-0.5 tracking-tighter" style="color: var(--text-muted)">{{ getDomain(page.url) }}</div>
230
+ </div>
231
+ </a>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </template>
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue'
2
+ import './style.css'
3
+ import App from './App.vue'
4
+
5
+ createApp(App).mount('#app')