@larkup/workspace-app 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +4 -2
- package/assets/github.svg +3 -0
- package/assets/icons/Confluence.png +0 -0
- package/assets/icons/Instagram.png +0 -0
- package/assets/icons/Northflank.png +0 -0
- package/assets/icons/Salesforce.png +0 -0
- package/assets/icons/agentskills.png +0 -0
- package/assets/icons/airtable.png +0 -0
- package/assets/icons/asana.png +0 -0
- package/assets/icons/ashby.png +0 -0
- package/assets/icons/audio.png +0 -0
- package/assets/icons/aws-kendra.png +0 -0
- package/assets/icons/aws.svg +38 -0
- package/assets/icons/azure-ai-search.png +0 -0
- package/assets/icons/azure.svg +1 -0
- package/assets/icons/bigquery.png +0 -0
- package/assets/icons/bing.png +0 -0
- package/assets/icons/bitbucket.png +0 -0
- package/assets/icons/brave.png +0 -0
- package/assets/icons/browserbase.png +0 -0
- package/assets/icons/calendly.png +0 -0
- package/assets/icons/chroma.png +0 -0
- package/assets/icons/cloudflare.png +0 -0
- package/assets/icons/cohere.svg +30 -0
- package/assets/icons/databricks.png +0 -0
- package/assets/icons/daytonaio.png +0 -0
- package/assets/icons/daytonaio2.png +0 -0
- package/assets/icons/deepgram.png +0 -0
- package/assets/icons/deepl.png +0 -0
- package/assets/icons/digital-ocean.webp +0 -0
- package/assets/icons/discord.png +0 -0
- package/assets/icons/e2b-2.png +0 -0
- package/assets/icons/e2b.png +0 -0
- package/assets/icons/elevenlabs.png +0 -0
- package/assets/icons/email.png +0 -0
- package/assets/icons/exa.png +0 -0
- package/assets/icons/excel.png +0 -0
- package/assets/icons/fal.png +0 -0
- package/assets/icons/firecrawl.png +0 -0
- package/assets/icons/firecrawl2.png +0 -0
- package/assets/icons/flyio.jpeg +0 -0
- package/assets/icons/gcp.svg +1 -0
- package/assets/icons/gemini.svg +1 -0
- package/assets/icons/gmail.png +0 -0
- package/assets/icons/google-analytics.png +0 -0
- package/assets/icons/google-calender.png +0 -0
- package/assets/icons/google-docs.png +0 -0
- package/assets/icons/google-drive.png +0 -0
- package/assets/icons/google-maps.png +0 -0
- package/assets/icons/google-meet.png +0 -0
- package/assets/icons/google-sheets.png +0 -0
- package/assets/icons/google-slides.png +0 -0
- package/assets/icons/google-tasks.png +0 -0
- package/assets/icons/google.png +0 -0
- package/assets/icons/grok.png +0 -0
- package/assets/icons/hubspot.png +0 -0
- package/assets/icons/image.png +0 -0
- package/assets/icons/jina.svg +1 -0
- package/assets/icons/jira.png +0 -0
- package/assets/icons/lancedb2.png +0 -0
- package/assets/icons/linear.png +0 -0
- package/assets/icons/linux.png +0 -0
- package/assets/icons/looker.png +0 -0
- package/assets/icons/luma.png +0 -0
- package/assets/icons/mac.png +0 -0
- package/assets/icons/markdown.png +0 -0
- package/assets/icons/mcp.png +0 -0
- package/assets/icons/mcp.svg +1 -0
- package/assets/icons/messenger.svg +1 -0
- package/assets/icons/microsoft-intra.png +0 -0
- package/assets/icons/microsoft-onenote.png +0 -0
- package/assets/icons/microsoft-planner.png +0 -0
- package/assets/icons/microsoft-teams.png +0 -0
- package/assets/icons/miro.png +0 -0
- package/assets/icons/mistral.svg +469 -0
- package/assets/icons/modal.png +0 -0
- package/assets/icons/ngrok.png +0 -0
- package/assets/icons/nomic.png +0 -0
- package/assets/icons/onedrive.png +0 -0
- package/assets/icons/openai.svg +1 -0
- package/assets/icons/outlook-calendar.png +0 -0
- package/assets/icons/outlook-email.png +0 -0
- package/assets/icons/outlook.png +0 -0
- package/assets/icons/pdf.png +0 -0
- package/assets/icons/personio.png +0 -0
- package/assets/icons/pgvector2.png +0 -0
- package/assets/icons/phone.png +0 -0
- package/assets/icons/pinecone.png +0 -0
- package/assets/icons/pipedrive.png +0 -0
- package/assets/icons/power-pi.png +0 -0
- package/assets/icons/pylon.png +0 -0
- package/assets/icons/python.png +0 -0
- package/assets/icons/qdrant.svg +35 -0
- package/assets/icons/revai.png +0 -0
- package/assets/icons/revai.svg +1 -0
- package/assets/icons/sap.png +0 -0
- package/assets/icons/serpapi.svg +34 -0
- package/assets/icons/serper.png +0 -0
- package/assets/icons/servicenow.png +0 -0
- package/assets/icons/sharepoint.png +0 -0
- package/assets/icons/slack.png +0 -0
- package/assets/icons/snowflake.png +0 -0
- package/assets/icons/statistia.png +0 -0
- package/assets/icons/stripe.png +0 -0
- package/assets/icons/supabase.png +0 -0
- package/assets/icons/tableau.png +0 -0
- package/assets/icons/tavily.png +0 -0
- package/assets/icons/telegram.png +0 -0
- package/assets/icons/typescript.png +0 -0
- package/assets/icons/vercel copy.svg +1 -0
- package/assets/icons/vercel.svg +1 -0
- package/assets/icons/vertex-ai.png +0 -0
- package/assets/icons/video.png +0 -0
- package/assets/icons/voyage-light.png +0 -0
- package/assets/icons/weaviate.webp +0 -0
- package/assets/icons/webcontainers.svg +11 -0
- package/assets/icons/webhook.png +0 -0
- package/assets/icons/whatsapp copy.png +0 -0
- package/assets/icons/whatsapp.png +0 -0
- package/assets/icons/windows.png +0 -0
- package/assets/icons/word.png +0 -0
- package/assets/icons/zendesk.png +0 -0
- package/assets/icons/zoom.png +0 -0
- package/assets/notion-white.png +0 -0
- package/assets/notion.png +0 -0
- package/dist-types/asset-server.d.ts +3 -0
- package/dist-types/asset-server.d.ts.map +1 -0
- package/dist-types/components/alerts/generic-alert.d.ts +15 -0
- package/dist-types/components/alerts/generic-alert.d.ts.map +1 -0
- package/dist-types/components/chat/canvas/doc-editor-provider.d.ts +62 -0
- package/dist-types/components/chat/canvas/doc-editor-provider.d.ts.map +1 -0
- package/dist-types/components/chat/canvas/document-canvas.d.ts +7 -0
- package/dist-types/components/chat/canvas/document-canvas.d.ts.map +1 -0
- package/dist-types/components/chat/canvas/document-field.d.ts +13 -0
- package/dist-types/components/chat/canvas/document-field.d.ts.map +1 -0
- package/dist-types/components/chat/canvas/document-viewer.d.ts +2 -0
- package/dist-types/components/chat/canvas/document-viewer.d.ts.map +1 -0
- package/dist-types/components/chat/canvas/file-attachment-button.d.ts +9 -0
- package/dist-types/components/chat/canvas/file-attachment-button.d.ts.map +1 -0
- package/dist-types/components/chat/chat-workspace.d.ts +4 -0
- package/dist-types/components/chat/chat-workspace.d.ts.map +1 -0
- package/dist-types/components/chat/message-actions.d.ts +11 -0
- package/dist-types/components/chat/message-actions.d.ts.map +1 -0
- package/dist-types/components/chat/message-item.d.ts +20 -0
- package/dist-types/components/chat/message-item.d.ts.map +1 -0
- package/dist-types/components/chat/message-queue.d.ts +15 -0
- package/dist-types/components/chat/message-queue.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-chart.d.ts +6 -0
- package/dist-types/components/chat/tools/chat-chart.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-citations.d.ts +32 -0
- package/dist-types/components/chat/tools/chat-citations.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-data-table.d.ts +14 -0
- package/dist-types/components/chat/tools/chat-data-table.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-file-export.d.ts +5 -0
- package/dist-types/components/chat/tools/chat-file-export.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-media-preview.d.ts +35 -0
- package/dist-types/components/chat/tools/chat-media-preview.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-sandbox-result.d.ts +17 -0
- package/dist-types/components/chat/tools/chat-sandbox-result.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-signature-request.d.ts +13 -0
- package/dist-types/components/chat/tools/chat-signature-request.d.ts.map +1 -0
- package/dist-types/components/chat/tools/chat-tabs.d.ts +17 -0
- package/dist-types/components/chat/tools/chat-tabs.d.ts.map +1 -0
- package/dist-types/components/chat/tools/corpus-data-result.d.ts +36 -0
- package/dist-types/components/chat/tools/corpus-data-result.d.ts.map +1 -0
- package/dist-types/components/chat/tools/knowledge-base-result.d.ts +14 -0
- package/dist-types/components/chat/tools/knowledge-base-result.d.ts.map +1 -0
- package/dist-types/components/chat/tools/visual-placement-modal.d.ts +23 -0
- package/dist-types/components/chat/tools/visual-placement-modal.d.ts.map +1 -0
- package/dist-types/components/configure/custom-model-modal.d.ts +13 -0
- package/dist-types/components/configure/custom-model-modal.d.ts.map +1 -0
- package/dist-types/components/configure/store-fields.d.ts +11 -0
- package/dist-types/components/configure/store-fields.d.ts.map +1 -0
- package/dist-types/components/data/corpus-panel.d.ts +12 -0
- package/dist-types/components/data/corpus-panel.d.ts.map +1 -0
- package/dist-types/components/data/data-entry-mode-switch.d.ts +13 -0
- package/dist-types/components/data/data-entry-mode-switch.d.ts.map +1 -0
- package/dist-types/components/data/data-primary-action.d.ts +7 -0
- package/dist-types/components/data/data-primary-action.d.ts.map +1 -0
- package/dist-types/components/data/data-workspace.d.ts +6 -0
- package/dist-types/components/data/data-workspace.d.ts.map +1 -0
- package/dist-types/components/data/groups-panel.d.ts +5 -0
- package/dist-types/components/data/groups-panel.d.ts.map +1 -0
- package/dist-types/components/data/integration-resources-panel.d.ts +9 -0
- package/dist-types/components/data/integration-resources-panel.d.ts.map +1 -0
- package/dist-types/components/data/integrations-panel.d.ts +5 -0
- package/dist-types/components/data/integrations-panel.d.ts.map +1 -0
- package/dist-types/components/data/jobs-panel.d.ts +6 -0
- package/dist-types/components/data/jobs-panel.d.ts.map +1 -0
- package/dist-types/components/data/jobs-tracking-button.d.ts +2 -0
- package/dist-types/components/data/jobs-tracking-button.d.ts.map +1 -0
- package/dist-types/components/data/live-media-progress.d.ts +9 -0
- package/dist-types/components/data/live-media-progress.d.ts.map +1 -0
- package/dist-types/components/data/media-indexing-jobs-panel.d.ts +34 -0
- package/dist-types/components/data/media-indexing-jobs-panel.d.ts.map +1 -0
- package/dist-types/components/data/media-panel.d.ts +58 -0
- package/dist-types/components/data/media-panel.d.ts.map +1 -0
- package/dist-types/components/data/notion-panel.d.ts +6 -0
- package/dist-types/components/data/notion-panel.d.ts.map +1 -0
- package/dist-types/components/data/paste-panel.d.ts +7 -0
- package/dist-types/components/data/paste-panel.d.ts.map +1 -0
- package/dist-types/components/data/project-data-page.d.ts +3 -0
- package/dist-types/components/data/project-data-page.d.ts.map +1 -0
- package/dist-types/components/data/scrape-panel.d.ts +9 -0
- package/dist-types/components/data/scrape-panel.d.ts.map +1 -0
- package/dist-types/components/data/tabular-preview.d.ts +10 -0
- package/dist-types/components/data/tabular-preview.d.ts.map +1 -0
- package/dist-types/components/data/tool-indexing-dialog.d.ts +54 -0
- package/dist-types/components/data/tool-indexing-dialog.d.ts.map +1 -0
- package/dist-types/components/data/upload-panel.d.ts +7 -0
- package/dist-types/components/data/upload-panel.d.ts.map +1 -0
- package/dist-types/components/data/web-crawler-status.d.ts +3 -0
- package/dist-types/components/data/web-crawler-status.d.ts.map +1 -0
- package/dist-types/components/projects/project-form-dialog.d.ts +10 -0
- package/dist-types/components/projects/project-form-dialog.d.ts.map +1 -0
- package/dist-types/components/projects/project-provider.d.ts +34 -0
- package/dist-types/components/projects/project-provider.d.ts.map +1 -0
- package/dist-types/components/settings/general/agent-customization-section.d.ts +2 -0
- package/dist-types/components/settings/general/agent-customization-section.d.ts.map +1 -0
- package/dist-types/components/settings/general/mcp-connections-section.d.ts +2 -0
- package/dist-types/components/settings/general/mcp-connections-section.d.ts.map +1 -0
- package/dist-types/components/settings/general/model-combobox.d.ts +24 -0
- package/dist-types/components/settings/general/model-combobox.d.ts.map +1 -0
- package/dist-types/components/settings/general/models-section.d.ts +2 -0
- package/dist-types/components/settings/general/models-section.d.ts.map +1 -0
- package/dist-types/components/settings/general/sandbox-section.d.ts +7 -0
- package/dist-types/components/settings/general/sandbox-section.d.ts.map +1 -0
- package/dist-types/components/settings/general/search-scraping-section.d.ts +2 -0
- package/dist-types/components/settings/general/search-scraping-section.d.ts.map +1 -0
- package/dist-types/components/settings/general/storage-indexing-section.d.ts +2 -0
- package/dist-types/components/settings/general/storage-indexing-section.d.ts.map +1 -0
- package/dist-types/components/ui/alert-dialog.d.ts +19 -0
- package/dist-types/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist-types/components/ui/alert.d.ts +11 -0
- package/dist-types/components/ui/alert.d.ts.map +1 -0
- package/dist-types/components/ui/badge.d.ts +8 -0
- package/dist-types/components/ui/badge.d.ts.map +1 -0
- package/dist-types/components/ui/card.d.ts +2 -0
- package/dist-types/components/ui/card.d.ts.map +1 -0
- package/dist-types/components/ui/checkbox.d.ts +4 -0
- package/dist-types/components/ui/checkbox.d.ts.map +1 -0
- package/dist-types/components/ui/collapsible.d.ts +6 -0
- package/dist-types/components/ui/collapsible.d.ts.map +1 -0
- package/dist-types/components/ui/command.d.ts +20 -0
- package/dist-types/components/ui/command.d.ts.map +1 -0
- package/dist-types/components/ui/dialog.d.ts +18 -0
- package/dist-types/components/ui/dialog.d.ts.map +1 -0
- package/dist-types/components/ui/dropdown-menu.d.ts +30 -0
- package/dist-types/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist-types/components/ui/input-group.d.ts +19 -0
- package/dist-types/components/ui/input-group.d.ts.map +1 -0
- package/dist-types/components/ui/input.d.ts +4 -0
- package/dist-types/components/ui/input.d.ts.map +1 -0
- package/dist-types/components/ui/line-tabs.d.ts +15 -0
- package/dist-types/components/ui/line-tabs.d.ts.map +1 -0
- package/dist-types/components/ui/popover.d.ts +10 -0
- package/dist-types/components/ui/popover.d.ts.map +1 -0
- package/dist-types/components/ui/progress.d.ts +8 -0
- package/dist-types/components/ui/progress.d.ts.map +1 -0
- package/dist-types/components/ui/scroll-area.d.ts +6 -0
- package/dist-types/components/ui/scroll-area.d.ts.map +1 -0
- package/dist-types/components/ui/sheet.d.ts +15 -0
- package/dist-types/components/ui/sheet.d.ts.map +1 -0
- package/dist-types/components/ui/slider.d.ts +4 -0
- package/dist-types/components/ui/slider.d.ts.map +1 -0
- package/dist-types/components/ui/switch.d.ts +6 -0
- package/dist-types/components/ui/switch.d.ts.map +1 -0
- package/dist-types/components/ui/table.d.ts +2 -0
- package/dist-types/components/ui/table.d.ts.map +1 -0
- package/dist-types/components/ui/tabs.d.ts +11 -0
- package/dist-types/components/ui/tabs.d.ts.map +1 -0
- package/dist-types/components/ui/textarea.d.ts +2 -0
- package/dist-types/components/ui/textarea.d.ts.map +1 -0
- package/dist-types/components/ui/tooltip.d.ts +7 -0
- package/dist-types/components/ui/tooltip.d.ts.map +1 -0
- package/dist-types/hooks/use-copy-to-clipboard.d.ts +5 -0
- package/dist-types/hooks/use-copy-to-clipboard.d.ts.map +1 -0
- package/dist-types/hooks/use-integration-auth.d.ts +10 -0
- package/dist-types/hooks/use-integration-auth.d.ts.map +1 -0
- package/dist-types/hooks/use-live-media-progress.d.ts +14 -0
- package/dist-types/hooks/use-live-media-progress.d.ts.map +1 -0
- package/dist-types/hooks/use-notion-auth.d.ts +9 -0
- package/dist-types/hooks/use-notion-auth.d.ts.map +1 -0
- package/dist-types/host.d.ts +20 -0
- package/dist-types/host.d.ts.map +1 -0
- package/dist-types/lib/browser-cache.d.ts +7 -0
- package/dist-types/lib/browser-cache.d.ts.map +1 -0
- package/dist-types/lib/chat/chart-config.d.ts +2 -0
- package/dist-types/lib/chat/chart-config.d.ts.map +1 -0
- package/dist-types/lib/chat/data-export.d.ts +2 -0
- package/dist-types/lib/chat/data-export.d.ts.map +1 -0
- package/dist-types/lib/chat/live-tool-progress.d.ts +44 -0
- package/dist-types/lib/chat/live-tool-progress.d.ts.map +1 -0
- package/dist-types/lib/chat/message-feedback.d.ts +2 -0
- package/dist-types/lib/chat/message-feedback.d.ts.map +1 -0
- package/dist-types/lib/chat/table-presentation.d.ts +2 -0
- package/dist-types/lib/chat/table-presentation.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-output.d.ts +3 -0
- package/dist-types/lib/chat/tool-output.d.ts.map +1 -0
- package/dist-types/lib/chat-supporting-clip.d.ts +11 -0
- package/dist-types/lib/chat-supporting-clip.d.ts.map +1 -0
- package/dist-types/lib/constants/stores.d.ts +6 -0
- package/dist-types/lib/constants/stores.d.ts.map +1 -0
- package/dist-types/lib/constants/tools.d.ts +27 -0
- package/dist-types/lib/constants/tools.d.ts.map +1 -0
- package/dist-types/lib/error-formatter.d.ts +2 -0
- package/dist-types/lib/error-formatter.d.ts.map +1 -0
- package/dist-types/lib/jobs-client.d.ts +7 -0
- package/dist-types/lib/jobs-client.d.ts.map +1 -0
- package/dist-types/lib/media/pdf.d.ts +6 -0
- package/dist-types/lib/media/pdf.d.ts.map +1 -0
- package/dist-types/lib/media/progress.d.ts +61 -0
- package/dist-types/lib/media/progress.d.ts.map +1 -0
- package/dist-types/lib/media/source-selection.d.ts +10 -0
- package/dist-types/lib/media/source-selection.d.ts.map +1 -0
- package/dist-types/lib/media/tool-indexing-estimate.d.ts +13 -0
- package/dist-types/lib/media/tool-indexing-estimate.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-capacity.d.ts +2 -0
- package/dist-types/lib/media/video-intelligence-capacity.d.ts.map +1 -0
- package/dist-types/lib/media/video-runtime-scope.d.ts +2 -0
- package/dist-types/lib/media/video-runtime-scope.d.ts.map +1 -0
- package/dist-types/lib/shared/error-formatter.d.ts +2 -0
- package/dist-types/lib/shared/error-formatter.d.ts.map +1 -0
- package/dist-types/lib/utils.d.ts +3 -0
- package/dist-types/lib/utils.d.ts.map +1 -0
- package/dist-types/storage.d.ts +5 -0
- package/dist-types/storage.d.ts.map +1 -0
- package/dist-types/store/chat-store.d.ts +19 -0
- package/dist-types/store/chat-store.d.ts.map +1 -0
- package/dist-types/store/scrape-store.d.ts +42 -0
- package/dist-types/store/scrape-store.d.ts.map +1 -0
- package/package.json +579 -3
- package/scripts/build-assets.mjs +12 -0
- package/src/asset-data.json +1 -0
- package/src/asset-server.ts +19 -0
- package/src/components/alerts/generic-alert.tsx +76 -0
- package/src/components/chat/canvas/doc-editor-provider.tsx +338 -0
- package/src/components/chat/canvas/document-canvas.tsx +98 -0
- package/src/components/chat/canvas/document-field.tsx +167 -0
- package/src/components/chat/canvas/document-viewer.tsx +446 -0
- package/src/components/chat/canvas/file-attachment-button.tsx +88 -0
- package/src/components/chat/chat-workspace.tsx +1552 -0
- package/src/components/chat/message-actions.tsx +96 -0
- package/src/components/chat/message-item.tsx +1398 -0
- package/src/components/chat/message-queue.tsx +86 -0
- package/src/components/chat/tools/chat-chart.tsx +558 -0
- package/src/components/chat/tools/chat-citations.tsx +152 -0
- package/src/components/chat/tools/chat-data-table.tsx +373 -0
- package/src/components/chat/tools/chat-file-export.tsx +49 -0
- package/src/components/chat/tools/chat-media-preview.tsx +466 -0
- package/src/components/chat/tools/chat-sandbox-result.tsx +130 -0
- package/src/components/chat/tools/chat-signature-request.tsx +810 -0
- package/src/components/chat/tools/chat-tabs.tsx +59 -0
- package/src/components/chat/tools/corpus-data-result.tsx +194 -0
- package/src/components/chat/tools/knowledge-base-result.tsx +345 -0
- package/src/components/chat/tools/visual-placement-modal.tsx +308 -0
- package/src/components/configure/custom-model-modal.tsx +209 -0
- package/src/components/configure/store-fields.tsx +129 -0
- package/src/components/data/corpus-panel.tsx +1223 -0
- package/src/components/data/data-entry-mode-switch.tsx +53 -0
- package/src/components/data/data-primary-action.ts +6 -0
- package/src/components/data/data-workspace.tsx +623 -0
- package/src/components/data/groups-panel.tsx +530 -0
- package/src/components/data/integration-resources-panel.tsx +282 -0
- package/src/components/data/integrations-panel.tsx +392 -0
- package/src/components/data/jobs-panel.tsx +490 -0
- package/src/components/data/jobs-tracking-button.tsx +224 -0
- package/src/components/data/live-media-progress.tsx +79 -0
- package/src/components/data/media-indexing-jobs-panel.tsx +190 -0
- package/src/components/data/media-panel.tsx +2905 -0
- package/src/components/data/notion-panel.tsx +408 -0
- package/src/components/data/paste-panel.tsx +82 -0
- package/src/components/data/project-data-page.tsx +55 -0
- package/src/components/data/scrape-panel.tsx +1290 -0
- package/src/components/data/tabular-preview.tsx +205 -0
- package/src/components/data/tool-indexing-dialog.tsx +210 -0
- package/src/components/data/upload-panel.tsx +1313 -0
- package/src/components/data/web-crawler-status.tsx +132 -0
- package/src/components/projects/project-form-dialog.tsx +97 -0
- package/src/components/projects/project-provider.tsx +143 -0
- package/src/components/settings/general/agent-customization-section.tsx +620 -0
- package/src/components/settings/general/mcp-connections-section.tsx +907 -0
- package/src/components/settings/general/model-combobox.tsx +235 -0
- package/src/components/settings/general/models-section.tsx +909 -0
- package/src/components/settings/general/sandbox-section.tsx +394 -0
- package/src/components/settings/general/search-scraping-section.tsx +757 -0
- package/src/components/settings/general/storage-indexing-section.tsx +394 -0
- package/src/components/ui/alert-dialog.tsx +162 -0
- package/src/components/ui/alert.tsx +69 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/card.tsx +9 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/command.tsx +175 -0
- package/src/components/ui/dialog.tsx +138 -0
- package/src/components/ui/dropdown-menu.tsx +267 -0
- package/src/components/ui/input-group.tsx +146 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/line-tabs.tsx +49 -0
- package/src/components/ui/popover.tsx +77 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/scroll-area.tsx +51 -0
- package/src/components/ui/sheet.tsx +125 -0
- package/src/components/ui/slider.tsx +52 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +10 -0
- package/src/components/ui/tabs.tsx +77 -0
- package/src/components/ui/textarea.tsx +1 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +25 -0
- package/src/hooks/use-integration-auth.ts +39 -0
- package/src/hooks/use-live-media-progress.ts +71 -0
- package/src/hooks/use-notion-auth.ts +66 -0
- package/src/host.ts +55 -0
- package/src/lib/browser-cache.ts +17 -0
- package/src/lib/chat/chart-config.ts +1 -0
- package/src/lib/chat/data-export.ts +1 -0
- package/src/lib/chat/live-tool-progress.ts +69 -0
- package/src/lib/chat/message-feedback.ts +1 -0
- package/src/lib/chat/table-presentation.ts +1 -0
- package/src/lib/chat/tool-output.ts +14 -0
- package/src/lib/chat-supporting-clip.ts +16 -0
- package/src/lib/constants/stores.ts +12 -0
- package/src/lib/constants/tools.ts +178 -0
- package/src/lib/error-formatter.ts +30 -0
- package/src/lib/jobs-client.ts +27 -0
- package/src/lib/media/pdf.ts +57 -0
- package/src/lib/media/progress.ts +209 -0
- package/src/lib/media/source-selection.ts +47 -0
- package/src/lib/media/tool-indexing-estimate.ts +39 -0
- package/src/lib/media/video-intelligence-capacity.ts +1 -0
- package/src/lib/media/video-runtime-scope.ts +1 -0
- package/src/lib/shared/error-formatter.ts +1 -0
- package/src/lib/utils.ts +6 -0
- package/src/source.css +1 -0
- package/src/storage.ts +16 -0
- package/src/store/chat-store.ts +19 -0
- package/src/store/scrape-store.ts +76 -0
- package/src/styles.d.ts +1 -0
|
@@ -0,0 +1,1398 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { workspaceUrl, workspaceFetch as fetch } from '../../host';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { ChatUserBubble } from '@larkup/workspace-ui';
|
|
6
|
+
import useSWR from 'swr';
|
|
7
|
+
import type { UIMessage } from 'ai';
|
|
8
|
+
import katex from 'katex';
|
|
9
|
+
import 'katex/dist/katex.min.css';
|
|
10
|
+
import { MessageActions } from './message-actions';
|
|
11
|
+
import { ChatChart, type ChartConfig } from './tools/chat-chart';
|
|
12
|
+
import { ChatDataTable, type DataTableConfig } from './tools/chat-data-table';
|
|
13
|
+
import { ChatFileExport } from './tools/chat-file-export';
|
|
14
|
+
import { ChatSandboxResult, type SandboxResultConfig } from './tools/chat-sandbox-result';
|
|
15
|
+
import { ChatTabs } from './tools/chat-tabs';
|
|
16
|
+
import { CorpusDataResult, type CorpusDataConfig } from './tools/corpus-data-result';
|
|
17
|
+
import { ChatSignatureRequest } from './tools/chat-signature-request';
|
|
18
|
+
import { Sparkles, FileEdit, CheckCircle2, Globe, ChevronDown } from 'lucide-react';
|
|
19
|
+
import { ChatMediaPreview } from './tools/chat-media-preview';
|
|
20
|
+
import { ChatCitations, type ChatCitationsUi } from './tools/chat-citations';
|
|
21
|
+
import { KnowledgeBaseResult } from './tools/knowledge-base-result';
|
|
22
|
+
import { useDocEditor } from './canvas/doc-editor-provider';
|
|
23
|
+
import { CHAT_TOOL_BEHAVIORS, getChatToolBehavior } from '../../lib/constants/tools';
|
|
24
|
+
import { decodeToolOutput } from '../../lib/chat/tool-output';
|
|
25
|
+
import {
|
|
26
|
+
isIndeterminateProgress,
|
|
27
|
+
keepToolProgressMonotonic,
|
|
28
|
+
smoothLiveToolProgress,
|
|
29
|
+
smoothPendingToolProgress,
|
|
30
|
+
type LiveToolActivity,
|
|
31
|
+
} from '../../lib/chat/live-tool-progress';
|
|
32
|
+
import { answerFeedbackForMessage, type AnswerFeedback } from '../../lib/chat/message-feedback';
|
|
33
|
+
|
|
34
|
+
export function MessageItem({
|
|
35
|
+
message,
|
|
36
|
+
isLast,
|
|
37
|
+
isStreaming,
|
|
38
|
+
addToolResult,
|
|
39
|
+
serverId,
|
|
40
|
+
autoOpenSupportingClip = false,
|
|
41
|
+
regenerate,
|
|
42
|
+
isBusy,
|
|
43
|
+
onFeedback,
|
|
44
|
+
}: {
|
|
45
|
+
message: UIMessage;
|
|
46
|
+
isLast?: boolean;
|
|
47
|
+
isStreaming?: boolean;
|
|
48
|
+
addToolResult?: Function;
|
|
49
|
+
serverId?: string | null;
|
|
50
|
+
/** Only the first assistant message in a chat may expand a clip initially. */
|
|
51
|
+
autoOpenSupportingClip?: boolean;
|
|
52
|
+
regenerate?: (options?: { messageId?: string }) => void;
|
|
53
|
+
/** True while a request is in flight — hides "Regenerate" on a trailing user message that's still waiting on a reply. */
|
|
54
|
+
isBusy?: boolean;
|
|
55
|
+
onFeedback?: (messageId: string, feedback: AnswerFeedback | undefined) => void;
|
|
56
|
+
}) {
|
|
57
|
+
const isUser = message.role === 'user';
|
|
58
|
+
const [progressClock, setProgressClock] = useState(() => Date.now());
|
|
59
|
+
const pendingToolStartedAt = useRef<Record<string, number>>({});
|
|
60
|
+
const [lastToolActivity, setLastToolActivity] = useState<LiveToolActivity | null>(null);
|
|
61
|
+
const [toolProgressFloors, setToolProgressFloors] = useState<Record<string, number>>({});
|
|
62
|
+
|
|
63
|
+
// The server gives us authoritative stage updates; this lightweight clock
|
|
64
|
+
// only animates the safe in-between estimate above. It is scoped to the
|
|
65
|
+
// active streamed answer so completed chat history never re-renders.
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!isLast || !isStreaming) return;
|
|
68
|
+
setProgressClock(Date.now());
|
|
69
|
+
const timer = window.setInterval(() => setProgressClock(Date.now()), 500);
|
|
70
|
+
return () => window.clearInterval(timer);
|
|
71
|
+
}, [isLast, isStreaming]);
|
|
72
|
+
|
|
73
|
+
// Any long-running tool call (any installed tool, not just video) can
|
|
74
|
+
// report live progress keyed by its own toolCallId -- see
|
|
75
|
+
// gpu-activity-store.ts. Only the currently-streaming message ever has an
|
|
76
|
+
// executing tool part, so there's nothing to match against otherwise.
|
|
77
|
+
const { data: liveToolActivity } = useSWR<{ activity: LiveToolActivity | null }>(
|
|
78
|
+
isLast && isStreaming
|
|
79
|
+
? workspaceUrl(
|
|
80
|
+
`/api/gpu-activity${serverId ? `?projectId=${encodeURIComponent(serverId)}` : ''}`,
|
|
81
|
+
)
|
|
82
|
+
: null,
|
|
83
|
+
(url: string) => fetch(url).then((r) => r.json()),
|
|
84
|
+
{
|
|
85
|
+
refreshInterval: 800,
|
|
86
|
+
dedupingInterval: 0,
|
|
87
|
+
keepPreviousData: true,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
const currentToolActivity =
|
|
91
|
+
liveToolActivity?.activity?.toolCallId && liveToolActivity.activity.phase
|
|
92
|
+
? liveToolActivity.activity
|
|
93
|
+
: lastToolActivity;
|
|
94
|
+
const smoothedToolPercent = currentToolActivity
|
|
95
|
+
? smoothLiveToolProgress(currentToolActivity, progressClock)
|
|
96
|
+
: 0;
|
|
97
|
+
const priorToolPercent = currentToolActivity?.toolCallId
|
|
98
|
+
? toolProgressFloors[currentToolActivity.toolCallId]
|
|
99
|
+
: undefined;
|
|
100
|
+
const visibleToolPercent = keepToolProgressMonotonic(priorToolPercent, smoothedToolPercent);
|
|
101
|
+
|
|
102
|
+
// A query may inspect several short source ranges in succession. The API
|
|
103
|
+
// clears each finished range before the next starts, but they are one chat
|
|
104
|
+
// action. Preserve its last visual floor and activity through that handoff
|
|
105
|
+
// so the inline bar never drops back to the beginning.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!liveToolActivity?.activity?.toolCallId) return;
|
|
108
|
+
setLastToolActivity(liveToolActivity.activity);
|
|
109
|
+
const callId = liveToolActivity.activity.toolCallId;
|
|
110
|
+
const next = smoothLiveToolProgress(liveToolActivity.activity, progressClock);
|
|
111
|
+
setToolProgressFloors((current) => {
|
|
112
|
+
const floor = keepToolProgressMonotonic(current[callId], next);
|
|
113
|
+
return current[callId] === floor ? current : { ...current, [callId]: floor };
|
|
114
|
+
});
|
|
115
|
+
}, [liveToolActivity?.activity, progressClock]);
|
|
116
|
+
// Shown for both phases -- a silent "Working…" row for the whole time a
|
|
117
|
+
// cold GPU worker takes to wake up reads as hung, even though the
|
|
118
|
+
// dedicated bottom-right ring is also showing that same wait.
|
|
119
|
+
const activeToolProgress = currentToolActivity?.toolCallId
|
|
120
|
+
? {
|
|
121
|
+
toolCallId: currentToolActivity.toolCallId,
|
|
122
|
+
percent: visibleToolPercent,
|
|
123
|
+
label: currentToolActivity.label,
|
|
124
|
+
message: currentToolActivity.message || currentToolActivity.label,
|
|
125
|
+
phase: currentToolActivity.phase,
|
|
126
|
+
}
|
|
127
|
+
: null;
|
|
128
|
+
|
|
129
|
+
let updateFromToolResult: ((result: any) => void) | undefined;
|
|
130
|
+
let openCanvas: ((file: File, options?: any) => Promise<void>) | undefined;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const editor = useDocEditor();
|
|
134
|
+
updateFromToolResult = editor.updateFromToolResult;
|
|
135
|
+
openCanvas = editor.openCanvas;
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
const anyMessage = message as any;
|
|
139
|
+
const parts: any[] = useMemo(() => {
|
|
140
|
+
const p: any[] = message.parts ? [...message.parts] : [];
|
|
141
|
+
if (anyMessage.toolInvocations && Array.isArray(anyMessage.toolInvocations)) {
|
|
142
|
+
anyMessage.toolInvocations.forEach((t: any) => {
|
|
143
|
+
if (
|
|
144
|
+
!p.some(
|
|
145
|
+
(existing: any) =>
|
|
146
|
+
existing.type === 'tool-invocation' &&
|
|
147
|
+
existing.toolInvocation?.toolCallId === t.toolCallId,
|
|
148
|
+
)
|
|
149
|
+
) {
|
|
150
|
+
p.push({ type: 'tool-invocation', toolInvocation: t });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return p;
|
|
155
|
+
}, [message.parts, anyMessage.toolInvocations]);
|
|
156
|
+
|
|
157
|
+
const appliedToolCallsRef = useRef<Set<string>>(new Set());
|
|
158
|
+
|
|
159
|
+
const docToolFingerprint = useMemo(() => {
|
|
160
|
+
if (!isLast) return '';
|
|
161
|
+
return parts
|
|
162
|
+
.filter((p: any) => {
|
|
163
|
+
const info = getToolInfo(p);
|
|
164
|
+
return (
|
|
165
|
+
info.isCompleted &&
|
|
166
|
+
info.output?.success &&
|
|
167
|
+
(info.toolName === 'fillDocumentForm' ||
|
|
168
|
+
info.toolName === 'editDocument' ||
|
|
169
|
+
info.toolName === 'requestDocumentSignature')
|
|
170
|
+
);
|
|
171
|
+
})
|
|
172
|
+
.map((p: any) => {
|
|
173
|
+
const ti = p.toolInvocation ?? p;
|
|
174
|
+
return ti.toolCallId || ti.id || '';
|
|
175
|
+
})
|
|
176
|
+
.join(',');
|
|
177
|
+
}, [parts, isLast]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!updateFromToolResult || !isLast || !docToolFingerprint) return;
|
|
181
|
+
|
|
182
|
+
const completedDocTools = parts.filter((p: any) => {
|
|
183
|
+
const info = getToolInfo(p);
|
|
184
|
+
return (
|
|
185
|
+
info.isCompleted &&
|
|
186
|
+
info.output?.success &&
|
|
187
|
+
(info.toolName === 'fillDocumentForm' ||
|
|
188
|
+
info.toolName === 'editDocument' ||
|
|
189
|
+
info.toolName === 'requestDocumentSignature')
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (completedDocTools.length > 0) {
|
|
194
|
+
completedDocTools.forEach((part: any) => {
|
|
195
|
+
const ti = part.toolInvocation ?? part;
|
|
196
|
+
const callId = ti.toolCallId || ti.id || '';
|
|
197
|
+
|
|
198
|
+
// Skip if we already applied this specific tool result
|
|
199
|
+
if (callId && appliedToolCallsRef.current.has(callId)) return;
|
|
200
|
+
|
|
201
|
+
const output = getToolInfo(part).output;
|
|
202
|
+
if (output && output.fileBase64) {
|
|
203
|
+
if (callId) appliedToolCallsRef.current.add(callId);
|
|
204
|
+
updateFromToolResult(output);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}, [docToolFingerprint, updateFromToolResult, isLast]);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!isLast || !addToolResult) return;
|
|
212
|
+
const signatureTools = parts.filter(
|
|
213
|
+
(p: any) => getToolInfo(p).toolName === 'requestDocumentSignature',
|
|
214
|
+
);
|
|
215
|
+
if (signatureTools.length > 1) {
|
|
216
|
+
// Keep the first one, auto-resolve the rest
|
|
217
|
+
for (let i = 1; i < signatureTools.length; i++) {
|
|
218
|
+
const info = getToolInfo(signatureTools[i]);
|
|
219
|
+
if (info.isExecuting) {
|
|
220
|
+
const ti = signatureTools[i].toolInvocation ?? signatureTools[i];
|
|
221
|
+
const callId = ti.toolCallId || ti.id;
|
|
222
|
+
if (callId && !appliedToolCallsRef.current.has(callId)) {
|
|
223
|
+
appliedToolCallsRef.current.add(callId);
|
|
224
|
+
addToolResult({
|
|
225
|
+
toolCallId: callId,
|
|
226
|
+
result: { success: true, message: 'Duplicate signature request auto-resolved.' },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, [parts, isLast, addToolResult]);
|
|
233
|
+
|
|
234
|
+
if (isUser) {
|
|
235
|
+
const text = parts
|
|
236
|
+
.filter((p: any) => p.type === 'text')
|
|
237
|
+
.map((p: any) => p.text)
|
|
238
|
+
.join('');
|
|
239
|
+
|
|
240
|
+
const attachments = anyMessage.experimental_attachments || [];
|
|
241
|
+
|
|
242
|
+
const partAttachments = parts
|
|
243
|
+
.filter((p: any) => p.type === 'image' || p.type === 'file')
|
|
244
|
+
.map((p: any) => {
|
|
245
|
+
let name = p.name;
|
|
246
|
+
let cType = p.mimeType || p.contentType || (p.type === 'image' ? 'image/png' : '');
|
|
247
|
+
|
|
248
|
+
if (!cType && p.url && typeof p.url === 'string' && p.url.startsWith('data:')) {
|
|
249
|
+
const match = p.url.match(/^data:([^;]+);/);
|
|
250
|
+
if (match) cType = match[1];
|
|
251
|
+
}
|
|
252
|
+
if (!cType) cType = 'application/octet-stream';
|
|
253
|
+
|
|
254
|
+
if (!name) {
|
|
255
|
+
if (p.url && typeof p.url === 'string' && !p.url.startsWith('data:')) {
|
|
256
|
+
name = p.url.split('/').pop()?.split('?')[0];
|
|
257
|
+
} else {
|
|
258
|
+
name = p.type === 'image' ? 'image.png' : 'Document';
|
|
259
|
+
if (cType === 'application/pdf') name = 'Document.pdf';
|
|
260
|
+
else if (cType === 'text/plain') name = 'Text.txt';
|
|
261
|
+
else if (cType.includes('word')) name = 'Document.docx';
|
|
262
|
+
else if (cType.includes('excel') || cType.includes('spreadsheet')) name = 'Data.xlsx';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
url: p.image || p.data || p.url,
|
|
267
|
+
contentType: cType,
|
|
268
|
+
name,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Deduplicate or combine (usually they are either in experimental_attachments or parts, rarely both)
|
|
273
|
+
const allAttachments = attachments.length > 0 ? attachments : partAttachments;
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div className="message user-message group flex justify-end" data-role="user">
|
|
277
|
+
<div className="max-w-[85%] flex flex-col gap-2 items-end">
|
|
278
|
+
{allAttachments.length > 0 && (
|
|
279
|
+
<div className="flex flex-wrap justify-end gap-2">
|
|
280
|
+
{allAttachments.map((att: any, i: number) => {
|
|
281
|
+
let cType = att.contentType || att.type || '';
|
|
282
|
+
const isImage = cType.startsWith('image/');
|
|
283
|
+
const isVideo = cType.startsWith('video/');
|
|
284
|
+
const isAudio = cType.startsWith('audio/');
|
|
285
|
+
let nameStr = att.name;
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
!cType &&
|
|
289
|
+
att.url &&
|
|
290
|
+
typeof att.url === 'string' &&
|
|
291
|
+
att.url.startsWith('data:')
|
|
292
|
+
) {
|
|
293
|
+
const match = att.url.match(/^data:([^;]+);/);
|
|
294
|
+
if (match) cType = match[1];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// AI SDK sometimes loses the MIME type and defaults to application/octet-stream
|
|
298
|
+
// We can sniff PDF magic bytes `%PDF-` -> `JVBERi0` in base64
|
|
299
|
+
if (
|
|
300
|
+
(!cType || cType === 'application/octet-stream') &&
|
|
301
|
+
att.url &&
|
|
302
|
+
typeof att.url === 'string'
|
|
303
|
+
) {
|
|
304
|
+
if (att.url.includes('JVBERi0')) {
|
|
305
|
+
cType = 'application/pdf';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!nameStr || nameStr === 'Attachment') {
|
|
310
|
+
nameStr = isImage ? 'image.png' : 'Document';
|
|
311
|
+
if (cType === 'application/pdf') nameStr = 'Document.pdf';
|
|
312
|
+
else if (cType === 'text/plain') nameStr = 'Text.txt';
|
|
313
|
+
else if (cType.includes('word')) nameStr = 'Document.docx';
|
|
314
|
+
else if (cType.includes('excel') || cType.includes('spreadsheet'))
|
|
315
|
+
nameStr = 'Data.xlsx';
|
|
316
|
+
} else if (cType === 'application/pdf' && !nameStr.toLowerCase().endsWith('.pdf')) {
|
|
317
|
+
nameStr += '.pdf';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const ext = nameStr.split('.').pop()?.toLowerCase() || '';
|
|
321
|
+
let iconPath = '/icons/image.png'; // default fallback for images and unknown files
|
|
322
|
+
if (
|
|
323
|
+
['csv', 'xls', 'xlsx'].includes(ext) ||
|
|
324
|
+
cType.includes('excel') ||
|
|
325
|
+
cType.includes('spreadsheet')
|
|
326
|
+
)
|
|
327
|
+
iconPath = '/icons/excel.png';
|
|
328
|
+
else if (['doc', 'docx'].includes(ext) || cType.includes('word'))
|
|
329
|
+
iconPath = '/icons/word.png';
|
|
330
|
+
else if (['md', 'markdown'].includes(ext)) iconPath = '/icons/markdown.png';
|
|
331
|
+
else if (['pdf'].includes(ext) || cType === 'application/pdf')
|
|
332
|
+
iconPath = '/icons/pdf.png';
|
|
333
|
+
else if (isVideo) iconPath = '/icons/video.png';
|
|
334
|
+
else if (isAudio) iconPath = '/icons/audio.png';
|
|
335
|
+
else if (!isImage && !ext) iconPath = '/icons/word.png'; // Generic file icon
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div
|
|
339
|
+
key={i}
|
|
340
|
+
onClick={() => {
|
|
341
|
+
if (openCanvas) {
|
|
342
|
+
if (att instanceof File || (att.name && att.size && !att.url)) {
|
|
343
|
+
// It's a File object provided directly from input
|
|
344
|
+
openCanvas(att as File, { background: false });
|
|
345
|
+
} else if (att.url) {
|
|
346
|
+
let fetchUrl = att.url;
|
|
347
|
+
// If it's a raw base64 string without data prefix, add it
|
|
348
|
+
if (!fetchUrl.startsWith('http') && !fetchUrl.startsWith('data:')) {
|
|
349
|
+
fetchUrl = `data:${
|
|
350
|
+
cType || 'application/octet-stream'
|
|
351
|
+
};base64,${fetchUrl}`;
|
|
352
|
+
}
|
|
353
|
+
fetch(fetchUrl)
|
|
354
|
+
.then((r) => r.blob())
|
|
355
|
+
.then((blob) => {
|
|
356
|
+
const file = new File([blob], nameStr, { type: cType });
|
|
357
|
+
openCanvas(file, { background: false });
|
|
358
|
+
})
|
|
359
|
+
.catch((err) => {
|
|
360
|
+
console.error('Failed to open attachment in canvas', err);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
365
|
+
className={`flex items-center gap-2 bg-muted/80 border border-border/60 rounded-xl h-12 px-3 p-1 shrink-0 ${
|
|
366
|
+
openCanvas && (att.url || att instanceof File || att.size)
|
|
367
|
+
? 'cursor-pointer hover:bg-muted'
|
|
368
|
+
: ''
|
|
369
|
+
}`}
|
|
370
|
+
>
|
|
371
|
+
<div className="size-7 shrink-0 rounded-md overflow-hidden bg-background border border-border/90 flex items-center justify-center p-1">
|
|
372
|
+
<img src={iconPath} alt={nameStr} className="w-full h-full object-contain" />
|
|
373
|
+
</div>
|
|
374
|
+
<div className="flex flex-col justify-center max-w-37.5">
|
|
375
|
+
<span className="text-xs font-medium truncate text-foreground">
|
|
376
|
+
{nameStr}
|
|
377
|
+
</span>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
})}
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
{text && <ChatUserBubble>{text}</ChatUserBubble>}
|
|
385
|
+
{text && (
|
|
386
|
+
<MessageActions
|
|
387
|
+
text={text}
|
|
388
|
+
className="opacity-0 transition group-hover:opacity-100 focus-within:opacity-100"
|
|
389
|
+
regenerateLabel="Resend from here"
|
|
390
|
+
onRegenerate={
|
|
391
|
+
!isBusy && regenerate ? () => regenerate({ messageId: message.id }) : undefined
|
|
392
|
+
}
|
|
393
|
+
/>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Assistant message
|
|
401
|
+
const kbParts = parts.filter((p: any) => {
|
|
402
|
+
const { toolName } = getToolInfo(p);
|
|
403
|
+
return getChatToolBehavior(toolName).placement === 'source';
|
|
404
|
+
});
|
|
405
|
+
// searchKnowledgeBase automatically enriches a video match with active
|
|
406
|
+
// evidence (queryVideoKnowledge's own result, ui.kind:'citations' included
|
|
407
|
+
// -- the same generic shape any tool's completed result can carry).
|
|
408
|
+
// Preserve that nested result as a visible citation card instead of
|
|
409
|
+
// making a completed GPU investigation look like ordinary RAG only.
|
|
410
|
+
const nestedVideoCitationsUi: ChatCitationsUi[] = kbParts.flatMap((part: any) => {
|
|
411
|
+
const output = getToolInfo(part).output;
|
|
412
|
+
const result = decodeToolOutput(output) as any;
|
|
413
|
+
return result?.videoEvidence?.success && result.videoEvidence.ui?.kind === 'citations'
|
|
414
|
+
? [result.videoEvidence.ui as ChatCitationsUi]
|
|
415
|
+
: [];
|
|
416
|
+
});
|
|
417
|
+
const isKnowledgeSearchActive = kbParts.some((part: any) => getToolInfo(part).isExecuting);
|
|
418
|
+
const activeKnowledgeProgress =
|
|
419
|
+
activeToolProgress &&
|
|
420
|
+
kbParts.some((part: any) => {
|
|
421
|
+
const invocation = part.toolInvocation ?? part;
|
|
422
|
+
return (invocation.toolCallId || invocation.id || '') === activeToolProgress.toolCallId;
|
|
423
|
+
})
|
|
424
|
+
? activeToolProgress
|
|
425
|
+
: null;
|
|
426
|
+
|
|
427
|
+
let signatureFound = false;
|
|
428
|
+
const toolParts = parts.filter((p: any) => {
|
|
429
|
+
const { toolName } = getToolInfo(p);
|
|
430
|
+
if (getChatToolBehavior(toolName).placement === 'source') return false;
|
|
431
|
+
if (toolName === 'requestDocumentSignature') {
|
|
432
|
+
if (signatureFound) return false;
|
|
433
|
+
signatureFound = true;
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const textParts = parts.filter((p: any) => p.type === 'text');
|
|
439
|
+
|
|
440
|
+
const { allCleanTexts, hasThinkTags } = useMemo(() => {
|
|
441
|
+
let hasTags = false;
|
|
442
|
+
const cleanTexts: string[] = [];
|
|
443
|
+
for (const part of textParts) {
|
|
444
|
+
const raw = part.text || '';
|
|
445
|
+
const { cleanText, reasoningText } = stripThinkTags(raw);
|
|
446
|
+
cleanTexts.push(cleanText);
|
|
447
|
+
if (reasoningText) {
|
|
448
|
+
hasTags = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { allCleanTexts: cleanTexts, hasThinkTags: hasTags };
|
|
452
|
+
}, [textParts]);
|
|
453
|
+
|
|
454
|
+
const answerText = useMemo(
|
|
455
|
+
() => allCleanTexts.filter(Boolean).join('\n\n').trim(),
|
|
456
|
+
[allCleanTexts],
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const answerFeedback = answerFeedbackForMessage(message);
|
|
460
|
+
const liked = answerFeedback === 'liked';
|
|
461
|
+
const disliked = answerFeedback === 'disliked';
|
|
462
|
+
|
|
463
|
+
const handleLike = () => {
|
|
464
|
+
onFeedback?.(message.id, liked ? undefined : 'liked');
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const handleDislike = () => {
|
|
468
|
+
onFeedback?.(message.id, disliked ? undefined : 'disliked');
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const isShimmering =
|
|
472
|
+
textParts.every((p: any) => !p.text || p.text.trim().length === 0) && isLast && isStreaming;
|
|
473
|
+
|
|
474
|
+
const isVizPart = (p: any) => {
|
|
475
|
+
const { toolName, isCompleted } = getToolInfo(p);
|
|
476
|
+
return getChatToolBehavior(toolName).placement === 'visualization' && isCompleted;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const vizParts = toolParts.filter(isVizPart);
|
|
480
|
+
const mediaToolParts = toolParts.filter(
|
|
481
|
+
(p: any) => getChatToolBehavior(getToolInfo(p).toolName).placement === 'media',
|
|
482
|
+
);
|
|
483
|
+
const webSearchParts = toolParts.filter(
|
|
484
|
+
(p: any) => getChatToolBehavior(getToolInfo(p).toolName).placement === 'web-search',
|
|
485
|
+
);
|
|
486
|
+
const nonVizToolParts = toolParts.filter((p: any) => {
|
|
487
|
+
const placement = getChatToolBehavior(getToolInfo(p).toolName).placement;
|
|
488
|
+
return !isVizPart(p) && placement === 'inline';
|
|
489
|
+
});
|
|
490
|
+
const firstCitationToolPartIndex = toolParts.findIndex(
|
|
491
|
+
(part: any) => getToolInfo(part).output?.ui?.kind === 'citations',
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const vizTabs =
|
|
495
|
+
vizParts.length > 1
|
|
496
|
+
? vizParts.map((p: any, i: number) => {
|
|
497
|
+
const { output } = getToolInfo(p);
|
|
498
|
+
return {
|
|
499
|
+
label: (output as ChartConfig)?.title || `Chart ${i + 1}`,
|
|
500
|
+
content: renderToolPart(p, i),
|
|
501
|
+
};
|
|
502
|
+
})
|
|
503
|
+
: null;
|
|
504
|
+
|
|
505
|
+
const executingParts = useMemo(
|
|
506
|
+
() =>
|
|
507
|
+
toolParts.filter((p: any) => {
|
|
508
|
+
const { isExecuting, toolName } = getToolInfo(p);
|
|
509
|
+
return isExecuting && getChatToolBehavior(toolName).placement !== 'source';
|
|
510
|
+
}),
|
|
511
|
+
[toolParts],
|
|
512
|
+
);
|
|
513
|
+
const progressForExecutingPart = (part: any) => {
|
|
514
|
+
const invocation = part.toolInvocation ?? part;
|
|
515
|
+
const toolCallId = invocation.toolCallId || invocation.id || '';
|
|
516
|
+
if (activeToolProgress?.toolCallId === toolCallId) return activeToolProgress;
|
|
517
|
+
pendingToolStartedAt.current[toolCallId] ??= progressClock;
|
|
518
|
+
const behavior = getChatToolBehavior(getToolInfo(part).toolName);
|
|
519
|
+
return {
|
|
520
|
+
toolCallId,
|
|
521
|
+
percent: smoothPendingToolProgress(pendingToolStartedAt.current[toolCallId], progressClock),
|
|
522
|
+
label: behavior.pendingLabel || 'Working…',
|
|
523
|
+
message: behavior.pendingLabel || 'Working…',
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<div className="message assistant-message flex flex-col gap-2" data-role="assistant">
|
|
529
|
+
{kbParts.length > 0 && (
|
|
530
|
+
<KnowledgeBaseResult
|
|
531
|
+
parts={kbParts}
|
|
532
|
+
isShimmering={isShimmering}
|
|
533
|
+
activity={activeKnowledgeProgress}
|
|
534
|
+
/>
|
|
535
|
+
)}
|
|
536
|
+
|
|
537
|
+
{nestedVideoCitationsUi.map((ui, i) => (
|
|
538
|
+
<ChatCitations
|
|
539
|
+
key={`nested-citations-${i}`}
|
|
540
|
+
ui={ui}
|
|
541
|
+
autoOpenSupportingClip={autoOpenSupportingClip && i === 0}
|
|
542
|
+
/>
|
|
543
|
+
))}
|
|
544
|
+
|
|
545
|
+
{webSearchParts.length > 0 && <WebSearchSummary parts={webSearchParts} />}
|
|
546
|
+
|
|
547
|
+
{nonVizToolParts
|
|
548
|
+
.filter((p: any) => getToolInfo(p).isCompleted)
|
|
549
|
+
.map((part: any, i: number) => {
|
|
550
|
+
const partIndex = toolParts.indexOf(part);
|
|
551
|
+
return renderToolPart(
|
|
552
|
+
part,
|
|
553
|
+
i,
|
|
554
|
+
addToolResult,
|
|
555
|
+
updateFromToolResult,
|
|
556
|
+
undefined,
|
|
557
|
+
autoOpenSupportingClip &&
|
|
558
|
+
nestedVideoCitationsUi.length === 0 &&
|
|
559
|
+
partIndex === firstCitationToolPartIndex,
|
|
560
|
+
);
|
|
561
|
+
})}
|
|
562
|
+
|
|
563
|
+
{vizTabs ? (
|
|
564
|
+
<ChatTabs config={{ tabs: vizTabs }} />
|
|
565
|
+
) : (
|
|
566
|
+
vizParts.map((part: any, i: number) =>
|
|
567
|
+
renderToolPart(part, i, addToolResult, updateFromToolResult),
|
|
568
|
+
)
|
|
569
|
+
)}
|
|
570
|
+
|
|
571
|
+
{executingParts.map((part: any, i: number) =>
|
|
572
|
+
renderToolPart(
|
|
573
|
+
part,
|
|
574
|
+
i,
|
|
575
|
+
addToolResult,
|
|
576
|
+
updateFromToolResult,
|
|
577
|
+
progressForExecutingPart(part),
|
|
578
|
+
),
|
|
579
|
+
)}
|
|
580
|
+
|
|
581
|
+
{isLast &&
|
|
582
|
+
isStreaming &&
|
|
583
|
+
executingParts.length === 0 &&
|
|
584
|
+
!isKnowledgeSearchActive &&
|
|
585
|
+
!hasThinkTags &&
|
|
586
|
+
textParts.every((p: any) => !p.text || p.text.trim().length === 0) && (
|
|
587
|
+
<div className="flex items-center gap-2">
|
|
588
|
+
<div className="size-6.5 bg-white border border-border rounded-full flex items-center justify-center p-1 animate-pulse">
|
|
589
|
+
<img src="/logo.png" alt="logo" className="size-4 animate-spin" />
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{/* Text parts — with markdown table detection + think-tag stripping */}
|
|
595
|
+
{textParts.map((part: any, i: number) => {
|
|
596
|
+
const rawText = part.text || '';
|
|
597
|
+
if (!rawText.trim()) return null;
|
|
598
|
+
|
|
599
|
+
const strippedText = allCleanTexts[i] ?? rawText;
|
|
600
|
+
const cleanText = strippedText.replace(/\[(?:IMAGE_REF|VIDEO_REF|AUDIO_REF):[^\]]+\]/g, '');
|
|
601
|
+
|
|
602
|
+
if (!cleanText.trim()) return null;
|
|
603
|
+
|
|
604
|
+
const segments = splitTextAndTables(cleanText);
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<div key={i} className="flex flex-col gap-4">
|
|
608
|
+
{segments.map((seg, j) => {
|
|
609
|
+
if (seg.type === 'table') {
|
|
610
|
+
const tableConfig: DataTableConfig = {
|
|
611
|
+
columns: seg.table.columns,
|
|
612
|
+
rows: seg.table.rows,
|
|
613
|
+
totalRows: seg.table.rows.length,
|
|
614
|
+
};
|
|
615
|
+
return <ChatDataTable key={`table-${j}`} config={tableConfig} />;
|
|
616
|
+
}
|
|
617
|
+
return (
|
|
618
|
+
<div
|
|
619
|
+
key={`text-${j}`}
|
|
620
|
+
className="assistant-text-content"
|
|
621
|
+
dangerouslySetInnerHTML={{
|
|
622
|
+
__html: renderMarkdown(seg.content),
|
|
623
|
+
}}
|
|
624
|
+
/>
|
|
625
|
+
);
|
|
626
|
+
})}
|
|
627
|
+
</div>
|
|
628
|
+
);
|
|
629
|
+
})}
|
|
630
|
+
|
|
631
|
+
{mediaToolParts
|
|
632
|
+
.filter((p: any) => getToolInfo(p).isCompleted)
|
|
633
|
+
.map((part: any, i: number) =>
|
|
634
|
+
renderToolPart(part, i, addToolResult, updateFromToolResult),
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{answerText && !(isLast && isStreaming) && (
|
|
638
|
+
<MessageActions
|
|
639
|
+
text={answerText}
|
|
640
|
+
liked={liked}
|
|
641
|
+
disliked={disliked}
|
|
642
|
+
onLike={onFeedback ? handleLike : undefined}
|
|
643
|
+
onDislike={onFeedback ? handleDislike : undefined}
|
|
644
|
+
onRegenerate={regenerate ? () => regenerate({ messageId: message.id }) : undefined}
|
|
645
|
+
/>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
interface ParsedTable {
|
|
652
|
+
columns: string[];
|
|
653
|
+
rows: Record<string, any>[];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function splitTextAndTables(
|
|
657
|
+
text: string,
|
|
658
|
+
): Array<{ type: 'text'; content: string } | { type: 'table'; table: ParsedTable }> {
|
|
659
|
+
if (!text) return [];
|
|
660
|
+
|
|
661
|
+
const lines = text.split('\n');
|
|
662
|
+
const segments: Array<{ type: 'text'; content: string } | { type: 'table'; table: ParsedTable }> =
|
|
663
|
+
[];
|
|
664
|
+
let currentText: string[] = [];
|
|
665
|
+
let tableLines: string[] = [];
|
|
666
|
+
let inTable = false;
|
|
667
|
+
|
|
668
|
+
const isTableRow = (line: string) => {
|
|
669
|
+
const trimmed = line.trim();
|
|
670
|
+
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 2;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const isSeparatorRow = (line: string) => /^\|[\s\-:|]+\|$/.test(line.trim());
|
|
674
|
+
|
|
675
|
+
const flushText = () => {
|
|
676
|
+
if (currentText.length > 0) {
|
|
677
|
+
const content = currentText.join('\n').trim();
|
|
678
|
+
if (content) {
|
|
679
|
+
segments.push({ type: 'text', content });
|
|
680
|
+
}
|
|
681
|
+
currentText = [];
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const flushTable = () => {
|
|
686
|
+
if (tableLines.length < 2) {
|
|
687
|
+
currentText.push(...tableLines);
|
|
688
|
+
tableLines = [];
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const headerLine = tableLines[0];
|
|
693
|
+
const columns = headerLine
|
|
694
|
+
.split('|')
|
|
695
|
+
.map((c) => c.trim())
|
|
696
|
+
.filter(Boolean);
|
|
697
|
+
|
|
698
|
+
let dataStart = 1;
|
|
699
|
+
if (tableLines.length > 1 && isSeparatorRow(tableLines[1])) {
|
|
700
|
+
dataStart = 2;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const rows: Record<string, any>[] = [];
|
|
704
|
+
for (let i = dataStart; i < tableLines.length; i++) {
|
|
705
|
+
const rawCells = tableLines[i]
|
|
706
|
+
.split('|')
|
|
707
|
+
.slice(1, -1)
|
|
708
|
+
.map((c) => c.trim());
|
|
709
|
+
|
|
710
|
+
const row: Record<string, any> = {};
|
|
711
|
+
columns.forEach((col, j) => {
|
|
712
|
+
const val = rawCells[j] ?? '';
|
|
713
|
+
const num = Number(val);
|
|
714
|
+
row[col] = !isNaN(num) && val !== '' ? num : val;
|
|
715
|
+
});
|
|
716
|
+
rows.push(row);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (columns.length > 0 && rows.length > 0) {
|
|
720
|
+
segments.push({
|
|
721
|
+
type: 'table',
|
|
722
|
+
table: { columns, rows },
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
tableLines = [];
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
for (const line of lines) {
|
|
729
|
+
if (isTableRow(line)) {
|
|
730
|
+
if (!inTable) {
|
|
731
|
+
flushText();
|
|
732
|
+
inTable = true;
|
|
733
|
+
}
|
|
734
|
+
if (!isSeparatorRow(line) || tableLines.length === 1) {
|
|
735
|
+
tableLines.push(line);
|
|
736
|
+
} else if (isSeparatorRow(line)) {
|
|
737
|
+
tableLines.push(line);
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
if (inTable) {
|
|
741
|
+
flushTable();
|
|
742
|
+
inTable = false;
|
|
743
|
+
}
|
|
744
|
+
currentText.push(line);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (inTable) {
|
|
749
|
+
flushTable();
|
|
750
|
+
}
|
|
751
|
+
flushText();
|
|
752
|
+
|
|
753
|
+
return segments;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function stripThinkTags(text: string): { cleanText: string; reasoningText: string } {
|
|
757
|
+
if (!text) return { cleanText: '', reasoningText: '' };
|
|
758
|
+
|
|
759
|
+
const reasoningParts: string[] = [];
|
|
760
|
+
let cleaned = stripLeakedToolCalls(text);
|
|
761
|
+
|
|
762
|
+
// 1. Extract complete <think>…</think> or <thought>…</thought> blocks
|
|
763
|
+
const thinkRegex = /<(?:think|thought)>([\s\S]*?)<\/(?:think|thought)>/gi;
|
|
764
|
+
let match;
|
|
765
|
+
while ((match = thinkRegex.exec(cleaned)) !== null) {
|
|
766
|
+
reasoningParts.push(match[1].trim());
|
|
767
|
+
}
|
|
768
|
+
cleaned = cleaned.replace(thinkRegex, '');
|
|
769
|
+
|
|
770
|
+
// 2. Extract unclosed <think> or <thought> (streaming, no closing tag yet)
|
|
771
|
+
const unclosedThinkRegex = /<(?:think|thought)>([\s\S]*)$/i;
|
|
772
|
+
const unclosedThinkMatch = cleaned.match(unclosedThinkRegex);
|
|
773
|
+
if (unclosedThinkMatch) {
|
|
774
|
+
reasoningParts.push(unclosedThinkMatch[1].trim());
|
|
775
|
+
cleaned = cleaned.replace(unclosedThinkRegex, '');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 3. Handle <result>...</result>
|
|
779
|
+
const resultRegex = /<result>([\s\S]*?)<\/result>/gi;
|
|
780
|
+
let resultMatch;
|
|
781
|
+
let foundResult = false;
|
|
782
|
+
let finalCleanText = '';
|
|
783
|
+
let textOutsideResult = cleaned;
|
|
784
|
+
|
|
785
|
+
while ((resultMatch = resultRegex.exec(cleaned)) !== null) {
|
|
786
|
+
foundResult = true;
|
|
787
|
+
finalCleanText += (finalCleanText ? '\n\n' : '') + resultMatch[1].trim();
|
|
788
|
+
textOutsideResult = textOutsideResult.replace(resultMatch[0], '');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Handle unclosed <result>
|
|
792
|
+
const unclosedResultRegex = /<result>([\s\S]*)$/i;
|
|
793
|
+
const unclosedResultMatch = textOutsideResult.match(unclosedResultRegex);
|
|
794
|
+
if (unclosedResultMatch) {
|
|
795
|
+
foundResult = true;
|
|
796
|
+
finalCleanText += (finalCleanText ? '\n\n' : '') + unclosedResultMatch[1].trim();
|
|
797
|
+
textOutsideResult = textOutsideResult.replace(unclosedResultRegex, '');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (foundResult) {
|
|
801
|
+
if (textOutsideResult.trim()) {
|
|
802
|
+
reasoningParts.push(textOutsideResult.trim());
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
cleanText: finalCleanText.trim(),
|
|
806
|
+
reasoningText: reasoningParts.filter(Boolean).join('\n\n'),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return {
|
|
811
|
+
cleanText: cleaned.trim(),
|
|
812
|
+
reasoningText: reasoningParts.filter(Boolean).join('\n\n'),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Some providers occasionally place a tool request in assistant text after
|
|
818
|
+
* sending (or instead of sending) the structured tool part. Tool calls are
|
|
819
|
+
* implementation detail, never useful answer content. Restrict removal to
|
|
820
|
+
* registered first-party names so ordinary prose and code remain intact.
|
|
821
|
+
*/
|
|
822
|
+
function stripLeakedToolCalls(text: string): string {
|
|
823
|
+
const names = new Set(Object.keys(CHAT_TOOL_BEHAVIORS));
|
|
824
|
+
const lines = text.split('\n');
|
|
825
|
+
const kept: string[] = [];
|
|
826
|
+
let callDepth = 0;
|
|
827
|
+
|
|
828
|
+
for (const line of lines) {
|
|
829
|
+
if (callDepth > 0) {
|
|
830
|
+
callDepth += countParentheses(line);
|
|
831
|
+
if (callDepth <= 0) callDepth = 0;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const name = line.match(/^\s*([A-Za-z_$][\w$]*)\s*\(/)?.[1];
|
|
835
|
+
if (name && names.has(name)) {
|
|
836
|
+
callDepth = countParentheses(line);
|
|
837
|
+
if (callDepth <= 0) callDepth = 0;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
kept.push(line);
|
|
841
|
+
}
|
|
842
|
+
return kept.join('\n');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function countParentheses(text: string): number {
|
|
846
|
+
let count = 0;
|
|
847
|
+
for (const char of text) {
|
|
848
|
+
if (char === '(') count += 1;
|
|
849
|
+
if (char === ')') count -= 1;
|
|
850
|
+
}
|
|
851
|
+
return count;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function getToolInfo(part: any): {
|
|
855
|
+
toolName: string;
|
|
856
|
+
isExecuting: boolean;
|
|
857
|
+
isCompleted: boolean;
|
|
858
|
+
output: any;
|
|
859
|
+
input: any;
|
|
860
|
+
} {
|
|
861
|
+
if (part.type === 'tool-invocation') {
|
|
862
|
+
const ti = part.toolInvocation;
|
|
863
|
+
const state = ti.state;
|
|
864
|
+
return {
|
|
865
|
+
toolName: ti.toolName,
|
|
866
|
+
isExecuting: state === 'partial-call' || state === 'call',
|
|
867
|
+
isCompleted:
|
|
868
|
+
(state === 'result' || state === 'output' || state === 'output-available') &&
|
|
869
|
+
ti.result !== undefined,
|
|
870
|
+
output: decodeToolOutput(ti.result) ?? ti.result,
|
|
871
|
+
input: ti.args,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (part.type?.startsWith('tool-') && part.type !== 'tool-invocation') {
|
|
876
|
+
const toolName = part.type.replace('tool-', '');
|
|
877
|
+
const state = part.state;
|
|
878
|
+
return {
|
|
879
|
+
toolName,
|
|
880
|
+
isExecuting: state === 'input-streaming' || state === 'input-available',
|
|
881
|
+
isCompleted:
|
|
882
|
+
(state === 'output' || state === 'output-available') && part.output !== undefined,
|
|
883
|
+
output: decodeToolOutput(part.output) ?? part.output,
|
|
884
|
+
input: part.input,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return {
|
|
889
|
+
toolName: '',
|
|
890
|
+
isExecuting: false,
|
|
891
|
+
isCompleted: false,
|
|
892
|
+
output: undefined,
|
|
893
|
+
input: undefined,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function renderToolPart(
|
|
898
|
+
part: any,
|
|
899
|
+
index: number,
|
|
900
|
+
addToolResult?: Function,
|
|
901
|
+
updateFromToolResult?: Function,
|
|
902
|
+
toolProgress?: {
|
|
903
|
+
toolCallId: string;
|
|
904
|
+
percent: number;
|
|
905
|
+
label: string;
|
|
906
|
+
message?: string;
|
|
907
|
+
} | null,
|
|
908
|
+
autoOpenSupportingClip = false,
|
|
909
|
+
): React.ReactNode | null {
|
|
910
|
+
const { toolName, isExecuting, isCompleted, output, input } = getToolInfo(part);
|
|
911
|
+
const behavior = getChatToolBehavior(toolName);
|
|
912
|
+
const callId = part.toolInvocation?.toolCallId || part.toolCallId || part.id || '';
|
|
913
|
+
|
|
914
|
+
if (isExecuting) {
|
|
915
|
+
if (behavior.placement === 'source') return null;
|
|
916
|
+
|
|
917
|
+
if (behavior.resultView === 'signature') {
|
|
918
|
+
return (
|
|
919
|
+
<ChatSignatureRequest
|
|
920
|
+
key={index}
|
|
921
|
+
detectedLocations={input?.detectedLocations}
|
|
922
|
+
toolCallId={callId}
|
|
923
|
+
addToolResult={addToolResult}
|
|
924
|
+
/>
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const liveProgress = toolProgress && toolProgress.toolCallId === callId ? toolProgress : null;
|
|
929
|
+
// Past the point where the remaining work has a measurable size, a
|
|
930
|
+
// percentage is a claim we cannot back. Drop it and shuttle the bar
|
|
931
|
+
// instead, so the wait still reads as active rather than stalled.
|
|
932
|
+
const indeterminate = liveProgress ? isIndeterminateProgress(liveProgress.percent) : false;
|
|
933
|
+
|
|
934
|
+
return (
|
|
935
|
+
<div key={index} className="flex min-w-0 flex-col gap-1.5 py-3">
|
|
936
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
937
|
+
<div className="size-6.5 bg-white dark:bg-card border border-border rounded-full flex items-center justify-center p-1">
|
|
938
|
+
<img src="/logo.png" alt="" className="size-4 animate-spin" />
|
|
939
|
+
</div>
|
|
940
|
+
<span
|
|
941
|
+
className="larkup-shimmer-text min-w-0 wrap-break-word text-[13px] font-medium leading-5"
|
|
942
|
+
aria-live="polite"
|
|
943
|
+
aria-atomic="true"
|
|
944
|
+
>
|
|
945
|
+
{liveProgress?.message || liveProgress?.label || behavior.pendingLabel || 'Working'}
|
|
946
|
+
</span>
|
|
947
|
+
</div>
|
|
948
|
+
{liveProgress && behavior.showProgressBar !== false ? (
|
|
949
|
+
<div className="ml-9 flex items-center gap-2">
|
|
950
|
+
<div className="h-1.5 w-48 overflow-hidden rounded-full bg-muted">
|
|
951
|
+
{indeterminate ? (
|
|
952
|
+
<div className="larkup-progress-shuttle h-full rounded-full bg-emerald-500" />
|
|
953
|
+
) : (
|
|
954
|
+
<div
|
|
955
|
+
className="h-full rounded-full bg-emerald-500 transition-[width] duration-700 ease-out"
|
|
956
|
+
style={{ width: `${Math.max(6, Math.min(100, liveProgress.percent))}%` }}
|
|
957
|
+
/>
|
|
958
|
+
)}
|
|
959
|
+
</div>
|
|
960
|
+
{!indeterminate ? (
|
|
961
|
+
<span className="text-[11px] tabular-nums text-muted-foreground">
|
|
962
|
+
{Math.round(liveProgress.percent)}%
|
|
963
|
+
</span>
|
|
964
|
+
) : null}
|
|
965
|
+
</div>
|
|
966
|
+
) : null}
|
|
967
|
+
</div>
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (isCompleted) {
|
|
972
|
+
const enterpriseUi = output?.ui;
|
|
973
|
+
if (enterpriseUi?.kind === 'notice') {
|
|
974
|
+
return (
|
|
975
|
+
<div
|
|
976
|
+
key={index}
|
|
977
|
+
className="rounded-xl border border-primary/20 bg-primary/5 px-4 py-3 text-sm"
|
|
978
|
+
>
|
|
979
|
+
<div className="font-medium">{enterpriseUi.title}</div>
|
|
980
|
+
<div className="mt-1 text-muted-foreground">{enterpriseUi.body}</div>
|
|
981
|
+
</div>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
if (enterpriseUi?.kind === 'card') {
|
|
985
|
+
return (
|
|
986
|
+
<div key={index} className="rounded-xl border border-border bg-card px-4 py-3 text-sm">
|
|
987
|
+
<div className="font-medium">{enterpriseUi.title}</div>
|
|
988
|
+
<dl className="mt-3 grid gap-2">
|
|
989
|
+
{(enterpriseUi.facts ?? []).map((fact: { label: string; value: string }) => (
|
|
990
|
+
<div className="flex items-start justify-between gap-5" key={fact.label}>
|
|
991
|
+
<dt className="text-muted-foreground">{fact.label}</dt>
|
|
992
|
+
<dd className="text-right">{fact.value}</dd>
|
|
993
|
+
</div>
|
|
994
|
+
))}
|
|
995
|
+
</dl>
|
|
996
|
+
</div>
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (enterpriseUi?.kind === 'table') {
|
|
1000
|
+
return (
|
|
1001
|
+
<div
|
|
1002
|
+
key={index}
|
|
1003
|
+
className="overflow-x-auto rounded-xl border border-border bg-card p-3 text-sm"
|
|
1004
|
+
>
|
|
1005
|
+
<div className="mb-3 font-medium">{enterpriseUi.title}</div>
|
|
1006
|
+
<table className="w-full text-left">
|
|
1007
|
+
<thead className="text-muted-foreground">
|
|
1008
|
+
<tr>
|
|
1009
|
+
{(enterpriseUi.columns ?? []).map((column: string) => (
|
|
1010
|
+
<th className="px-2 py-1" key={column}>
|
|
1011
|
+
{column}
|
|
1012
|
+
</th>
|
|
1013
|
+
))}
|
|
1014
|
+
</tr>
|
|
1015
|
+
</thead>
|
|
1016
|
+
<tbody>
|
|
1017
|
+
{(enterpriseUi.rows ?? []).map((row: Record<string, string>, rowIndex: number) => (
|
|
1018
|
+
<tr className="border-t border-border/60" key={rowIndex}>
|
|
1019
|
+
{(enterpriseUi.columns ?? []).map((column: string) => (
|
|
1020
|
+
<td className="px-2 py-2" key={column}>
|
|
1021
|
+
{row[column]}
|
|
1022
|
+
</td>
|
|
1023
|
+
))}
|
|
1024
|
+
</tr>
|
|
1025
|
+
))}
|
|
1026
|
+
</tbody>
|
|
1027
|
+
</table>
|
|
1028
|
+
</div>
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (enterpriseUi?.kind === 'citations') {
|
|
1032
|
+
return (
|
|
1033
|
+
<ChatCitations
|
|
1034
|
+
key={index}
|
|
1035
|
+
ui={enterpriseUi}
|
|
1036
|
+
assetId={output?.mediaAssetId}
|
|
1037
|
+
autoOpenSupportingClip={autoOpenSupportingClip}
|
|
1038
|
+
/>
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
switch (behavior.resultView) {
|
|
1042
|
+
case 'data-table': {
|
|
1043
|
+
if (output.error) return null;
|
|
1044
|
+
const tableConfig: DataTableConfig = {
|
|
1045
|
+
columns: output.columns ?? [],
|
|
1046
|
+
rows: output.rows ?? [],
|
|
1047
|
+
totalRows: output.totalRows ?? 0,
|
|
1048
|
+
aggregationResults: output.aggregationResults,
|
|
1049
|
+
};
|
|
1050
|
+
if (tableConfig.rows.length === 0 && !tableConfig.aggregationResults) return null;
|
|
1051
|
+
const visualization = output.visualization as ChartConfig | undefined;
|
|
1052
|
+
return (
|
|
1053
|
+
<div key={index}>
|
|
1054
|
+
<ChatDataTable config={tableConfig} compact={behavior.compactResult} />
|
|
1055
|
+
{visualization?.data?.length ? <ChatChart config={visualization} /> : null}
|
|
1056
|
+
</div>
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
case 'file-export': {
|
|
1061
|
+
if (!output?.success || !output?.fileBase64) return null;
|
|
1062
|
+
return <ChatFileExport key={index} artifact={output} />;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
case 'none': {
|
|
1066
|
+
if (behavior.placement !== 'visualization') return null;
|
|
1067
|
+
const chartConfig = output as ChartConfig;
|
|
1068
|
+
if (!chartConfig) return null;
|
|
1069
|
+
return <ChatChart key={index} config={chartConfig} />;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
case 'sandbox': {
|
|
1073
|
+
const result = output as SandboxResultConfig;
|
|
1074
|
+
return <ChatSandboxResult key={index} config={result} />;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
case 'corpus': {
|
|
1078
|
+
const corpusConfig = output as CorpusDataConfig;
|
|
1079
|
+
if (!corpusConfig) return null;
|
|
1080
|
+
return <CorpusDataResult key={index} config={corpusConfig} />;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
case 'document-edit': {
|
|
1084
|
+
if (!output.success) return null;
|
|
1085
|
+
|
|
1086
|
+
const fileName = output.fileName || 'Document';
|
|
1087
|
+
const cType = output.mimeType || '';
|
|
1088
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
1089
|
+
let iconPath = '/icons/image.png'; // default fallback for images and unknown files
|
|
1090
|
+
if (
|
|
1091
|
+
['csv', 'xls', 'xlsx'].includes(ext) ||
|
|
1092
|
+
cType.includes('excel') ||
|
|
1093
|
+
cType.includes('spreadsheet')
|
|
1094
|
+
)
|
|
1095
|
+
iconPath = '/icons/excel.png';
|
|
1096
|
+
else if (['doc', 'docx'].includes(ext) || cType.includes('word'))
|
|
1097
|
+
iconPath = '/icons/word.png';
|
|
1098
|
+
else if (['md', 'markdown'].includes(ext)) iconPath = '/icons/markdown.png';
|
|
1099
|
+
else if (['pdf'].includes(ext) || cType === 'application/pdf') iconPath = '/icons/pdf.png';
|
|
1100
|
+
else if (!ext) iconPath = '/icons/word.png';
|
|
1101
|
+
|
|
1102
|
+
return (
|
|
1103
|
+
<div
|
|
1104
|
+
key={index}
|
|
1105
|
+
onClick={() => {
|
|
1106
|
+
if (updateFromToolResult && output.fileBase64) {
|
|
1107
|
+
updateFromToolResult(output);
|
|
1108
|
+
}
|
|
1109
|
+
}}
|
|
1110
|
+
className="group flex items-center gap-3 rounded-xl border border-border/60 bg-emerald-50/50 dark:bg-emerald-900/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-400 cursor-pointer hover:bg-emerald-100/50 dark:hover:bg-emerald-900/20 transition"
|
|
1111
|
+
>
|
|
1112
|
+
<div className="size-8 shrink-0 rounded-md overflow-hidden bg-white border border-emerald-200/50 flex items-center justify-center p-1.5 35">
|
|
1113
|
+
<img src={iconPath} alt={fileName} className="w-full h-full object-contain" />
|
|
1114
|
+
</div>
|
|
1115
|
+
<div className="flex flex-col flex-1">
|
|
1116
|
+
<span className="font-medium text-emerald-800 dark:text-emerald-300">
|
|
1117
|
+
{toolName === 'fillDocumentForm' ? 'Form fields updated in ' : 'Edited '}
|
|
1118
|
+
<span className="font-bold">{fileName}</span>
|
|
1119
|
+
</span>
|
|
1120
|
+
<span className="text-xs opacity-80 mt-0.5">
|
|
1121
|
+
{output.updatedFields?.length || 0}{' '}
|
|
1122
|
+
{output.updatedFields?.length === 1 ? 'change' : 'changes'} applied. Preview updated
|
|
1123
|
+
in Canvas.
|
|
1124
|
+
</span>
|
|
1125
|
+
</div>
|
|
1126
|
+
{output.fileBase64 && (
|
|
1127
|
+
<div className="opacity-0 group-hover:opacity-100 text-xs font-medium text-emerald-600 dark:text-emerald-400 flex items-center gap-1 transition">
|
|
1128
|
+
<FileEdit className="size-3" />
|
|
1129
|
+
View this version
|
|
1130
|
+
</div>
|
|
1131
|
+
)}
|
|
1132
|
+
</div>
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
case 'signature': {
|
|
1137
|
+
if (!output?.success) return null;
|
|
1138
|
+
return (
|
|
1139
|
+
<div
|
|
1140
|
+
key={index}
|
|
1141
|
+
onClick={() => {
|
|
1142
|
+
if (updateFromToolResult && output.fileBase64) {
|
|
1143
|
+
updateFromToolResult(output);
|
|
1144
|
+
}
|
|
1145
|
+
}}
|
|
1146
|
+
className="group flex items-center gap-3 rounded-xl border border-border/60 bg-emerald-50/50 dark:bg-emerald-900/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-400 mt-2 cursor-pointer hover:bg-emerald-100/50 dark:hover:bg-emerald-900/20 transition"
|
|
1147
|
+
>
|
|
1148
|
+
<CheckCircle2 className="size-5 shrink-0 text-emerald-500" />
|
|
1149
|
+
<div className="flex-1 flex items-center justify-between">
|
|
1150
|
+
<span className="font-medium text-emerald-800 dark:text-emerald-300">
|
|
1151
|
+
Document signed successfully.
|
|
1152
|
+
</span>
|
|
1153
|
+
{output.fileBase64 && (
|
|
1154
|
+
<div className="opacity-0 group-hover:opacity-100 text-xs font-medium text-emerald-600 dark:text-emerald-400 flex items-center gap-1 transition">
|
|
1155
|
+
<FileEdit className="size-3" />
|
|
1156
|
+
View this version
|
|
1157
|
+
</div>
|
|
1158
|
+
)}
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
case 'image-analysis': {
|
|
1165
|
+
if (output.error) return null;
|
|
1166
|
+
return (
|
|
1167
|
+
<div key={index} className="mb-2 w-full">
|
|
1168
|
+
<div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
|
|
1169
|
+
<Sparkles className="h-3.5 w-3.5 shrink-0" />
|
|
1170
|
+
<span className="truncate max-w-62.5 sm:max-w-100">Analyzed indexed image</span>
|
|
1171
|
+
</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
case 'media': {
|
|
1177
|
+
if (
|
|
1178
|
+
!output?.success ||
|
|
1179
|
+
!output.assetId ||
|
|
1180
|
+
!['image', 'video', 'audio', 'frame-preview'].includes(output.mediaType)
|
|
1181
|
+
) {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
return (
|
|
1185
|
+
<ChatMediaPreview
|
|
1186
|
+
key={index}
|
|
1187
|
+
assetId={output.assetId}
|
|
1188
|
+
mediaType={output.mediaType}
|
|
1189
|
+
fileName={output.fileName}
|
|
1190
|
+
mediaUrl={output.mediaUrl}
|
|
1191
|
+
sourceUrl={output.sourceUrl}
|
|
1192
|
+
startSecs={output.startSecs}
|
|
1193
|
+
endSecs={output.endSecs}
|
|
1194
|
+
/>
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
default:
|
|
1199
|
+
return null;
|
|
1200
|
+
// return (
|
|
1201
|
+
// <div key={index} className="mb-2 w-full">
|
|
1202
|
+
// <div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
|
|
1203
|
+
// <CheckCircle2 className="h-3.5 w-3.5 shrink-0" />
|
|
1204
|
+
// <span className="truncate max-w-62.5 sm:max-w-100">Used {toolName}</span>
|
|
1205
|
+
// </div>
|
|
1206
|
+
// </div>
|
|
1207
|
+
// );
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function WebSearchSummary({ parts }: { parts: any[] }) {
|
|
1215
|
+
const [open, setOpen] = useState(false);
|
|
1216
|
+
const searches = parts.map((part) => {
|
|
1217
|
+
const info = getToolInfo(part);
|
|
1218
|
+
const results = Array.isArray(info.output?.results) ? info.output.results : [];
|
|
1219
|
+
return { query: info.input?.query as string | undefined, results, running: info.isExecuting };
|
|
1220
|
+
});
|
|
1221
|
+
const resultCount = searches.reduce((count, search) => count + search.results.length, 0);
|
|
1222
|
+
const running = searches.some((search) => search.running);
|
|
1223
|
+
|
|
1224
|
+
return (
|
|
1225
|
+
<div className="mb-2 w-full">
|
|
1226
|
+
<button
|
|
1227
|
+
type="button"
|
|
1228
|
+
onClick={() => (resultCount > 0 || searches.length > 1) && setOpen((value) => !value)}
|
|
1229
|
+
className="inline-flex items-center gap-2 rounded-md border border-border/50 bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted/50"
|
|
1230
|
+
>
|
|
1231
|
+
<Globe className="size-3.5 shrink-0" />
|
|
1232
|
+
<span>{running ? 'Searching the web…' : 'Searched the web'}</span>
|
|
1233
|
+
{!running && resultCount > 0 ? (
|
|
1234
|
+
<span className="rounded-full bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-foreground">
|
|
1235
|
+
{resultCount} result{resultCount === 1 ? '' : 's'}
|
|
1236
|
+
</span>
|
|
1237
|
+
) : null}
|
|
1238
|
+
{!running && (resultCount > 0 || searches.length > 1) ? (
|
|
1239
|
+
<ChevronDown className={`size-3 transition-transform ${open ? 'rotate-180' : ''}`} />
|
|
1240
|
+
) : null}
|
|
1241
|
+
</button>
|
|
1242
|
+
|
|
1243
|
+
{open ? (
|
|
1244
|
+
<div className="mt-2 max-w-xl overflow-hidden rounded-lg border border-border/60 bg-background text-xs">
|
|
1245
|
+
{searches
|
|
1246
|
+
.flatMap((search) => search.results)
|
|
1247
|
+
.slice(0, 5)
|
|
1248
|
+
.map((result: any, index) => (
|
|
1249
|
+
<a
|
|
1250
|
+
key={`${result.url ?? result.title ?? index}-${index}`}
|
|
1251
|
+
href={result.url}
|
|
1252
|
+
target="_blank"
|
|
1253
|
+
rel="noreferrer"
|
|
1254
|
+
className="block border-b border-border/50 px-3 py-2.5 last:border-0 hover:bg-muted/40"
|
|
1255
|
+
>
|
|
1256
|
+
<span className="block truncate font-medium text-foreground">
|
|
1257
|
+
{result.title || result.url}
|
|
1258
|
+
</span>
|
|
1259
|
+
{result.snippet ? (
|
|
1260
|
+
<span className="mt-0.5 block line-clamp-2 text-muted-foreground">
|
|
1261
|
+
{result.snippet}
|
|
1262
|
+
</span>
|
|
1263
|
+
) : null}
|
|
1264
|
+
</a>
|
|
1265
|
+
))}
|
|
1266
|
+
</div>
|
|
1267
|
+
) : null}
|
|
1268
|
+
</div>
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function normalizeRepeatedOrderedListMarkers(text: string): string {
|
|
1273
|
+
const lines = text.split('\n');
|
|
1274
|
+
let previousRepeatedMarker: number | null = null;
|
|
1275
|
+
let nextNumber = 2;
|
|
1276
|
+
|
|
1277
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1278
|
+
const marker = lines[index].match(/^(\s*)1\.\s+(.+)$/);
|
|
1279
|
+
if (!marker) {
|
|
1280
|
+
if (/^\s*\d+\.\s+/.test(lines[index])) {
|
|
1281
|
+
previousRepeatedMarker = null;
|
|
1282
|
+
nextNumber = 2;
|
|
1283
|
+
}
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const onlySupportingListContentSincePrevious =
|
|
1288
|
+
previousRepeatedMarker !== null &&
|
|
1289
|
+
lines
|
|
1290
|
+
.slice(previousRepeatedMarker + 1, index)
|
|
1291
|
+
.every((line) => !line.trim() || /^\s*[-*+]\s+/.test(line));
|
|
1292
|
+
|
|
1293
|
+
if (onlySupportingListContentSincePrevious) {
|
|
1294
|
+
lines[index] = `${marker[1]}${nextNumber}. ${marker[2]}`;
|
|
1295
|
+
nextNumber += 1;
|
|
1296
|
+
} else {
|
|
1297
|
+
nextNumber = 2;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
previousRepeatedMarker = index;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
return lines.join('\n');
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export function renderMarkdown(text: string): string {
|
|
1307
|
+
if (!text) return '';
|
|
1308
|
+
|
|
1309
|
+
let html = normalizeRepeatedOrderedListMarkers(text)
|
|
1310
|
+
.replace(/&/g, '&')
|
|
1311
|
+
.replace(/</g, '<')
|
|
1312
|
+
.replace(/>/g, '>');
|
|
1313
|
+
|
|
1314
|
+
// Math blocks \[ ... \] and $$ ... $$
|
|
1315
|
+
html = html.replace(/\\\[([\s\S]+?)\\\]|\$\$([\s\S]+?)\$\$/g, (match, p1, p2) => {
|
|
1316
|
+
const math = p1 || p2;
|
|
1317
|
+
try {
|
|
1318
|
+
return katex.renderToString(math, { displayMode: true, throwOnError: false });
|
|
1319
|
+
} catch {
|
|
1320
|
+
return match;
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// Inline math \( ... \) and $ ... $
|
|
1325
|
+
html = html.replace(/\\\(([\s\S]+?)\\\)|\$([^$\n]+?)\$/g, (match, p1, p2) => {
|
|
1326
|
+
const math = p1 || p2;
|
|
1327
|
+
try {
|
|
1328
|
+
return katex.renderToString(math, { displayMode: false, throwOnError: false });
|
|
1329
|
+
} catch {
|
|
1330
|
+
return match;
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
html = html.replace(
|
|
1335
|
+
/!\[([^\]]*)\]\([^)]*\)/g,
|
|
1336
|
+
(_match, alt) =>
|
|
1337
|
+
`<span class="text-muted-foreground italic">[Image unavailable${
|
|
1338
|
+
alt ? `: ${alt}` : ''
|
|
1339
|
+
}]</span>`,
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1342
|
+
// Code blocks — styled premium
|
|
1343
|
+
html = html.replace(
|
|
1344
|
+
/```(\w*)\n([\s\S]*?)```/g,
|
|
1345
|
+
'<pre class="msg-code-block"><code>$2</code></pre>',
|
|
1346
|
+
);
|
|
1347
|
+
|
|
1348
|
+
// Inline code
|
|
1349
|
+
html = html.replace(/`([^`]+)`/g, '<code class="msg-inline-code">$1</code>');
|
|
1350
|
+
|
|
1351
|
+
// Bold
|
|
1352
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong class="msg-bold">$1</strong>');
|
|
1353
|
+
|
|
1354
|
+
// Italic
|
|
1355
|
+
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
1356
|
+
|
|
1357
|
+
// Links and Media (Video/Audio)
|
|
1358
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, url) => {
|
|
1359
|
+
const lowerUrl = url.toLowerCase();
|
|
1360
|
+
if (lowerUrl.match(/\.(mp4|webm|ogg)$/) || lowerUrl.includes('video')) {
|
|
1361
|
+
return `<video src="${url}" controls preload="metadata" class="max-w-90 w-full rounded-lg my-2 border border-border bg-card"></video>`;
|
|
1362
|
+
}
|
|
1363
|
+
if (lowerUrl.match(/\.(mp3|wav|m4a|oga)$/) || lowerUrl.includes('audio')) {
|
|
1364
|
+
return `<audio src="${url}" controls preload="metadata" class="max-w-80 w-full my-2"></audio>`;
|
|
1365
|
+
}
|
|
1366
|
+
return `<a href="${url}" target="_blank" rel="noreferrer" class="msg-link">${label}</a>`;
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
html = html.replace(/^### (.+)$/gm, '<h3 class="msg-h3">$1</h3>');
|
|
1370
|
+
html = html.replace(/^## (.+)$/gm, '<h2 class="msg-h2">$1</h2>');
|
|
1371
|
+
html = html.replace(/^# (.+)$/gm, '<h1 class="msg-h1">$1</h1>');
|
|
1372
|
+
|
|
1373
|
+
// Unordered lists — collect consecutive lines into <ul>
|
|
1374
|
+
html = html.replace(/^- (.+)$/gm, '<li class="msg-li">$1</li>');
|
|
1375
|
+
// Wrap consecutive <li> in <ul> (including when separated by blank lines)
|
|
1376
|
+
html = html.replace(
|
|
1377
|
+
/(<li class="msg-li">.*?<\/li>\n?)+/g,
|
|
1378
|
+
(match) => `<ul class="msg-ul">${match}</ul>`,
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
html = html.replace(/(^\d+\. .+$)(\n\n)((?=\d+\. ))/gm, '$1\n$3');
|
|
1382
|
+
|
|
1383
|
+
html = html.replace(/^(\d+)\. (.+)$/gm, '<li class="msg-li-ordered" data-order="$1">$2</li>');
|
|
1384
|
+
html = html.replace(/(<li class="msg-li-ordered" data-order="\d+">.*?<\/li>\n?)+/g, (match) => {
|
|
1385
|
+
const start = match.match(/data-order="(\d+)"/)?.[1] ?? '1';
|
|
1386
|
+
return `<ol class="msg-ol" start="${start}">${match.replace(/ data-order="\d+"/g, '')}</ol>`;
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// Paragraphs
|
|
1390
|
+
html = html.replace(/\n\n/g, '</p><p class="msg-p">');
|
|
1391
|
+
|
|
1392
|
+
html = html.replace(/(<?\/?(?:ul|ol|li|h1|h2|h3|pre)[^>]*>)\n+/g, '$1');
|
|
1393
|
+
html = html.replace(/\n+(<\/?(?:ul|ol|li|h1|h2|h3|pre)[^>]*>)/g, '$1');
|
|
1394
|
+
|
|
1395
|
+
html = html.replace(/\n/g, '<br/>');
|
|
1396
|
+
|
|
1397
|
+
return html;
|
|
1398
|
+
}
|