@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,1552 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { workspaceUrl, workspaceFetch as fetch } from '../../host';
|
|
4
|
+
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
5
|
+
import { useChat } from '@ai-sdk/react';
|
|
6
|
+
import { DefaultChatTransport } from 'ai';
|
|
7
|
+
import type { RagConfig } from '@larkup/core/types';
|
|
8
|
+
import {
|
|
9
|
+
ArrowUp,
|
|
10
|
+
SquarePen,
|
|
11
|
+
RotateCcw,
|
|
12
|
+
TriangleAlert,
|
|
13
|
+
Loader2,
|
|
14
|
+
ChevronDown,
|
|
15
|
+
MessageCircle,
|
|
16
|
+
Search,
|
|
17
|
+
X,
|
|
18
|
+
History,
|
|
19
|
+
Trash2,
|
|
20
|
+
Plus,
|
|
21
|
+
Globe,
|
|
22
|
+
Paperclip,
|
|
23
|
+
ListPlus,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
import useSWR, { mutate as globalMutate } from 'swr';
|
|
26
|
+
import { MessageItem } from './message-item';
|
|
27
|
+
import { MessageQueue, type QueuedMessage } from './message-queue';
|
|
28
|
+
import { JobsTrackingButton } from '../data/jobs-tracking-button';
|
|
29
|
+
import { shouldAutoOpenSupportingClip } from '../../lib/chat-supporting-clip';
|
|
30
|
+
import { useProject } from '../projects/project-provider';
|
|
31
|
+
import { cn } from '../../lib/utils';
|
|
32
|
+
import { getProviderMeta, ProviderIcon } from '@larkup/workspace-ui/provider-icon';
|
|
33
|
+
import { get, set, del, keys } from '../../storage';
|
|
34
|
+
import { useRouter } from 'next/navigation';
|
|
35
|
+
import { useChatStore } from '../../store/chat-store';
|
|
36
|
+
import { toast } from 'sonner';
|
|
37
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
38
|
+
import {
|
|
39
|
+
Sheet,
|
|
40
|
+
SheetClose,
|
|
41
|
+
SheetContent,
|
|
42
|
+
SheetHeader,
|
|
43
|
+
SheetTitle,
|
|
44
|
+
SheetTrigger,
|
|
45
|
+
} from '../ui/sheet';
|
|
46
|
+
import {
|
|
47
|
+
AlertDialog,
|
|
48
|
+
AlertDialogAction,
|
|
49
|
+
AlertDialogCancel,
|
|
50
|
+
AlertDialogContent,
|
|
51
|
+
AlertDialogDescription,
|
|
52
|
+
AlertDialogFooter,
|
|
53
|
+
AlertDialogHeader,
|
|
54
|
+
AlertDialogTitle,
|
|
55
|
+
AlertDialogTrigger,
|
|
56
|
+
} from '../ui/alert-dialog';
|
|
57
|
+
import { DocEditorProvider, useDocEditor } from './canvas/doc-editor-provider';
|
|
58
|
+
import { DocumentCanvas } from './canvas/document-canvas';
|
|
59
|
+
import { FileAttachmentButton } from './canvas/file-attachment-button';
|
|
60
|
+
import {
|
|
61
|
+
DropdownMenu,
|
|
62
|
+
DropdownMenuContent,
|
|
63
|
+
DropdownMenuItem,
|
|
64
|
+
DropdownMenuTrigger,
|
|
65
|
+
} from '../ui/dropdown-menu';
|
|
66
|
+
import { Switch } from '../ui/switch';
|
|
67
|
+
import { ChatComposerSurface, ChatEmptyState, ChatMessagesViewport } from '@larkup/workspace-ui';
|
|
68
|
+
import {
|
|
69
|
+
answerFeedbackForMessage,
|
|
70
|
+
groundedAnswerFeedbackContext,
|
|
71
|
+
updateAnswerFeedback,
|
|
72
|
+
type AnswerFeedback,
|
|
73
|
+
} from '../../lib/chat/message-feedback';
|
|
74
|
+
|
|
75
|
+
const fetcher = async <T,>(url: string): Promise<T> => {
|
|
76
|
+
const response = await fetch(url);
|
|
77
|
+
const payload = await response.json().catch(() => null);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const message =
|
|
80
|
+
payload &&
|
|
81
|
+
typeof payload === 'object' &&
|
|
82
|
+
'error' in payload &&
|
|
83
|
+
typeof payload.error === 'string'
|
|
84
|
+
? payload.error
|
|
85
|
+
: 'Could not load chat configuration.';
|
|
86
|
+
throw new Error(message);
|
|
87
|
+
}
|
|
88
|
+
return payload as T;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function compactToolResult(value: any): any {
|
|
92
|
+
if (!value || typeof value !== 'object') return value;
|
|
93
|
+
if (value.type === 'json' && 'value' in value) {
|
|
94
|
+
return { ...value, value: compactToolResult(value.value) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const compact = { ...value };
|
|
98
|
+
if (Array.isArray(compact.rows)) compact.rows = compact.rows.slice(0, 50);
|
|
99
|
+
if (Array.isArray(compact.data)) compact.data = compact.data.slice(0, 100);
|
|
100
|
+
if (typeof compact.stdout === 'string') compact.stdout = compact.stdout.slice(0, 4_000);
|
|
101
|
+
if (typeof compact.stderr === 'string') compact.stderr = compact.stderr.slice(0, 1_000);
|
|
102
|
+
if (typeof compact.fileBase64 === 'string') delete compact.fileBase64;
|
|
103
|
+
if (Array.isArray(compact.pages)) delete compact.pages;
|
|
104
|
+
return compact;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Compact messages for the LLM context to prevent massive payloads (like chart data,
|
|
109
|
+
* table rows, and document base64s) from bloating the request and exceeding token limits.
|
|
110
|
+
*/
|
|
111
|
+
function compactMessagesForLLM(messages: any[]) {
|
|
112
|
+
return messages.map((message, index) => {
|
|
113
|
+
// Keep the latest message completely intact
|
|
114
|
+
if (index === messages.length - 1) return message;
|
|
115
|
+
|
|
116
|
+
const { toolInvocations, experimental_attachments, parts, ...rest } = message;
|
|
117
|
+
|
|
118
|
+
// Keep tool invocations but strip heavy data fields from the results
|
|
119
|
+
const compactToolInvocations = Array.isArray(toolInvocations)
|
|
120
|
+
? toolInvocations.map((ti) => {
|
|
121
|
+
if (ti.state === 'result' && ti.result) {
|
|
122
|
+
return { ...ti, result: compactToolResult(ti.result) };
|
|
123
|
+
}
|
|
124
|
+
return ti;
|
|
125
|
+
})
|
|
126
|
+
: toolInvocations;
|
|
127
|
+
|
|
128
|
+
// Do the same for parts
|
|
129
|
+
const compactParts = Array.isArray(parts)
|
|
130
|
+
? parts.map((part) => {
|
|
131
|
+
if (part.type === 'tool-invocation') {
|
|
132
|
+
const ti = part.toolInvocation;
|
|
133
|
+
if (ti?.state === 'result' && ti.result) {
|
|
134
|
+
return { ...part, toolInvocation: { ...ti, result: compactToolResult(ti.result) } };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// AI SDK v5 serializes completed tool calls as `tool-<name>` parts.
|
|
138
|
+
// Keep the metadata needed by the server-side evidence ledger while
|
|
139
|
+
// bounding rows/chart points before the next chat request.
|
|
140
|
+
if (part.type?.startsWith('tool-')) {
|
|
141
|
+
return {
|
|
142
|
+
...part,
|
|
143
|
+
...(part.output !== undefined ? { output: compactToolResult(part.output) } : {}),
|
|
144
|
+
...(part.result !== undefined ? { result: compactToolResult(part.result) } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return part;
|
|
148
|
+
})
|
|
149
|
+
: parts;
|
|
150
|
+
|
|
151
|
+
// Strip attachment payloads to just labels to save context window
|
|
152
|
+
const attachmentLabels = Array.isArray(experimental_attachments)
|
|
153
|
+
? experimental_attachments.map((attachment) => ({
|
|
154
|
+
name: attachment?.name,
|
|
155
|
+
contentType: attachment?.contentType,
|
|
156
|
+
size: attachment?.size,
|
|
157
|
+
}))
|
|
158
|
+
: experimental_attachments;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...rest,
|
|
162
|
+
...(compactParts ? { parts: compactParts } : {}),
|
|
163
|
+
...(compactToolInvocations ? { toolInvocations: compactToolInvocations } : {}),
|
|
164
|
+
...(attachmentLabels ? { experimental_attachments: attachmentLabels } : {}),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface AvailableModel {
|
|
170
|
+
id: string;
|
|
171
|
+
label: string;
|
|
172
|
+
provider: string;
|
|
173
|
+
context_window?: number;
|
|
174
|
+
tags?: string[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getFileIconPath(file: File) {
|
|
178
|
+
const extension = file.name.split('.').pop()?.toLowerCase();
|
|
179
|
+
const mimeType = file.type;
|
|
180
|
+
|
|
181
|
+
if (['csv', 'xls', 'xlsx'].includes(extension || '')) return '/icons/excel.png';
|
|
182
|
+
if (['doc', 'docx'].includes(extension || '')) return '/icons/word.png';
|
|
183
|
+
if (['md', 'markdown'].includes(extension || '')) return '/icons/markdown.png';
|
|
184
|
+
if (['pdf'].includes(extension || '')) return '/icons/pdf.png';
|
|
185
|
+
|
|
186
|
+
if (mimeType.startsWith('video/')) return '/icons/video.png';
|
|
187
|
+
if (mimeType.startsWith('audio/')) return '/icons/audio.png';
|
|
188
|
+
if (mimeType.startsWith('image/')) return '/icons/image.png';
|
|
189
|
+
|
|
190
|
+
return '/icons/image.png'; // Fallback
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function AttachmentCard({
|
|
194
|
+
file,
|
|
195
|
+
onClick,
|
|
196
|
+
onRemove,
|
|
197
|
+
}: {
|
|
198
|
+
file: File;
|
|
199
|
+
onClick?: () => void;
|
|
200
|
+
onRemove: () => void;
|
|
201
|
+
}) {
|
|
202
|
+
const iconPath = getFileIconPath(file);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
className="relative mt-2 group flex items-center gap-2 bg-muted/50 border border-border/60 rounded-xl h-12 px-3 p-1 shrink-0 cursor-pointer hover:bg-muted/80 transition-colors"
|
|
207
|
+
onClick={onClick}
|
|
208
|
+
>
|
|
209
|
+
<div className="size-7 shrink-0 rounded-md overflow-hidden bg-background border border-border/90 flex items-center justify-center p-1">
|
|
210
|
+
<img src={iconPath} alt={file.name} className="w-full h-full object-contain" />
|
|
211
|
+
</div>
|
|
212
|
+
<div className="flex flex-col justify-center max-w-30">
|
|
213
|
+
<span className="text-xs font-medium truncate text-foreground">{file.name}</span>
|
|
214
|
+
<span className="text-[10px] text-muted-foreground">
|
|
215
|
+
{(file.size / 1024).toFixed(0)} KB
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={(e) => {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
e.stopPropagation();
|
|
223
|
+
onRemove();
|
|
224
|
+
}}
|
|
225
|
+
className="absolute cursor-pointer -top-3 -right-2 flex h-5 w-5 items-center justify-center rounded-full bg-background border border-border text-muted-foreground hover:bg-destructive hover:border-destructive hover:text-white transition opacity-0 group-hover:opacity-100"
|
|
226
|
+
>
|
|
227
|
+
<X className="size-3" />
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface ChatStatus {
|
|
234
|
+
ready: boolean;
|
|
235
|
+
indexed: boolean;
|
|
236
|
+
blockers: string[];
|
|
237
|
+
provider: string;
|
|
238
|
+
chatModelId: string;
|
|
239
|
+
availableModels: AvailableModel[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function ChatWorkspace({ chatId }: { chatId?: string } = {}) {
|
|
243
|
+
return (
|
|
244
|
+
<DocEditorProvider>
|
|
245
|
+
<DocumentCanvas>
|
|
246
|
+
<ChatWorkspaceInner chatId={chatId} />
|
|
247
|
+
</DocumentCanvas>
|
|
248
|
+
</DocEditorProvider>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function ChatWorkspaceInner({ chatId }: { chatId?: string }) {
|
|
253
|
+
const { activeProject } = useProject();
|
|
254
|
+
const { sessionId, parsedDocument, openCanvas, isLoading: isDocLoading } = useDocEditor();
|
|
255
|
+
const docEditorState = { sessionId, fields: parsedDocument?.fields || [] };
|
|
256
|
+
const projectId = activeProject?.id ?? null;
|
|
257
|
+
|
|
258
|
+
// Track dragging state
|
|
259
|
+
const dragCounter = useRef(0);
|
|
260
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
261
|
+
|
|
262
|
+
const statusKey = projectId
|
|
263
|
+
? workspaceUrl(`/api/chat/status?projectId=${encodeURIComponent(projectId)}`)
|
|
264
|
+
: workspaceUrl('/api/chat/status');
|
|
265
|
+
const {
|
|
266
|
+
data: status,
|
|
267
|
+
error: statusError,
|
|
268
|
+
isLoading: statusLoading,
|
|
269
|
+
} = useSWR<ChatStatus>(statusKey, fetcher, {
|
|
270
|
+
refreshInterval: 10000,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const { data: configData, mutate: mutateConfig } = useSWR<{ config: RagConfig }>(
|
|
274
|
+
workspaceUrl('/api/config'),
|
|
275
|
+
fetcher,
|
|
276
|
+
);
|
|
277
|
+
const router = useRouter();
|
|
278
|
+
|
|
279
|
+
const { selectedModel, setSelectedModel } = useChatStore();
|
|
280
|
+
const [showModelSelect, setShowModelSelect] = useState(false);
|
|
281
|
+
const [modelSearch, setModelSearch] = useState('');
|
|
282
|
+
const [input, setInput] = useState('');
|
|
283
|
+
const [attachments, setAttachments] = useState<File[]>([]);
|
|
284
|
+
const [pendingMessage, setPendingMessage] = useState<{ text: string; files?: any } | null>(null);
|
|
285
|
+
const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
|
|
286
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
287
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const el = textareaRef.current;
|
|
291
|
+
if (!el) return;
|
|
292
|
+
el.style.height = 'auto';
|
|
293
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
294
|
+
}, [input]);
|
|
295
|
+
|
|
296
|
+
const [currentChatId, setCurrentChatId] = useState<string>(chatId || '');
|
|
297
|
+
const [history, setHistory] = useState<{ id: string; title: string; updatedAt: number }[]>([]);
|
|
298
|
+
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
|
299
|
+
const [historySearch, setHistorySearch] = useState('');
|
|
300
|
+
const [isInitializingChat, setIsInitializingChat] = useState(!!chatId);
|
|
301
|
+
|
|
302
|
+
// Load history on mount
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
get('chat_sessions').then((val) => {
|
|
305
|
+
if (val) setHistory(val);
|
|
306
|
+
});
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
// Update selected model when status loads
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!status?.chatModelId) return;
|
|
312
|
+
const available = status.availableModels?.some((model) => model.id === selectedModel);
|
|
313
|
+
if (!selectedModel || !available) setSelectedModel(status.chatModelId);
|
|
314
|
+
}, [status?.chatModelId, status?.availableModels, selectedModel, setSelectedModel]);
|
|
315
|
+
|
|
316
|
+
// Focus search input when dropdown opens
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (showModelSelect) {
|
|
319
|
+
// Small delay to let the DOM render
|
|
320
|
+
requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
321
|
+
} else {
|
|
322
|
+
setModelSearch('');
|
|
323
|
+
}
|
|
324
|
+
}, [showModelSelect]);
|
|
325
|
+
|
|
326
|
+
const chatBody = useMemo(
|
|
327
|
+
() => ({
|
|
328
|
+
projectId,
|
|
329
|
+
chatModelId: selectedModel || undefined,
|
|
330
|
+
docSessionId: docEditorState.sessionId,
|
|
331
|
+
docFields: docEditorState.fields,
|
|
332
|
+
}),
|
|
333
|
+
[projectId, selectedModel, docEditorState.sessionId, JSON.stringify(docEditorState.fields)],
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const chatBodyRef = useRef(chatBody);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
chatBodyRef.current = chatBody;
|
|
339
|
+
}, [chatBody]);
|
|
340
|
+
|
|
341
|
+
const transport = useMemo(
|
|
342
|
+
() =>
|
|
343
|
+
new DefaultChatTransport({
|
|
344
|
+
api: workspaceUrl('/api/chat'),
|
|
345
|
+
headers: {
|
|
346
|
+
'Content-Type': 'application/json',
|
|
347
|
+
},
|
|
348
|
+
prepareSendMessagesRequest({ messages, id, body }) {
|
|
349
|
+
return {
|
|
350
|
+
body: {
|
|
351
|
+
messages: compactMessagesForLLM(messages),
|
|
352
|
+
id,
|
|
353
|
+
...chatBodyRef.current,
|
|
354
|
+
...body,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
[],
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const {
|
|
363
|
+
messages,
|
|
364
|
+
sendMessage,
|
|
365
|
+
status: chatStatus,
|
|
366
|
+
setMessages,
|
|
367
|
+
error,
|
|
368
|
+
regenerate,
|
|
369
|
+
addToolResult,
|
|
370
|
+
} = useChat({
|
|
371
|
+
transport,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Initialize specific chat if ID is provided via prop
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
setQueuedMessages([]);
|
|
377
|
+
if (chatId) {
|
|
378
|
+
setIsInitializingChat(true);
|
|
379
|
+
get(`chat_messages_${chatId}`).then((msgs) => {
|
|
380
|
+
if (msgs) {
|
|
381
|
+
setMessages(msgs);
|
|
382
|
+
setCurrentChatId(chatId);
|
|
383
|
+
}
|
|
384
|
+
setIsInitializingChat(false);
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
setIsInitializingChat(false);
|
|
388
|
+
}
|
|
389
|
+
}, [chatId, setMessages]);
|
|
390
|
+
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (pendingMessage && docEditorState.sessionId && !isDocLoading) {
|
|
393
|
+
sendMessage(pendingMessage);
|
|
394
|
+
setPendingMessage(null);
|
|
395
|
+
}
|
|
396
|
+
}, [pendingMessage, docEditorState.sessionId, isDocLoading, sendMessage]);
|
|
397
|
+
|
|
398
|
+
// Auto-open PDF if the AI uses a document tool and the canvas is not open
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
if (!docEditorState.sessionId && openCanvas && messages.length > 0 && !isDocLoading) {
|
|
401
|
+
const lastMsg = messages[messages.length - 1] as any;
|
|
402
|
+
if (lastMsg.role === 'assistant' && lastMsg.toolInvocations) {
|
|
403
|
+
const hasDocTool = lastMsg.toolInvocations.some((t: any) =>
|
|
404
|
+
['requestDocumentSignature', 'fillDocumentForm', 'editDocument'].includes(t.toolName),
|
|
405
|
+
);
|
|
406
|
+
if (hasDocTool) {
|
|
407
|
+
// Find the last PDF attachment in the chat
|
|
408
|
+
let targetAtt: any = null;
|
|
409
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
410
|
+
const m = messages[i] as any;
|
|
411
|
+
if (m.experimental_attachments) {
|
|
412
|
+
const pdfs = m.experimental_attachments.filter(
|
|
413
|
+
(a: any) =>
|
|
414
|
+
a.contentType?.includes('pdf') || a.name?.toLowerCase().endsWith('.pdf'),
|
|
415
|
+
);
|
|
416
|
+
if (pdfs.length > 0) {
|
|
417
|
+
targetAtt = pdfs[pdfs.length - 1];
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (targetAtt && targetAtt.url) {
|
|
424
|
+
let fetchUrl = targetAtt.url;
|
|
425
|
+
if (!fetchUrl.startsWith('http') && !fetchUrl.startsWith('data:')) {
|
|
426
|
+
fetchUrl = `data:${targetAtt.contentType || 'application/pdf'};base64,${fetchUrl}`;
|
|
427
|
+
}
|
|
428
|
+
fetch(fetchUrl)
|
|
429
|
+
.then((r) => r.blob())
|
|
430
|
+
.then((blob) => {
|
|
431
|
+
const file = new File([blob], targetAtt.name || 'document.pdf', {
|
|
432
|
+
type: targetAtt.contentType || 'application/pdf',
|
|
433
|
+
});
|
|
434
|
+
openCanvas(file, { background: false });
|
|
435
|
+
})
|
|
436
|
+
.catch((err) => console.error('Auto-open failed', err));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}, [messages, docEditorState.sessionId, openCanvas, isDocLoading]);
|
|
442
|
+
|
|
443
|
+
// Reset chat context when switching workspaces to prevent overlap
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
setMessages([]);
|
|
446
|
+
setCurrentChatId('');
|
|
447
|
+
setQueuedMessages([]);
|
|
448
|
+
}, [projectId, setMessages]);
|
|
449
|
+
|
|
450
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
451
|
+
const shouldAutoScrollRef = useRef(true);
|
|
452
|
+
const isBusy = chatStatus === 'submitted' || chatStatus === 'streaming';
|
|
453
|
+
|
|
454
|
+
// Drain the queue one message at a time once the assistant is free again
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (queuedMessages.length === 0 || isBusy || isDocLoading || pendingMessage) return;
|
|
457
|
+
const [next, ...rest] = queuedMessages;
|
|
458
|
+
setQueuedMessages(rest);
|
|
459
|
+
shouldAutoScrollRef.current = true;
|
|
460
|
+
const dt = new DataTransfer();
|
|
461
|
+
(next.files ?? []).forEach((file) => dt.items.add(file));
|
|
462
|
+
sendMessage({ text: next.text, files: dt.files.length > 0 ? dt.files : undefined });
|
|
463
|
+
}, [queuedMessages, isBusy, isDocLoading, pendingMessage, sendMessage]);
|
|
464
|
+
|
|
465
|
+
const handleMessageScroll = useCallback(() => {
|
|
466
|
+
const el = scrollRef.current;
|
|
467
|
+
if (!el) return;
|
|
468
|
+
shouldAutoScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
469
|
+
}, []);
|
|
470
|
+
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
const el = scrollRef.current;
|
|
473
|
+
if (!el || !shouldAutoScrollRef.current) return;
|
|
474
|
+
const id = requestAnimationFrame(() => {
|
|
475
|
+
el.scrollTo({
|
|
476
|
+
top: el.scrollHeight,
|
|
477
|
+
behavior: chatStatus === 'streaming' ? 'auto' : 'smooth',
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
return () => cancelAnimationFrame(id);
|
|
481
|
+
}, [messages, chatStatus]);
|
|
482
|
+
// Save messages to history — debounced to prevent cascading re-renders during streaming
|
|
483
|
+
const historySaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
484
|
+
const messagesRef = useRef(messages);
|
|
485
|
+
messagesRef.current = messages;
|
|
486
|
+
const feedbackSyncRevision = useRef<Record<string, number>>({});
|
|
487
|
+
|
|
488
|
+
const handleMessageFeedback = useCallback(
|
|
489
|
+
(messageId: string, feedback: AnswerFeedback | undefined) => {
|
|
490
|
+
const currentMessages = messagesRef.current;
|
|
491
|
+
const currentMessage = currentMessages.find((message) => message.id === messageId);
|
|
492
|
+
const previousFeedback = currentMessage
|
|
493
|
+
? answerFeedbackForMessage(currentMessage)
|
|
494
|
+
: undefined;
|
|
495
|
+
const cacheContext = groundedAnswerFeedbackContext(currentMessages, messageId);
|
|
496
|
+
const nextMessages = updateAnswerFeedback(currentMessages, messageId, feedback);
|
|
497
|
+
messagesRef.current = nextMessages;
|
|
498
|
+
setMessages(nextMessages);
|
|
499
|
+
|
|
500
|
+
// Persist immediately instead of waiting for the normal history debounce;
|
|
501
|
+
// a reload directly after clicking a thumb must not lose the selection.
|
|
502
|
+
const activeId = currentChatId || crypto.randomUUID();
|
|
503
|
+
if (!currentChatId) {
|
|
504
|
+
setCurrentChatId(activeId);
|
|
505
|
+
if (window.location.pathname === '/chat') {
|
|
506
|
+
window.history.replaceState(null, '', `/chat/${activeId}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
void set(`chat_messages_${activeId}`, nextMessages).catch((storageError) => {
|
|
510
|
+
console.error('Failed to save answer feedback', storageError);
|
|
511
|
+
toast.error('Could not save this feedback. Please try again.');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Only plain-document grounded answers enter the reusable answer cache.
|
|
515
|
+
// Media, exports, and multi-tool results keep their specialized paths.
|
|
516
|
+
if (!cacheContext) {
|
|
517
|
+
if (feedback === 'liked') {
|
|
518
|
+
toast.success(
|
|
519
|
+
'Feedback saved. This answer type will use fresh tool results when repeated.',
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const revision = (feedbackSyncRevision.current[messageId] ?? 0) + 1;
|
|
525
|
+
feedbackSyncRevision.current[messageId] = revision;
|
|
526
|
+
void fetch(workspaceUrl('/api/chat/feedback'), {
|
|
527
|
+
method: 'POST',
|
|
528
|
+
headers: { 'Content-Type': 'application/json' },
|
|
529
|
+
keepalive: true,
|
|
530
|
+
body: JSON.stringify({
|
|
531
|
+
projectId,
|
|
532
|
+
feedback,
|
|
533
|
+
...cacheContext,
|
|
534
|
+
}),
|
|
535
|
+
})
|
|
536
|
+
.then(async (response) => {
|
|
537
|
+
const result = (await response.json().catch(() => null)) as {
|
|
538
|
+
cached?: boolean;
|
|
539
|
+
removed?: boolean;
|
|
540
|
+
error?: string;
|
|
541
|
+
} | null;
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
throw new Error(result?.error || 'Could not update the answer cache.');
|
|
544
|
+
}
|
|
545
|
+
if (feedback === 'liked' && result?.cached) {
|
|
546
|
+
toast.success('Answer saved to Larkup cache');
|
|
547
|
+
} else if (feedback === 'disliked' && result?.removed) {
|
|
548
|
+
toast.success('Cached answer removed');
|
|
549
|
+
}
|
|
550
|
+
void globalMutate(workspaceUrl('/api/system/cache'));
|
|
551
|
+
})
|
|
552
|
+
.catch((feedbackError) => {
|
|
553
|
+
if (feedbackSyncRevision.current[messageId] !== revision) return;
|
|
554
|
+
const rolledBack = updateAnswerFeedback(messagesRef.current, messageId, previousFeedback);
|
|
555
|
+
messagesRef.current = rolledBack;
|
|
556
|
+
setMessages(rolledBack);
|
|
557
|
+
void set(`chat_messages_${activeId}`, rolledBack);
|
|
558
|
+
toast.error(
|
|
559
|
+
feedbackError instanceof Error
|
|
560
|
+
? feedbackError.message
|
|
561
|
+
: 'Could not update the answer cache.',
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
[currentChatId, projectId, setMessages],
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
useEffect(() => {
|
|
569
|
+
if (messages.length === 0) return;
|
|
570
|
+
// Don't save while actively streaming — wait for it to settle
|
|
571
|
+
if (chatStatus === 'streaming' || chatStatus === 'submitted') return;
|
|
572
|
+
|
|
573
|
+
// Debounce: clear previous timer
|
|
574
|
+
if (historySaveTimer.current) clearTimeout(historySaveTimer.current);
|
|
575
|
+
|
|
576
|
+
historySaveTimer.current = setTimeout(() => {
|
|
577
|
+
const currentMessages = messagesRef.current;
|
|
578
|
+
if (currentMessages.length === 0) return;
|
|
579
|
+
|
|
580
|
+
let activeId = currentChatId;
|
|
581
|
+
if (!activeId) {
|
|
582
|
+
activeId = crypto.randomUUID();
|
|
583
|
+
setCurrentChatId(activeId);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Save full messages to IDB so UI (charts, tables) can be restored on reload
|
|
587
|
+
set(`chat_messages_${activeId}`, currentMessages);
|
|
588
|
+
|
|
589
|
+
if (window.location.pathname === '/chat') {
|
|
590
|
+
window.history.replaceState(null, '', `/chat/${activeId}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
setHistory((prev) => {
|
|
594
|
+
const existing = prev.find((p) => p.id === activeId);
|
|
595
|
+
const firstMsg = currentMessages[0];
|
|
596
|
+
const firstMsgText =
|
|
597
|
+
(firstMsg as any)?.parts
|
|
598
|
+
?.filter((p: any) => p.type === 'text')
|
|
599
|
+
?.map((p: any) => p.text)
|
|
600
|
+
?.join('') ||
|
|
601
|
+
(firstMsg as any)?.content ||
|
|
602
|
+
(firstMsg as any)?.text;
|
|
603
|
+
|
|
604
|
+
const title = firstMsgText ? firstMsgText.substring(0, 40) : 'New Chat';
|
|
605
|
+
|
|
606
|
+
// Prevent update if nothing changed
|
|
607
|
+
if (existing && existing.title === title) {
|
|
608
|
+
return prev;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let next;
|
|
612
|
+
if (existing) {
|
|
613
|
+
next = prev.map((p) => (p.id === activeId ? { ...p, title, updatedAt: Date.now() } : p));
|
|
614
|
+
} else {
|
|
615
|
+
next = [{ id: activeId, title, updatedAt: Date.now() }, ...prev];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
set('chat_sessions', next);
|
|
619
|
+
return next;
|
|
620
|
+
});
|
|
621
|
+
}, 500);
|
|
622
|
+
|
|
623
|
+
return () => {
|
|
624
|
+
if (historySaveTimer.current) clearTimeout(historySaveTimer.current);
|
|
625
|
+
};
|
|
626
|
+
}, [messages, currentChatId, chatStatus]);
|
|
627
|
+
|
|
628
|
+
// Group and filter models
|
|
629
|
+
const groupedModels = useMemo(() => {
|
|
630
|
+
const models = status?.availableModels ?? [];
|
|
631
|
+
const query = modelSearch.toLowerCase().trim();
|
|
632
|
+
const filtered = query
|
|
633
|
+
? models.filter(
|
|
634
|
+
(m) =>
|
|
635
|
+
m.label.toLowerCase().includes(query) ||
|
|
636
|
+
m.id.toLowerCase().includes(query) ||
|
|
637
|
+
m.provider.toLowerCase().includes(query),
|
|
638
|
+
)
|
|
639
|
+
: models;
|
|
640
|
+
|
|
641
|
+
const groups = new Map<string, AvailableModel[]>();
|
|
642
|
+
for (const m of filtered) {
|
|
643
|
+
const existing = groups.get(m.provider) ?? [];
|
|
644
|
+
existing.push(m);
|
|
645
|
+
groups.set(m.provider, existing);
|
|
646
|
+
}
|
|
647
|
+
return groups;
|
|
648
|
+
}, [status?.availableModels, modelSearch]);
|
|
649
|
+
|
|
650
|
+
const filteredHistory = useMemo(() => {
|
|
651
|
+
const sorted = [...history].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
652
|
+
const query = historySearch.toLowerCase().trim();
|
|
653
|
+
return query ? sorted.filter((chat) => chat.title.toLowerCase().includes(query)) : sorted;
|
|
654
|
+
}, [history, historySearch]);
|
|
655
|
+
|
|
656
|
+
function removeFromQueue(id: string) {
|
|
657
|
+
setQueuedMessages((prev) => prev.filter((m) => m.id !== id));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function moveQueueItem(id: string, direction: -1 | 1) {
|
|
661
|
+
setQueuedMessages((prev) => {
|
|
662
|
+
const index = prev.findIndex((m) => m.id === id);
|
|
663
|
+
const target = index + direction;
|
|
664
|
+
if (index === -1 || target < 0 || target >= prev.length) return prev;
|
|
665
|
+
const next = [...prev];
|
|
666
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
667
|
+
return next;
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function editQueueItem(id: string) {
|
|
672
|
+
const item = queuedMessages.find((m) => m.id === id);
|
|
673
|
+
if (!item) return;
|
|
674
|
+
setQueuedMessages((prev) => prev.filter((m) => m.id !== id));
|
|
675
|
+
setInput(item.text);
|
|
676
|
+
setAttachments(item.files ?? []);
|
|
677
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function handleSubmit(e?: React.FormEvent) {
|
|
681
|
+
if (e) e.preventDefault();
|
|
682
|
+
const text = input.trim();
|
|
683
|
+
if (!text && attachments.length === 0) return;
|
|
684
|
+
|
|
685
|
+
if (isBusy || isDocLoading) {
|
|
686
|
+
setQueuedMessages((prev) => [
|
|
687
|
+
...prev,
|
|
688
|
+
{ id: crypto.randomUUID(), text, files: attachments.length > 0 ? attachments : undefined },
|
|
689
|
+
]);
|
|
690
|
+
setInput('');
|
|
691
|
+
setAttachments([]);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
shouldAutoScrollRef.current = true;
|
|
696
|
+
|
|
697
|
+
const dt = new DataTransfer();
|
|
698
|
+
attachments.forEach((file) => dt.items.add(file));
|
|
699
|
+
const files = dt.files.length > 0 ? dt.files : undefined;
|
|
700
|
+
|
|
701
|
+
if (files) {
|
|
702
|
+
const docFile = Array.from(files).find(
|
|
703
|
+
(f) =>
|
|
704
|
+
f.type === 'application/pdf' ||
|
|
705
|
+
f.name.toLowerCase().endsWith('.pdf') ||
|
|
706
|
+
f.name.toLowerCase().endsWith('.doc') ||
|
|
707
|
+
f.name.toLowerCase().endsWith('.docx'),
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// If we have a document that needs opening, open it BEFORE sending the message!
|
|
711
|
+
if (docFile && !docEditorState.sessionId) {
|
|
712
|
+
toast.info('Preparing document...');
|
|
713
|
+
setPendingMessage({ text, files });
|
|
714
|
+
openCanvas(docFile, { background: false }).catch((err) => {
|
|
715
|
+
toast.error(err.message || 'Failed to open document');
|
|
716
|
+
setPendingMessage(null);
|
|
717
|
+
// Send the message anyway so the user's text isn't lost
|
|
718
|
+
sendMessage({ text, files });
|
|
719
|
+
});
|
|
720
|
+
setInput('');
|
|
721
|
+
setAttachments([]);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
sendMessage({ text, files });
|
|
726
|
+
} else {
|
|
727
|
+
let docToOpen: any = null;
|
|
728
|
+
if (!docEditorState.sessionId) {
|
|
729
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
730
|
+
const m = messages[i] as any;
|
|
731
|
+
if (m.experimental_attachments) {
|
|
732
|
+
const docs = m.experimental_attachments.filter(
|
|
733
|
+
(a: any) =>
|
|
734
|
+
a.contentType?.includes('pdf') ||
|
|
735
|
+
a.name?.toLowerCase().endsWith('.pdf') ||
|
|
736
|
+
a.name?.toLowerCase().endsWith('.doc') ||
|
|
737
|
+
a.name?.toLowerCase().endsWith('.docx'),
|
|
738
|
+
);
|
|
739
|
+
if (docs.length > 0) {
|
|
740
|
+
docToOpen = docs[docs.length - 1];
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (docToOpen) {
|
|
748
|
+
toast.info('Preparing document...');
|
|
749
|
+
let fetchUrl = docToOpen.url;
|
|
750
|
+
if (!fetchUrl.startsWith('http') && !fetchUrl.startsWith('data:')) {
|
|
751
|
+
fetchUrl = `data:${docToOpen.contentType || 'application/pdf'};base64,${fetchUrl}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
setPendingMessage({ text });
|
|
755
|
+
fetch(fetchUrl)
|
|
756
|
+
.then((r) => r.blob())
|
|
757
|
+
.then((blob) => {
|
|
758
|
+
const file = new File([blob], docToOpen.name || 'document.pdf', {
|
|
759
|
+
type: docToOpen.contentType || 'application/pdf',
|
|
760
|
+
});
|
|
761
|
+
openCanvas(file, { background: false }).catch((err) => {
|
|
762
|
+
toast.error(err.message || 'Failed to open document');
|
|
763
|
+
setPendingMessage(null);
|
|
764
|
+
sendMessage({ text });
|
|
765
|
+
});
|
|
766
|
+
})
|
|
767
|
+
.catch((err) => {
|
|
768
|
+
console.error('Auto-open failed', err);
|
|
769
|
+
setPendingMessage(null);
|
|
770
|
+
sendMessage({ text });
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
setInput('');
|
|
774
|
+
setAttachments([]);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
sendMessage({ text });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
setInput('');
|
|
782
|
+
setAttachments([]);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function newChat() {
|
|
786
|
+
setMessages([]);
|
|
787
|
+
setInput('');
|
|
788
|
+
setQueuedMessages([]);
|
|
789
|
+
setCurrentChatId(crypto.randomUUID());
|
|
790
|
+
window.history.pushState(null, '', '/chat');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
useEffect(() => {
|
|
794
|
+
const handleNewChat = () => newChat();
|
|
795
|
+
window.addEventListener('new-chat', handleNewChat);
|
|
796
|
+
return () => window.removeEventListener('new-chat', handleNewChat);
|
|
797
|
+
}, []);
|
|
798
|
+
|
|
799
|
+
async function handleWebSearchToggle() {
|
|
800
|
+
const config = configData?.config;
|
|
801
|
+
if (!config) return;
|
|
802
|
+
|
|
803
|
+
const provider = config.webSearchProvider || 'tavily';
|
|
804
|
+
const apiKeys: Record<string, string | undefined> = {
|
|
805
|
+
tavily: config.tavilyApiKey,
|
|
806
|
+
serper: config.serperApiKey,
|
|
807
|
+
google: config.serperApiKey,
|
|
808
|
+
brave: config.braveApiKey,
|
|
809
|
+
bing: config.bingApiKey,
|
|
810
|
+
exa: config.exaApiKey,
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
if (provider !== 'local' && !apiKeys[provider]) {
|
|
814
|
+
toast.error('Web Search API key is missing. Please configure it in Settings.');
|
|
815
|
+
router.push('/settings?section=search-web');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const nextState = !config.webSearchEnabled;
|
|
820
|
+
try {
|
|
821
|
+
await fetch(workspaceUrl('/api/config'), {
|
|
822
|
+
method: 'PUT',
|
|
823
|
+
headers: { 'Content-Type': 'application/json' },
|
|
824
|
+
body: JSON.stringify({ ...config, webSearchEnabled: nextState }),
|
|
825
|
+
});
|
|
826
|
+
await mutateConfig();
|
|
827
|
+
toast.success(`Web Search ${nextState ? 'enabled' : 'disabled'}`);
|
|
828
|
+
} catch (e: any) {
|
|
829
|
+
toast.error(e.message || 'Failed to update web search setting');
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function loadChat(id: string) {
|
|
834
|
+
const msgs = await get(`chat_messages_${id}`);
|
|
835
|
+
if (msgs) {
|
|
836
|
+
setMessages(msgs);
|
|
837
|
+
setCurrentChatId(id);
|
|
838
|
+
setIsHistoryOpen(false);
|
|
839
|
+
window.history.pushState(null, '', `/chat/${id}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function deleteChat(id: string) {
|
|
844
|
+
await del(`chat_messages_${id}`);
|
|
845
|
+
setHistory((prev) => {
|
|
846
|
+
const next = prev.filter((p) => p.id !== id);
|
|
847
|
+
set('chat_sessions', next);
|
|
848
|
+
return next;
|
|
849
|
+
});
|
|
850
|
+
if (currentChatId === id) {
|
|
851
|
+
setMessages([]);
|
|
852
|
+
setInput('');
|
|
853
|
+
setCurrentChatId('');
|
|
854
|
+
window.history.pushState(null, '', '/chat');
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function deleteAllChats() {
|
|
859
|
+
try {
|
|
860
|
+
const chatStorageKeys = (await keys()).filter(
|
|
861
|
+
(key): key is string =>
|
|
862
|
+
typeof key === 'string' && (key === 'chat_sessions' || key.startsWith('chat_messages_')),
|
|
863
|
+
);
|
|
864
|
+
await Promise.all(chatStorageKeys.map((key) => del(key)));
|
|
865
|
+
setHistory([]);
|
|
866
|
+
setHistorySearch('');
|
|
867
|
+
setMessages([]);
|
|
868
|
+
setQueuedMessages([]);
|
|
869
|
+
setCurrentChatId('');
|
|
870
|
+
window.history.pushState(null, '', '/chat');
|
|
871
|
+
toast.success('Chat history cleared');
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.error('Failed to clear chat history', error);
|
|
874
|
+
toast.error('Could not clear chat history. Please try again.');
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
879
|
+
e.preventDefault();
|
|
880
|
+
e.stopPropagation();
|
|
881
|
+
dragCounter.current++;
|
|
882
|
+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
|
883
|
+
if (Array.from(e.dataTransfer.items).some((item) => item.kind === 'file')) {
|
|
884
|
+
setIsDragging(true);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}, []);
|
|
888
|
+
|
|
889
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
890
|
+
e.preventDefault();
|
|
891
|
+
e.stopPropagation();
|
|
892
|
+
dragCounter.current--;
|
|
893
|
+
if (dragCounter.current === 0) {
|
|
894
|
+
setIsDragging(false);
|
|
895
|
+
}
|
|
896
|
+
}, []);
|
|
897
|
+
|
|
898
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
899
|
+
e.preventDefault();
|
|
900
|
+
e.stopPropagation();
|
|
901
|
+
}, []);
|
|
902
|
+
|
|
903
|
+
const handleDrop = useCallback(
|
|
904
|
+
async (e: React.DragEvent) => {
|
|
905
|
+
e.preventDefault();
|
|
906
|
+
e.stopPropagation();
|
|
907
|
+
setIsDragging(false);
|
|
908
|
+
dragCounter.current = 0;
|
|
909
|
+
|
|
910
|
+
const fileList = e.dataTransfer.files;
|
|
911
|
+
if (!fileList || fileList.length === 0) return;
|
|
912
|
+
|
|
913
|
+
const newAttachments = Array.from(fileList);
|
|
914
|
+
|
|
915
|
+
if (newAttachments.length === 0) {
|
|
916
|
+
toast.error('No valid files dropped.');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
921
|
+
|
|
922
|
+
// Auto-open PDF if dropped
|
|
923
|
+
const pdfFile = newAttachments.find(
|
|
924
|
+
(f) => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'),
|
|
925
|
+
);
|
|
926
|
+
if (pdfFile) {
|
|
927
|
+
openCanvas(pdfFile, { background: false }).catch((err) => {
|
|
928
|
+
toast.error(err.message || 'Failed to auto-open document');
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
[openCanvas],
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
const isEmpty = messages.length === 0 && !isInitializingChat;
|
|
936
|
+
const ready = status?.ready ?? false;
|
|
937
|
+
const selectedModelData = status?.availableModels?.find((m) => m.id === selectedModel);
|
|
938
|
+
|
|
939
|
+
if (statusLoading) {
|
|
940
|
+
return (
|
|
941
|
+
<div className="flex flex-1 items-center justify-center">
|
|
942
|
+
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
|
943
|
+
</div>
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return (
|
|
948
|
+
<div
|
|
949
|
+
className={cn(
|
|
950
|
+
'flex h-full w-full flex-1 flex-col overflow-hidden relative',
|
|
951
|
+
isDragging && 'bg-primary/5',
|
|
952
|
+
)}
|
|
953
|
+
onDragEnter={handleDragEnter}
|
|
954
|
+
onDragLeave={handleDragLeave}
|
|
955
|
+
onDragOver={handleDragOver}
|
|
956
|
+
onDrop={handleDrop}
|
|
957
|
+
>
|
|
958
|
+
{isDragging && (
|
|
959
|
+
<div className="absolute inset-0 z-100 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm border-2 border-dashed border-primary/50 m-4 rounded-3xl pointer-events-none">
|
|
960
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-primary/10 text-primary mb-4">
|
|
961
|
+
<Paperclip className="size-10" />
|
|
962
|
+
</div>
|
|
963
|
+
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
|
964
|
+
Drop document to attach
|
|
965
|
+
</h2>
|
|
966
|
+
<p className="text-sm text-muted-foreground mt-1">PDF, Word, PPT, TXT</p>
|
|
967
|
+
</div>
|
|
968
|
+
)}
|
|
969
|
+
{/* Header bar */}
|
|
970
|
+
<div className="relative z-40 flex items-center justify-between px-4 py-2">
|
|
971
|
+
<div className="flex items-center gap-3">{/* We moved model selector to the right */}</div>
|
|
972
|
+
<div className="flex items-center gap-2">
|
|
973
|
+
{/* Model selector with search */}
|
|
974
|
+
{status?.availableModels && status.availableModels.length > 0 && (
|
|
975
|
+
<div className="relative">
|
|
976
|
+
<button
|
|
977
|
+
type="button"
|
|
978
|
+
onClick={() => setShowModelSelect((o) => !o)}
|
|
979
|
+
className="flex w-75 justify-between items-center gap-1.5 rounded-lg border border-border bg-white dark:bg-card px-3 py-1.5 text-xs text-foreground transition hover:bg-muted"
|
|
980
|
+
>
|
|
981
|
+
<span className="flex items-center gap-2 truncate">
|
|
982
|
+
{selectedModelData && (
|
|
983
|
+
<ProviderIcon
|
|
984
|
+
src={getProviderMeta(selectedModelData.provider).iconSrc}
|
|
985
|
+
alt={selectedModelData.provider}
|
|
986
|
+
pillBg={getProviderMeta(selectedModelData.provider).pillBg}
|
|
987
|
+
size={16}
|
|
988
|
+
/>
|
|
989
|
+
)}
|
|
990
|
+
<span className="truncate">
|
|
991
|
+
{selectedModelData?.label || selectedModel || 'Select model'}
|
|
992
|
+
</span>
|
|
993
|
+
</span>
|
|
994
|
+
<ChevronDown
|
|
995
|
+
className={cn(
|
|
996
|
+
'size-3 shrink-0 text-muted-foreground transition-transform',
|
|
997
|
+
showModelSelect && 'rotate-180',
|
|
998
|
+
)}
|
|
999
|
+
/>
|
|
1000
|
+
</button>
|
|
1001
|
+
{showModelSelect && (
|
|
1002
|
+
<>
|
|
1003
|
+
<div className="fixed inset-0 z-40" onClick={() => setShowModelSelect(false)} />
|
|
1004
|
+
<div className="absolute right-0 top-full z-50 mt-1 w-75 rounded-lg border border-border bg-card ">
|
|
1005
|
+
{/* Search input */}
|
|
1006
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
1007
|
+
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1008
|
+
<input
|
|
1009
|
+
ref={searchInputRef}
|
|
1010
|
+
type="text"
|
|
1011
|
+
value={modelSearch}
|
|
1012
|
+
onChange={(e) => setModelSearch(e.target.value)}
|
|
1013
|
+
placeholder="Search models..."
|
|
1014
|
+
className="flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground"
|
|
1015
|
+
/>
|
|
1016
|
+
{modelSearch && (
|
|
1017
|
+
<button
|
|
1018
|
+
type="button"
|
|
1019
|
+
onClick={() => setModelSearch('')}
|
|
1020
|
+
className="text-muted-foreground hover:text-foreground"
|
|
1021
|
+
>
|
|
1022
|
+
<X className="size-3" />
|
|
1023
|
+
</button>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
|
|
1027
|
+
{/* Grouped model list */}
|
|
1028
|
+
<div className="max-h-72 overflow-y-auto p-1 [&::-webkit-scrollbar]:hidden">
|
|
1029
|
+
{groupedModels.size === 0 ? (
|
|
1030
|
+
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
|
|
1031
|
+
No models found
|
|
1032
|
+
</div>
|
|
1033
|
+
) : (
|
|
1034
|
+
Array.from(groupedModels.entries()).map(([provider, models]) => {
|
|
1035
|
+
const meta = getProviderMeta(provider);
|
|
1036
|
+
return (
|
|
1037
|
+
<div key={provider} className="mb-1">
|
|
1038
|
+
<div className="flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-foreground/70">
|
|
1039
|
+
{meta.label}
|
|
1040
|
+
<span className="text-[10px] text-muted-foreground/50">
|
|
1041
|
+
({models.length})
|
|
1042
|
+
</span>
|
|
1043
|
+
</div>
|
|
1044
|
+
|
|
1045
|
+
{/* Models */}
|
|
1046
|
+
{models.map((m) => (
|
|
1047
|
+
<button
|
|
1048
|
+
key={m.id}
|
|
1049
|
+
type="button"
|
|
1050
|
+
onClick={() => {
|
|
1051
|
+
setSelectedModel(m.id);
|
|
1052
|
+
setShowModelSelect(false);
|
|
1053
|
+
}}
|
|
1054
|
+
className={cn(
|
|
1055
|
+
'flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-xs transition',
|
|
1056
|
+
m.id === selectedModel
|
|
1057
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
1058
|
+
: 'text-foreground hover:bg-muted',
|
|
1059
|
+
)}
|
|
1060
|
+
>
|
|
1061
|
+
<span className="flex items-center gap-2 truncate">
|
|
1062
|
+
<ProviderIcon
|
|
1063
|
+
src={meta.iconSrc}
|
|
1064
|
+
alt={meta.label}
|
|
1065
|
+
pillBg={meta.pillBg}
|
|
1066
|
+
size={14}
|
|
1067
|
+
/>
|
|
1068
|
+
<span className="truncate">{m.label}</span>
|
|
1069
|
+
</span>
|
|
1070
|
+
<span className="flex items-center gap-1.5 shrink-0 ml-2">
|
|
1071
|
+
{m.context_window && (
|
|
1072
|
+
<span className="text-[9px] font-mono text-muted-foreground/60">
|
|
1073
|
+
{m.context_window >= 1_000_000
|
|
1074
|
+
? `${(m.context_window / 1_000_000).toFixed(0)}M`
|
|
1075
|
+
: `${(m.context_window / 1000).toFixed(0)}K`}
|
|
1076
|
+
</span>
|
|
1077
|
+
)}
|
|
1078
|
+
{/* {m.tags?.includes("reasoning") && (
|
|
1079
|
+
<span className="rounded bg-violet-100 dark:bg-violet-900/30 px-1 py-0.5 text-[8px] font-medium text-violet-700 dark:text-violet-300">
|
|
1080
|
+
reasoning
|
|
1081
|
+
</span>
|
|
1082
|
+
)} */}
|
|
1083
|
+
</span>
|
|
1084
|
+
</button>
|
|
1085
|
+
))}
|
|
1086
|
+
</div>
|
|
1087
|
+
);
|
|
1088
|
+
})
|
|
1089
|
+
)}
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
</>
|
|
1093
|
+
)}
|
|
1094
|
+
</div>
|
|
1095
|
+
)}
|
|
1096
|
+
|
|
1097
|
+
<div className="h-4 w-px bg-border mx-1" />
|
|
1098
|
+
|
|
1099
|
+
<TooltipProvider delay={50}>
|
|
1100
|
+
<Sheet
|
|
1101
|
+
open={isHistoryOpen}
|
|
1102
|
+
onOpenChange={(open) => {
|
|
1103
|
+
setIsHistoryOpen(open);
|
|
1104
|
+
if (!open) setHistorySearch('');
|
|
1105
|
+
}}
|
|
1106
|
+
>
|
|
1107
|
+
<Tooltip>
|
|
1108
|
+
<TooltipTrigger
|
|
1109
|
+
render={
|
|
1110
|
+
<SheetTrigger
|
|
1111
|
+
render={
|
|
1112
|
+
<button
|
|
1113
|
+
type="button"
|
|
1114
|
+
aria-label="Chat history"
|
|
1115
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
|
1116
|
+
>
|
|
1117
|
+
<History className="h-4 w-4" />
|
|
1118
|
+
</button>
|
|
1119
|
+
}
|
|
1120
|
+
/>
|
|
1121
|
+
}
|
|
1122
|
+
/>
|
|
1123
|
+
<TooltipContent>Chat History</TooltipContent>
|
|
1124
|
+
</Tooltip>
|
|
1125
|
+
<SheetContent
|
|
1126
|
+
side="left"
|
|
1127
|
+
showCloseButton={false}
|
|
1128
|
+
className="w-75 sm:w-87.5 p-0 flex flex-col"
|
|
1129
|
+
>
|
|
1130
|
+
<SheetHeader className="p-4 pb-3 border-b">
|
|
1131
|
+
<div className="flex items-center justify-between gap-2">
|
|
1132
|
+
<SheetTitle className="text-sm font-semibold">Chat History</SheetTitle>
|
|
1133
|
+
<div className="flex items-center gap-1">
|
|
1134
|
+
<AlertDialog>
|
|
1135
|
+
<Tooltip>
|
|
1136
|
+
<TooltipTrigger
|
|
1137
|
+
render={
|
|
1138
|
+
<AlertDialogTrigger
|
|
1139
|
+
render={
|
|
1140
|
+
<button
|
|
1141
|
+
type="button"
|
|
1142
|
+
aria-label="Clear chat history"
|
|
1143
|
+
disabled={history.length === 0}
|
|
1144
|
+
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive disabled:pointer-events-none disabled:opacity-40"
|
|
1145
|
+
>
|
|
1146
|
+
<Trash2 className="size-3.5" />
|
|
1147
|
+
</button>
|
|
1148
|
+
}
|
|
1149
|
+
/>
|
|
1150
|
+
}
|
|
1151
|
+
/>
|
|
1152
|
+
<TooltipContent>Clear all chat history</TooltipContent>
|
|
1153
|
+
</Tooltip>
|
|
1154
|
+
<AlertDialogContent>
|
|
1155
|
+
<AlertDialogHeader>
|
|
1156
|
+
<AlertDialogTitle>Delete all chat history?</AlertDialogTitle>
|
|
1157
|
+
<AlertDialogDescription>
|
|
1158
|
+
This will permanently delete all saved chats on this device. This
|
|
1159
|
+
action cannot be undone.
|
|
1160
|
+
</AlertDialogDescription>
|
|
1161
|
+
</AlertDialogHeader>
|
|
1162
|
+
<AlertDialogFooter>
|
|
1163
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
1164
|
+
<AlertDialogAction
|
|
1165
|
+
onClick={() => void deleteAllChats()}
|
|
1166
|
+
className="bg-destructive text-white hover:bg-destructive/90"
|
|
1167
|
+
>
|
|
1168
|
+
Delete all
|
|
1169
|
+
</AlertDialogAction>
|
|
1170
|
+
</AlertDialogFooter>
|
|
1171
|
+
</AlertDialogContent>
|
|
1172
|
+
</AlertDialog>
|
|
1173
|
+
<SheetClose
|
|
1174
|
+
render={
|
|
1175
|
+
<button
|
|
1176
|
+
type="button"
|
|
1177
|
+
aria-label="Close chat history"
|
|
1178
|
+
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
|
1179
|
+
>
|
|
1180
|
+
<X className="size-3.5" />
|
|
1181
|
+
</button>
|
|
1182
|
+
}
|
|
1183
|
+
/>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
<div className="mt-3 flex items-center gap-2 rounded-lg border border-border bg-muted/50 px-2.5 py-1.5">
|
|
1187
|
+
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1188
|
+
<input
|
|
1189
|
+
type="text"
|
|
1190
|
+
value={historySearch}
|
|
1191
|
+
onChange={(e) => setHistorySearch(e.target.value)}
|
|
1192
|
+
placeholder="Search chats..."
|
|
1193
|
+
className="flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground"
|
|
1194
|
+
/>
|
|
1195
|
+
{historySearch && (
|
|
1196
|
+
<button
|
|
1197
|
+
type="button"
|
|
1198
|
+
onClick={() => setHistorySearch('')}
|
|
1199
|
+
className="text-muted-foreground hover:text-foreground"
|
|
1200
|
+
>
|
|
1201
|
+
<X className="size-3" />
|
|
1202
|
+
</button>
|
|
1203
|
+
)}
|
|
1204
|
+
</div>
|
|
1205
|
+
</SheetHeader>
|
|
1206
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
1207
|
+
{history.length === 0 ? (
|
|
1208
|
+
<div className="p-4 text-center text-xs text-muted-foreground">
|
|
1209
|
+
No chat history
|
|
1210
|
+
</div>
|
|
1211
|
+
) : filteredHistory.length === 0 ? (
|
|
1212
|
+
<div className="p-4 text-center text-xs text-muted-foreground">
|
|
1213
|
+
No chats match "{historySearch}"
|
|
1214
|
+
</div>
|
|
1215
|
+
) : (
|
|
1216
|
+
filteredHistory.map((chat) => (
|
|
1217
|
+
<div
|
|
1218
|
+
key={chat.id}
|
|
1219
|
+
onClick={() => loadChat(chat.id)}
|
|
1220
|
+
className={cn(
|
|
1221
|
+
'group flex items-center justify-between rounded-md px-3 py-2 text-sm cursor-pointer transition hover:bg-muted mb-1',
|
|
1222
|
+
currentChatId === chat.id && 'bg-muted font-medium',
|
|
1223
|
+
)}
|
|
1224
|
+
>
|
|
1225
|
+
<div className="truncate pr-4 flex-1 text-xs">{chat.title}</div>
|
|
1226
|
+
<AlertDialog>
|
|
1227
|
+
<AlertDialogTrigger
|
|
1228
|
+
render={
|
|
1229
|
+
<button
|
|
1230
|
+
onClick={(e) => e.stopPropagation()}
|
|
1231
|
+
className="text-muted-foreground opacity-0 transition group-hover:opacity-100 hover:text-destructive"
|
|
1232
|
+
>
|
|
1233
|
+
<Trash2 className="size-3.5" />
|
|
1234
|
+
</button>
|
|
1235
|
+
}
|
|
1236
|
+
/>
|
|
1237
|
+
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
|
|
1238
|
+
<AlertDialogHeader>
|
|
1239
|
+
<AlertDialogTitle>Delete Chat</AlertDialogTitle>
|
|
1240
|
+
<AlertDialogDescription>
|
|
1241
|
+
Are you sure you want to delete this chat? This action cannot be
|
|
1242
|
+
undone.
|
|
1243
|
+
</AlertDialogDescription>
|
|
1244
|
+
</AlertDialogHeader>
|
|
1245
|
+
<AlertDialogFooter>
|
|
1246
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
1247
|
+
<AlertDialogAction
|
|
1248
|
+
onClick={() => deleteChat(chat.id)}
|
|
1249
|
+
className="bg-destructive text-white hover:bg-destructive/90"
|
|
1250
|
+
>
|
|
1251
|
+
Delete
|
|
1252
|
+
</AlertDialogAction>
|
|
1253
|
+
</AlertDialogFooter>
|
|
1254
|
+
</AlertDialogContent>
|
|
1255
|
+
</AlertDialog>
|
|
1256
|
+
</div>
|
|
1257
|
+
))
|
|
1258
|
+
)}
|
|
1259
|
+
</div>
|
|
1260
|
+
</SheetContent>
|
|
1261
|
+
</Sheet>
|
|
1262
|
+
|
|
1263
|
+
<Tooltip>
|
|
1264
|
+
<TooltipTrigger
|
|
1265
|
+
render={
|
|
1266
|
+
<button
|
|
1267
|
+
type="button"
|
|
1268
|
+
onClick={newChat}
|
|
1269
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
|
1270
|
+
>
|
|
1271
|
+
<SquarePen className="h-4 w-4" />
|
|
1272
|
+
</button>
|
|
1273
|
+
}
|
|
1274
|
+
/>
|
|
1275
|
+
<TooltipContent>New Chat</TooltipContent>
|
|
1276
|
+
</Tooltip>
|
|
1277
|
+
<JobsTrackingButton />
|
|
1278
|
+
</TooltipProvider>
|
|
1279
|
+
</div>
|
|
1280
|
+
</div>
|
|
1281
|
+
|
|
1282
|
+
{/* Messages area */}
|
|
1283
|
+
<ChatMessagesViewport viewportRef={scrollRef} onScroll={handleMessageScroll}>
|
|
1284
|
+
{!ready ? (
|
|
1285
|
+
<div className="flex flex-col items-center justify-center gap-3 pt-[18vh] text-center">
|
|
1286
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white border text-muted-foreground">
|
|
1287
|
+
<MessageCircle className="size-7" />
|
|
1288
|
+
</div>
|
|
1289
|
+
<h1 className="text-xl font-semibold tracking-tight text-foreground">
|
|
1290
|
+
{statusError ? 'Chat temporarily unavailable' : 'Setup Required'}
|
|
1291
|
+
</h1>
|
|
1292
|
+
<p className="max-w-md text-sm leading-relaxed text-muted-foreground mb-2">
|
|
1293
|
+
{statusError
|
|
1294
|
+
? 'Chat services are temporarily unavailable. Larkup will retry automatically.'
|
|
1295
|
+
: status?.blockers?.join(' ') ||
|
|
1296
|
+
'Connect a chat model in Settings to start chatting.'}
|
|
1297
|
+
</p>
|
|
1298
|
+
{!statusError && (
|
|
1299
|
+
<a
|
|
1300
|
+
href={
|
|
1301
|
+
status?.blockers?.some((b) => b.includes('API Key') || b.includes('Settings'))
|
|
1302
|
+
? '/settings/models'
|
|
1303
|
+
: '/settings'
|
|
1304
|
+
}
|
|
1305
|
+
className="inline-flex h-9 items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
|
|
1306
|
+
>
|
|
1307
|
+
Go to Settings
|
|
1308
|
+
</a>
|
|
1309
|
+
)}
|
|
1310
|
+
</div>
|
|
1311
|
+
) : isEmpty ? (
|
|
1312
|
+
<ChatEmptyState
|
|
1313
|
+
mark={
|
|
1314
|
+
<>
|
|
1315
|
+
<img src="/logo.png" alt="Logo" className="size-9 dark:hidden" />
|
|
1316
|
+
<img src="/logo-light.png" alt="Logo" className="size-9 dark:block hidden" />
|
|
1317
|
+
</>
|
|
1318
|
+
}
|
|
1319
|
+
/>
|
|
1320
|
+
) : (
|
|
1321
|
+
<div className="flex flex-col gap-6">
|
|
1322
|
+
{messages.map((m, idx) => (
|
|
1323
|
+
<MessageItem
|
|
1324
|
+
key={m.id}
|
|
1325
|
+
message={m}
|
|
1326
|
+
isLast={idx === messages.length - 1}
|
|
1327
|
+
isStreaming={chatStatus === 'streaming'}
|
|
1328
|
+
addToolResult={addToolResult}
|
|
1329
|
+
serverId={projectId}
|
|
1330
|
+
autoOpenSupportingClip={shouldAutoOpenSupportingClip(messages, idx)}
|
|
1331
|
+
regenerate={regenerate}
|
|
1332
|
+
isBusy={isBusy}
|
|
1333
|
+
onFeedback={handleMessageFeedback}
|
|
1334
|
+
/>
|
|
1335
|
+
))}
|
|
1336
|
+
{chatStatus === 'submitted' || isInitializingChat ? (
|
|
1337
|
+
<div className="flex items-center gap-2">
|
|
1338
|
+
<div className="size-7 bg-white border border-border rounded-full flex items-center justify-center p-1 animate-pulse">
|
|
1339
|
+
<img src="/logo.png" alt="logo" className="size-4 animate-spin" />
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
) : null}
|
|
1343
|
+
{error
|
|
1344
|
+
? (() => {
|
|
1345
|
+
const raw = error.message || 'Something went wrong while generating a response.';
|
|
1346
|
+
// Sanitize: if the message contains URLs or is too long, clean it up
|
|
1347
|
+
let displayMessage = raw;
|
|
1348
|
+
if (
|
|
1349
|
+
raw.includes('rate-limited') ||
|
|
1350
|
+
raw.includes('rate_limit') ||
|
|
1351
|
+
raw.includes('429')
|
|
1352
|
+
) {
|
|
1353
|
+
displayMessage =
|
|
1354
|
+
'Vercel AI Gateway could not serve this model because it is rate-limited. We tried compatible backups. Try again shortly, choose another model, or add AI Gateway credits / a provider key in Settings.';
|
|
1355
|
+
} else if (raw.includes('unavailable tool') || raw.includes('NoSuchTool')) {
|
|
1356
|
+
displayMessage =
|
|
1357
|
+
'The model tried an unavailable action. Please try your question again.';
|
|
1358
|
+
} else if (
|
|
1359
|
+
raw.length > 200 ||
|
|
1360
|
+
raw.includes('http://') ||
|
|
1361
|
+
raw.includes('https://')
|
|
1362
|
+
) {
|
|
1363
|
+
displayMessage =
|
|
1364
|
+
'Something went wrong while generating a response. Please try again.';
|
|
1365
|
+
}
|
|
1366
|
+
return (
|
|
1367
|
+
<div className="flex items-start gap-3 rounded-xl border border-destructive/30 bg-destructive/5 px-4 py-3">
|
|
1368
|
+
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
|
1369
|
+
<div className="flex-1">
|
|
1370
|
+
<p className="text-sm text-foreground">{displayMessage}</p>
|
|
1371
|
+
<button
|
|
1372
|
+
type="button"
|
|
1373
|
+
onClick={() => regenerate()}
|
|
1374
|
+
className="mt-2 inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-foreground transition hover:bg-secondary"
|
|
1375
|
+
>
|
|
1376
|
+
<RotateCcw className="h-3.5 w-3.5" />
|
|
1377
|
+
Try again
|
|
1378
|
+
</button>
|
|
1379
|
+
</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
);
|
|
1382
|
+
})()
|
|
1383
|
+
: null}
|
|
1384
|
+
</div>
|
|
1385
|
+
)}
|
|
1386
|
+
</ChatMessagesViewport>
|
|
1387
|
+
|
|
1388
|
+
{/* Input area */}
|
|
1389
|
+
{ready && (
|
|
1390
|
+
<div className="relative z-10 mt-auto px-4 pb-5 pt-3 sm:px-6">
|
|
1391
|
+
{queuedMessages.length > 0 && (
|
|
1392
|
+
<MessageQueue
|
|
1393
|
+
items={queuedMessages}
|
|
1394
|
+
onRemove={removeFromQueue}
|
|
1395
|
+
onMoveUp={(id) => moveQueueItem(id, -1)}
|
|
1396
|
+
onMoveDown={(id) => moveQueueItem(id, 1)}
|
|
1397
|
+
onEdit={editQueueItem}
|
|
1398
|
+
/>
|
|
1399
|
+
)}
|
|
1400
|
+
<form onSubmit={handleSubmit} className="mx-auto w-full max-w-3xl">
|
|
1401
|
+
<ChatComposerSurface>
|
|
1402
|
+
<DropdownMenu>
|
|
1403
|
+
<DropdownMenuTrigger
|
|
1404
|
+
render={
|
|
1405
|
+
<button
|
|
1406
|
+
type="button"
|
|
1407
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-muted-foreground transition hover:bg-secondary hover:text-foreground mt-1"
|
|
1408
|
+
>
|
|
1409
|
+
<Plus className="h-4.5 w-4.5" />
|
|
1410
|
+
</button>
|
|
1411
|
+
}
|
|
1412
|
+
/>
|
|
1413
|
+
<DropdownMenuContent
|
|
1414
|
+
side="top"
|
|
1415
|
+
align="start"
|
|
1416
|
+
className="w-56 gap-1 flex flex-col"
|
|
1417
|
+
sideOffset={8}
|
|
1418
|
+
>
|
|
1419
|
+
{/* File Attachment Button hidden input, clicked programmatically */}
|
|
1420
|
+
<DropdownMenuItem
|
|
1421
|
+
onClick={(e) => {
|
|
1422
|
+
e.preventDefault();
|
|
1423
|
+
const fileInput = document.createElement('input');
|
|
1424
|
+
fileInput.type = 'file';
|
|
1425
|
+
fileInput.multiple = true;
|
|
1426
|
+
fileInput.accept = '*/*';
|
|
1427
|
+
fileInput.onchange = async (ev: any) => {
|
|
1428
|
+
const fileList = ev.target.files;
|
|
1429
|
+
if (!fileList || fileList.length === 0) return;
|
|
1430
|
+
const newAttachments = Array.from(fileList) as File[];
|
|
1431
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
1432
|
+
|
|
1433
|
+
const pdfFile = newAttachments.find(
|
|
1434
|
+
(f) =>
|
|
1435
|
+
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'),
|
|
1436
|
+
);
|
|
1437
|
+
if (pdfFile) {
|
|
1438
|
+
openCanvas(pdfFile, { background: false }).catch((err) => {
|
|
1439
|
+
toast.error(err.message || 'Failed to auto-open document');
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
fileInput.click();
|
|
1444
|
+
}}
|
|
1445
|
+
className="gap-2 cursor-pointer"
|
|
1446
|
+
>
|
|
1447
|
+
<Paperclip className="size-4" />
|
|
1448
|
+
<span>Attach Document</span>
|
|
1449
|
+
</DropdownMenuItem>
|
|
1450
|
+
{/* <DropdownMenuSeparator /> */}
|
|
1451
|
+
<DropdownMenuItem
|
|
1452
|
+
onClick={(e) => {
|
|
1453
|
+
e.preventDefault();
|
|
1454
|
+
handleWebSearchToggle();
|
|
1455
|
+
}}
|
|
1456
|
+
className="gap-2 justify-between cursor-pointer"
|
|
1457
|
+
>
|
|
1458
|
+
<div className="flex items-center gap-2">
|
|
1459
|
+
<Globe className="size-4" />
|
|
1460
|
+
<span>Web Search</span>
|
|
1461
|
+
</div>
|
|
1462
|
+
<Switch
|
|
1463
|
+
checked={configData?.config?.webSearchEnabled || false}
|
|
1464
|
+
className="pointer-events-none"
|
|
1465
|
+
/>
|
|
1466
|
+
</DropdownMenuItem>
|
|
1467
|
+
</DropdownMenuContent>
|
|
1468
|
+
</DropdownMenu>
|
|
1469
|
+
<div className="hidden">
|
|
1470
|
+
<FileAttachmentButton id="chat-file-attachment" />
|
|
1471
|
+
</div>
|
|
1472
|
+
|
|
1473
|
+
<div className="flex flex-col flex-1 gap-2">
|
|
1474
|
+
{/* Attachments List */}
|
|
1475
|
+
{attachments.length > 0 && (
|
|
1476
|
+
<div className="flex w-full overflow-x-auto items-center gap-2 px-1 pt-1 pb-2 [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/60">
|
|
1477
|
+
{attachments.map((file, idx) => (
|
|
1478
|
+
<AttachmentCard
|
|
1479
|
+
key={`${file.name}-${idx}`}
|
|
1480
|
+
file={file}
|
|
1481
|
+
onClick={() => {
|
|
1482
|
+
openCanvas(file, { background: false }).catch((err) => {
|
|
1483
|
+
toast.error(err.message || 'Failed to open document');
|
|
1484
|
+
});
|
|
1485
|
+
}}
|
|
1486
|
+
onRemove={() => {
|
|
1487
|
+
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
|
1488
|
+
}}
|
|
1489
|
+
/>
|
|
1490
|
+
))}
|
|
1491
|
+
</div>
|
|
1492
|
+
)}
|
|
1493
|
+
|
|
1494
|
+
<textarea
|
|
1495
|
+
ref={textareaRef}
|
|
1496
|
+
value={input}
|
|
1497
|
+
onChange={(e) => setInput(e.target.value)}
|
|
1498
|
+
onKeyDown={(e) => {
|
|
1499
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1500
|
+
e.preventDefault();
|
|
1501
|
+
handleSubmit(e);
|
|
1502
|
+
}
|
|
1503
|
+
}}
|
|
1504
|
+
onPaste={(e) => {
|
|
1505
|
+
const items = e.clipboardData?.items;
|
|
1506
|
+
if (!items) return;
|
|
1507
|
+
const pastedFiles: File[] = [];
|
|
1508
|
+
for (let i = 0; i < items.length; i++) {
|
|
1509
|
+
if (items[i].kind === 'file') {
|
|
1510
|
+
const file = items[i].getAsFile();
|
|
1511
|
+
if (file) pastedFiles.push(file);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (pastedFiles.length > 0) {
|
|
1515
|
+
setAttachments((prev) => [...prev, ...pastedFiles]);
|
|
1516
|
+
const pdfFile = pastedFiles.find(
|
|
1517
|
+
(f) =>
|
|
1518
|
+
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'),
|
|
1519
|
+
);
|
|
1520
|
+
if (pdfFile) {
|
|
1521
|
+
openCanvas(pdfFile, { background: false }).catch((err) => {
|
|
1522
|
+
toast.error(err.message || 'Failed to auto-open pasted document');
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}}
|
|
1527
|
+
rows={1}
|
|
1528
|
+
disabled={!ready}
|
|
1529
|
+
placeholder="How can I help you…"
|
|
1530
|
+
className="max-h-48 min-h-11 w-full resize-none overflow-y-auto bg-transparent px-2 py-2.5 text-[15px] leading-relaxed text-foreground outline-none placeholder:text-muted-foreground scrollbar-none [&::-webkit-scrollbar]:hidden disabled:opacity-50"
|
|
1531
|
+
/>
|
|
1532
|
+
</div>
|
|
1533
|
+
|
|
1534
|
+
<button
|
|
1535
|
+
type="submit"
|
|
1536
|
+
disabled={!input.trim() && attachments.length === 0}
|
|
1537
|
+
title={isBusy || isDocLoading ? 'Add to queue' : 'Send'}
|
|
1538
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground transition hover:opacity-90 disabled:opacity-40"
|
|
1539
|
+
>
|
|
1540
|
+
{isBusy || isDocLoading ? (
|
|
1541
|
+
<ListPlus className="h-4.5 w-4.5" />
|
|
1542
|
+
) : (
|
|
1543
|
+
<ArrowUp className="h-4.5 w-4.5" />
|
|
1544
|
+
)}
|
|
1545
|
+
</button>
|
|
1546
|
+
</ChatComposerSurface>
|
|
1547
|
+
</form>
|
|
1548
|
+
</div>
|
|
1549
|
+
)}
|
|
1550
|
+
</div>
|
|
1551
|
+
);
|
|
1552
|
+
}
|