entari-plugin-hyw 3.4.2__py3-none-any.whl → 3.5.0rc1__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.

Files changed (90) hide show
  1. entari_plugin_hyw/__init__.py +14 -89
  2. entari_plugin_hyw/assets/card-dist/index.html +135 -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/card-ui/.gitignore +24 -0
  23. entari_plugin_hyw/card-ui/README.md +5 -0
  24. entari_plugin_hyw/card-ui/index.html +16 -0
  25. entari_plugin_hyw/card-ui/package-lock.json +2342 -0
  26. entari_plugin_hyw/card-ui/package.json +31 -0
  27. entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
  28. entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
  29. entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
  30. entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
  31. entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
  32. entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
  33. entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
  34. entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
  35. entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
  36. entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
  37. entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
  38. entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
  39. entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
  40. entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
  41. entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
  42. entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
  43. entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
  44. entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
  45. entari_plugin_hyw/card-ui/public/vite.svg +1 -0
  46. entari_plugin_hyw/card-ui/src/App.vue +216 -0
  47. entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
  48. entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
  49. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +330 -0
  50. entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
  51. entari_plugin_hyw/card-ui/src/components/StageCard.vue +163 -0
  52. entari_plugin_hyw/card-ui/src/main.ts +5 -0
  53. entari_plugin_hyw/card-ui/src/style.css +8 -0
  54. entari_plugin_hyw/card-ui/src/types.ts +51 -0
  55. entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
  56. entari_plugin_hyw/card-ui/tsconfig.json +7 -0
  57. entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
  58. entari_plugin_hyw/card-ui/vite.config.ts +16 -0
  59. entari_plugin_hyw/core/config.py +0 -3
  60. entari_plugin_hyw/core/pipeline.py +136 -61
  61. entari_plugin_hyw/core/render_vue.py +255 -0
  62. entari_plugin_hyw/test_output/render_0.jpg +0 -0
  63. entari_plugin_hyw/test_output/render_1.jpg +0 -0
  64. entari_plugin_hyw/test_output/render_2.jpg +0 -0
  65. entari_plugin_hyw/test_output/render_3.jpg +0 -0
  66. entari_plugin_hyw/test_output/render_4.jpg +0 -0
  67. entari_plugin_hyw/tests/ui_test_output.jpg +0 -0
  68. entari_plugin_hyw/tests/verify_ui.py +139 -0
  69. entari_plugin_hyw/utils/misc.py +0 -3
  70. entari_plugin_hyw/utils/prompts.py +65 -63
  71. {entari_plugin_hyw-3.4.2.dist-info → entari_plugin_hyw-3.5.0rc1.dist-info}/METADATA +5 -2
  72. entari_plugin_hyw-3.5.0rc1.dist-info/RECORD +99 -0
  73. entari_plugin_hyw/assets/libs/highlight.css +0 -10
  74. entari_plugin_hyw/assets/libs/highlight.js +0 -1213
  75. entari_plugin_hyw/assets/libs/katex-auto-render.js +0 -1
  76. entari_plugin_hyw/assets/libs/katex.css +0 -1
  77. entari_plugin_hyw/assets/libs/katex.js +0 -1
  78. entari_plugin_hyw/assets/libs/tailwind.css +0 -1
  79. entari_plugin_hyw/assets/package-lock.json +0 -953
  80. entari_plugin_hyw/assets/package.json +0 -16
  81. entari_plugin_hyw/assets/tailwind.config.js +0 -12
  82. entari_plugin_hyw/assets/tailwind.input.css +0 -235
  83. entari_plugin_hyw/assets/template.html +0 -157
  84. entari_plugin_hyw/assets/template.html.bak +0 -157
  85. entari_plugin_hyw/assets/template.j2 +0 -400
  86. entari_plugin_hyw/core/render.py +0 -630
  87. entari_plugin_hyw/utils/prompts_cn.py +0 -119
  88. entari_plugin_hyw-3.4.2.dist-info/RECORD +0 -49
  89. {entari_plugin_hyw-3.4.2.dist-info → entari_plugin_hyw-3.5.0rc1.dist-info}/WHEEL +0 -0
  90. {entari_plugin_hyw-3.4.2.dist-info → entari_plugin_hyw-3.5.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,330 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { marked, type Tokens } from 'marked'
4
+ import hljs from 'highlight.js/lib/core'
5
+ // Import only common languages to reduce bundle size
6
+ import python from 'highlight.js/lib/languages/python'
7
+ import javascript from 'highlight.js/lib/languages/javascript'
8
+ import typescript from 'highlight.js/lib/languages/typescript'
9
+ import json from 'highlight.js/lib/languages/json'
10
+ import bash from 'highlight.js/lib/languages/bash'
11
+ import css from 'highlight.js/lib/languages/css'
12
+ import xml from 'highlight.js/lib/languages/xml'
13
+ import java from 'highlight.js/lib/languages/java'
14
+ import cpp from 'highlight.js/lib/languages/cpp'
15
+ import go from 'highlight.js/lib/languages/go'
16
+ import rust from 'highlight.js/lib/languages/rust'
17
+ import sql from 'highlight.js/lib/languages/sql'
18
+ import markdown from 'highlight.js/lib/languages/markdown'
19
+ import shell from 'highlight.js/lib/languages/shell'
20
+ import yaml from 'highlight.js/lib/languages/yaml'
21
+ import properties from 'highlight.js/lib/languages/properties'
22
+
23
+ hljs.registerLanguage('python', python)
24
+ hljs.registerLanguage('javascript', javascript)
25
+ hljs.registerLanguage('js', javascript)
26
+ hljs.registerLanguage('typescript', typescript)
27
+ hljs.registerLanguage('ts', typescript)
28
+ hljs.registerLanguage('json', json)
29
+ hljs.registerLanguage('bash', bash)
30
+ hljs.registerLanguage('sh', bash)
31
+ hljs.registerLanguage('shell', shell)
32
+ hljs.registerLanguage('zsh', bash)
33
+ hljs.registerLanguage('css', css)
34
+ hljs.registerLanguage('html', xml)
35
+ hljs.registerLanguage('xml', xml)
36
+ hljs.registerLanguage('java', java)
37
+ hljs.registerLanguage('cpp', cpp)
38
+ hljs.registerLanguage('c', cpp)
39
+ hljs.registerLanguage('go', go)
40
+ hljs.registerLanguage('rust', rust)
41
+ hljs.registerLanguage('sql', sql)
42
+ hljs.registerLanguage('markdown', markdown)
43
+ hljs.registerLanguage('md', markdown)
44
+ hljs.registerLanguage('yaml', yaml)
45
+ hljs.registerLanguage('yml', yaml)
46
+ hljs.registerLanguage('properties', properties)
47
+ hljs.registerLanguage('ini', properties)
48
+ hljs.registerLanguage('conf', properties)
49
+
50
+ import 'highlight.js/styles/github.css'
51
+
52
+ const props = defineProps<{
53
+ markdown: string
54
+ numSearchRefs?: number
55
+ numPageRefs?: number
56
+ bare?: boolean // When true, tables and code blocks render without window decoration
57
+ }>()
58
+
59
+ // Configure marked with syntax highlighting
60
+ marked.setOptions({
61
+ breaks: true,
62
+ gfm: true,
63
+ })
64
+
65
+ // Custom renderer for code blocks with technical layout
66
+ const renderer = new marked.Renderer()
67
+ renderer.code = ({ text, lang }: Tokens.Code): string => {
68
+ const language = lang || 'text'
69
+ let highlighted = ''
70
+ if (lang && hljs.getLanguage(lang)) {
71
+ try {
72
+ highlighted = hljs.highlight(text, { language: lang }).value
73
+ } catch {
74
+ highlighted = hljs.highlightAuto(text).value
75
+ }
76
+ } else {
77
+ highlighted = hljs.highlightAuto(text).value
78
+ }
79
+
80
+ // Bare mode: just the code, no window decoration
81
+ if (props.bare) {
82
+ return `<pre class="!mt-0 !mb-0 !rounded-none !bg-gray-50 !p-4 overflow-x-auto border-b border-gray-100"><code class="hljs language-${language} text-[13px] leading-relaxed font-mono">${highlighted}</code></pre>`
83
+ }
84
+
85
+ // Dynamic Icon mapping
86
+ const getLangIcon = (l: string) => {
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)
105
+
106
+ return `
107
+ <div class="my-6 space-y-1 group">
108
+ <div class="flex items-center justify-between px-3 py-1.5 bg-gray-50 ">
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>
117
+ <div class="">
118
+ <pre class="!mt-0 !mb-0 !rounded-none !bg-white !p-4 overflow-x-auto"><code class="hljs language-${language} text-[13px] leading-relaxed font-mono">${highlighted}</code></pre>
119
+ </div>
120
+ </div>
121
+ `
122
+ }
123
+
124
+ marked.use({ renderer })
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
+ // Convert markdown to HTML
134
+ let html = marked.parse(md) as string
135
+
136
+ // Render <summary> tags as technical highlight blocks
137
+ html = html.replace(/<summary>([\s\S]*?)<\/summary>/g, (_, content) => {
138
+ return `
139
+ <div class="my-8 group">
140
+ <div class="flex items-center justify-between px-3 py-1.5 bg-gray-50 ">
141
+ <div class="flex items-center gap-2">
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 ">
150
+ ${content}
151
+ </div>
152
+ </div>
153
+ `
154
+ })
155
+
156
+ // Wrap tables in crisp technical borders
157
+ html = html.replace(/<table[^>]*>([\s\S]*?)<\/table>/g, (_, content) => {
158
+ // Parse table content to simple structure
159
+ const rows = content.match(/<tr[^>]*>[\s\S]*?<\/tr>/g) || []
160
+
161
+ // Extract headers
162
+ const headerRow = rows[0] || ''
163
+ const headers = (headerRow.match(/<th[^>]*>([\s\S]*?)<\/th>/g) || []).map((h: string) => {
164
+ const alignMatch = h.match(/align="([^"]*)"/)
165
+ const align = alignMatch ? alignMatch[1] : 'left'
166
+ const text = h.replace(/<[^>]+>/g, '')
167
+ return { text, align }
168
+ })
169
+
170
+ // Extract body rows
171
+ const bodyRows = rows.slice(1).map((row: string) => {
172
+ return (row.match(/<td[^>]*>([\s\S]*?)<\/td>/g) || []).map((c: string, i: number) => {
173
+ const alignMatch = c.match(/align="([^"]*)"/)
174
+ const align = alignMatch ? alignMatch[1] : (headers[i]?.align || 'left')
175
+ const innerHtml = c.replace(/^<td[^>]*>|<\/td>$/g, '')
176
+ return { html: innerHtml, align }
177
+ })
178
+ })
179
+
180
+ const containerClass = "w-full bg-white text-[12px] select-text";
181
+
182
+ let gridHtml = `<div class="${containerClass}">`
183
+
184
+ const allRows: any[] = [headers.map((h: any) => ({ html: h.text, align: h.align })), ...bodyRows];
185
+
186
+ allRows.forEach((row: any[], rowIndex: number) => {
187
+ const isHeader = rowIndex === 0;
188
+ const rowBg = isHeader
189
+ ? 'bg-white text-gray-900 font-black uppercase tracking-tight'
190
+ : (rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50/30');
191
+ const borderB = rowIndex < allRows.length - 1 ? 'border-b border-gray-200' : '';
192
+
193
+ gridHtml += `<div class="flex w-full ${rowBg} ${borderB}">`;
194
+
195
+ row.forEach((cell: any, colIndex: number) => {
196
+ const justify = cell.align === 'center' ? 'justify-center text-center' : (cell.align === 'right' ? 'justify-end text-right' : 'justify-start');
197
+ const borderClass = colIndex === row.length - 1 ? '' : 'border-r border-gray-100';
198
+
199
+ gridHtml += `<div class="flex-1 py-2.5 px-3 min-w-0 break-words flex items-center leading-tight ${justify} ${borderClass}">
200
+ <span>${cell.html}</span>
201
+ </div>`;
202
+ });
203
+ gridHtml += `</div>`;
204
+ });
205
+ gridHtml += `</div>`;
206
+
207
+ if (props.bare) {
208
+ return `<div class="overflow-x-auto border-b border-gray-200">${gridHtml}</div>`
209
+ }
210
+
211
+ return `
212
+ <div class="my-6 group">
213
+ <div class="overflow-x-auto bg-white p-0 border-t border-gray-100">
214
+ ${gridHtml}
215
+ </div>
216
+ </div>
217
+ `
218
+ })
219
+
220
+ // Convert [N] citations to rectangular sharp badges
221
+ html = html.replace(/\[(\d+)\]/g, (_, n) => {
222
+ const num = parseInt(n)
223
+ return `<span class="relative -top-1.5 text-[10px] font-bold text-blue-600 mx-0.5 cursor-default select-none transition-all">${num}</span>`
224
+ })
225
+
226
+ return html
227
+ })
228
+ </script>
229
+
230
+ <template>
231
+ <div ref="contentRef"
232
+ class="prose prose-slate max-w-none
233
+ prose-headings:text-gray-900 prose-headings:font-black prose-headings:mb-2 prose-headings:mt-6 prose-headings:tracking-tight
234
+ prose-p:text-gray-800 prose-p:leading-relaxed prose-p:my-3
235
+ 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.9em] prose-code:font-mono prose-code:text-gray-900
237
+ 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 prose-img:
239
+ prose-ol:list-decimal prose-ol:pl-4 prose-ol:list-outside
240
+ [&>*:first-child]:!mt-0"
241
+ v-html="processedHtml">
242
+ </div>
243
+ </template>
244
+
245
+ <style>
246
+ /* Highlight.js theme - minimal */
247
+ .hljs {
248
+ background: transparent !important;
249
+ padding: 0 !important;
250
+ }
251
+
252
+ /* Custom List Styling - Premium technical bullet */
253
+ .prose ul {
254
+ list-style: none !important;
255
+ padding-left: 0.25rem !important;
256
+ margin-top: 0.75rem !important;
257
+ margin-bottom: 0.75rem !important;
258
+ }
259
+
260
+ .prose ul > li {
261
+ position: relative !important;
262
+ padding-left: 1.5rem !important;
263
+ margin-top: 0.5rem !important;
264
+ margin-bottom: 0.5rem !important;
265
+ line-height: 1.6 !important;
266
+ }
267
+
268
+ .prose ul > li::before {
269
+ content: "" !important;
270
+ position: absolute !important;
271
+ left: 0 !important;
272
+ top: 0.6em !important;
273
+ width: 6px !important;
274
+ height: 6px !important;
275
+ background-color: #ef4444 !important; /* Red-500 */
276
+ border-radius: 0 !important;
277
+ }
278
+
279
+ /* Nested list styling */
280
+ .prose ul ul {
281
+ margin-top: 0.25rem !important;
282
+ margin-bottom: 0.25rem !important;
283
+ padding-left: 1rem !important;
284
+ }
285
+
286
+ .prose ul ul > li {
287
+ padding-left: 1.25rem !important;
288
+ margin-top: 0.25rem !important;
289
+ margin-bottom: 0.25rem !important;
290
+ }
291
+
292
+ .prose ul ul > li::before {
293
+ width: 5px !important;
294
+ height: 5px !important;
295
+ background-color: #ef4444 !important; /* Red-500 - same as parent, slightly smaller */
296
+ top: 0.65em !important;
297
+ }
298
+
299
+ /* Custom Blockquote Styling - Dual Red Lines */
300
+ .prose blockquote {
301
+ border-left: none !important;
302
+ padding-left: 1rem !important;
303
+ margin-left: 0 !important;
304
+ position: relative !important;
305
+ font-style: italic !important;
306
+ color: #1f2937 !important; /* gray-800 */
307
+ }
308
+
309
+ .prose blockquote::before {
310
+ content: "" !important;
311
+ position: absolute !important;
312
+ left: 0 !important;
313
+ top: 0 !important;
314
+ bottom: 0 !important;
315
+ width: 3px !important;
316
+ background-color: #ef4444 !important; /* Red-500 - thick line */
317
+ }
318
+
319
+
320
+
321
+ /* Ensure images don't have artifacts */
322
+ .prose img {
323
+ display: block;
324
+ margin-left: auto;
325
+ margin-right: auto;
326
+ }
327
+ .prose pre {
328
+ border: none !important;
329
+ }
330
+ </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,163 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { Icon } from '@iconify/vue'
4
+ import type { Stage } from '../types'
5
+
6
+ const failedImages = ref<Record<string, boolean>>({})
7
+
8
+ function handleImageError(url: string) {
9
+ failedImages.value[url] = true
10
+ }
11
+
12
+ const props = defineProps<{
13
+ stage: Stage
14
+ isFirst?: boolean
15
+ isLast?: boolean
16
+ prevStageName?: string
17
+ }>()
18
+
19
+ function getDomain(url: string): string {
20
+ try {
21
+ return new URL(url).hostname.replace('www.', '')
22
+ } catch {
23
+ return url
24
+ }
25
+ }
26
+
27
+ function getFavicon(url: string): string {
28
+ const domain = getDomain(url)
29
+ return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`
30
+ }
31
+
32
+ function formatTime(seconds: number): string {
33
+ return `${seconds.toFixed(2)}s`
34
+ }
35
+
36
+ function formatCost(dollars: number): string {
37
+ return dollars > 0 ? `$${dollars.toFixed(6)}` : '$0'
38
+ }
39
+
40
+ function getModelShort(model: string): string {
41
+ const short = model.includes('/') ? model.split('/').pop() || model : model
42
+ return short.length > 25 ? short.slice(0, 23) + '…' : short
43
+ }
44
+
45
+ function getStageTheme(name?: string) {
46
+ if (!name) return themes['default']
47
+ const key = name.toLowerCase()
48
+
49
+ if (key.includes('search')) return themes['search']
50
+ if (key.includes('crawl') || key.includes('page')) return themes['crawler']
51
+ if (key.includes('agent')) return themes['agent']
52
+ if (key.includes('instruct')) return themes['instruct']
53
+ if (key.includes('vision')) return themes['vision']
54
+
55
+ return themes['default']
56
+ }
57
+
58
+ const themes: Record<string, any> = {
59
+ 'search': { color: 'text-blue-600', bg: 'bg-blue-50', line: 'bg-red-300', iconBg: 'bg-blue-100/50', dotBg: 'bg-red-500', icon: 'mdi:magnify' },
60
+ 'crawler': { color: 'text-orange-600', bg: 'bg-orange-50', line: 'bg-red-300', iconBg: 'bg-orange-100/50', dotBg: 'bg-red-500', icon: 'mdi:web' },
61
+ 'agent': { color: 'text-purple-600', bg: 'bg-purple-50', line: 'bg-red-300', iconBg: 'bg-white/80', dotBg: 'bg-red-500', icon: 'mdi:robot' },
62
+ 'instruct': { color: 'text-red-600', bg: 'bg-red-50', line: 'bg-red-300', iconBg: 'bg-white/80', dotBg: 'bg-red-500', icon: 'mdi:lightning-bolt' },
63
+ 'vision': { color: 'text-green-600', bg: 'bg-green-50', line: 'bg-red-300', iconBg: 'bg-green-100/50', dotBg: 'bg-red-500', icon: 'mdi:eye' },
64
+ 'default': { color: 'text-gray-600', bg: 'bg-gray-50', line: 'bg-red-300', iconBg: 'bg-gray-100/50', dotBg: 'bg-red-500', icon: 'mdi:circle' }
65
+ }
66
+
67
+ function getIcon(name: string): string {
68
+ const key = name.toLowerCase()
69
+ if (key.includes('search')) return 'mdi:magnify'
70
+ if (key.includes('crawl') || key.includes('page')) return 'mdi:web'
71
+ if (key.includes('agent')) return 'mdi:robot'
72
+ if (key.includes('instruct')) return 'mdi:lightning-bolt'
73
+ if (key.includes('vision')) return 'mdi:eye'
74
+ return 'mdi:circle'
75
+ }
76
+
77
+ function getModelLogo(model: string): string | undefined {
78
+ if (!model) return undefined
79
+ const m = model.toLowerCase()
80
+ if (m.includes('openai') || m.includes('gpt')) return 'logos/openai.svg'
81
+ if (m.includes('claude') || m.includes('anthropic')) return 'logos/anthropic.svg'
82
+ if (m.includes('gemini') || m.includes('google')) return 'logos/google.svg'
83
+ if (m.includes('deepseek')) return 'logos/deepseek.png'
84
+ if (m.includes('huggingface')) return 'logos/huggingface.png'
85
+ if (m.includes('mistral')) return 'logos/mistral.png'
86
+ if (m.includes('perplexity')) return 'logos/perplexity.svg'
87
+ if (m.includes('cerebras')) return 'logos/cerebras.svg'
88
+ if (m.includes('grok')) return 'logos/grok.png'
89
+ if (m.includes('qwen')) return 'logos/qwen.png'
90
+ if (m.includes('minimax')) return 'logos/minimax.png'
91
+ if (m.includes('nvidia') || m.includes('nvida')) return 'logos/nvida.png'
92
+ if (m.includes('azure') || m.includes('microsoft')) return 'logos/microsoft.svg'
93
+ if (m.includes('xai')) return 'logos/xai.png'
94
+ if (m.includes('xiaomi')) return 'logos/xiaomi.png'
95
+ if (m.includes('zai')) return 'logos/zai.png'
96
+ return undefined
97
+ }
98
+ </script>
99
+
100
+ <template>
101
+ <div class="relative">
102
+ <!-- Content -->
103
+ <div class="flex-1 min-w-0 pl-2">
104
+ <div class="rounded-none overflow-hidden bg-white">
105
+
106
+ <!-- Header -->
107
+ <div :class="['bg-white px-3 py-1.5 flex items-center justify-between gap-2']">
108
+ <div class="flex items-center gap-2">
109
+ <div :class="['w-6 h-6 flex items-center justify-center shrink-0 overflow-hidden border border-gray-100', getStageTheme(stage.name).iconBg, getStageTheme(stage.name).color]">
110
+ <img v-if="getModelLogo(stage.model)" :src="getModelLogo(stage.model)" class="w-4 h-4 object-contain" />
111
+ <Icon v-else :icon="getIcon(stage.name)" class="text-xs" />
112
+ </div>
113
+ <div class="flex flex-col">
114
+ <span class="font-black text-xs text-gray-900 uppercase tracking-tight">{{ stage.name }}</span>
115
+ <span class="text-[10px] font-mono text-gray-400 tabular-nums tracking-tighter">{{ getModelShort(stage.model) }}</span>
116
+ </div>
117
+ </div>
118
+ <div v-if="stage.time > 0 || stage.cost > 0" class="text-[10px] text-gray-400 font-mono flex items-center justify-end gap-2 leading-tight min-w-[120px]">
119
+ <span v-if="stage.cost > 0">{{ formatCost(stage.cost) }}</span>
120
+ <span v-if="stage.time > 0 && stage.cost > 0" class="text-gray-300">·</span>
121
+ <span v-if="stage.time > 0">{{ formatTime(stage.time) }}</span>
122
+ </div>
123
+ </div>
124
+
125
+
126
+ <div v-if="stage.references?.length || stage.image_references?.length" class="bg-white pl-8 relative">
127
+ <div v-if="stage.references?.length" class="divide-y divide-gray-50 relative z-10">
128
+ <a v-for="(ref, idx) in stage.references" :key="idx"
129
+ :href="ref.url" target="_blank"
130
+ class="flex items-start gap-2 pr-3 py-2 hover:bg-gray-50 transition-colors group">
131
+ <!-- Favicon - Aligned with Title -->
132
+ <img :src="getFavicon(ref.url)" class="w-3 h-3 rounded-none shrink-0 object-contain mt-[3px]">
133
+
134
+ <!-- Content: Title and Domain -->
135
+ <div class="flex-1 min-w-0 flex flex-col">
136
+ <div class="flex items-center gap-2">
137
+ <span class="flex-1 text-xs font-bold text-gray-800 truncate leading-tight tracking-tight">{{ ref.title }}</span>
138
+ <!-- Neutralized Citation Design -->
139
+ <span class="shrink-0 text-[10px] font-bold text-blue-600 flex items-center justify-center">{{ idx + 1 }}</span>
140
+ </div>
141
+ <div class="text-[10px] font-mono text-gray-400 truncate mt-0.5 tracking-tighter">{{ getDomain(ref.url) }}</div>
142
+ </div>
143
+ </a>
144
+ </div>
145
+
146
+ <!-- Image Search Results -->
147
+ <div v-if="stage.image_references?.length" class="pr-2 py-2 relative z-10">
148
+ <div class="grid grid-cols-3 gap-1">
149
+ <a v-for="(img, idx) in stage.image_references" :key="idx"
150
+ v-show="!failedImages[img.url]"
151
+ :href="img.url" target="_blank"
152
+ class="relative aspect-square overflow-hidden bg-white border border-gray-200 transition-colors">
153
+ <img :src="img.thumbnail || img.url"
154
+ @error="handleImageError(img.url)"
155
+ class="w-full h-full object-cover">
156
+ </a>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </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')
@@ -0,0 +1,8 @@
1
+ @import "tailwindcss";
2
+ @plugin "daisyui";
3
+ @plugin "@tailwindcss/typography";
4
+
5
+ /* Custom styles */
6
+ body {
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8
+ }
@@ -0,0 +1,51 @@
1
+ // Type definitions for render data
2
+ // Python only passes raw data, all processing happens in frontend
3
+
4
+ export interface Stage {
5
+ name: string
6
+ model: string
7
+ provider: string
8
+ icon_name?: string // Icon identifier (e.g., "google", "openai")
9
+ time: number // Time in seconds (raw number)
10
+ cost: number // Cost in dollars (raw number)
11
+ references?: Reference[]
12
+ image_references?: ImageReference[]
13
+ crawled_pages?: CrawledPage[]
14
+ }
15
+
16
+ export interface Reference {
17
+ title: string
18
+ url: string
19
+ }
20
+
21
+ export interface ImageReference {
22
+ title: string
23
+ url: string
24
+ thumbnail?: string
25
+ }
26
+
27
+ export interface CrawledPage {
28
+ title: string
29
+ url: string
30
+ }
31
+
32
+ export interface Stats {
33
+ total_time?: number
34
+ vision_duration?: number
35
+ }
36
+
37
+ export interface Flags {
38
+ has_vision: boolean
39
+ has_search: boolean
40
+ }
41
+
42
+ // Raw data from Python - minimal processing
43
+ export interface RenderData {
44
+ markdown: string // Raw markdown content
45
+ stages: Stage[]
46
+ references: Reference[] // All references for citation
47
+ page_references: Reference[]
48
+ image_references: ImageReference[]
49
+ stats: Stats
50
+ total_time: number
51
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "types": ["vite/client"],
6
+
7
+ /* Linting */
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "erasableSyntaxOnly": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "noUncheckedSideEffectImports": true
14
+ },
15
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
16
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+ import { viteSingleFile } from 'vite-plugin-singlefile'
5
+
6
+ export default defineConfig({
7
+ plugins: [
8
+ vue(),
9
+ tailwindcss(),
10
+ viteSingleFile(),
11
+ ],
12
+ build: {
13
+ outDir: '../assets/card-dist',
14
+ emptyOutDir: true,
15
+ },
16
+ })