entari-plugin-hyw 4.0.0rc6__py3-none-any.whl → 4.0.0rc8__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 (114) hide show
  1. entari_plugin_hyw/Untitled-1 +1865 -0
  2. entari_plugin_hyw/__init__.py +733 -379
  3. entari_plugin_hyw/history.py +60 -57
  4. entari_plugin_hyw/misc.py +3 -0
  5. entari_plugin_hyw/search_cache.py +154 -0
  6. {entari_plugin_hyw-4.0.0rc6.dist-info → entari_plugin_hyw-4.0.0rc8.dist-info}/METADATA +3 -1
  7. entari_plugin_hyw-4.0.0rc8.dist-info/RECORD +68 -0
  8. {entari_plugin_hyw-4.0.0rc6.dist-info → entari_plugin_hyw-4.0.0rc8.dist-info}/WHEEL +1 -1
  9. {entari_plugin_hyw-4.0.0rc6.dist-info → entari_plugin_hyw-4.0.0rc8.dist-info}/top_level.txt +1 -0
  10. hyw_core/__init__.py +94 -0
  11. hyw_core/browser_control/__init__.py +65 -0
  12. hyw_core/browser_control/assets/card-dist/index.html +409 -0
  13. hyw_core/browser_control/assets/index.html +5691 -0
  14. hyw_core/browser_control/engines/__init__.py +17 -0
  15. hyw_core/browser_control/engines/default.py +166 -0
  16. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/duckduckgo.py +42 -8
  17. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/google.py +1 -1
  18. {entari_plugin_hyw/browser → hyw_core/browser_control}/manager.py +15 -8
  19. entari_plugin_hyw/render_vue.py → hyw_core/browser_control/renderer.py +29 -14
  20. hyw_core/browser_control/service.py +720 -0
  21. hyw_core/config.py +154 -0
  22. hyw_core/core.py +322 -0
  23. hyw_core/definitions.py +83 -0
  24. entari_plugin_hyw/modular_pipeline.py → hyw_core/pipeline.py +204 -86
  25. {entari_plugin_hyw → hyw_core}/search.py +60 -19
  26. hyw_core/stages/__init__.py +21 -0
  27. entari_plugin_hyw/stage_base.py → hyw_core/stages/base.py +3 -0
  28. entari_plugin_hyw/stage_summary.py → hyw_core/stages/summary.py +36 -7
  29. entari_plugin_hyw/assets/card-dist/index.html +0 -387
  30. entari_plugin_hyw/browser/__init__.py +0 -10
  31. entari_plugin_hyw/browser/engines/bing.py +0 -95
  32. entari_plugin_hyw/browser/service.py +0 -304
  33. entari_plugin_hyw/card-ui/.gitignore +0 -24
  34. entari_plugin_hyw/card-ui/README.md +0 -5
  35. entari_plugin_hyw/card-ui/index.html +0 -16
  36. entari_plugin_hyw/card-ui/package-lock.json +0 -2342
  37. entari_plugin_hyw/card-ui/package.json +0 -31
  38. entari_plugin_hyw/card-ui/public/logos/anthropic.svg +0 -1
  39. entari_plugin_hyw/card-ui/public/logos/cerebras.svg +0 -9
  40. entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
  41. entari_plugin_hyw/card-ui/public/logos/gemini.svg +0 -1
  42. entari_plugin_hyw/card-ui/public/logos/google.svg +0 -1
  43. entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
  44. entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
  45. entari_plugin_hyw/card-ui/public/logos/microsoft.svg +0 -15
  46. entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
  47. entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
  48. entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
  49. entari_plugin_hyw/card-ui/public/logos/openai.svg +0 -1
  50. entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
  51. entari_plugin_hyw/card-ui/public/logos/perplexity.svg +0 -24
  52. entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
  53. entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
  54. entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
  55. entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
  56. entari_plugin_hyw/card-ui/public/vite.svg +0 -1
  57. entari_plugin_hyw/card-ui/src/App.vue +0 -756
  58. entari_plugin_hyw/card-ui/src/assets/vue.svg +0 -1
  59. entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +0 -41
  60. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +0 -382
  61. entari_plugin_hyw/card-ui/src/components/SectionCard.vue +0 -41
  62. entari_plugin_hyw/card-ui/src/components/StageCard.vue +0 -240
  63. entari_plugin_hyw/card-ui/src/main.ts +0 -5
  64. entari_plugin_hyw/card-ui/src/style.css +0 -29
  65. entari_plugin_hyw/card-ui/src/test_regex.js +0 -103
  66. entari_plugin_hyw/card-ui/src/types.ts +0 -61
  67. entari_plugin_hyw/card-ui/tsconfig.app.json +0 -16
  68. entari_plugin_hyw/card-ui/tsconfig.json +0 -7
  69. entari_plugin_hyw/card-ui/tsconfig.node.json +0 -26
  70. entari_plugin_hyw/card-ui/vite.config.ts +0 -16
  71. entari_plugin_hyw/definitions.py +0 -155
  72. entari_plugin_hyw/stage_instruct.py +0 -345
  73. entari_plugin_hyw/stage_instruct_deepsearch.py +0 -104
  74. entari_plugin_hyw-4.0.0rc6.dist-info/RECORD +0 -100
  75. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/anthropic.svg +0 -0
  76. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/cerebras.svg +0 -0
  77. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/deepseek.png +0 -0
  78. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/gemini.svg +0 -0
  79. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/google.svg +0 -0
  80. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/grok.png +0 -0
  81. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/huggingface.png +0 -0
  82. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/microsoft.svg +0 -0
  83. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/minimax.png +0 -0
  84. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/mistral.png +0 -0
  85. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/nvida.png +0 -0
  86. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openai.svg +0 -0
  87. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openrouter.png +0 -0
  88. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/perplexity.svg +0 -0
  89. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/qwen.png +0 -0
  90. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xai.png +0 -0
  91. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xiaomi.png +0 -0
  92. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/zai.png +0 -0
  93. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/vite.svg +0 -0
  94. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/anthropic.svg +0 -0
  95. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/cerebras.svg +0 -0
  96. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/deepseek.png +0 -0
  97. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/gemini.svg +0 -0
  98. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/google.svg +0 -0
  99. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/grok.png +0 -0
  100. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/huggingface.png +0 -0
  101. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/microsoft.svg +0 -0
  102. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/minimax.png +0 -0
  103. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/mistral.png +0 -0
  104. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/nvida.png +0 -0
  105. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openai.svg +0 -0
  106. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openrouter.png +0 -0
  107. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/perplexity.svg +0 -0
  108. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/qwen.png +0 -0
  109. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xai.png +0 -0
  110. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xiaomi.png +0 -0
  111. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/zai.png +0 -0
  112. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/base.py +0 -0
  113. {entari_plugin_hyw/browser → hyw_core/browser_control}/landing.html +0 -0
  114. {entari_plugin_hyw → hyw_core}/image_cache.py +0 -0
@@ -1,756 +0,0 @@
1
- <script setup lang="ts">
2
- import { ref, computed, onMounted } from 'vue'
3
- import { Icon } from '@iconify/vue'
4
-
5
- import type { RenderData, Reference } from './types'
6
- import MarkdownContent from './components/MarkdownContent.vue'
7
-
8
- // Import icons for Flow area
9
- import iconOpenai from './assets/icon/openai.svg'
10
- import iconGemini from './assets/icon/gemini.svg'
11
- import iconAnthropic from './assets/icon/anthropic.svg'
12
- import iconDeepseek from './assets/icon/deepseek.png'
13
- import iconQwen from './assets/icon/qwen.png'
14
- import iconMistral from './assets/icon/mistral.png'
15
- import iconGrok from './assets/icon/grok.png'
16
- import iconHuggingface from './assets/icon/huggingface.png'
17
- import iconCerebras from './assets/icon/cerebras.svg'
18
- import iconMinimax from './assets/icon/minimax.png'
19
- import iconPerplexity from './assets/icon/perplexity.svg'
20
- import iconNvidia from './assets/icon/nvida.png'
21
- import iconMicrosoft from './assets/icon/microsoft.svg'
22
- import iconXiaomi from './assets/icon/xiaomi.png'
23
- import iconOpenrouter from './assets/icon/openrouter.png'
24
-
25
- // Get icon for card type
26
- const getCardIcon = (contentType?: string): string => {
27
- switch (contentType) {
28
- case 'summary': return 'mdi:text-box-outline'
29
- case 'code': return 'mdi:code-braces'
30
- case 'table': return 'mdi:table'
31
- default: return 'mdi:card-outline'
32
- }
33
- }
34
-
35
-
36
-
37
- // Get display label for card
38
- const getCardLabel = (contentType?: string, language?: string): string => {
39
- switch (contentType) {
40
- case 'summary': return 'Summary'
41
- case 'code': return language ? language.charAt(0).toUpperCase() + language.slice(1) : 'Code'
42
- case 'table': return 'Table'
43
- default: return ''
44
- }
45
- }
46
-
47
- declare global {
48
- interface Window {
49
- RENDER_DATA: RenderData
50
- updateRenderData: (data: RenderData) => void
51
- }
52
- }
53
-
54
- const data = ref<RenderData | null>(null)
55
-
56
- // Expose update method for Python to call
57
- window.updateRenderData = (newData: RenderData) => {
58
- data.value = newData
59
- }
60
-
61
- const numSearchRefs = computed(() => data.value?.references?.length || 0)
62
- const numPageRefs = computed(() => data.value?.page_references?.length || 0)
63
-
64
-
65
- // Helper: Strips content before the first H1 heading (e.g., AI "thought" prefixes)
66
- const stripPrefixBeforeH1 = (text: string): string => {
67
- // Find the first line starting with "# " (H1)
68
- const h1Match = text.match(/^#\s+/m)
69
- const summaryMatch = text.match(/<summary>/)
70
-
71
- let startIndex = -1
72
-
73
- if (h1Match && h1Match.index !== undefined) {
74
- startIndex = h1Match.index
75
- }
76
-
77
- if (summaryMatch && summaryMatch.index !== undefined) {
78
- // If summary is found and is BEFORE the H1 (or no H1 found), start from summary
79
- if (startIndex === -1 || summaryMatch.index < startIndex) {
80
- startIndex = summaryMatch.index
81
- }
82
- }
83
-
84
- if (startIndex !== -1) {
85
- return text.substring(startIndex)
86
- }
87
-
88
- // If no H1 found, return text as-is (fallback)
89
- return text
90
- }
91
-
92
- // Reorder citations and return cleaned markdown + reordered refs
93
- const reorderedData = computed(() => {
94
- const originalMd = stripPrefixBeforeH1(data.value?.markdown || '')
95
- if (!originalMd) return { markdown: '', references: [] }
96
-
97
- const searchRefs = (data.value?.references || []).map((r, i) => ({...r, type: 'search', _orig: i + 1}))
98
- const pageRefs = (data.value?.page_references || []).map((r, i) => ({...r, type: 'page', _orig: (data.value?.references?.length || 0) + i + 1}))
99
- const allRefs = [...searchRefs, ...pageRefs]
100
-
101
- const citationRegex = /\[(\d+)\]/g
102
- const usageOrder: number[] = []
103
- let match
104
- // Scan for usage order
105
- while ((match = citationRegex.exec(originalMd)) !== null) {
106
- const id = parseInt(match[1]!)
107
- if (!usageOrder.includes(id)) usageOrder.push(id)
108
- }
109
-
110
- const idMap = new Map()
111
- const newReferences: any[] = []
112
-
113
- // 1. Used refs
114
- usageOrder.forEach((oldId, idx) => {
115
- const newId = idx + 1
116
- idMap.set(oldId, newId)
117
- const sourceRef = allRefs[oldId - 1]
118
- if (sourceRef) newReferences.push({ ...sourceRef, original_idx: newId })
119
- })
120
-
121
- // 2. Unused refs
122
- allRefs.forEach((ref, idx) => {
123
- const oldId = idx + 1
124
- if (!idMap.has(oldId)) {
125
- const newId = newReferences.length + 1
126
- idMap.set(oldId, newId)
127
- newReferences.push({ ...ref, original_idx: newId })
128
- }
129
- })
130
-
131
- const newMd = originalMd.replace(citationRegex, (m, n) => {
132
- const newId = idMap.get(parseInt(n))
133
- return newId ? `[${newId}]` : m
134
- })
135
-
136
- return { markdown: newMd, references: newReferences }
137
- })
138
-
139
- const referencesList = computed(() => reorderedData.value.references)
140
-
141
- const mainTitle = computed(() => {
142
- const md = reorderedData.value.markdown || ''
143
- const match = md.match(/^#\s+(.+)$/m)
144
- return match && match[1] ? match[1].trim() : ''
145
- })
146
-
147
- // Process title to support <u> underline tags
148
- const processedTitle = computed(() => {
149
- return mainTitle.value.replace(/<u>([^<]*)<\/u>/g, (_, content) => {
150
- return `<span class="underline decoration-[5px] underline-offset-8" style="text-decoration-color: var(--theme-color)">${content}</span>`
151
- })
152
- })
153
-
154
- function getDomain(url: string): string {
155
- try {
156
- const urlObj = new URL(url)
157
- const hostname = urlObj.hostname.replace('www.', '')
158
- let pathname = urlObj.pathname === '/' ? '' : decodeURIComponent(urlObj.pathname)
159
-
160
- // Truncate if too long
161
- const maxLen = 40
162
- let result = hostname + pathname
163
- if (result.length > maxLen) {
164
- result = result.slice(0, maxLen - 3) + '...'
165
- }
166
- return result
167
- } catch {
168
- return url.length > 40 ? url.slice(0, 37) + '...' : url
169
- }
170
- }
171
-
172
- function getFavicon(url: string): string {
173
- const domain = getDomain(url)
174
- return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`
175
- }
176
-
177
- /**
178
- * Robustly formats an image source.
179
- * Handles:
180
- * 1. Absolute URLs (http, https, //)
181
- * 2. Data URIs (data:image/...)
182
- * 3. Raw Base64 strings (fallbacks to data URI)
183
- */
184
- function getImageUrl(src: string): string {
185
- if (!src) return ''
186
-
187
- // 1. Data URI
188
- if (src.startsWith('data:')) return src
189
-
190
- // 2. Protocol-relative or Absolute URL
191
- if (src.startsWith('//') || src.startsWith('http:') || src.startsWith('https:')) {
192
- return src
193
- }
194
-
195
- // 3. Assume raw base64 (remove potential whitespace)
196
- const cleanBase64 = src.trim()
197
- if (cleanBase64.length > 0) {
198
- return `data:image/jpeg;base64,${cleanBase64}`
199
- }
200
-
201
- return src
202
- }
203
-
204
- function isValidImage(src: string): boolean {
205
- if (!src) return false
206
- if (src.startsWith('http') || src.startsWith('//')) return true
207
- if (src.length < 20) return false // Too short for meaningful data
208
- return true
209
- }
210
-
211
- // Get all instruct/analysis stages for aggregation
212
- const instructStages = computed(() => data.value?.stages?.filter(s =>
213
- s.name?.toLowerCase() === 'instruct' ||
214
- s.name?.toLowerCase().startsWith('analysis') ||
215
- s.provider?.toLowerCase() === 'instruct'
216
- ) || [])
217
-
218
- // Aggregated instruct data (sum of all rounds)
219
- const instructStage = computed(() => {
220
- const stages = instructStages.value
221
- if (!stages.length) return null
222
-
223
- // Get first stage as base for model/icon info
224
- const first = stages[0]
225
-
226
- // Sum time, usage, and cost from all rounds
227
- const totalTime = stages.reduce((sum, s) => sum + (s.time || 0), 0)
228
- const totalInputTokens = stages.reduce((sum, s) => sum + (s.usage?.input_tokens || 0), 0)
229
- const totalOutputTokens = stages.reduce((sum, s) => sum + (s.usage?.output_tokens || 0), 0)
230
- const totalCost = stages.reduce((sum, s) => sum + (s.cost || 0), 0)
231
-
232
- return {
233
- ...first,
234
- time: totalTime,
235
- usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens },
236
- cost: totalCost
237
- }
238
- })
239
-
240
- const summaryStage = computed(() => data.value?.stages?.find(s => s.name?.toLowerCase() === 'summary' || s.name?.toLowerCase() === 'agent'))
241
- // searchStage removed - no longer needed for display
242
-
243
- // Collect all extracted images from references
244
- const galleryImages = computed(() => {
245
- const refs = (data.value?.references || []) as Reference[]
246
- const images: string[] = []
247
- const seenHashes = new Set<string>()
248
-
249
- // Strategy: Balanced picking
250
- // 1. First Pass: Try to pick 1-2 from each
251
- for (const ref of refs) {
252
- if (ref.images && Array.isArray(ref.images)) {
253
- let count = 0
254
- for (const b64 of ref.images) {
255
- if (!isValidImage(b64)) continue
256
- const hash = `${b64.substring(0, 100)}_${b64.length}`
257
- if (!seenHashes.has(hash)) {
258
- seenHashes.add(hash)
259
- images.push(b64)
260
- count++
261
- if (count >= 2) break
262
- }
263
- }
264
- }
265
- }
266
-
267
- // 2. Second Pass: If still too few, pick more from anyone
268
- if (images.length < 8) {
269
- for (const ref of refs) {
270
- if (ref.images && Array.isArray(ref.images)) {
271
- for (const b64 of ref.images) {
272
- if (!isValidImage(b64)) continue
273
- const hash = `${b64.substring(0, 100)}_${b64.length}`
274
- if (!seenHashes.has(hash)) {
275
- seenHashes.add(hash)
276
- images.push(b64)
277
- if (images.length >= 12) break
278
- }
279
- }
280
- }
281
- if (images.length >= 12) break
282
- }
283
- }
284
-
285
- console.log(`[Gallery] Selected ${images.length} images. First 100 chars of #1:`, images[0]?.substring(0, 100))
286
- return images.slice(0, 12)
287
- })
288
-
289
-
290
-
291
-
292
- const dedent = (text: string) => {
293
- const lines = text.split('\n')
294
- // Find minimum indentation of non-empty lines
295
- let minIndent = Infinity
296
- for (const line of lines) {
297
- if (line.trim().length === 0) continue
298
- const leadingSpace = line.match(/^\s*/)?.[0].length || 0
299
- if (leadingSpace < minIndent) minIndent = leadingSpace
300
- }
301
-
302
- if (minIndent === Infinity || minIndent === 0) return text
303
-
304
- return lines.map(line => {
305
- if (line.trim().length === 0) return ''
306
- return line.substring(minIndent)
307
- }).join('\n')
308
- }
309
-
310
-
311
- const themeColor = computed(() => data.value?.theme_color || '#ef4444')
312
-
313
- // Calculate relative luminance to determine if color is light or dark
314
- const getLuminance = (hex: string): number => {
315
- const match = hex.replace('#', '').match(/.{2}/g)
316
- if (!match) return 0
317
- const [r, g, b] = match.map(x => {
318
- const c = parseInt(x, 16) / 255
319
- return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
320
- })
321
- return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (b ?? 0)
322
- }
323
-
324
- // Auto text color: dark text on light bg, white text on dark bg
325
- const headerTextColor = computed(() => {
326
- const luminance = getLuminance(themeColor.value)
327
- return luminance > 0.4 ? '#1f2937' : '#ffffff' // gray-800 or white
328
- })
329
-
330
-
331
-
332
- const getIconPath = (stage?: any): string => {
333
- if (!stage) return iconOpenai
334
- const model = (stage.model || '').toLowerCase()
335
- const provider = (stage.provider || '').toLowerCase()
336
-
337
- // Match to imported icons
338
- if (model.includes('gpt') || model.includes('o1') || provider.includes('openai')) return iconOpenai
339
- if (model.includes('gemini') || provider.includes('google')) return iconGemini
340
- if (model.includes('claude') || provider.includes('anthropic')) return iconAnthropic
341
- if (model.includes('deepseek') || provider.includes('deepseek')) return iconDeepseek
342
- if (model.includes('qwen') || provider.includes('qwen') || provider.includes('alibaba')) return iconQwen
343
- if (model.includes('mistral') || provider.includes('mistral')) return iconMistral
344
- if (model.includes('grok') || provider.includes('xai')) return iconGrok
345
- if (model.includes('huggingface')) return iconHuggingface
346
- if (model.includes('cerebras')) return iconCerebras
347
- if (model.includes('minimax')) return iconMinimax
348
- if (model.includes('perplexity')) return iconPerplexity
349
- if (model.includes('nvidia')) return iconNvidia
350
- if (model.includes('phi') || provider.includes('microsoft')) return iconMicrosoft
351
- if (model.includes('xiaomi') || model.includes('mimo')) return iconXiaomi
352
-
353
- return iconOpenrouter // Default fallback
354
- }
355
-
356
-
357
- const themeStyle = computed(() => ({
358
- '--theme-color': themeColor.value,
359
- '--header-text-color': headerTextColor.value,
360
- '--text-primary': '#2c2c2e', // Warm dark gray for headings (Apple HIG inspired)
361
- '--text-body': '#3a3a3c', // Softer reading color for body text
362
- '--text-muted': '#86868b', // Lighter muted secondary text (updated from #636366)
363
- '--border-color': '#e5e7eb', // gray-200, for borders
364
- '--bg-subtle': '#f9fafb' // gray-50, for subtle backgrounds
365
- }))
366
-
367
-
368
- const parsedSections = computed(() => {
369
- const md = reorderedData.value.markdown || ''
370
- if (!md) return []
371
-
372
- let content = md.replace(/^#\s+.+$/m, '')
373
- content = content.replace(/(?:^|\n)\s*(?:#{1,3}|\*\*)\s*(?:References|Citations|Sources)[\s\S]*$/i, '')
374
- content = content.trim()
375
-
376
- const sections: Array<{ type: 'markdown' | 'card', content: string, title?: string, contentType?: 'table' | 'code' | 'summary', language?: string }> = []
377
-
378
- // Combine regex involves complexity, so we'll use a tokenizer approach
379
- // split tokens by Code Block or Table
380
- // split tokens by Code Block or Table or Summary
381
- const combinedRegex = /(```[\s\S]*?```|((?:^|\n)\|[^\n]*\|(?:\n\|[^\n]*\|)*)|<summary>[\s\S]*?<\/summary>)/
382
-
383
- let remaining = content
384
-
385
- while (remaining) {
386
- const match = remaining.match(combinedRegex)
387
- if (!match) {
388
- if (remaining.trim()) {
389
- sections.push({ type: 'markdown', content: remaining.trim() })
390
- }
391
- break
392
- }
393
-
394
- const index = match.index!
395
- const matchedStr = match[0]
396
- const preText = remaining.substring(0, index)
397
-
398
- if (preText.trim()) {
399
- sections.push({ type: 'markdown', content: preText.trim() })
400
- }
401
-
402
- // Determine type
403
- const isCode = matchedStr.startsWith('```')
404
- const isSummary = matchedStr.startsWith('<summary>')
405
- // Tables might match with a leading newline, trim it for checking but render carefully
406
- const isTable = !isCode && !isSummary && matchedStr.trim().startsWith('|')
407
-
408
- if (isCode || isTable || isSummary) {
409
- let language = ''
410
- let content = matchedStr.trim()
411
-
412
- if (isCode) {
413
- const match = matchedStr.match(/^```(\w+)/)
414
- if (match && match[1]) language = match[1]
415
- } else if (isSummary) {
416
- // Strip tags
417
- content = content.replace(/^<summary>/, '').replace(/<\/summary>$/, '')
418
- content = dedent(content)
419
- }
420
-
421
- sections.push({
422
- type: 'card',
423
- title: isCode ? 'Code' : (isSummary ? 'Summary' : 'Table'),
424
- content: content,
425
- contentType: isCode ? 'code' : (isSummary ? 'summary' : 'table'),
426
- language: language
427
- })
428
- } else {
429
- // Should not happen if regex is correct, but safe fallback
430
- sections.push({ type: 'markdown', content: matchedStr })
431
- }
432
-
433
- remaining = remaining.substring(index + matchedStr.length)
434
- }
435
-
436
- return sections
437
- })
438
-
439
- onMounted(() => {
440
- if (window.RENDER_DATA && Object.keys(window.RENDER_DATA).length > 0) {
441
- data.value = window.RENDER_DATA
442
- } else {
443
- // Demo data for development preview
444
- data.value = {
445
- markdown: `# Entari Headless Browser System
446
- This interface demonstrates the capabilities of the Entari Headless Browser system. It generates high-resolution, pixel-perfect captures of AI interactions [1].
447
-
448
- <summary>
449
- The system renders Markdown, Code, Tables, and complex UI layouts using a headless browser, optimized for archiving and sharing AI logic flows. This summary block highlights key information [2].
450
- </summary>
451
-
452
- ## Component Showcase
453
-
454
- ### Code Highlighting
455
- \`\`\`python
456
- class EntariBrowser:
457
- def capture(self, url: str) -> bytes:
458
- """Captures a screenshot of the given URL."""
459
- return self.driver.get_screenshot_as_png()
460
- \`\`\`
461
-
462
- ### Data Tables
463
- | Feature | Status | Priority |
464
- | :--- | :--- | :--- |
465
- | Markdown | ✅ Supported | High |
466
- | Syntax Highlight | ✅ Supported | Medium |
467
- | Tables | ✅ Supported | Low |
468
-
469
- ## Citation Handling
470
- The system automatically handles citations like [1] and [2], reordering them dynamically to match the flow.`,
471
- total_time: 1.5,
472
- stages: [
473
- {
474
- name: 'instruct',
475
- model: 'entari/demo-v1',
476
- provider: 'Entari',
477
- time: 0.8,
478
- cost: 0.001,
479
- },
480
- {
481
- name: 'summary',
482
- model: 'entari/summary-v1',
483
- provider: 'Entari',
484
- time: 0.7,
485
- cost: 0.0005,
486
- }
487
- ],
488
- references: [
489
- {
490
- title: 'Entari Project Documentation',
491
- url: 'https://github.com/entari/docs',
492
- snippet: 'Official documentation for Entari framework...'
493
- },
494
- {
495
- title: 'Headless Browser Concepts',
496
- url: 'https://en.wikipedia.org/wiki/Headless_browser',
497
- snippet: 'A **headless browser** is a web browser without a graphical user interface.'
498
- }
499
- ],
500
- page_references: [
501
- {
502
- title: 'Vue.js Framework',
503
- url: 'https://vuejs.org/',
504
- snippet: 'The Progressive JavaScript Framework. Approachable, Performant, and Versatile.'
505
- }
506
- ],
507
- image_references: [],
508
- stats: { total_time: 1.5 },
509
- theme_color: '#ef4444'
510
- }
511
- }
512
- })
513
- </script>
514
-
515
- <template>
516
- <div id="app-wrapper" class="min-h-screen w-full flex justify-center bg-[#f2f2f2]" :style="themeStyle">
517
- <!--
518
- Container Scaling:
519
- Width: 560px
520
- Zoom: 1.5
521
- Resulting Visual Width: 840px
522
- -->
523
- <div class="origin-top my-10" :style="{ zoom: 1.5 }">
524
- <div id="main-container" class="w-[560px] min-h-[500px] px-8 py-10 space-y-6 bg-[#f2f2f2]" data-theme="light">
525
-
526
- <!-- Title -->
527
- <header v-if="mainTitle" class="mb-6">
528
- <h1 class="text-[32px] font-black leading-tight tracking-tighter uppercase tabular-nums" style="color: var(--text-primary)" v-html="processedTitle"></h1>
529
- </header>
530
-
531
- <!-- Content Sections -->
532
- <template v-for="(section, idx) in parsedSections" :key="idx">
533
-
534
- <!-- Standard Markdown -->
535
- <div v-if="section.type === 'markdown'">
536
- <MarkdownContent
537
- :markdown="section.content"
538
- :num-search-refs="numSearchRefs"
539
- :num-page-refs="numPageRefs"
540
- class="prose-h2:text-[22px] prose-h2:font-black prose-h2:uppercase prose-h2:tracking-tight prose-h2:mb-4 prose-h2:text-gray-800"
541
- />
542
- </div>
543
-
544
- <!-- Special Card (Table/Code/Summary) -->
545
- <div v-else-if="section.type === 'card'" class="relative">
546
- <!-- Corner Rectangle Badge with Icon and Label -->
547
- <div
548
- class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center justify-center gap-1.5"
549
- :style="{ backgroundColor: themeColor, color: headerTextColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.15)' }"
550
- >
551
- <Icon :icon="getCardIcon(section.contentType)" class="text-[14px]" />
552
- <span class="text-[12px] font-bold uppercase tracking-wide">{{ getCardLabel(section.contentType, section.language) }}</span>
553
- </div>
554
- <div
555
- class="shadow-sm shadow-black/5 bg-white"
556
- :class="[
557
- section.contentType === 'summary' ? 'pt-8 px-5 pb-4 text-base leading-relaxed text-justify break-words' : '',
558
- section.contentType === 'code' ? 'pt-7 pb-2' : '',
559
- section.contentType === 'table' ? 'pt-5' : ''
560
- ]"
561
- >
562
- <MarkdownContent
563
- :markdown="section.content"
564
- :bare="true"
565
- :num-search-refs="numSearchRefs"
566
- :num-page-refs="numPageRefs"
567
- />
568
- </div>
569
- </div>
570
-
571
- </template>
572
-
573
- <!-- Sources Section (Bibliography) - Styled as Card -->
574
- <div v-if="referencesList.length" class="relative group/sources">
575
- <!-- Corner Rectangle Badge -->
576
- <div
577
- class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center justify-center gap-1.5"
578
- :style="{ backgroundColor: themeColor, color: headerTextColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.15)' }"
579
- >
580
- <Icon icon="mdi:book-open-page-variant-outline" class="text-[14px]" />
581
- <span class="text-[12px] font-bold uppercase tracking-wide">Sources</span>
582
- </div>
583
-
584
- <div class="shadow-sm shadow-black/5 bg-white pt-10 px-5 pb-6 space-y-6">
585
- <div v-for="ref in referencesList" :key="ref.url" class="group/item flex items-start gap-3 pl-0.5">
586
- <!-- Number -->
587
- <div class="shrink-0 w-5 h-5 text-[14px] font-bold flex items-center justify-center pt-0.5"
588
- :style="{ color: themeColor }">
589
- {{ ref.original_idx }}
590
- </div>
591
-
592
- <!-- Content -->
593
- <div class="flex-1 min-w-0">
594
- <!-- Title -->
595
- <a :href="ref.url" target="_blank" class="block mb-0.5">
596
- <div class="text-[16px] font-bold leading-tight group-hover/item:text-[var(--theme-color)] transition-colors" style="color: var(--text-primary)">
597
- {{ ref.title }}
598
- </div>
599
- </a>
600
-
601
- <!-- Domain & Favicon -->
602
- <div class="flex items-center gap-2.5 text-[10px] font-mono mb-2" style="color: var(--text-muted)">
603
- <img :src="getFavicon(ref.url)" class="w-3 h-3 object-contain rounded-sm">
604
- <span>{{ getDomain(ref.url) }}</span>
605
- </div>
606
-
607
- <!-- Snippet / Screenshot (Condition: Must have snippet or raw screenshot) -->
608
- <div v-if="ref.raw_screenshot_b64 || ref.snippet"
609
- class="mt-1.5 pl-3 py-0.5"
610
- :class="[(ref.is_fetched || ref.type === 'page') ? 'border-l-[3px]' : 'border-l-2 border-transparent']"
611
- :style="(ref.is_fetched || ref.type === 'page') ? { borderColor: themeColor } : {}"
612
- >
613
- <!-- Real page screenshot if available -->
614
- <img v-if="ref.raw_screenshot_b64"
615
- :src="getImageUrl(ref.raw_screenshot_b64)"
616
- class="max-w-full h-auto rounded-sm border border-gray-200 shadow-sm"
617
- alt="Page preview"
618
- />
619
- <!-- Fallback to markdown snippet -->
620
- <MarkdownContent v-else
621
- :markdown="ref.snippet"
622
- :bare="true"
623
- :compact="true"
624
- />
625
- </div>
626
- </div>
627
- </div>
628
- </div>
629
- </div>
630
-
631
- <!-- Gallery Section (Extracted Images) - Masonry Layout -->
632
- <div v-if="galleryImages.length" class="relative group/gallery mb-8">
633
- <!-- Corner Badge -->
634
- <div
635
- class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center justify-center gap-1.5"
636
- :style="{ backgroundColor: themeColor, color: headerTextColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.15)' }"
637
- >
638
- <Icon icon="mdi:image-multiple-outline" class="text-[14px]" />
639
- <span class="text-[12px] font-bold uppercase tracking-wide">Gallery</span>
640
- </div>
641
-
642
- <div class="shadow-sm shadow-black/5 bg-white pt-10 px-6 pb-6">
643
- <!-- Masonry Layout: 2 Columns -->
644
- <div class="columns-2 gap-4 space-y-4">
645
- <div v-for="(img, idx) in galleryImages" :key="idx" class="break-inside-avoid relative rounded-sm overflow-hidden border border-gray-100 bg-gray-50">
646
- <img
647
- :src="getImageUrl(img)"
648
- class="w-full h-auto block object-cover transform hover:scale-105 transition-transform duration-500"
649
- loading="lazy"
650
- />
651
- </div>
652
- </div>
653
- </div>
654
- </div>
655
-
656
- <!-- Flow: Unified Stage Info Area -->
657
- <div v-if="instructStage || summaryStage" class="relative group/flow">
658
- <!-- Corner Badge -->
659
- <div
660
- class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center justify-center gap-1.5"
661
- :style="{ backgroundColor: themeColor, color: headerTextColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.15)' }"
662
- >
663
- <Icon icon="mdi:sitemap-outline" class="text-[14px]" />
664
- <span class="text-[12px] font-bold uppercase tracking-wide">Flow</span>
665
- </div>
666
-
667
- <!-- Flow Content (Timeline Style) -->
668
- <div class="shadow-sm shadow-black/5 bg-white pt-8 px-6 pb-8">
669
- <div class="space-y-8 relative">
670
-
671
- <!-- Instruct Stage -->
672
- <div v-if="instructStage" class="relative flex items-start gap-4 z-10 w-full">
673
- <!-- Node: Brand Logo -->
674
- <div class="shrink-0 w-6 h-6 flex items-center justify-center bg-white">
675
- <img :src="getIconPath(instructStage)" class="w-5 h-5 object-contain" alt="" />
676
- </div>
677
- <!-- Content -->
678
- <div class="flex-1 min-w-0 pt-1">
679
- <div class="text-[17px] font-bold uppercase tracking-tight mb-1.5 leading-none" style="color: var(--text-primary)">Instruct</div>
680
- <div class="flex items-center justify-between gap-x-4 text-[13px] font-mono leading-tight w-full" style="color: var(--text-muted)">
681
- <span class="truncate max-w-[180px]" :title="instructStage.model">{{ instructStage.model }}</span>
682
-
683
- <div class="flex items-center gap-4 shrink-0">
684
- <div class="flex items-center gap-1.5 opacity-80">
685
- <Icon icon="mdi:clock-outline" class="text-[13px]" />
686
- <span>{{ (instructStage.time || 0).toFixed(2) }}s</span>
687
- </div>
688
- <template v-if="instructStage.cost">
689
- <div class="flex items-center gap-0.5 opacity-80">
690
- <span>${{ instructStage.cost.toFixed(5) }}</span>
691
- </div>
692
- </template>
693
- </div>
694
- </div>
695
- </div>
696
- </div>
697
-
698
- <!-- Summary Stage -->
699
- <div v-if="summaryStage" class="relative flex items-start gap-4 z-10 w-full">
700
- <!-- Node: Brand Logo -->
701
- <div class="shrink-0 w-6 h-6 flex items-center justify-center bg-white">
702
- <img :src="getIconPath(summaryStage)" class="w-5 h-5 object-contain" alt="" />
703
- </div>
704
- <!-- Content -->
705
- <div class="flex-1 min-w-0 pt-1">
706
- <div class="text-[17px] font-bold uppercase tracking-tight mb-1.5 leading-none" style="color: var(--text-primary)">Summary</div>
707
- <div class="flex items-center justify-between gap-x-4 text-[13px] font-mono leading-tight w-full" style="color: var(--text-muted)">
708
- <span class="truncate max-w-[180px]" :title="summaryStage.model">{{ summaryStage.model }}</span>
709
-
710
- <div class="flex items-center gap-4 shrink-0">
711
- <div class="flex items-center gap-1.5 opacity-80">
712
- <Icon icon="mdi:clock-outline" class="text-[13px]" />
713
- <span>{{ summaryStage.time?.toFixed(2) }}s</span>
714
- </div>
715
- <template v-if="summaryStage.cost">
716
- <div class="flex items-center gap-0.5 opacity-80">
717
- <span>${{ summaryStage.cost.toFixed(5) }}</span>
718
- </div>
719
- </template>
720
- </div>
721
- </div>
722
- </div>
723
- </div>
724
-
725
- </div>
726
- </div>
727
- </div>
728
-
729
- </div>
730
- </div>
731
- </div>
732
- </template>
733
-
734
- <style>
735
- /* Global background fix to prevent white bottom strip */
736
- :root, html, body {
737
- background-color: #f2f2f2 !important;
738
- margin: 0;
739
- padding: 0;
740
- overflow: hidden !important; /* Force hide scrollbars on root */
741
- scrollbar-width: none; /* Firefox */
742
- -ms-overflow-style: none; /* IE and Edge */
743
- }
744
-
745
- /* Hide scrollbars for all elements */
746
- *::-webkit-scrollbar {
747
- display: none !important;
748
- width: 0 !important;
749
- height: 0 !important;
750
- }
751
-
752
- * {
753
- scrollbar-width: none !important;
754
- -ms-overflow-style: none !important;
755
- }
756
- </style>