From 901e9eba5d28bfba96b67bed47858f87e567364a Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Thu, 4 Sep 2025 08:37:26 -0500 Subject: [PATCH] feat(frontend): Enhanced output rendering system for agent runs (#10819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 image ## Related Issues - Implements SECRT-1209 --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Nicholas Tindle --- autogpt_platform/frontend/package.json | 8 + autogpt_platform/frontend/pnpm-lock.yaml | 494 +++++++++++++++++- .../components/agent-run-details-view.tsx | 31 +- .../components/agent-run-output-view.tsx | 169 ++++++ .../components/OutputActions.tsx | 86 +++ .../components/OutputItem.tsx | 68 +++ .../components/output-renderers/index.ts | 20 + .../renderers/CodeRenderer.tsx | 135 +++++ .../renderers/ImageRenderer.tsx | 208 ++++++++ .../renderers/JSONRenderer.tsx | 204 ++++++++ .../renderers/MarkdownRenderer.tsx | 447 ++++++++++++++++ .../renderers/TextRenderer.tsx | 71 +++ .../renderers/VideoRenderer.tsx | 169 ++++++ .../components/output-renderers/types.ts | 60 +++ .../components/output-renderers/utils/copy.ts | 115 ++++ .../output-renderers/utils/download.ts | 74 +++ .../services/feature-flags/use-get-flag.ts | 3 + 17 files changed, 2325 insertions(+), 37 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputActions.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputItem.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/index.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/CodeRenderer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/ImageRenderer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/JSONRenderer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/MarkdownRenderer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/TextRenderer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/VideoRenderer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/types.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/utils/copy.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/utils/download.ts diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 9dac75574d..6453f0dc05 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -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", diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index 6e0c9fafb9..423d62f505 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -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)): diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx index 3489debdc7..8fcd5d3152 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx @@ -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 && ( - - - Output - - - {agentRunOutputs !== undefined ? ( - Object.entries(agentRunOutputs).map( - ([key, { title, values }]) => ( -
- - {values.map((value, i) => ( -

- {value} -

- ))} - {/* TODO: pretty type-dependent rendering */} -
- ), - ) - ) : ( - - )} -
-
+ )} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx new file mode 100644 index 0000000000..f6f5779562 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx @@ -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; + } + > + | 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 ? ( + + +
+ Output + {outputItems.length > 0 && ( + ({ + value: item.value, + metadata: item.metadata, + renderer: item.renderer, + }))} + /> + )} +
+
+ + + {agentRunOutputs !== undefined ? ( + outputItems.length > 0 ? ( + outputItems.map((item) => ( + + )) + ) : ( +

+ No outputs to display +

+ ) + ) : ( + + )} +
+
+ ) : ( + + + Output + + + + {agentRunOutputs !== undefined ? ( + Object.entries(agentRunOutputs).map( + ([key, { title, values }]) => ( +
+ + {values.map((value, i) => ( +

+ {value} +

+ ))} + {/* TODO: pretty type-dependent rendering */} +
+ ), + ) + ) : ( + + )} +
+
+ )} + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputActions.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputActions.tsx new file mode 100644 index 0000000000..bde1944413 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputActions.tsx @@ -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 ( +
+ + + +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputItem.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputItem.tsx new file mode 100644 index 0000000000..b30ec20962 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/components/OutputItem.tsx @@ -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 ( +
setShowCopyButton(true)} + onMouseLeave={() => setShowCopyButton(false)} + > + {label && ( + + )} + +
+ {renderer.render(value, metadata)} + + {canCopy && showCopyButton && ( + + )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/index.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/index.ts new file mode 100644 index 0000000000..ba26054eb2 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/index.ts @@ -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"; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/CodeRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/CodeRenderer.tsx new file mode 100644 index 0000000000..93df7d8ddd --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/CodeRenderer.tsx @@ -0,0 +1,135 @@ +import React from "react"; +import { + OutputRenderer, + OutputMetadata, + DownloadContent, + CopyContent, +} from "../types"; + +function getFileExtension(language: string): string { + const extensionMap: Record = { + 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 ( +
+ {metadata?.language && ( +
+ {language} +
+ )} +
+        {codeValue}
+      
+
+ ); +} + +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, +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/ImageRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/ImageRenderer.tsx new file mode 100644 index 0000000000..7e2256f246 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/ImageRenderer.tsx @@ -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 = { + 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 ( +
+ {altText} +
+ ); +} + +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, +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/JSONRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/JSONRenderer.tsx new file mode 100644 index 0000000000..668f9179c3 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/JSONRenderer.tsx @@ -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 ; +} + +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>({}); + + const toggleCollapse = (key: string) => { + setCollapsed((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + const renderValue = (value: any, key: string = ""): React.ReactNode => { + if (value === null) + return null; + if (value === undefined) + return undefined; + + if (typeof value === "boolean") { + return {value.toString()}; + } + + if (typeof value === "number") { + return {value}; + } + + if (typeof value === "string") { + return "{value}"; + } + + if (Array.isArray(value)) { + const isCollapsed = collapsed[key]; + const itemCount = value.length; + + if (itemCount === 0) { + return []; + } + + return ( +
+ + {!isCollapsed && ( +
+ {value.map((item, index) => ( +
+ {index}: + {renderValue(item, `${key}[${index}]`)} +
+ ))} +
+ )} +
+ ); + } + + if (typeof value === "object") { + const isCollapsed = collapsed[key]; + const keys = Object.keys(value); + + if (keys.length === 0) { + return {"{}"}; + } + + return ( +
+ + {!isCollapsed && ( +
+ {keys.map((objKey) => ( +
+ + "{objKey}": + + {renderValue(value[objKey], `${key}.${objKey}`)} +
+ ))} +
+ )} +
+ ); + } + + return {String(value)}; + }; + + return ( +
+ {renderValue(data, "root")} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/MarkdownRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/MarkdownRenderer.tsx new file mode 100644 index 0000000000..9b7f1c84ba --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/output-renderers/renderers/MarkdownRenderer.tsx @@ -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_) + /(?\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 + /(? 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 ( + + {url} + + ); + } + + if (videoExtensions.some((ext) => embedUrl.toLowerCase().includes(ext))) { + return ( +
+ +
+ ); + } + + return ( +
+
+