mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): Enhanced output rendering system for agent runs (#10819)
## Summary Introduces a modular, extensible output renderer system supporting multiple content types (text, code, images, videos, JSON, markdown) for agent run outputs. The system includes smart clipboard operations, concatenated downloads, and rich markdown rendering with LaTeX math and video embedding support. ## Changes 🏗️ ### Core Output Rendering System - **Added extensible renderer architecture** (`output-renderers/types.ts`) - Plugin-based system with priority ordering - Registry pattern for automatic renderer selection - Support for custom metadata and MIME types ### Output Renderers - **TextRenderer**: Plain text with proper formatting and line breaks - **CodeRenderer**: Syntax-highlighted code blocks with language detection - **JSONRenderer**: Collapsible, formatted JSON with syntax highlighting - **ImageRenderer**: Image display with support for URLs, data URIs, and file uploads - **VideoRenderer**: Embedded video player for YouTube, Vimeo, and direct video files - **MarkdownRenderer**: Rich markdown with: - GitHub Flavored Markdown (tables, task lists, strikethrough) - LaTeX math rendering via KaTeX (inline `$...$` and display `$$...$$`) - Syntax highlighting via highlight.js - Video embedding (YouTube/Vimeo URLs auto-convert to embeds) - Clickable heading anchors - Dark mode support ### User Interface Components - **OutputItem**: Individual output display with renderer selection - **OutputActions**: Hover-based action buttons for: - Copy to clipboard with smart MIME type detection - Download with intelligent concatenation (text files merge, binaries separate) - Share functionality (placeholder for future implementation) - **AgentRunOutputView**: Main output view component with feature flag integration ### Clipboard & Download Features - Smart clipboard operations using native ClipboardItem API - MIME type detection and browser capability checking - Fallback strategies for unsupported content types - Concatenated downloads for text-based outputs - Individual downloads for binary content ### Feature Flag Integration - Added `ENABLE_ENHANCED_OUTPUT_HANDLING` flag to LaunchDarkly - Backwards compatible with existing output display - Graceful fallback for disabled feature flag ### Styling & UX - Max width constraints (950px card, 660px content) - Hover-based action buttons for clean interface - Dark mode support across all renderers - Responsive design for various content types - Loading states and error handling ## Test Plan 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: ### Test Scenarios: - [x] **Basic Output Rendering** - [x] Execute agent with text output - verify proper formatting - [x] Execute agent with JSON output - verify collapsible tree view - [x] Execute agent with code output - verify syntax highlighting - [x] **Rich Content** - [x] Test markdown rendering with headers, lists, tables - [x] Test LaTeX math expressions (inline and display) - [x] Test code blocks within markdown - [x] Test task lists and strikethrough - [x] **Media Handling** - [x] Upload and display PNG/JPEG images - [x] Test video URL embedding (YouTube/Vimeo) - [x] Test direct video file playback - [x] **Clipboard Operations** - [x] Copy plain text output - [x] Copy formatted code - [x] Copy JSON data - [x] Copy markdown content - [x] Verify fallback for unsupported MIME types - [x] **Download Functionality** - [x] Download single text output - [x] Download multiple text outputs (verify concatenation) - [x] Download mixed content (verify separate files) - [x] Download images and binary content - [x] **Feature Flag** - [x] Enable flag - verify enhanced rendering - [x] Disable flag - verify fallback to original view - [x] Check backwards compatibility - [x] **Edge Cases** - [x] Large JSON objects (performance) - [x] Very long text outputs - [x] Mixed content types in single run - [x] Malformed markdown - [x] Invalid video URLs ## Dependencies Added - `react-markdown` (9.0.3) - Already present - `remark-gfm` (4.0.1) - GitHub Flavored Markdown - `remark-math` (6.0.0) - LaTeX math support - `rehype-katex` (7.0.1) - Math rendering - `katex` (0.16.22) - Math typesetting - `rehype-highlight` (7.0.2) - Syntax highlighting - `highlight.js` (11.11.1) - Highlighting library - `rehype-slug` (6.0.0) - Heading anchors - `rehype-autolink-headings` (7.1.0) - Clickable headings ## Notes - Mermaid diagram support was attempted but removed due to compatibility issues - Share functionality is stubbed out for future implementation - PNG file upload rendering issue has logging in place for debugging - All components follow existing UI patterns and use Tailwind CSS ## Screenshots <img width="1656" height="1250" alt="image" src="https://github.com/user-attachments/assets/af7542fe-db89-4521-aaf5-19e33d48a409" /> ## Related Issues - Implements SECRT-1209 --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Nicholas Tindle <ntindle@users.noreply.github.com>
This commit is contained in:
@@ -66,7 +66,9 @@
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"framer-motion": "12.23.12",
|
||||
"geist": "1.4.2",
|
||||
"highlight.js": "11.11.1",
|
||||
"jaro-winkler": "0.2.8",
|
||||
"katex": "0.16.22",
|
||||
"launchdarkly-react-client-sdk": "3.8.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "0.539.0",
|
||||
@@ -86,6 +88,12 @@
|
||||
"react-shepherd": "6.1.9",
|
||||
"react-window": "1.8.11",
|
||||
"recharts": "2.15.3",
|
||||
"rehype-autolink-headings": "7.1.0",
|
||||
"rehype-highlight": "7.0.2",
|
||||
"rehype-katex": "7.0.1",
|
||||
"rehype-slug": "6.0.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"remark-math": "6.0.0",
|
||||
"shepherd.js": "14.5.1",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "2.6.0",
|
||||
|
||||
494
autogpt_platform/frontend/pnpm-lock.yaml
generated
494
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -131,9 +131,15 @@ importers:
|
||||
geist:
|
||||
specifier: 1.4.2
|
||||
version: 1.4.2(next@15.4.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
highlight.js:
|
||||
specifier: 11.11.1
|
||||
version: 11.11.1
|
||||
jaro-winkler:
|
||||
specifier: 0.2.8
|
||||
version: 0.2.8
|
||||
katex:
|
||||
specifier: 0.16.22
|
||||
version: 0.16.22
|
||||
launchdarkly-react-client-sdk:
|
||||
specifier: 3.8.1
|
||||
version: 3.8.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -191,6 +197,24 @@ importers:
|
||||
recharts:
|
||||
specifier: 2.15.3
|
||||
version: 2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
rehype-autolink-headings:
|
||||
specifier: 7.1.0
|
||||
version: 7.1.0
|
||||
rehype-highlight:
|
||||
specifier: 7.0.2
|
||||
version: 7.0.2
|
||||
rehype-katex:
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1
|
||||
rehype-slug:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
remark-gfm:
|
||||
specifier: 4.0.1
|
||||
version: 4.0.1
|
||||
remark-math:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
shepherd.js:
|
||||
specifier: 14.5.1
|
||||
version: 14.5.1
|
||||
@@ -2900,6 +2924,9 @@ packages:
|
||||
'@types/junit-report-builder@3.0.2':
|
||||
resolution: {integrity: sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==}
|
||||
|
||||
'@types/katex@0.16.7':
|
||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||
|
||||
'@types/lodash@4.17.20':
|
||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
||||
|
||||
@@ -4119,6 +4146,10 @@ packages:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4189,6 +4220,10 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
escape-string-regexp@5.0.0:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
eslint-config-next@15.4.6:
|
||||
resolution: {integrity: sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==}
|
||||
peerDependencies:
|
||||
@@ -4550,6 +4585,9 @@ packages:
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
github-slugger@2.0.0:
|
||||
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -4636,12 +4674,42 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-from-dom@5.0.1:
|
||||
resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==}
|
||||
|
||||
hast-util-from-html-isomorphic@2.0.0:
|
||||
resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==}
|
||||
|
||||
hast-util-from-html@2.0.3:
|
||||
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||
|
||||
hast-util-heading-rank@3.0.0:
|
||||
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
|
||||
|
||||
hast-util-is-element@3.0.0:
|
||||
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||
|
||||
hast-util-to-string@3.0.1:
|
||||
resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
|
||||
|
||||
hast-util-to-text@4.0.2:
|
||||
resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
@@ -4649,6 +4717,10 @@ packages:
|
||||
headers-polyfill@4.0.3:
|
||||
resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
|
||||
|
||||
@@ -5015,6 +5087,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZNOIIGMzqCGcHQEA2Q4rIQQ3Df6gSIfne+X9Rly9Bc2y55KxAZu8iGv+n2pP0bLf0XAOctJZgeloC54hWzCahQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
katex@0.16.22:
|
||||
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
|
||||
hasBin: true
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -5129,6 +5205,9 @@ packages:
|
||||
lower-case@2.0.2:
|
||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||
|
||||
lowlight@3.3.0:
|
||||
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -5162,6 +5241,9 @@ packages:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
||||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5169,9 +5251,33 @@ packages:
|
||||
md5.js@1.3.5:
|
||||
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
|
||||
|
||||
mdast-util-from-markdown@2.0.2:
|
||||
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
|
||||
|
||||
mdast-util-gfm-autolink-literal@2.0.1:
|
||||
resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
|
||||
|
||||
mdast-util-gfm-footnote@2.1.0:
|
||||
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
||||
|
||||
mdast-util-gfm-strikethrough@2.0.0:
|
||||
resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
|
||||
|
||||
mdast-util-gfm-table@2.0.0:
|
||||
resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
|
||||
|
||||
mdast-util-gfm-task-list-item@2.0.0:
|
||||
resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
|
||||
|
||||
mdast-util-gfm@3.1.0:
|
||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||
|
||||
mdast-util-math@3.0.0:
|
||||
resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
|
||||
|
||||
@@ -5213,6 +5319,30 @@ packages:
|
||||
micromark-core-commonmark@2.0.3:
|
||||
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
||||
|
||||
micromark-extension-gfm-autolink-literal@2.1.0:
|
||||
resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
|
||||
|
||||
micromark-extension-gfm-footnote@2.1.0:
|
||||
resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
|
||||
|
||||
micromark-extension-gfm-strikethrough@2.1.0:
|
||||
resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
|
||||
|
||||
micromark-extension-gfm-table@2.1.1:
|
||||
resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
|
||||
|
||||
micromark-extension-gfm-tagfilter@2.0.0:
|
||||
resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
|
||||
|
||||
micromark-extension-gfm-task-list-item@2.1.0:
|
||||
resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
|
||||
|
||||
micromark-extension-gfm@3.0.0:
|
||||
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
|
||||
|
||||
micromark-extension-math@3.1.0:
|
||||
resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
|
||||
|
||||
micromark-factory-destination@2.0.1:
|
||||
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
|
||||
|
||||
@@ -5629,6 +5759,9 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
party-js@2.2.0:
|
||||
resolution: {integrity: sha512-50hGuALCpvDTrQLPQ1fgUgxKIWAH28ShVkmeK/3zhO0YJyCqkhrZhQEkWPxDYLvbFJ7YAXyROmFEu35gKpZLtQ==}
|
||||
|
||||
@@ -6178,16 +6311,37 @@ packages:
|
||||
resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
|
||||
hasBin: true
|
||||
|
||||
rehype-autolink-headings@7.1.0:
|
||||
resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
|
||||
|
||||
rehype-highlight@7.0.2:
|
||||
resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==}
|
||||
|
||||
rehype-katex@7.0.1:
|
||||
resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
|
||||
|
||||
rehype-slug@6.0.0:
|
||||
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
|
||||
|
||||
relateurl@0.2.7:
|
||||
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
remark-math@6.0.0:
|
||||
resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
|
||||
|
||||
remark-parse@11.0.0:
|
||||
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
|
||||
|
||||
remark-rehype@11.1.2:
|
||||
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
|
||||
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
renderkid@3.0.0:
|
||||
resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==}
|
||||
|
||||
@@ -6858,12 +7012,18 @@ packages:
|
||||
unified@11.0.5:
|
||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||
|
||||
unist-util-find-after@5.0.0:
|
||||
resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
|
||||
|
||||
unist-util-is@6.0.0:
|
||||
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
|
||||
|
||||
unist-util-remove-position@5.0.0:
|
||||
resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==}
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||
|
||||
@@ -6970,6 +7130,9 @@ packages:
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
|
||||
@@ -6989,6 +7152,9 @@ packages:
|
||||
resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
@@ -10194,6 +10360,8 @@ snapshots:
|
||||
|
||||
'@types/junit-report-builder@3.0.2': {}
|
||||
|
||||
'@types/katex@0.16.7': {}
|
||||
|
||||
'@types/lodash@4.17.20': {}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
@@ -11486,6 +11654,8 @@ snapshots:
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
error-ex@1.3.2:
|
||||
@@ -11652,6 +11822,8 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
eslint-config-next@15.4.6(eslint@8.57.1)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.4.6
|
||||
@@ -11660,8 +11832,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.39.1(eslint@8.57.1)(typescript@5.9.2)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@@ -11680,7 +11852,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.1
|
||||
@@ -11691,22 +11863,22 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.39.1(eslint@8.57.1)(typescript@5.9.2)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -11717,7 +11889,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -12110,6 +12282,8 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
github-slugger@2.0.0: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -12207,6 +12381,51 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-from-dom@5.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hastscript: 9.0.1
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-from-html-isomorphic@2.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-from-dom: 5.0.1
|
||||
hast-util-from-html: 2.0.3
|
||||
unist-util-remove-position: 5.0.0
|
||||
|
||||
hast-util-from-html@2.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
devlop: 1.1.0
|
||||
hast-util-from-parse5: 8.0.3
|
||||
parse5: 7.3.0
|
||||
vfile: 6.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
devlop: 1.1.0
|
||||
hastscript: 9.0.1
|
||||
property-information: 7.1.0
|
||||
vfile: 6.0.3
|
||||
vfile-location: 5.0.3
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-heading-rank@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-is-element@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -12227,14 +12446,35 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-string@3.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-to-text@4.0.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
hast-util-is-element: 3.0.0
|
||||
unist-util-find-after: 5.0.0
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hastscript@9.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-parse-selector: 4.0.0
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
headers-polyfill@4.0.3: {}
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
dependencies:
|
||||
hash.js: 1.1.7
|
||||
@@ -12587,6 +12827,10 @@ snapshots:
|
||||
make-dir: 3.1.0
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
katex@0.16.22:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -12689,6 +12933,12 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
lowlight@3.3.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
devlop: 1.1.0
|
||||
highlight.js: 11.11.1
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -12724,6 +12974,8 @@ snapshots:
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
md5.js@1.3.5:
|
||||
@@ -12732,6 +12984,13 @@ snapshots:
|
||||
inherits: 2.0.4
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
escape-string-regexp: 5.0.0
|
||||
unist-util-is: 6.0.0
|
||||
unist-util-visit-parents: 6.0.1
|
||||
|
||||
mdast-util-from-markdown@2.0.2:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -12749,6 +13008,75 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-autolink-literal@2.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
ccount: 2.0.1
|
||||
devlop: 1.1.0
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
micromark-util-character: 2.1.1
|
||||
|
||||
mdast-util-gfm-footnote@2.1.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
micromark-util-normalize-identifier: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-strikethrough@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-table@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
markdown-table: 3.0.4
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-task-list-item@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm@3.1.0:
|
||||
dependencies:
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-gfm-autolink-literal: 2.0.1
|
||||
mdast-util-gfm-footnote: 2.1.0
|
||||
mdast-util-gfm-strikethrough: 2.0.0
|
||||
mdast-util-gfm-table: 2.0.0
|
||||
mdast-util-gfm-task-list-item: 2.0.0
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-math@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
longest-streak: 3.1.0
|
||||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unist-util-remove-position: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
@@ -12852,6 +13180,74 @@ snapshots:
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-autolink-literal@2.1.0:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-footnote@2.1.0:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-normalize-identifier: 2.0.1
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-strikethrough@2.1.0:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-util-chunked: 2.0.1
|
||||
micromark-util-classify-character: 2.0.1
|
||||
micromark-util-resolve-all: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-table@2.1.1:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-tagfilter@2.0.0:
|
||||
dependencies:
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-task-list-item@2.1.0:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm@3.0.0:
|
||||
dependencies:
|
||||
micromark-extension-gfm-autolink-literal: 2.1.0
|
||||
micromark-extension-gfm-footnote: 2.1.0
|
||||
micromark-extension-gfm-strikethrough: 2.1.0
|
||||
micromark-extension-gfm-table: 2.1.1
|
||||
micromark-extension-gfm-tagfilter: 2.0.0
|
||||
micromark-extension-gfm-task-list-item: 2.1.0
|
||||
micromark-util-combine-extensions: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-math@3.1.0:
|
||||
dependencies:
|
||||
'@types/katex': 0.16.7
|
||||
devlop: 1.1.0
|
||||
katex: 0.16.22
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-factory-destination@2.0.1:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
@@ -13404,6 +13800,10 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
party-js@2.2.0: {}
|
||||
|
||||
pascal-case@3.1.2:
|
||||
@@ -13916,8 +14316,63 @@ snapshots:
|
||||
dependencies:
|
||||
jsesc: 3.0.2
|
||||
|
||||
rehype-autolink-headings@7.1.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
hast-util-heading-rank: 3.0.0
|
||||
hast-util-is-element: 3.0.0
|
||||
unified: 11.0.5
|
||||
unist-util-visit: 5.0.0
|
||||
|
||||
rehype-highlight@7.0.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-text: 4.0.2
|
||||
lowlight: 3.3.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
|
||||
rehype-katex@7.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/katex': 0.16.7
|
||||
hast-util-from-html-isomorphic: 2.0.0
|
||||
hast-util-to-text: 4.0.2
|
||||
katex: 0.16.22
|
||||
unist-util-visit-parents: 6.0.1
|
||||
vfile: 6.0.3
|
||||
|
||||
rehype-slug@6.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
github-slugger: 2.0.0
|
||||
hast-util-heading-rank: 3.0.0
|
||||
hast-util-to-string: 3.0.1
|
||||
unist-util-visit: 5.0.0
|
||||
|
||||
relateurl@0.2.7: {}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-gfm: 3.1.0
|
||||
micromark-extension-gfm: 3.0.0
|
||||
remark-parse: 11.0.0
|
||||
remark-stringify: 11.0.0
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-math@6.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-math: 3.0.0
|
||||
micromark-extension-math: 3.1.0
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-parse@11.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -13935,6 +14390,12 @@ snapshots:
|
||||
unified: 11.0.5
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-stringify@11.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
renderkid@3.0.0:
|
||||
dependencies:
|
||||
css-select: 4.3.0
|
||||
@@ -14736,6 +15197,11 @@ snapshots:
|
||||
trough: 2.2.0
|
||||
vfile: 6.0.3
|
||||
|
||||
unist-util-find-after@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.0
|
||||
|
||||
unist-util-is@6.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -14744,6 +15210,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-remove-position@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-visit: 5.0.0
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -14871,6 +15342,11 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile: 6.0.3
|
||||
|
||||
vfile-message@4.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -14909,6 +15385,8 @@ snapshots:
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-dev-middleware@6.1.3(webpack@5.101.1(esbuild@0.25.9)):
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
|
||||
|
||||
import { AgentRunStatus, agentRunStatusMap } from "./agent-run-status-chip";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { AgentRunOutputView } from "./agent-run-output-view";
|
||||
|
||||
export function AgentRunDetailsView({
|
||||
agent,
|
||||
@@ -296,35 +297,7 @@ export function AgentRunDetailsView({
|
||||
)}
|
||||
|
||||
{agentRunOutputs !== null && (
|
||||
<Card className="agpt-box">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-lg">Output</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{agentRunOutputs !== undefined ? (
|
||||
Object.entries(agentRunOutputs).map(
|
||||
([key, { title, values }]) => (
|
||||
<div key={key} className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">
|
||||
{title || key}
|
||||
</label>
|
||||
{values.map((value, i) => (
|
||||
<p
|
||||
className="resize-none overflow-x-auto whitespace-pre-wrap break-words border-none text-sm text-neutral-700 disabled:cursor-not-allowed"
|
||||
key={i}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
{/* TODO: pretty type-dependent rendering */}
|
||||
</div>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<LoadingBox spinnerSize={12} className="h-24" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AgentRunOutputView agentRunOutputs={agentRunOutputs} />
|
||||
)}
|
||||
|
||||
<Card className="agpt-box">
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { globalRegistry, OutputItem, OutputActions } from "./output-renderers";
|
||||
import type { OutputMetadata } from "./output-renderers";
|
||||
|
||||
export function AgentRunOutputView({
|
||||
agentRunOutputs,
|
||||
}: {
|
||||
agentRunOutputs:
|
||||
| Record<
|
||||
string,
|
||||
{
|
||||
title?: string;
|
||||
/* type: BlockIOSubType; */
|
||||
values: Array<React.ReactNode>;
|
||||
}
|
||||
>
|
||||
| undefined;
|
||||
}) {
|
||||
const enableEnhancedOutputHandling = useGetFlag(
|
||||
Flag.ENABLE_ENHANCED_OUTPUT_HANDLING,
|
||||
);
|
||||
|
||||
// Prepare items for the renderer system
|
||||
const outputItems = useMemo(() => {
|
||||
if (!agentRunOutputs) return [];
|
||||
|
||||
const items: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
value: unknown;
|
||||
metadata?: OutputMetadata;
|
||||
renderer: any;
|
||||
}> = [];
|
||||
|
||||
Object.entries(agentRunOutputs).forEach(([key, { title, values }]) => {
|
||||
values.forEach((value, index) => {
|
||||
// Enhanced metadata extraction
|
||||
const metadata: OutputMetadata = {};
|
||||
|
||||
// Type guard to safely access properties
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!React.isValidElement(value)
|
||||
) {
|
||||
const objValue = value as any;
|
||||
if (objValue.type) metadata.type = objValue.type;
|
||||
if (objValue.mimeType) metadata.mimeType = objValue.mimeType;
|
||||
if (objValue.filename) metadata.filename = objValue.filename;
|
||||
}
|
||||
|
||||
const renderer = globalRegistry.getRenderer(value, metadata);
|
||||
if (renderer) {
|
||||
items.push({
|
||||
key: `${key}-${index}`,
|
||||
label: index === 0 ? title || key : "",
|
||||
value,
|
||||
metadata,
|
||||
renderer,
|
||||
});
|
||||
} else {
|
||||
const textRenderer = globalRegistry
|
||||
.getAllRenderers()
|
||||
.find((r) => r.name === "TextRenderer");
|
||||
if (textRenderer) {
|
||||
items.push({
|
||||
key: `${key}-${index}`,
|
||||
label: index === 0 ? title || key : "",
|
||||
value: JSON.stringify(value, null, 2),
|
||||
metadata,
|
||||
renderer: textRenderer,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [agentRunOutputs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{enableEnhancedOutputHandling ? (
|
||||
<Card className="agpt-box" style={{ maxWidth: "950px" }}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="font-poppins text-lg">Output</CardTitle>
|
||||
{outputItems.length > 0 && (
|
||||
<OutputActions
|
||||
items={outputItems.map((item) => ({
|
||||
value: item.value,
|
||||
metadata: item.metadata,
|
||||
renderer: item.renderer,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent
|
||||
className="flex flex-col gap-4"
|
||||
style={{ maxWidth: "660px" }}
|
||||
>
|
||||
{agentRunOutputs !== undefined ? (
|
||||
outputItems.length > 0 ? (
|
||||
outputItems.map((item) => (
|
||||
<OutputItem
|
||||
key={item.key}
|
||||
value={item.value}
|
||||
metadata={item.metadata}
|
||||
renderer={item.renderer}
|
||||
label={item.label}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No outputs to display
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<LoadingBox spinnerSize={12} className="h-24" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="agpt-box" style={{ maxWidth: "950px" }}>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-lg">Output</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent
|
||||
className="flex flex-col gap-4"
|
||||
style={{ maxWidth: "660px" }}
|
||||
>
|
||||
{agentRunOutputs !== undefined ? (
|
||||
Object.entries(agentRunOutputs).map(
|
||||
([key, { title, values }]) => (
|
||||
<div key={key} className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">
|
||||
{title || key}
|
||||
</label>
|
||||
{values.map((value, i) => (
|
||||
<p
|
||||
className="resize-none overflow-x-auto whitespace-pre-wrap break-words border-none text-sm text-neutral-700 disabled:cursor-not-allowed"
|
||||
key={i}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
{/* TODO: pretty type-dependent rendering */}
|
||||
</div>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<LoadingBox spinnerSize={12} className="h-24" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { CheckIcon, CopyIcon, DownloadIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { OutputRenderer, OutputMetadata } from "../types";
|
||||
import { downloadOutputs } from "../utils/download";
|
||||
|
||||
interface OutputActionsProps {
|
||||
items: Array<{
|
||||
value: unknown;
|
||||
metadata?: OutputMetadata;
|
||||
renderer: OutputRenderer;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function OutputActions({ items }: OutputActionsProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyAll = async () => {
|
||||
const textContents: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const copyContent = item.renderer.getCopyContent(
|
||||
item.value,
|
||||
item.metadata,
|
||||
);
|
||||
if (
|
||||
copyContent &&
|
||||
item.renderer.isConcatenable(item.value, item.metadata)
|
||||
) {
|
||||
// For concatenable items, extract the text
|
||||
let text: string;
|
||||
if (typeof copyContent.data === "string") {
|
||||
text = copyContent.data;
|
||||
} else if (copyContent.fallbackText) {
|
||||
text = copyContent.fallbackText;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
textContents.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (textContents.length > 0) {
|
||||
const combinedText = textContents.join("\n\n");
|
||||
try {
|
||||
await navigator.clipboard.writeText(combinedText);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadAll = () => {
|
||||
downloadOutputs(items);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCopyAll}
|
||||
aria-label="Copy all text outputs"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="size-4 text-green-600" />
|
||||
) : (
|
||||
<CopyIcon className="size-4 text-neutral-500" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDownloadAll}
|
||||
aria-label="Download outputs"
|
||||
>
|
||||
<DownloadIcon className="size-4 text-neutral-500" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||
import { OutputRenderer, OutputMetadata } from "../types";
|
||||
import { copyToClipboard } from "../utils/copy";
|
||||
|
||||
interface OutputItemProps {
|
||||
value: any;
|
||||
metadata?: OutputMetadata;
|
||||
renderer: OutputRenderer;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function OutputItem({
|
||||
value,
|
||||
metadata,
|
||||
renderer,
|
||||
label,
|
||||
}: OutputItemProps) {
|
||||
const [showCopyButton, setShowCopyButton] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
const copyContent = renderer.getCopyContent(value, metadata);
|
||||
if (copyContent) {
|
||||
try {
|
||||
await copyToClipboard(copyContent);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canCopy = renderer.getCopyContent(value, metadata) !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setShowCopyButton(true)}
|
||||
onMouseLeave={() => setShowCopyButton(false)}
|
||||
>
|
||||
{label && (
|
||||
<label className="mb-1.5 block text-sm font-medium">{label}</label>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{renderer.render(value, metadata)}
|
||||
|
||||
{canCopy && showCopyButton && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute right-2 top-2 rounded-md border border-gray-200 bg-background/80 p-1.5 backdrop-blur-sm transition-all duration-200 hover:bg-gray-100"
|
||||
aria-label="Copy content"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4 text-gray-600" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { globalRegistry } from "./types";
|
||||
import { textRenderer } from "./renderers/TextRenderer";
|
||||
import { codeRenderer } from "./renderers/CodeRenderer";
|
||||
import { imageRenderer } from "./renderers/ImageRenderer";
|
||||
import { videoRenderer } from "./renderers/VideoRenderer";
|
||||
import { jsonRenderer } from "./renderers/JSONRenderer";
|
||||
import { markdownRenderer } from "./renderers/MarkdownRenderer";
|
||||
|
||||
// Register all renderers in priority order
|
||||
globalRegistry.register(videoRenderer);
|
||||
globalRegistry.register(imageRenderer);
|
||||
globalRegistry.register(codeRenderer);
|
||||
globalRegistry.register(markdownRenderer);
|
||||
globalRegistry.register(jsonRenderer);
|
||||
globalRegistry.register(textRenderer);
|
||||
|
||||
export { globalRegistry };
|
||||
export type { OutputRenderer, OutputMetadata, DownloadContent } from "./types";
|
||||
export { OutputItem } from "./components/OutputItem";
|
||||
export { OutputActions } from "./components/OutputActions";
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
import {
|
||||
OutputRenderer,
|
||||
OutputMetadata,
|
||||
DownloadContent,
|
||||
CopyContent,
|
||||
} from "../types";
|
||||
|
||||
function getFileExtension(language: string): string {
|
||||
const extensionMap: Record<string, string> = {
|
||||
javascript: "js",
|
||||
typescript: "ts",
|
||||
python: "py",
|
||||
java: "java",
|
||||
csharp: "cs",
|
||||
cpp: "cpp",
|
||||
c: "c",
|
||||
html: "html",
|
||||
css: "css",
|
||||
json: "json",
|
||||
xml: "xml",
|
||||
yaml: "yaml",
|
||||
markdown: "md",
|
||||
sql: "sql",
|
||||
bash: "sh",
|
||||
shell: "sh",
|
||||
plaintext: "txt",
|
||||
};
|
||||
|
||||
return extensionMap[language.toLowerCase()] || "txt";
|
||||
}
|
||||
|
||||
function canRenderCode(value: unknown, metadata?: OutputMetadata): boolean {
|
||||
if (metadata?.type === "code" || metadata?.language) {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
if (typeof value !== "string") return false;
|
||||
|
||||
const markdownIndicators = [
|
||||
/^#{1,6}\s+/m,
|
||||
/\*\*[^*]+\*\*/,
|
||||
/\[([^\]]+)\]\(([^)]+)\)/,
|
||||
/^>\s+/m,
|
||||
/^\s*[-*+]\s+\w+/m,
|
||||
/!\[([^\]]*)\]\(([^)]+)\)/,
|
||||
];
|
||||
|
||||
let markdownMatches = 0;
|
||||
for (const pattern of markdownIndicators) {
|
||||
if (pattern.test(value)) {
|
||||
markdownMatches++;
|
||||
if (markdownMatches >= 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const codeIndicators = [
|
||||
/^(function|const|let|var|class|import|export|if|for|while)\s/m,
|
||||
/^def\s+\w+\s*\(/m,
|
||||
/^import\s+/m,
|
||||
/^from\s+\w+\s+import/m,
|
||||
/^\s*<[^>]+>/,
|
||||
/[{}[\]();]/,
|
||||
];
|
||||
|
||||
return codeIndicators.some((pattern) => pattern.test(value));
|
||||
}
|
||||
|
||||
function renderCode(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): React.ReactNode {
|
||||
const codeValue = String(value);
|
||||
const language = metadata?.language || "plaintext";
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
{metadata?.language && (
|
||||
<div className="absolute right-2 top-2 rounded bg-background/80 px-2 py-1 text-xs text-muted-foreground">
|
||||
{language}
|
||||
</div>
|
||||
)}
|
||||
<pre className="overflow-x-auto rounded-md bg-muted p-3">
|
||||
<code className="font-mono text-sm">{codeValue}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCopyContentCode(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): CopyContent | null {
|
||||
const codeValue = String(value);
|
||||
return {
|
||||
mimeType: "text/plain",
|
||||
data: codeValue,
|
||||
fallbackText: codeValue,
|
||||
};
|
||||
}
|
||||
|
||||
function getDownloadContentCode(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): DownloadContent | null {
|
||||
const codeValue = String(value);
|
||||
const language = metadata?.language || "txt";
|
||||
const extension = getFileExtension(language);
|
||||
const blob = new Blob([codeValue], { type: "text/plain" });
|
||||
|
||||
return {
|
||||
data: blob,
|
||||
filename: metadata?.filename || `code.${extension}`,
|
||||
mimeType: "text/plain",
|
||||
};
|
||||
}
|
||||
|
||||
function isConcatenableCode(
|
||||
_value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const codeRenderer: OutputRenderer = {
|
||||
name: "CodeRenderer",
|
||||
priority: 30,
|
||||
canRender: canRenderCode,
|
||||
render: renderCode,
|
||||
getCopyContent: getCopyContentCode,
|
||||
getDownloadContent: getDownloadContentCode,
|
||||
isConcatenable: isConcatenableCode,
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
import React from "react";
|
||||
import {
|
||||
OutputRenderer,
|
||||
OutputMetadata,
|
||||
DownloadContent,
|
||||
CopyContent,
|
||||
} from "../types";
|
||||
|
||||
const imageExtensions = [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".bmp",
|
||||
".svg",
|
||||
".webp",
|
||||
".ico",
|
||||
];
|
||||
|
||||
const imageMimeTypes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
"image/svg+xml",
|
||||
"image/webp",
|
||||
"image/x-icon",
|
||||
];
|
||||
|
||||
function guessMimeType(url: string): string | null {
|
||||
const extension = url.split(".").pop()?.toLowerCase();
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
png: "image/png",
|
||||
gif: "image/gif",
|
||||
bmp: "image/bmp",
|
||||
svg: "image/svg+xml",
|
||||
webp: "image/webp",
|
||||
ico: "image/x-icon",
|
||||
};
|
||||
return extension ? mimeMap[extension] || null : null;
|
||||
}
|
||||
|
||||
function canRenderImage(value: unknown, metadata?: OutputMetadata): boolean {
|
||||
if (
|
||||
metadata?.type === "image" ||
|
||||
(metadata?.mimeType && imageMimeTypes.includes(metadata.mimeType))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const obj = value as any;
|
||||
if (obj.url || obj.data || obj.path) {
|
||||
const urlOrData = obj.url || obj.data || obj.path;
|
||||
|
||||
if (typeof urlOrData === "string") {
|
||||
if (urlOrData.startsWith("data:image/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
urlOrData.startsWith("http://") ||
|
||||
urlOrData.startsWith("https://")
|
||||
) {
|
||||
const hasImageExt = imageExtensions.some((ext) =>
|
||||
urlOrData.toLowerCase().includes(ext),
|
||||
);
|
||||
return hasImageExt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.filename) {
|
||||
const hasImageExt = imageExtensions.some((ext) =>
|
||||
obj.filename.toLowerCase().endsWith(ext),
|
||||
);
|
||||
return hasImageExt;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
if (value.startsWith("data:image/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||
const hasImageExt = imageExtensions.some((ext) =>
|
||||
value.toLowerCase().includes(ext),
|
||||
);
|
||||
return hasImageExt;
|
||||
}
|
||||
|
||||
if (metadata?.filename) {
|
||||
const hasImageExt = imageExtensions.some((ext) =>
|
||||
metadata.filename!.toLowerCase().endsWith(ext),
|
||||
);
|
||||
return hasImageExt;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderImage(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): React.ReactNode {
|
||||
const imageUrl = String(value);
|
||||
const altText = metadata?.filename || "Output image";
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={altText}
|
||||
className="h-auto max-w-full rounded-md border border-gray-200"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCopyContentImage(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): CopyContent | null {
|
||||
const imageUrl = String(value);
|
||||
|
||||
if (imageUrl.startsWith("data:")) {
|
||||
const mimeMatch = imageUrl.match(/data:([^;]+)/);
|
||||
const mimeType = mimeMatch?.[1] || "image/png";
|
||||
|
||||
return {
|
||||
mimeType: mimeType,
|
||||
data: async () => {
|
||||
const response = await fetch(imageUrl);
|
||||
return await response.blob();
|
||||
},
|
||||
alternativeMimeTypes: ["image/png", "text/plain"],
|
||||
fallbackText: imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = metadata?.mimeType || guessMimeType(imageUrl) || "image/png";
|
||||
|
||||
return {
|
||||
mimeType: mimeType,
|
||||
data: async () => {
|
||||
const response = await fetch(imageUrl);
|
||||
return await response.blob();
|
||||
},
|
||||
alternativeMimeTypes: ["image/png", "text/plain"],
|
||||
fallbackText: imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function getDownloadContentImage(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): DownloadContent | null {
|
||||
const imageUrl = String(value);
|
||||
|
||||
if (imageUrl.startsWith("data:")) {
|
||||
const [mimeInfo, base64Data] = imageUrl.split(",");
|
||||
const mimeType = mimeInfo.match(/data:([^;]+)/)?.[1] || "image/png";
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: mimeType });
|
||||
|
||||
const extension = mimeType.split("/")[1] || "png";
|
||||
return {
|
||||
data: blob,
|
||||
filename: metadata?.filename || `image.${extension}`,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: imageUrl,
|
||||
filename: metadata?.filename || "image.png",
|
||||
mimeType: metadata?.mimeType || "image/png",
|
||||
};
|
||||
}
|
||||
|
||||
function isConcatenableImage(
|
||||
_value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const imageRenderer: OutputRenderer = {
|
||||
name: "ImageRenderer",
|
||||
priority: 40,
|
||||
canRender: canRenderImage,
|
||||
render: renderImage,
|
||||
getCopyContent: getCopyContentImage,
|
||||
getDownloadContent: getDownloadContentImage,
|
||||
isConcatenable: isConcatenableImage,
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { CaretDown, CaretRight } from "@phosphor-icons/react";
|
||||
import {
|
||||
OutputRenderer,
|
||||
OutputMetadata,
|
||||
DownloadContent,
|
||||
CopyContent,
|
||||
} from "../types";
|
||||
|
||||
function canRenderJSON(value: unknown, _metadata?: OutputMetadata): boolean {
|
||||
if (_metadata?.type === "json") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderJSON(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): React.ReactNode {
|
||||
let jsonData = value;
|
||||
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
jsonData = JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return <JSONViewer data={jsonData} />;
|
||||
}
|
||||
|
||||
function getCopyContentJSON(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): CopyContent | null {
|
||||
const jsonString =
|
||||
typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
||||
|
||||
return {
|
||||
mimeType: "application/json",
|
||||
data: jsonString,
|
||||
alternativeMimeTypes: ["text/plain"],
|
||||
fallbackText: jsonString,
|
||||
};
|
||||
}
|
||||
|
||||
function getDownloadContentJSON(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): DownloadContent | null {
|
||||
const jsonString =
|
||||
typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
||||
const blob = new Blob([jsonString], { type: "application/json" });
|
||||
|
||||
return {
|
||||
data: blob,
|
||||
filename: _metadata?.filename || "output.json",
|
||||
mimeType: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
function isConcatenableJSON(
|
||||
_value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const jsonRenderer: OutputRenderer = {
|
||||
name: "JSONRenderer",
|
||||
priority: 20,
|
||||
canRender: canRenderJSON,
|
||||
render: renderJSON,
|
||||
getCopyContent: getCopyContentJSON,
|
||||
getDownloadContent: getDownloadContentJSON,
|
||||
isConcatenable: isConcatenableJSON,
|
||||
};
|
||||
|
||||
function JSONViewer({ data }: { data: any }) {
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleCollapse = (key: string) => {
|
||||
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const renderValue = (value: any, key: string = ""): React.ReactNode => {
|
||||
if (value === null)
|
||||
return <span className="text-muted-foreground">null</span>;
|
||||
if (value === undefined)
|
||||
return <span className="text-muted-foreground">undefined</span>;
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return <span className="text-blue-600">{value.toString()}</span>;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return <span className="text-green-600">{value}</span>;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return <span className="text-orange-600">"{value}"</span>;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const isCollapsed = collapsed[key];
|
||||
const itemCount = value.length;
|
||||
|
||||
if (itemCount === 0) {
|
||||
return <span className="text-muted-foreground">[]</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<button
|
||||
onClick={() => toggleCollapse(key)}
|
||||
className="inline-flex items-center rounded px-1 hover:bg-muted"
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<CaretRight className="size-3" />
|
||||
) : (
|
||||
<CaretDown className="size-3" />
|
||||
)}
|
||||
<span className="ml-1 text-muted-foreground">
|
||||
Array({itemCount})
|
||||
</span>
|
||||
</button>
|
||||
{!isCollapsed && (
|
||||
<div className="ml-4 mt-1">
|
||||
{value.map((item, index) => (
|
||||
<div key={index} className="flex">
|
||||
<span className="mr-2 text-muted-foreground">{index}:</span>
|
||||
{renderValue(item, `${key}[${index}]`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
const isCollapsed = collapsed[key];
|
||||
const keys = Object.keys(value);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return <span className="text-muted-foreground">{"{}"}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<button
|
||||
onClick={() => toggleCollapse(key)}
|
||||
className="inline-flex items-center rounded px-1 hover:bg-muted"
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<CaretRight className="size-3" />
|
||||
) : (
|
||||
<CaretDown className="size-3" />
|
||||
)}
|
||||
<span className="ml-1 text-muted-foreground">Object</span>
|
||||
</button>
|
||||
{!isCollapsed && (
|
||||
<div className="ml-4 mt-1">
|
||||
{keys.map((objKey) => (
|
||||
<div key={objKey} className="flex">
|
||||
<span className="mr-2 text-purple-600">
|
||||
"{objKey}":
|
||||
</span>
|
||||
{renderValue(value[objKey], `${key}.${objKey}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="text-muted-foreground">{String(value)}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-md bg-muted p-3 font-mono text-sm">
|
||||
{renderValue(data, "root")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import {
|
||||
OutputRenderer,
|
||||
OutputMetadata,
|
||||
DownloadContent,
|
||||
CopyContent,
|
||||
} from "../types";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
const markdownPatterns = [
|
||||
/```[\s\S]*?```/u, // Fenced code blocks (check first)
|
||||
/^#{1,6}\s+\S+/gmu, // ATX headers (require content)
|
||||
/\*\*[^*\n]+?\*\*/u, // **bold**
|
||||
/__(?!_)[^_\n]+?__(?!_)/u, // __bold__ (avoid ___/snake_case_)
|
||||
/(?<!\*)\*(?!\*)(?:[^*\n]|(?<=\\)\*)+?(?<!\\)\*(?!\*)/u, // *italic* (try to avoid **)
|
||||
/(?<!_)_(?!_)(?:[^_\n]|(?<=\\)_)+?(?<!\\)_(?!_)/u, // _italic_ with guards
|
||||
/\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/u, // Links with optional title (simple)
|
||||
/!\[([^\]\n]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/u, // Images with optional title (simple)
|
||||
/`[^`\n]+`/u, // Inline code
|
||||
/^(?:\s*[-*+]\s+\S.*)(?:\n\s*[-*+]\s+\S.*)+$/gmu, // UL list (≥2 items)
|
||||
/^(?:\s*\d+\.\s+\S.*)(?:\n\s*\d+\.\s+\S.*)+$/gmu, // OL list (≥2 items)
|
||||
/^>\s+\S.*/gm, // Blockquotes
|
||||
/^\|[^|\n]+(\|[^|\n]+)+\|\s*$/gm, // Table row (at least two cells)
|
||||
/^\s*\|(?:\s*:?[-=]{3,}\s*\|)+\s*$/gm, // Table separator row
|
||||
/\$\$[\s\S]+?\$\$/u, // Display math
|
||||
/(?<!\\)(?<!\w)\$[^$\n]+?\$(?!\w)/u, // Inline math: avoid prices/ids
|
||||
];
|
||||
|
||||
const videoExtensions = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
|
||||
|
||||
function isVideoUrl(url: string): boolean {
|
||||
if (url.includes("youtube.com/watch") || url.includes("youtu.be/")) {
|
||||
return true;
|
||||
}
|
||||
if (url.includes("vimeo.com/")) {
|
||||
return true;
|
||||
}
|
||||
return videoExtensions.some((ext) => url.toLowerCase().includes(ext));
|
||||
}
|
||||
|
||||
function getVideoEmbedUrl(url: string): string | null {
|
||||
const youtubeMatch = url.match(
|
||||
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/,
|
||||
);
|
||||
if (youtubeMatch) {
|
||||
return `https://www.youtube.com/embed/${youtubeMatch[1]}`;
|
||||
}
|
||||
|
||||
const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
|
||||
if (vimeoMatch) {
|
||||
return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
|
||||
}
|
||||
|
||||
if (videoExtensions.some((ext) => url.toLowerCase().includes(ext))) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderVideoEmbed(url: string): React.ReactNode {
|
||||
const embedUrl = getVideoEmbedUrl(url);
|
||||
|
||||
if (!embedUrl) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (videoExtensions.some((ext) => embedUrl.toLowerCase().includes(ext))) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
<video
|
||||
controls
|
||||
className="w-full max-w-2xl rounded-lg shadow-md"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={embedUrl} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<div className="relative aspect-video">
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
title="Embedded video player"
|
||||
className="absolute left-0 top-0 h-full w-full rounded-lg shadow-md"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function canRenderMarkdown(value: unknown, metadata?: OutputMetadata): boolean {
|
||||
if (
|
||||
metadata?.type === "markdown" ||
|
||||
metadata?.mimeType === "text/markdown" ||
|
||||
metadata?.mimeType === "text/x-markdown"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (metadata?.filename?.toLowerCase().endsWith(".md")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let matchCount = 0;
|
||||
const requiredMatches = 2;
|
||||
|
||||
for (const pattern of markdownPatterns) {
|
||||
if (pattern.test(value)) {
|
||||
matchCount++;
|
||||
if (matchCount >= requiredMatches) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderMarkdown(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): React.ReactNode {
|
||||
const markdownContent = String(value);
|
||||
|
||||
return (
|
||||
<div className="markdown-output">
|
||||
<ReactMarkdown
|
||||
className="prose prose-sm dark:prose-invert max-w-none"
|
||||
remarkPlugins={[
|
||||
remarkGfm, // GitHub Flavored Markdown (tables, task lists, strikethrough)
|
||||
remarkMath, // Math support for LaTeX
|
||||
]}
|
||||
rehypePlugins={[
|
||||
rehypeKatex, // Render math with KaTeX
|
||||
rehypeHighlight, // Syntax highlighting for code blocks
|
||||
rehypeSlug, // Add IDs to headings
|
||||
[rehypeAutolinkHeadings, { behavior: "wrap" }], // Make headings clickable
|
||||
]}
|
||||
components={{
|
||||
// Custom components for better rendering
|
||||
pre: ({ children, ...props }) => (
|
||||
<pre
|
||||
className="my-4 overflow-x-auto rounded-md bg-gray-900 p-4 dark:bg-gray-950"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
code: ({ children, ...props }: any) => {
|
||||
// Check if it's inline code by looking at the parent
|
||||
const isInline = !props.className?.includes("language-");
|
||||
if (isInline) {
|
||||
return (
|
||||
<code
|
||||
className="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
// Block code is handled by rehype-highlight
|
||||
return (
|
||||
<code className="font-mono text-sm text-gray-100" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
a: ({ children, href, ...props }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-black underline decoration-1 underline-offset-2 transition-colors"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
blockquote: ({ children, ...props }) => (
|
||||
<blockquote
|
||||
className="my-4 border-l-4 border-blue-500 pl-4 italic text-gray-700 dark:border-blue-400 dark:text-gray-300"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
table: ({ children, ...props }) => (
|
||||
<div className="my-4 overflow-x-auto">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 rounded-lg border border-gray-200 dark:divide-gray-700 dark:border-gray-700"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
th: ({ children, ...props }) => (
|
||||
<th
|
||||
className="bg-gray-50 px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children, ...props }) => (
|
||||
<td
|
||||
className="border-t border-gray-200 px-4 py-3 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-400"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
// GitHub Flavored Markdown task lists
|
||||
input: ({ ...props }: any) => {
|
||||
if (props.type === "checkbox") {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
disabled
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <input {...props} />;
|
||||
},
|
||||
// Better list styling
|
||||
ul: ({ children, ...props }: any) => (
|
||||
<ul
|
||||
className={`my-4 list-disc space-y-2 pl-6 ${
|
||||
props.className?.includes("contains-task-list")
|
||||
? "list-none pl-0"
|
||||
: ""
|
||||
}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children, ...props }) => (
|
||||
<ol className="my-4 list-decimal space-y-2 pl-6" {...props}>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children, ...props }: any) => (
|
||||
<li
|
||||
className={`text-gray-700 dark:text-gray-300 ${
|
||||
props.className?.includes("task-list-item")
|
||||
? "flex items-start"
|
||||
: ""
|
||||
}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
// Better heading styles
|
||||
h1: ({ children, ...props }) => (
|
||||
<h1
|
||||
className="my-6 text-3xl font-bold text-gray-900 dark:text-gray-100"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children, ...props }) => (
|
||||
<h2
|
||||
className="my-5 text-2xl font-semibold text-gray-800 dark:text-gray-200"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children, ...props }) => (
|
||||
<h3
|
||||
className="my-4 text-xl font-semibold text-gray-800 dark:text-gray-200"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children, ...props }) => (
|
||||
<h4
|
||||
className="my-3 text-lg font-medium text-gray-700 dark:text-gray-300"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
h5: ({ children, ...props }) => (
|
||||
<h5
|
||||
className="my-2 text-base font-medium text-gray-700 dark:text-gray-300"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h5>
|
||||
),
|
||||
h6: ({ children, ...props }) => (
|
||||
<h6
|
||||
className="my-2 text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h6>
|
||||
),
|
||||
// Horizontal rule
|
||||
hr: ({ ...props }) => (
|
||||
<hr
|
||||
className="my-6 border-gray-300 dark:border-gray-700"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// Strikethrough (GFM)
|
||||
del: ({ children, ...props }) => (
|
||||
<del
|
||||
className="text-gray-500 line-through dark:text-gray-500"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</del>
|
||||
),
|
||||
// Image handling
|
||||
img: ({ src, alt, ...props }) => {
|
||||
// Check if it's a video URL pattern
|
||||
if (src && isVideoUrl(src)) {
|
||||
return renderVideoEmbed(src);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="my-4 h-auto max-w-full rounded-lg shadow-md"
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
// Custom paragraph to handle standalone video URLs
|
||||
p: ({ children, ...props }) => {
|
||||
// Check if paragraph contains just a video URL
|
||||
if (typeof children === "string" && isVideoUrl(children.trim())) {
|
||||
return renderVideoEmbed(children.trim());
|
||||
}
|
||||
|
||||
// Check for video URLs in link children
|
||||
if (React.Children.count(children) === 1) {
|
||||
const child = React.Children.toArray(children)[0];
|
||||
if (React.isValidElement(child) && child.type === "a") {
|
||||
const href = child.props.href;
|
||||
if (href && isVideoUrl(href)) {
|
||||
return renderVideoEmbed(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className="my-3 leading-relaxed text-gray-700 dark:text-gray-300"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdownContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCopyContentMarkdown(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): CopyContent | null {
|
||||
const markdownText = String(value);
|
||||
return {
|
||||
mimeType: "text/markdown",
|
||||
data: markdownText,
|
||||
alternativeMimeTypes: ["text/plain"],
|
||||
fallbackText: markdownText,
|
||||
};
|
||||
}
|
||||
|
||||
function getDownloadContentMarkdown(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): DownloadContent | null {
|
||||
const markdownText = String(value);
|
||||
const blob = new Blob([markdownText], { type: "text/markdown" });
|
||||
|
||||
return {
|
||||
data: blob,
|
||||
filename: metadata?.filename || "output.md",
|
||||
mimeType: "text/markdown",
|
||||
};
|
||||
}
|
||||
|
||||
function isConcatenableMarkdown(
|
||||
_value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const markdownRenderer: OutputRenderer = {
|
||||
name: "MarkdownRenderer",
|
||||
priority: 35,
|
||||
canRender: canRenderMarkdown,
|
||||
render: renderMarkdown,
|
||||
getCopyContent: getCopyContentMarkdown,
|
||||
getDownloadContent: getDownloadContentMarkdown,
|
||||
isConcatenable: isConcatenableMarkdown,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import {
|
||||
OutputRenderer,
|
||||
OutputMetadata,
|
||||
DownloadContent,
|
||||
CopyContent,
|
||||
} from "../types";
|
||||
|
||||
function canRenderText(value: unknown, _metadata?: OutputMetadata): boolean {
|
||||
return (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
);
|
||||
}
|
||||
|
||||
function renderText(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): React.ReactNode {
|
||||
const textValue = String(value);
|
||||
|
||||
return (
|
||||
<p className="resize-none overflow-x-auto whitespace-pre-wrap break-words border-none text-sm text-neutral-700">
|
||||
{textValue}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function getCopyContentText(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): CopyContent | null {
|
||||
const textValue = String(value);
|
||||
return {
|
||||
mimeType: "text/plain",
|
||||
data: textValue,
|
||||
fallbackText: textValue,
|
||||
};
|
||||
}
|
||||
|
||||
function getDownloadContentText(
|
||||
value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): DownloadContent | null {
|
||||
const textValue = String(value);
|
||||
const blob = new Blob([textValue], { type: "text/plain" });
|
||||
|
||||
return {
|
||||
data: blob,
|
||||
filename: _metadata?.filename || "output.txt",
|
||||
mimeType: "text/plain",
|
||||
};
|
||||
}
|
||||
|
||||
function isConcatenableText(
|
||||
_value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const textRenderer: OutputRenderer = {
|
||||
name: "TextRenderer",
|
||||
priority: 0,
|
||||
canRender: canRenderText,
|
||||
render: renderText,
|
||||
getCopyContent: getCopyContentText,
|
||||
getDownloadContent: getDownloadContentText,
|
||||
isConcatenable: isConcatenableText,
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
import React from "react";
|
||||
import {
|
||||
OutputRenderer,
|
||||
OutputMetadata,
|
||||
DownloadContent,
|
||||
CopyContent,
|
||||
} from "../types";
|
||||
|
||||
const videoExtensions = [
|
||||
".mp4",
|
||||
".webm",
|
||||
".ogg",
|
||||
".mov",
|
||||
".avi",
|
||||
".mkv",
|
||||
".m4v",
|
||||
];
|
||||
|
||||
const videoMimeTypes = [
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/ogg",
|
||||
"video/quicktime",
|
||||
"video/x-msvideo",
|
||||
"video/x-matroska",
|
||||
];
|
||||
|
||||
function guessMimeType(url: string): string | null {
|
||||
const extension = url.split(".").pop()?.toLowerCase();
|
||||
const mimeMap: Record<string, string> = {
|
||||
mp4: "video/mp4",
|
||||
webm: "video/webm",
|
||||
ogg: "video/ogg",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
mkv: "video/x-matroska",
|
||||
m4v: "video/mp4",
|
||||
};
|
||||
return extension ? mimeMap[extension] || null : null;
|
||||
}
|
||||
|
||||
function canRenderVideo(value: unknown, metadata?: OutputMetadata): boolean {
|
||||
if (
|
||||
metadata?.type === "video" ||
|
||||
(metadata?.mimeType && videoMimeTypes.includes(metadata.mimeType))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
if (value.startsWith("data:video/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||
return videoExtensions.some((ext) => value.toLowerCase().includes(ext));
|
||||
}
|
||||
|
||||
if (metadata?.filename) {
|
||||
return videoExtensions.some((ext) =>
|
||||
metadata.filename!.toLowerCase().endsWith(ext),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderVideo(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): React.ReactNode {
|
||||
const videoUrl = String(value);
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<video
|
||||
controls
|
||||
className="h-auto max-w-full rounded-md border border-gray-200"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={videoUrl} type={metadata?.mimeType || "video/mp4"} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCopyContentVideo(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): CopyContent | null {
|
||||
const videoUrl = String(value);
|
||||
|
||||
if (videoUrl.startsWith("data:")) {
|
||||
const mimeMatch = videoUrl.match(/data:([^;]+)/);
|
||||
const mimeType = mimeMatch?.[1] || "video/mp4";
|
||||
|
||||
return {
|
||||
mimeType: mimeType,
|
||||
data: videoUrl,
|
||||
alternativeMimeTypes: ["text/plain"],
|
||||
fallbackText: videoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = metadata?.mimeType || guessMimeType(videoUrl) || "video/mp4";
|
||||
|
||||
return {
|
||||
mimeType: mimeType,
|
||||
data: async () => {
|
||||
const response = await fetch(videoUrl);
|
||||
return await response.blob();
|
||||
},
|
||||
alternativeMimeTypes: ["text/plain"],
|
||||
fallbackText: videoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function getDownloadContentVideo(
|
||||
value: unknown,
|
||||
metadata?: OutputMetadata,
|
||||
): DownloadContent | null {
|
||||
const videoUrl = String(value);
|
||||
|
||||
if (videoUrl.startsWith("data:")) {
|
||||
const [mimeInfo, base64Data] = videoUrl.split(",");
|
||||
const mimeType = mimeInfo.match(/data:([^;]+)/)?.[1] || "video/mp4";
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: mimeType });
|
||||
|
||||
const extension = mimeType.split("/")[1] || "mp4";
|
||||
return {
|
||||
data: blob,
|
||||
filename: metadata?.filename || `video.${extension}`,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: videoUrl,
|
||||
filename: metadata?.filename || "video.mp4",
|
||||
mimeType: metadata?.mimeType || "video/mp4",
|
||||
};
|
||||
}
|
||||
|
||||
function isConcatenableVideo(
|
||||
_value: unknown,
|
||||
_metadata?: OutputMetadata,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const videoRenderer: OutputRenderer = {
|
||||
name: "VideoRenderer",
|
||||
priority: 45,
|
||||
canRender: canRenderVideo,
|
||||
render: renderVideo,
|
||||
getCopyContent: getCopyContentVideo,
|
||||
getDownloadContent: getDownloadContentVideo,
|
||||
isConcatenable: isConcatenableVideo,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface OutputMetadata {
|
||||
type?: string;
|
||||
language?: string;
|
||||
mimeType?: string;
|
||||
filename?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface DownloadContent {
|
||||
data: Blob | string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface CopyContent {
|
||||
mimeType: string; // Primary MIME type to try
|
||||
data: Blob | string | (() => Promise<Blob | string>); // Data or async function to get data
|
||||
fallbackText?: string; // Optional fallback text if rich copy fails
|
||||
alternativeMimeTypes?: string[]; // Alternative MIME types to try if primary isn't supported
|
||||
}
|
||||
|
||||
export interface OutputRenderer {
|
||||
name: string;
|
||||
priority: number;
|
||||
canRender(value: any, metadata?: OutputMetadata): boolean;
|
||||
render(value: any, metadata?: OutputMetadata): ReactNode;
|
||||
getCopyContent(value: any, metadata?: OutputMetadata): CopyContent | null;
|
||||
getDownloadContent(
|
||||
value: any,
|
||||
metadata?: OutputMetadata,
|
||||
): DownloadContent | null;
|
||||
isConcatenable(value: any, metadata?: OutputMetadata): boolean;
|
||||
}
|
||||
|
||||
export class OutputRendererRegistry {
|
||||
private renderers: OutputRenderer[] = [];
|
||||
|
||||
register(renderer: OutputRenderer): void {
|
||||
const index = this.renderers.findIndex(
|
||||
(r) => r.priority < renderer.priority,
|
||||
);
|
||||
if (index === -1) {
|
||||
this.renderers.push(renderer);
|
||||
} else {
|
||||
this.renderers.splice(index, 0, renderer);
|
||||
}
|
||||
}
|
||||
|
||||
getRenderer(value: any, metadata?: OutputMetadata): OutputRenderer | null {
|
||||
return this.renderers.find((r) => r.canRender(value, metadata)) || null;
|
||||
}
|
||||
|
||||
getAllRenderers(): OutputRenderer[] {
|
||||
return [...this.renderers];
|
||||
}
|
||||
}
|
||||
|
||||
export const globalRegistry = new OutputRendererRegistry();
|
||||
@@ -0,0 +1,115 @@
|
||||
import { CopyContent } from "../types";
|
||||
|
||||
export function isClipboardTypeSupported(mimeType: string): boolean {
|
||||
// ClipboardItem.supports() is the proper way to check
|
||||
if ("ClipboardItem" in window && "supports" in ClipboardItem) {
|
||||
return ClipboardItem.supports(mimeType);
|
||||
}
|
||||
|
||||
// Fallback for browsers that don't support the supports() method
|
||||
// These are generally supported
|
||||
const fallbackSupported = ["text/plain", "text/html", "image/png"];
|
||||
|
||||
return fallbackSupported.includes(mimeType);
|
||||
}
|
||||
|
||||
export function getSupportedClipboardType(
|
||||
preferredTypes: string[],
|
||||
): string | null {
|
||||
for (const type of preferredTypes) {
|
||||
if (isClipboardTypeSupported(type)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function copyToClipboard(copyContent: CopyContent): Promise<void> {
|
||||
try {
|
||||
// Determine the best supported MIME type
|
||||
const supportedTypes = [
|
||||
copyContent.mimeType,
|
||||
...(copyContent.alternativeMimeTypes || []),
|
||||
];
|
||||
const bestType = getSupportedClipboardType(supportedTypes);
|
||||
|
||||
if (!bestType) {
|
||||
// No supported type found, use fallback text if available
|
||||
if (copyContent.fallbackText) {
|
||||
await navigator.clipboard.writeText(copyContent.fallbackText);
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`None of the MIME types are supported: ${supportedTypes.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the data (resolve if it's a function)
|
||||
let data = copyContent.data;
|
||||
if (typeof data === "function") {
|
||||
data = await data();
|
||||
}
|
||||
|
||||
// If data is already a Blob, use it directly
|
||||
if (data instanceof Blob) {
|
||||
// If we need a different MIME type than the blob has, recreate it
|
||||
if (bestType !== data.type && bestType !== copyContent.mimeType) {
|
||||
data = new Blob([data], { type: bestType });
|
||||
}
|
||||
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[bestType]: data,
|
||||
}),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If data is a string
|
||||
if (typeof data === "string") {
|
||||
// For plain text, use the simpler writeText API
|
||||
if (bestType === "text/plain") {
|
||||
await navigator.clipboard.writeText(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// For other text formats (HTML, JSON, etc.), create a blob
|
||||
const blob = new Blob([data], { type: bestType });
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[bestType]: blob,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
} catch (_error) {
|
||||
// If rich copy fails and we have fallback text, try that
|
||||
if (copyContent.fallbackText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(copyContent.fallbackText);
|
||||
return;
|
||||
} catch {
|
||||
// Even fallback failed
|
||||
}
|
||||
}
|
||||
throw _error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAndCopyImage(imageUrl: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) throw new Error("Failed to fetch image");
|
||||
|
||||
const blob = await response.blob();
|
||||
const mimeType = blob.type || "image/png";
|
||||
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[mimeType]: blob,
|
||||
}),
|
||||
]);
|
||||
} catch (_error) {
|
||||
// If fetching fails (e.g., CORS), fall back to copying the URL
|
||||
await navigator.clipboard.writeText(imageUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { OutputRenderer, OutputMetadata } from "../types";
|
||||
|
||||
export interface DownloadItem {
|
||||
value: any;
|
||||
metadata?: OutputMetadata;
|
||||
renderer: OutputRenderer;
|
||||
}
|
||||
|
||||
export async function downloadOutputs(items: DownloadItem[]) {
|
||||
const concatenableTexts: string[] = [];
|
||||
const nonConcatenableDownloads: Array<{ blob: Blob; filename: string }> = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.renderer.isConcatenable(item.value, item.metadata)) {
|
||||
const copyContent = item.renderer.getCopyContent(
|
||||
item.value,
|
||||
item.metadata,
|
||||
);
|
||||
if (copyContent) {
|
||||
// Extract text from CopyContent
|
||||
let text: string;
|
||||
if (typeof copyContent.data === "string") {
|
||||
text = copyContent.data;
|
||||
} else if (copyContent.fallbackText) {
|
||||
text = copyContent.fallbackText;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
concatenableTexts.push(text);
|
||||
}
|
||||
} else {
|
||||
const downloadContent = item.renderer.getDownloadContent(
|
||||
item.value,
|
||||
item.metadata,
|
||||
);
|
||||
if (downloadContent) {
|
||||
if (typeof downloadContent.data === "string") {
|
||||
if (downloadContent.data.startsWith("http")) {
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadContent.data;
|
||||
link.download = downloadContent.filename;
|
||||
link.click();
|
||||
}
|
||||
} else {
|
||||
nonConcatenableDownloads.push({
|
||||
blob: downloadContent.data as Blob,
|
||||
filename: downloadContent.filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (concatenableTexts.length > 0) {
|
||||
const combinedText = concatenableTexts.join("\n\n---\n\n");
|
||||
const blob = new Blob([combinedText], { type: "text/plain" });
|
||||
downloadBlob(blob, "combined_output.txt");
|
||||
}
|
||||
|
||||
for (const download of nonConcatenableDownloads) {
|
||||
downloadBlob(download.blob, download.filename);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export enum Flag {
|
||||
NEW_BLOCK_MENU = "new-block-menu",
|
||||
NEW_AGENT_RUNS = "new-agent-runs",
|
||||
GRAPH_SEARCH = "graph-search",
|
||||
ENABLE_ENHANCED_OUTPUT_HANDLING = "enable-enhanced-output-handling",
|
||||
}
|
||||
|
||||
export type FlagValues = {
|
||||
@@ -17,6 +18,7 @@ export type FlagValues = {
|
||||
[Flag.NEW_BLOCK_MENU]: boolean;
|
||||
[Flag.NEW_AGENT_RUNS]: boolean;
|
||||
[Flag.GRAPH_SEARCH]: boolean;
|
||||
[Flag.ENABLE_ENHANCED_OUTPUT_HANDLING]: boolean;
|
||||
};
|
||||
|
||||
const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true";
|
||||
@@ -27,6 +29,7 @@ const mockFlags = {
|
||||
[Flag.NEW_BLOCK_MENU]: false,
|
||||
[Flag.NEW_AGENT_RUNS]: false,
|
||||
[Flag.GRAPH_SEARCH]: true,
|
||||
[Flag.ENABLE_ENHANCED_OUTPUT_HANDLING]: false,
|
||||
};
|
||||
|
||||
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {
|
||||
|
||||
Reference in New Issue
Block a user