entari-plugin-hyw 4.0.0rc4__py3-none-any.whl → 4.0.0rc6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of entari-plugin-hyw might be problematic. Click here for more details.

Files changed (30) hide show
  1. entari_plugin_hyw/__init__.py +216 -75
  2. entari_plugin_hyw/assets/card-dist/index.html +70 -79
  3. entari_plugin_hyw/browser/__init__.py +10 -0
  4. entari_plugin_hyw/browser/engines/base.py +13 -0
  5. entari_plugin_hyw/browser/engines/bing.py +95 -0
  6. entari_plugin_hyw/browser/engines/duckduckgo.py +137 -0
  7. entari_plugin_hyw/browser/engines/google.py +155 -0
  8. entari_plugin_hyw/browser/landing.html +172 -0
  9. entari_plugin_hyw/browser/manager.py +153 -0
  10. entari_plugin_hyw/browser/service.py +304 -0
  11. entari_plugin_hyw/card-ui/src/App.vue +526 -182
  12. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +7 -11
  13. entari_plugin_hyw/card-ui/src/components/StageCard.vue +33 -30
  14. entari_plugin_hyw/card-ui/src/types.ts +9 -0
  15. entari_plugin_hyw/definitions.py +155 -0
  16. entari_plugin_hyw/history.py +111 -33
  17. entari_plugin_hyw/misc.py +34 -0
  18. entari_plugin_hyw/modular_pipeline.py +384 -0
  19. entari_plugin_hyw/render_vue.py +326 -239
  20. entari_plugin_hyw/search.py +95 -708
  21. entari_plugin_hyw/stage_base.py +92 -0
  22. entari_plugin_hyw/stage_instruct.py +345 -0
  23. entari_plugin_hyw/stage_instruct_deepsearch.py +104 -0
  24. entari_plugin_hyw/stage_summary.py +164 -0
  25. {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/METADATA +4 -4
  26. {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/RECORD +28 -16
  27. entari_plugin_hyw/pipeline.py +0 -1219
  28. entari_plugin_hyw/prompts.py +0 -47
  29. {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/WHEEL +0 -0
  30. {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/top_level.txt +0 -0
@@ -2,20 +2,38 @@
2
2
  import { ref, computed, onMounted } from 'vue'
3
3
  import { Icon } from '@iconify/vue'
4
4
 
5
- import type { RenderData } from './types'
6
- import StageCard from './components/StageCard.vue'
5
+ import type { RenderData, Reference } from './types'
7
6
  import MarkdownContent from './components/MarkdownContent.vue'
8
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
+
9
25
  // Get icon for card type
10
26
  const getCardIcon = (contentType?: string): string => {
11
27
  switch (contentType) {
12
- case 'summary': return 'mdi:file-document-outline'
28
+ case 'summary': return 'mdi:text-box-outline'
13
29
  case 'code': return 'mdi:code-braces'
14
30
  case 'table': return 'mdi:table'
15
31
  default: return 'mdi:card-outline'
16
32
  }
17
33
  }
18
34
 
35
+
36
+
19
37
  // Get display label for card
20
38
  const getCardLabel = (contentType?: string, language?: string): string => {
21
39
  switch (contentType) {
@@ -43,34 +61,85 @@ window.updateRenderData = (newData: RenderData) => {
43
61
  const numSearchRefs = computed(() => data.value?.references?.length || 0)
44
62
  const numPageRefs = computed(() => data.value?.page_references?.length || 0)
45
63
 
46
- // Calculate the reference offset for each stage (for unified badge numbering)
47
- const getRefOffset = (stageIndex: number): number => {
48
- if (!data.value?.stages) return 0
49
- let offset = 0
50
- for (let i = 0; i < stageIndex; i++) {
51
- const stage = data.value.stages[i]
52
- if (stage) {
53
- offset += (stage.references?.length || 0) + (stage.crawled_pages?.length || 0)
54
- }
55
- }
56
- return offset
57
- }
58
64
 
59
65
  // Helper: Strips content before the first H1 heading (e.g., AI "thought" prefixes)
60
66
  const stripPrefixBeforeH1 = (text: string): string => {
61
67
  // Find the first line starting with "# " (H1)
62
68
  const h1Match = text.match(/^#\s+/m)
69
+ const summaryMatch = text.match(/<summary>/)
70
+
71
+ let startIndex = -1
72
+
63
73
  if (h1Match && h1Match.index !== undefined) {
64
- // If found, return everything starting from that H1
65
- // This effectively discards any "thought" blocks or "### ASSISTANT" prefixes appearing before it.
66
- return text.substring(h1Match.index)
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
+ }
67
82
  }
83
+
84
+ if (startIndex !== -1) {
85
+ return text.substring(startIndex)
86
+ }
87
+
68
88
  // If no H1 found, return text as-is (fallback)
69
89
  return text
70
90
  }
71
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
+
72
141
  const mainTitle = computed(() => {
73
- const md = stripPrefixBeforeH1(data.value?.markdown || '')
142
+ const md = reorderedData.value.markdown || ''
74
143
  const match = md.match(/^#\s+(.+)$/m)
75
144
  return match && match[1] ? match[1].trim() : ''
76
145
  })
@@ -82,6 +151,141 @@ const processedTitle = computed(() => {
82
151
  })
83
152
  })
84
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
+
85
289
 
86
290
 
87
291
 
@@ -123,24 +327,47 @@ const headerTextColor = computed(() => {
123
327
  return luminance > 0.4 ? '#1f2937' : '#ffffff' // gray-800 or white
124
328
  })
125
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
+
126
357
  const themeStyle = computed(() => ({
127
358
  '--theme-color': themeColor.value,
128
359
  '--header-text-color': headerTextColor.value,
129
360
  '--text-primary': '#2c2c2e', // Warm dark gray for headings (Apple HIG inspired)
130
361
  '--text-body': '#3a3a3c', // Softer reading color for body text
131
- '--text-muted': '#636366', // Muted secondary text
362
+ '--text-muted': '#86868b', // Lighter muted secondary text (updated from #636366)
132
363
  '--border-color': '#e5e7eb', // gray-200, for borders
133
364
  '--bg-subtle': '#f9fafb' // gray-50, for subtle backgrounds
134
365
  }))
135
366
 
136
367
 
137
368
  const parsedSections = computed(() => {
138
- const rawMd = data.value?.markdown || ''
139
- if (!rawMd) return []
140
-
141
- // Robustness: Strip any content (AI thoughts, system role prefixes) before the first H1 heading
142
- // User request: "Match the first big header, ignore what comes before it" ("匹配第一个大标题 无视前面的")
143
- const md = stripPrefixBeforeH1(rawMd)
369
+ const md = reorderedData.value.markdown || ''
370
+ if (!md) return []
144
371
 
145
372
  let content = md.replace(/^#\s+.+$/m, '')
146
373
  content = content.replace(/(?:^|\n)\s*(?:#{1,3}|\*\*)\s*(?:References|Citations|Sources)[\s\S]*$/i, '')
@@ -215,114 +442,70 @@ onMounted(() => {
215
442
  } else {
216
443
  // Demo data for development preview
217
444
  data.value = {
218
- markdown: `# 终极硬核整合包格雷科技新视野
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].
219
447
 
220
448
  <summary>
221
- 《格雷科技:新视野》(GregTech: New Horizons,简称 GTNH)是一款基于 Minecraft 1.7.10 版本的深度硬核科技向整合包。它以 GregTech 5 Unofficial 为核心,通过超过 8 年的持续开发,将 300 多个模组深度集成,构建了极其严苛且逻辑严密的科技树,是公认的生存挑战巅峰之作。
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].
222
450
  </summary>
223
451
 
224
- ## 核心机制与游戏体验
225
- GTNH 的核心在于"格雷化"改造,几乎所有模组的合成表都经过重新设计,以匹配其严苛的阶级制度 [4][8]。玩家需要从原始的石器时代开始,历经蒸汽时代、电力时代,最终向星际航行迈进。其游戏过程极其漫长,旨在让玩家在每一毫秒的进度中感受工业发展的成就感 [3][7]。
226
-
227
- ![GTNH 游戏场景](https://i.ytimg.com/vi/5T-oSWAgaMM/maxresdefault.jpg)
228
-
229
- ## 科技阶层与任务系统
230
- 整合包拥有 15 个清晰的科技等级(Tiers),最终目标是建造"星门"(Stargate)[2]。为了引导玩家不迷失在复杂的工业流程中,GTNH 内置了超过 3900 条任务的巨型任务书,涵盖了从基础生存到高阶多方块结构的详细指导 [4][7]。
231
-
232
- - 15 个科技等级
233
- - 任务数量:3900+
234
- - 最终目标:建造"星门"
235
-
236
- > 机动战士高达系列是日本动画史上最具影响力的动画作品之一,深受全球观众的喜爱。
237
-
238
- | 特性 | 详细描述 |
239
- | :--- | :--- |
240
- | **基础版本** | Minecraft 1.7.10 (高度优化) |
241
- | **任务数量** | 3900+ 任务引导 [7] |
242
- | **科技阶层** | 15 个技术等级 [2] |
243
- | **核心模组** | GregTech 5 Unofficial, Thaumcraft 等 [8] |
244
-
245
- ## 安装与运行建议
246
- 由于其高度集成的特性,官方强烈建议使用 **Prism Launcher** 进行安装和管理 [5]。在运行环境方面,虽然基于旧版 MC,但通过社区努力,目前推荐使用 **Java 17-25** 版本以获得最佳的内存管理和性能优化,确保大型自动化工厂运行流畅 [5]。
247
-
248
- \`\`\`bash
249
- curl -s https://raw.githubusercontent.com/GTNewHorizons/GT-New-Horizons-Modpack/master/README.md
250
- java -version
251
- java -Xmx1024M -Xms1024M -jar prism-launcher.jar
252
- \`\`\``,
253
- total_time: 8.5,
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,
254
472
  stages: [
255
473
  {
256
474
  name: 'instruct',
257
- model: 'qwen/qwen3-235b-a22b-2507',
258
- provider: 'Qwen',
259
- time: 1.83,
260
- cost: 0.0002,
261
- },
262
- {
263
- name: 'search',
264
- model: '',
265
- provider: '',
266
- time: 0.5,
267
- cost: 0.0,
268
- references: [
269
- { title: 'GTNH 2025 Server Information', url: 'https://stonelegion.com/mc-gtnh-2026/' },
270
- { title: 'GT New Horizons Wiki', url: 'https://gtnh.miraheze.org/wiki/Main_Page' },
271
- { title: 'GT New Horizons - GitHub', url: 'https://github.com/GTNewHorizons/GT-New-Horizons-Modpack' },
272
- { title: 'GT New Horizons - CurseForge', url: 'https://www.curseforge.com/minecraft/modpacks/gt-new-horizons' },
273
- { title: 'Installing and Migrating - GTNH', url: 'https://gtnh.miraheze.org/wiki/Installing_and_Migrating' },
274
- { title: 'Modlist - GT New Horizons', url: 'https://wiki.gtnewhorizons.com/wiki/Modlist' },
275
- { title: 'GregTech: New Horizons - Home', url: 'https://www.gtnewhorizons.com/' },
276
- { title: 'GT New Horizons - FTB Wiki', url: 'https://ftb.fandom.com/wiki/GT_New_Horizons' }
277
- ],
278
- image_references: [
279
- { title: 'GTNH Live Lets Play', url: 'https://i.ytimg.com/vi/5T-oSWAgaMM/maxresdefault.jpg', thumbnail: 'https://tse4.mm.bing.net/th/id/OIP.b_56VnY4nyrzeqp1JetmFQHaEK?pid=Api' },
280
- { title: 'GTNH Modpack Cover', url: 'https://i.mcmod.cn/modpack/cover/20240113/1705139595_29797_dSkE.jpg', thumbnail: 'https://tse1.mm.bing.net/th/id/OIP.KNKaZX1d_4Ueq6vpl1qJNAHaEo?pid=Api' },
281
- { title: 'GTNH Steam Age', url: 'https://i.ytimg.com/vi/8IPwXxqB71w/maxresdefault.jpg', thumbnail: 'https://tse4.mm.bing.net/th/id/OIP.P-KrnI4GBH21yPgwpNPSzAHaEK?pid=Api' },
282
- { title: 'GTNH MCMod Cover', url: 'https://i.mcmod.cn/post/cover/20230201/1675241030_2_VqDc.jpg', thumbnail: 'https://tse2.mm.bing.net/th/id/OIP.GvYz7YWrg-fnpAHjOiW3OAHaEo?pid=Api' },
283
- { title: 'GTNH Tectech Tutorial', url: 'http://i0.hdslb.com/bfs/archive/1ed1e53341fd44018138f2823b2fe6c499fb9c9c.jpg', thumbnail: 'https://tse4.mm.bing.net/th/id/OIP.0Wg7xFHTjhxIV9hKuUo4xwHaEo?pid=Api' }
284
- ]
285
- },
286
- {
287
- name: 'crawler',
288
- model: '',
289
- provider: '',
290
- time: 2.5,
291
- cost: 0.0,
292
- crawled_pages: [
293
- { title: 'GregTech: New Horizons Official Wiki', url: 'https://gtnh.miraheze.org/wiki/Main_Page' },
294
- { title: 'GT New Horizons Modpack Download', url: 'https://www.curseforge.com/minecraft/modpacks/gt-new-horizons' },
295
- { title: 'Installing and Migrating Guide', url: 'https://gtnh.miraheze.org/wiki/Installing_and_Migrating' }
296
- ]
475
+ model: 'entari/demo-v1',
476
+ provider: 'Entari',
477
+ time: 0.8,
478
+ cost: 0.001,
297
479
  },
298
480
  {
299
- name: 'agent',
300
- model: 'google/gemini-3-flash-preview',
301
- provider: 'Google',
302
- time: 13.0,
303
- cost: 0.0018,
481
+ name: 'summary',
482
+ model: 'entari/summary-v1',
483
+ provider: 'Entari',
484
+ time: 0.7,
485
+ cost: 0.0005,
304
486
  }
305
487
  ],
306
488
  references: [
307
- { title: 'GTNH 2025 Server Information', url: 'https://stonelegion.com/mc-gtnh-2026/' },
308
- { title: 'GT New Horizons Wiki', url: 'https://gtnh.miraheze.org/wiki/Main_Page' },
309
- { title: 'GT New Horizons - GitHub', url: 'https://github.com/GTNewHorizons/GT-New-Horizons-Modpack' },
310
- { title: 'GT New Horizons - CurseForge', url: 'https://www.curseforge.com/minecraft/modpacks/gt-new-horizons' },
311
- { title: 'Installing and Migrating - GTNH', url: 'https://gtnh.miraheze.org/wiki/Installing_and_Migrating' },
312
- { title: 'Modlist - GT New Horizons', url: 'https://wiki.gtnewhorizons.com/wiki/Modlist' },
313
- { title: 'GregTech: New Horizons - Home', url: 'https://www.gtnewhorizons.com/' },
314
- { title: 'GT New Horizons - FTB Wiki', url: 'https://ftb.fandom.com/wiki/GT_New_Horizons' }
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
+ }
315
499
  ],
316
500
  page_references: [
317
- { title: 'GregTech: New Horizons Official Wiki', url: 'https://gtnh.miraheze.org/wiki/Main_Page' },
318
- { title: 'GT New Horizons Modpack Download', url: 'https://www.curseforge.com/minecraft/modpacks/gt-new-horizons' }
319
- ],
320
- image_references: [
321
- { title: 'GTNH Live Lets Play', url: 'https://i.ytimg.com/vi/5T-oSWAgaMM/maxresdefault.jpg', thumbnail: 'https://tse4.mm.bing.net/th/id/OIP.b_56VnY4nyrzeqp1JetmFQHaEK?pid=Api' },
322
- { title: 'GTNH Modpack Cover', url: 'https://i.mcmod.cn/modpack/cover/20240113/1705139595_29797_dSkE.jpg', thumbnail: 'https://tse1.mm.bing.net/th/id/OIP.KNKaZX1d_4Ueq6vpl1qJNAHaEo?pid=Api' },
323
- { title: 'GTNH Steam Age', url: 'https://i.ytimg.com/vi/8IPwXxqB71w/maxresdefault.jpg', thumbnail: 'https://tse4.mm.bing.net/th/id/OIP.P-KrnI4GBH21yPgwpNPSzAHaEK?pid=Api' }
501
+ {
502
+ title: 'Vue.js Framework',
503
+ url: 'https://vuejs.org/',
504
+ snippet: 'The Progressive JavaScript Framework. Approachable, Performant, and Versatile.'
505
+ }
324
506
  ],
325
- stats: { total_time: 8.5 },
507
+ image_references: [],
508
+ stats: { total_time: 1.5 },
326
509
  theme_color: '#ef4444'
327
510
  }
328
511
  }
@@ -330,83 +513,244 @@ java -Xmx1024M -Xms1024M -jar prism-launcher.jar
330
513
  </script>
331
514
 
332
515
  <template>
333
- <div class="bg-[#f2f2f2] flex justify-center" :style="themeStyle">
334
- <!-- Main container with explicit background for screenshot capture -->
335
- <div id="main-container" class="w-[540px] pt-16 pb-12 space-y-8 !bg-[#f2f2f2]" data-theme="light">
336
-
337
- <!-- Title -->
338
- <header v-if="mainTitle" class="px-6 mb-8">
339
- <!-- Removed Time/Icon Badge as requested -->
340
- <h1 class="text-4xl font-black leading-tight tracking-tighter uppercase tabular-nums" style="color: var(--text-primary)" v-html="processedTitle"></h1>
341
- </header>
342
-
343
- <!-- Content Sections -->
344
- <template v-for="(section, idx) in parsedSections" :key="idx">
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">
345
525
 
346
- <!-- Standard Markdown -->
347
- <div v-if="section.type === 'markdown'" class="px-7">
348
- <MarkdownContent
349
- :markdown="section.content"
350
- :num-search-refs="numSearchRefs"
351
- :num-page-refs="numPageRefs"
352
- class="prose-h2:text-[26px] prose-h2:font-black prose-h2:uppercase prose-h2:tracking-tight prose-h2:mb-4 prose-h2:text-gray-800"
353
- />
354
- </div>
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>
355
543
 
356
- <!-- Special Card (Table/Code/Summary) -->
357
- <div v-else-if="section.type === 'card'" class="mx-6 relative">
358
- <!-- Corner Rectangle Badge with Icon and Label -->
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 -->
359
576
  <div
360
- class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center gap-1.5"
577
+ class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center justify-center gap-1.5"
361
578
  :style="{ backgroundColor: themeColor, color: headerTextColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.15)' }"
362
579
  >
363
- <Icon :icon="getCardIcon(section.contentType)" class="text-base" />
364
- <span class="text-xs font-bold uppercase tracking-wide">{{ getCardLabel(section.contentType, section.language) }}</span>
580
+ <Icon icon="mdi:book-open-page-variant-outline" class="text-[14px]" />
581
+ <span class="text-[12px] font-bold uppercase tracking-wide">Sources</span>
365
582
  </div>
366
- <div
367
- class="shadow-sm shadow-black/10 bg-white"
368
- :class="[
369
- section.contentType === 'summary' ? 'pt-8 px-5 pb-3 text-base leading-relaxed' : '',
370
- section.contentType === 'code' ? 'pt-7 pb-2' : '',
371
- section.contentType === 'table' ? 'pt-5' : ''
372
- ]"
373
- >
374
- <MarkdownContent
375
- :markdown="section.content"
376
- :bare="true"
377
- :num-search-refs="numSearchRefs"
378
- :num-page-refs="numPageRefs"
379
- />
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>
380
628
  </div>
381
629
  </div>
382
630
 
383
- </template>
384
-
385
- <!-- Workflow -->
386
- <div v-if="data?.stages?.length" class="mx-6 relative">
387
- <!-- Corner Rectangle Badge with Icon and Label -->
388
- <div
389
- class="absolute -top-2 -left-2 h-7 px-2.5 z-10 flex items-center gap-1.5"
390
- :style="{ backgroundColor: themeColor, color: headerTextColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.15)' }"
391
- >
392
- <Icon icon="mdi:link-variant" class="text-base" />
393
- <span class="text-xs font-bold uppercase tracking-wide">Flow</span>
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>
394
654
  </div>
395
- <div class="p-2 pt-5 bg-white shadow-sm shadow-black/10">
396
- <StageCard
397
- v-for="(stage, index) in data.stages"
398
- :key="index"
399
- :stage="stage"
400
- :is-first="index === 0"
401
- :is-last="index === data.stages.length - 1"
402
- :prev-stage-name="index > 0 ? data.stages[index - 1]?.name : undefined"
403
- :ref-offset="getRefOffset(index)"
404
- />
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>
405
727
  </div>
406
- </div>
407
728
 
729
+ </div>
408
730
  </div>
409
731
  </div>
410
732
  </template>
411
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
+ }
412
751
 
752
+ * {
753
+ scrollbar-width: none !important;
754
+ -ms-overflow-style: none !important;
755
+ }
756
+ </style>