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