entari-plugin-hyw 4.0.0rc5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of entari-plugin-hyw might be problematic. Click here for more details.
- entari_plugin_hyw/__init__.py +532 -0
- entari_plugin_hyw/assets/card-dist/index.html +387 -0
- entari_plugin_hyw/assets/card-dist/logos/anthropic.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/cerebras.svg +9 -0
- entari_plugin_hyw/assets/card-dist/logos/deepseek.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/gemini.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/google.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/grok.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/huggingface.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/microsoft.svg +15 -0
- entari_plugin_hyw/assets/card-dist/logos/minimax.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/mistral.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/nvida.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/openai.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/openrouter.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/perplexity.svg +24 -0
- entari_plugin_hyw/assets/card-dist/logos/qwen.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/xai.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/xiaomi.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/zai.png +0 -0
- entari_plugin_hyw/assets/card-dist/vite.svg +1 -0
- entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
- entari_plugin_hyw/assets/icon/cerebras.svg +9 -0
- entari_plugin_hyw/assets/icon/deepseek.png +0 -0
- entari_plugin_hyw/assets/icon/gemini.svg +1 -0
- entari_plugin_hyw/assets/icon/google.svg +1 -0
- entari_plugin_hyw/assets/icon/grok.png +0 -0
- entari_plugin_hyw/assets/icon/huggingface.png +0 -0
- entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
- entari_plugin_hyw/assets/icon/minimax.png +0 -0
- entari_plugin_hyw/assets/icon/mistral.png +0 -0
- entari_plugin_hyw/assets/icon/nvida.png +0 -0
- entari_plugin_hyw/assets/icon/openai.svg +1 -0
- entari_plugin_hyw/assets/icon/openrouter.png +0 -0
- entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
- entari_plugin_hyw/assets/icon/qwen.png +0 -0
- entari_plugin_hyw/assets/icon/xai.png +0 -0
- entari_plugin_hyw/assets/icon/xiaomi.png +0 -0
- entari_plugin_hyw/assets/icon/zai.png +0 -0
- entari_plugin_hyw/browser/__init__.py +10 -0
- entari_plugin_hyw/browser/engines/base.py +13 -0
- entari_plugin_hyw/browser/engines/bing.py +95 -0
- entari_plugin_hyw/browser/engines/searxng.py +137 -0
- entari_plugin_hyw/browser/landing.html +172 -0
- entari_plugin_hyw/browser/manager.py +153 -0
- entari_plugin_hyw/browser/service.py +275 -0
- entari_plugin_hyw/card-ui/.gitignore +24 -0
- entari_plugin_hyw/card-ui/README.md +5 -0
- entari_plugin_hyw/card-ui/index.html +16 -0
- entari_plugin_hyw/card-ui/package-lock.json +2342 -0
- entari_plugin_hyw/card-ui/package.json +31 -0
- entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
- entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
- entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
- entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
- entari_plugin_hyw/card-ui/public/vite.svg +1 -0
- entari_plugin_hyw/card-ui/src/App.vue +756 -0
- entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
- entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +382 -0
- entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +240 -0
- entari_plugin_hyw/card-ui/src/main.ts +5 -0
- entari_plugin_hyw/card-ui/src/style.css +29 -0
- entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
- entari_plugin_hyw/card-ui/src/types.ts +61 -0
- entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
- entari_plugin_hyw/card-ui/tsconfig.json +7 -0
- entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
- entari_plugin_hyw/card-ui/vite.config.ts +16 -0
- entari_plugin_hyw/definitions.py +130 -0
- entari_plugin_hyw/history.py +248 -0
- entari_plugin_hyw/image_cache.py +274 -0
- entari_plugin_hyw/misc.py +135 -0
- entari_plugin_hyw/modular_pipeline.py +351 -0
- entari_plugin_hyw/render_vue.py +401 -0
- entari_plugin_hyw/search.py +116 -0
- entari_plugin_hyw/stage_base.py +88 -0
- entari_plugin_hyw/stage_instruct.py +328 -0
- entari_plugin_hyw/stage_instruct_review.py +92 -0
- entari_plugin_hyw/stage_summary.py +164 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/METADATA +116 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/RECORD +99 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/WHEEL +5 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,756 @@
|
|
|
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>
|