@larkup/workspace-app 0.0.0-stage → 0.2.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,1313 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
workspaceStorageKey,
|
|
5
|
+
workspaceStorageScope,
|
|
6
|
+
workspaceUrl,
|
|
7
|
+
workspaceFetch as fetch,
|
|
8
|
+
} from '../../host';
|
|
9
|
+
import { SourceUploadDropzone } from '@larkup/workspace-ui';
|
|
10
|
+
|
|
11
|
+
import { useEffect, useRef, useState } from 'react';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import { formatErrorMessage } from '../../lib/shared/error-formatter';
|
|
14
|
+
import {
|
|
15
|
+
FileUp,
|
|
16
|
+
Loader2,
|
|
17
|
+
X,
|
|
18
|
+
Settings2,
|
|
19
|
+
Columns,
|
|
20
|
+
Plus,
|
|
21
|
+
Database,
|
|
22
|
+
Image as ImageIcon,
|
|
23
|
+
AlertTriangle,
|
|
24
|
+
Link2,
|
|
25
|
+
Eye,
|
|
26
|
+
} from 'lucide-react';
|
|
27
|
+
import { Button } from '@larkup/workspace-ui/button';
|
|
28
|
+
import { Input } from '../ui/input';
|
|
29
|
+
import { cn } from '../../lib/utils';
|
|
30
|
+
import { TabularPreview } from './tabular-preview';
|
|
31
|
+
import type { DataPrimaryAction } from './data-primary-action';
|
|
32
|
+
import Papa from 'papaparse';
|
|
33
|
+
import * as XLSX from 'xlsx';
|
|
34
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog';
|
|
35
|
+
import {
|
|
36
|
+
Select,
|
|
37
|
+
SelectContent,
|
|
38
|
+
SelectItem,
|
|
39
|
+
SelectTrigger,
|
|
40
|
+
SelectValue,
|
|
41
|
+
} from '@larkup/workspace-ui/select';
|
|
42
|
+
import { Checkbox } from '../ui/checkbox';
|
|
43
|
+
import { Label } from '@larkup/workspace-ui/label';
|
|
44
|
+
import { Switch } from '../ui/switch';
|
|
45
|
+
import { Progress } from '../ui/progress';
|
|
46
|
+
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
|
47
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
48
|
+
import { DataEntryModeSwitch } from './data-entry-mode-switch';
|
|
49
|
+
|
|
50
|
+
const ACCEPT = '.txt,.md,.markdown,.toml,.json,.csv,.html,.htm,.log,.xlsx,.xls,.pdf,.doc,.docx';
|
|
51
|
+
const CONTENT_PREVIEW_LIMIT = 100_000;
|
|
52
|
+
|
|
53
|
+
type FileFormat = 'plain' | 'lines' | 'structured';
|
|
54
|
+
|
|
55
|
+
interface StagedFile {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
size: number;
|
|
59
|
+
format: FileFormat;
|
|
60
|
+
rawContent?: string;
|
|
61
|
+
rows?: any[];
|
|
62
|
+
keys?: string[];
|
|
63
|
+
// mapping for structured
|
|
64
|
+
titleKey?: string;
|
|
65
|
+
contentKeys?: string[];
|
|
66
|
+
contentSeparator?: string;
|
|
67
|
+
metadataKeys?: string[];
|
|
68
|
+
globalMetadata?: { key: string; value: string }[];
|
|
69
|
+
/** When true, structured data is also saved as a TabularDataset for analytics */
|
|
70
|
+
indexAsTabular?: boolean;
|
|
71
|
+
/** Expanded preview state */
|
|
72
|
+
showPreview?: boolean;
|
|
73
|
+
/** When true, images from PDF will be extracted and indexed */
|
|
74
|
+
indexImages?: boolean;
|
|
75
|
+
/** The original file object for client-side processing */
|
|
76
|
+
fileObject?: File;
|
|
77
|
+
/** The public URL used to import this file, retained as source metadata. */
|
|
78
|
+
sourceUrl?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface ImageIndexingCapability {
|
|
82
|
+
available: boolean;
|
|
83
|
+
message?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stagedFilesByScope = new Map<string, StagedFile[]>();
|
|
87
|
+
const UPLOAD_QUEUE_KEY = 'larkup.upload-queue.v1';
|
|
88
|
+
|
|
89
|
+
function restoreQueue(key: string): StagedFile[] {
|
|
90
|
+
if (typeof window === 'undefined') return [];
|
|
91
|
+
try {
|
|
92
|
+
const saved = sessionStorage.getItem(key);
|
|
93
|
+
return saved ? (JSON.parse(saved) as StagedFile[]) : [];
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function persistQueue(files: StagedFile[], key: string) {
|
|
100
|
+
try {
|
|
101
|
+
const serializable = files.map(({ fileObject: _fileObject, ...file }) => file);
|
|
102
|
+
sessionStorage.setItem(key, JSON.stringify(serializable));
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function UploadPanel({
|
|
107
|
+
onAdded,
|
|
108
|
+
onActionChange,
|
|
109
|
+
groupId,
|
|
110
|
+
}: {
|
|
111
|
+
onAdded: () => void;
|
|
112
|
+
onActionChange?: (action: DataPrimaryAction | null) => void;
|
|
113
|
+
groupId?: string;
|
|
114
|
+
}) {
|
|
115
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
116
|
+
const scope = useRef(workspaceStorageScope()).current;
|
|
117
|
+
const queueKey = useRef(workspaceStorageKey(UPLOAD_QUEUE_KEY)).current;
|
|
118
|
+
const initialStaged = stagedFilesByScope.get(scope) ?? restoreQueue(queueKey);
|
|
119
|
+
const [staged, setStagedState] = useState<StagedFile[]>(initialStaged);
|
|
120
|
+
const stagedRef = useRef(initialStaged);
|
|
121
|
+
const savingRef = useRef(false);
|
|
122
|
+
const groupIdRef = useRef(groupId ?? 'default');
|
|
123
|
+
groupIdRef.current = groupId ?? 'default';
|
|
124
|
+
const [indexAllImages, setIndexAllImages] = useState(false);
|
|
125
|
+
const [imageIndexingCapability, setImageIndexingCapability] =
|
|
126
|
+
useState<ImageIndexingCapability | null>(null);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
let active = true;
|
|
130
|
+
void fetch(workspaceUrl('/api/config'))
|
|
131
|
+
.then((response) => (response.ok ? response.json() : Promise.reject()))
|
|
132
|
+
.then((data) => {
|
|
133
|
+
if (!active) return;
|
|
134
|
+
const capability = data?.capabilities?.imageIndexing;
|
|
135
|
+
setImageIndexingCapability(
|
|
136
|
+
capability?.available
|
|
137
|
+
? { available: true }
|
|
138
|
+
: {
|
|
139
|
+
available: false,
|
|
140
|
+
message:
|
|
141
|
+
capability?.message ??
|
|
142
|
+
'No vision-capable model is configured. PDF images will not be indexed.',
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {
|
|
147
|
+
if (active) {
|
|
148
|
+
setImageIndexingCapability({
|
|
149
|
+
available: false,
|
|
150
|
+
message: 'Could not verify a vision model. PDF images will not be indexed.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return () => {
|
|
155
|
+
active = false;
|
|
156
|
+
};
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const setStaged = (val: React.SetStateAction<StagedFile[]>) => {
|
|
160
|
+
const next = typeof val === 'function' ? val(stagedRef.current) : val;
|
|
161
|
+
stagedRef.current = next;
|
|
162
|
+
stagedFilesByScope.set(scope, next);
|
|
163
|
+
persistQueue(next, queueKey);
|
|
164
|
+
setStagedState(next);
|
|
165
|
+
};
|
|
166
|
+
const imageIndexingAvailable = imageIndexingCapability?.available === true;
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!imageIndexingCapability || imageIndexingCapability.available) return;
|
|
170
|
+
setIndexAllImages(false);
|
|
171
|
+
setStaged((files) =>
|
|
172
|
+
files.map((file) =>
|
|
173
|
+
file.name.toLowerCase().endsWith('.pdf') ? { ...file, indexImages: false } : file,
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
}, [imageIndexingCapability]);
|
|
177
|
+
const [dragging, setDragging] = useState(false);
|
|
178
|
+
const [entryTab, setEntryTab] = useState<'upload' | 'url'>('upload');
|
|
179
|
+
const [remoteUrl, setRemoteUrl] = useState('');
|
|
180
|
+
const [loadingRemoteFile, setLoadingRemoteFile] = useState(false);
|
|
181
|
+
const [saving, setSaving] = useState(false);
|
|
182
|
+
const [progress, setProgress] = useState<{
|
|
183
|
+
message?: string;
|
|
184
|
+
current: number;
|
|
185
|
+
total: number;
|
|
186
|
+
} | null>(null);
|
|
187
|
+
|
|
188
|
+
// Mapping state
|
|
189
|
+
const [editingFileId, setEditingFileId] = useState<string | null>(null);
|
|
190
|
+
const [previewingFileId, setPreviewingFileId] = useState<string | null>(null);
|
|
191
|
+
const editingFile = staged.find((f) => f.id === editingFileId);
|
|
192
|
+
const previewingFile = staged.find((f) => f.id === previewingFileId);
|
|
193
|
+
const isRemoteEntry = entryTab === 'url';
|
|
194
|
+
|
|
195
|
+
async function readFiles(files: FileList | File[], sourceUrl?: string) {
|
|
196
|
+
const joinedActiveSave = savingRef.current;
|
|
197
|
+
const next: StagedFile[] = [];
|
|
198
|
+
const stage = (file: StagedFile) => next.push({ ...file, sourceUrl });
|
|
199
|
+
for (const file of Array.from(files)) {
|
|
200
|
+
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
201
|
+
|
|
202
|
+
// Prevent dragging media into the documents tab
|
|
203
|
+
const isRemotePlainText = Boolean(sourceUrl && file.type.startsWith('text/'));
|
|
204
|
+
if (ext && !ACCEPT.includes(`.${ext}`) && !isRemotePlainText) {
|
|
205
|
+
toast.error(
|
|
206
|
+
`Unsupported file type: ${file.name}. Please upload media files in the Media tab.`,
|
|
207
|
+
);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const id = Math.random().toString(36).slice(2);
|
|
212
|
+
try {
|
|
213
|
+
if (ext === 'csv') {
|
|
214
|
+
const content = await file.text();
|
|
215
|
+
const result = Papa.parse(content, {
|
|
216
|
+
header: true,
|
|
217
|
+
skipEmptyLines: true,
|
|
218
|
+
});
|
|
219
|
+
if (result.data.length > 0) {
|
|
220
|
+
const keys = Object.keys(result.data[0] as object);
|
|
221
|
+
stage({
|
|
222
|
+
id,
|
|
223
|
+
name: file.name,
|
|
224
|
+
size: file.size,
|
|
225
|
+
format: 'structured',
|
|
226
|
+
rows: result.data,
|
|
227
|
+
keys,
|
|
228
|
+
titleKey: keys[0],
|
|
229
|
+
contentKeys: keys,
|
|
230
|
+
contentSeparator: ', ',
|
|
231
|
+
metadataKeys: keys.slice(1),
|
|
232
|
+
globalMetadata: [],
|
|
233
|
+
indexAsTabular: true,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
} else if (ext === 'xlsx' || ext === 'xls') {
|
|
237
|
+
const data = await file.arrayBuffer();
|
|
238
|
+
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
|
|
239
|
+
const hasMultipleSheets = workbook.SheetNames.length > 1;
|
|
240
|
+
let indexedSheets = 0;
|
|
241
|
+
for (const sheetName of workbook.SheetNames) {
|
|
242
|
+
const sheet = workbook.Sheets[sheetName];
|
|
243
|
+
const rawRows = XLSX.utils.sheet_to_json(sheet, { defval: null }) as Record<
|
|
244
|
+
string,
|
|
245
|
+
any
|
|
246
|
+
>[];
|
|
247
|
+
const rows = rawRows.map((row) => {
|
|
248
|
+
const out: Record<string, any> = {};
|
|
249
|
+
for (const [k, v] of Object.entries(row)) {
|
|
250
|
+
out[k] = v instanceof Date ? v.toISOString().split('T')[0] : v;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
});
|
|
254
|
+
if (rows.length === 0) continue;
|
|
255
|
+
const keys = Object.keys(rows[0] as object);
|
|
256
|
+
stage({
|
|
257
|
+
id: `${id}-${indexedSheets}`,
|
|
258
|
+
name: hasMultipleSheets ? `${file.name} — ${sheetName}` : file.name,
|
|
259
|
+
size: file.size,
|
|
260
|
+
format: 'structured',
|
|
261
|
+
rows,
|
|
262
|
+
keys,
|
|
263
|
+
titleKey: keys[0],
|
|
264
|
+
contentKeys: keys,
|
|
265
|
+
contentSeparator: ', ',
|
|
266
|
+
metadataKeys: keys.slice(1),
|
|
267
|
+
globalMetadata: [],
|
|
268
|
+
indexAsTabular: true,
|
|
269
|
+
});
|
|
270
|
+
indexedSheets++;
|
|
271
|
+
}
|
|
272
|
+
if (indexedSheets === 0) {
|
|
273
|
+
toast.error(`Could not find tabular rows in ${file.name}.`);
|
|
274
|
+
}
|
|
275
|
+
} else if (ext === 'json') {
|
|
276
|
+
const content = await file.text();
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(content);
|
|
279
|
+
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object') {
|
|
280
|
+
const keys = Object.keys(parsed[0]);
|
|
281
|
+
stage({
|
|
282
|
+
id,
|
|
283
|
+
name: file.name,
|
|
284
|
+
size: file.size,
|
|
285
|
+
format: 'structured',
|
|
286
|
+
rows: parsed,
|
|
287
|
+
keys,
|
|
288
|
+
titleKey: keys[0],
|
|
289
|
+
contentKeys: keys,
|
|
290
|
+
contentSeparator: ', ',
|
|
291
|
+
metadataKeys: keys.slice(1),
|
|
292
|
+
globalMetadata: [],
|
|
293
|
+
indexAsTabular: true,
|
|
294
|
+
});
|
|
295
|
+
} else {
|
|
296
|
+
stage({
|
|
297
|
+
id,
|
|
298
|
+
name: file.name,
|
|
299
|
+
size: file.size,
|
|
300
|
+
format: 'plain',
|
|
301
|
+
rawContent: content,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
stage({
|
|
306
|
+
id,
|
|
307
|
+
name: file.name,
|
|
308
|
+
size: file.size,
|
|
309
|
+
format: 'plain',
|
|
310
|
+
rawContent: content,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
} else if (ext === 'pdf' || ext === 'doc' || ext === 'docx') {
|
|
314
|
+
const formData = new FormData();
|
|
315
|
+
formData.append('file', file);
|
|
316
|
+
|
|
317
|
+
const res = await fetch(workspaceUrl('/api/parse-file'), {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
body: formData,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const data = await res.json().catch(() => ({}));
|
|
323
|
+
if (!res.ok) {
|
|
324
|
+
throw new Error(data.error || `Could not extract text from ${file.name}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const { text } = data;
|
|
328
|
+
const hasReadableText = data.hasReadableText ?? Boolean(text?.trim());
|
|
329
|
+
const imageOnlyPdf = ext === 'pdf' && !hasReadableText;
|
|
330
|
+
if (!hasReadableText && !imageOnlyPdf) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
'This PDF does not contain selectable text. Try another copy of the file.',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (imageOnlyPdf && imageIndexingCapability?.available === false) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
'This PDF contains scanned or image-only pages. Configure a vision model in Settings → AI Models to index it.',
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (imageOnlyPdf) {
|
|
341
|
+
toast.message(
|
|
342
|
+
`No selectable text found in ${file.name}; PDF image indexing was enabled automatically.`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
stage({
|
|
346
|
+
id,
|
|
347
|
+
name: file.name,
|
|
348
|
+
size: file.size,
|
|
349
|
+
format: 'plain',
|
|
350
|
+
rawContent: hasReadableText ? text : undefined,
|
|
351
|
+
fileObject: file,
|
|
352
|
+
indexImages: indexAllImages || imageOnlyPdf,
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
const content = await file.text();
|
|
356
|
+
stage({
|
|
357
|
+
id,
|
|
358
|
+
name: file.name,
|
|
359
|
+
size: file.size,
|
|
360
|
+
format: 'plain',
|
|
361
|
+
rawContent: content,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
toast.error(`Could not read ${file.name}. ${formatErrorMessage(err)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
setStaged((prev) => [...prev, ...next]);
|
|
369
|
+
if (joinedActiveSave && next.length > 0) {
|
|
370
|
+
toast.message(`${next.length} file${next.length === 1 ? '' : 's'} added to the queue`);
|
|
371
|
+
window.setTimeout(() => void ingest(), 0);
|
|
372
|
+
}
|
|
373
|
+
return next;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function loadRemoteFile() {
|
|
377
|
+
const requestedUrl = remoteUrl.trim();
|
|
378
|
+
if (!requestedUrl) {
|
|
379
|
+
toast.error('Paste a public link to a file first.');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
setLoadingRemoteFile(true);
|
|
384
|
+
try {
|
|
385
|
+
const response = await fetch(workspaceUrl('/api/files/remote'), {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
388
|
+
body: JSON.stringify({ url: requestedUrl }),
|
|
389
|
+
});
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
const body = await response.json().catch(() => ({}));
|
|
392
|
+
throw new Error(body.error || 'Could not load this remote file.');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const fileName = decodeURIComponent(
|
|
396
|
+
response.headers.get('X-Larkup-File-Name') || 'remote-file',
|
|
397
|
+
);
|
|
398
|
+
const sourceUrl = decodeURIComponent(
|
|
399
|
+
response.headers.get('X-Larkup-Source-Url') || requestedUrl,
|
|
400
|
+
);
|
|
401
|
+
const bytes = await response.arrayBuffer();
|
|
402
|
+
if (!bytes.byteLength) throw new Error('This remote file is empty.');
|
|
403
|
+
|
|
404
|
+
const loadedFiles = await readFiles(
|
|
405
|
+
[new File([bytes], fileName, { type: response.headers.get('content-type') || '' })],
|
|
406
|
+
sourceUrl,
|
|
407
|
+
);
|
|
408
|
+
if (loadedFiles.length === 0) return;
|
|
409
|
+
setRemoteUrl('');
|
|
410
|
+
setEntryTab('upload');
|
|
411
|
+
await ingest(loadedFiles);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
toast.error(formatErrorMessage(error));
|
|
414
|
+
} finally {
|
|
415
|
+
setLoadingRemoteFile(false);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function ingest(files?: StagedFile[]) {
|
|
420
|
+
if (savingRef.current || (files?.length ?? stagedRef.current.length) === 0) return;
|
|
421
|
+
savingRef.current = true;
|
|
422
|
+
setSaving(true);
|
|
423
|
+
let ok = 0;
|
|
424
|
+
|
|
425
|
+
const filesToIngest = files ? [...files] : [...stagedRef.current];
|
|
426
|
+
const payloads: any[] = [];
|
|
427
|
+
const destinationGroupId = groupIdRef.current;
|
|
428
|
+
const tabularDatasetIds = new Map<string, string>();
|
|
429
|
+
|
|
430
|
+
for (const f of filesToIngest) {
|
|
431
|
+
if (f.format === 'structured' && f.rows && f.indexAsTabular) {
|
|
432
|
+
try {
|
|
433
|
+
const res = await fetch(workspaceUrl('/api/tabular'), {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: { 'Content-Type': 'application/json' },
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
fileName: f.name,
|
|
438
|
+
rows: f.rows,
|
|
439
|
+
groupId: destinationGroupId,
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
if (res.ok) {
|
|
443
|
+
const result = await res.json();
|
|
444
|
+
if (typeof result.id === 'string') tabularDatasetIds.set(f.id, result.id);
|
|
445
|
+
toast.success(
|
|
446
|
+
`Saved "${f.name}" as tabular dataset (${result.rowCount} rows, ${result.columns.length} columns)`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
/* continue — still add as documents */
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Then: create document payloads
|
|
456
|
+
for (const f of filesToIngest) {
|
|
457
|
+
if (f.format === 'plain' && (f.rawContent || f.name.toLowerCase().endsWith('.pdf'))) {
|
|
458
|
+
let fileUrl = undefined;
|
|
459
|
+
// Upload the physical file so chat can link to it
|
|
460
|
+
if (f.fileObject) {
|
|
461
|
+
try {
|
|
462
|
+
const formData = new FormData();
|
|
463
|
+
formData.append('file', f.fileObject);
|
|
464
|
+
const uploadRes = await fetch(workspaceUrl('/api/upload-file'), {
|
|
465
|
+
method: 'POST',
|
|
466
|
+
body: formData,
|
|
467
|
+
});
|
|
468
|
+
if (uploadRes.ok) {
|
|
469
|
+
const { url } = await uploadRes.json();
|
|
470
|
+
fileUrl = url;
|
|
471
|
+
}
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.error('Failed to upload file for citation link', err);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const hasText = f.rawContent && f.rawContent.trim().length > 0;
|
|
478
|
+
const uploadedImages: any[] = [];
|
|
479
|
+
let skippedImageCount = 0;
|
|
480
|
+
|
|
481
|
+
// Extract images if requested (for PDFs)
|
|
482
|
+
if (
|
|
483
|
+
f.indexImages &&
|
|
484
|
+
imageIndexingAvailable &&
|
|
485
|
+
f.fileObject &&
|
|
486
|
+
f.name.toLowerCase().endsWith('.pdf')
|
|
487
|
+
) {
|
|
488
|
+
try {
|
|
489
|
+
setProgress({ message: `Extracting images from ${f.name}...`, current: 0, total: 100 });
|
|
490
|
+
const { extractImagesFromPDF } = await import('../../lib/media/pdf');
|
|
491
|
+
const images = await extractImagesFromPDF(f.fileObject);
|
|
492
|
+
const BATCH_SIZE = 5;
|
|
493
|
+
for (let i = 0; i < images.length; i += BATCH_SIZE) {
|
|
494
|
+
const batch = images.slice(i, i + BATCH_SIZE);
|
|
495
|
+
|
|
496
|
+
await Promise.all(
|
|
497
|
+
batch.map(async (img, batchIdx) => {
|
|
498
|
+
const currentIdx = i + batchIdx;
|
|
499
|
+
setProgress({
|
|
500
|
+
message: `Analyzing image ${currentIdx + 1} of ${images.length} from ${
|
|
501
|
+
f.name
|
|
502
|
+
}...`,
|
|
503
|
+
current: currentIdx,
|
|
504
|
+
total: images.length || 1,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const res = await fetch(img.base64);
|
|
508
|
+
const blob = await res.blob();
|
|
509
|
+
const file = new File([blob], `image-${img.index}.jpg`, { type: 'image/jpeg' });
|
|
510
|
+
|
|
511
|
+
const formData = new FormData();
|
|
512
|
+
formData.append('file', file);
|
|
513
|
+
const uploadRes = await fetch(workspaceUrl('/api/upload-file'), {
|
|
514
|
+
method: 'POST',
|
|
515
|
+
body: formData,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (uploadRes.ok) {
|
|
519
|
+
const { url } = await uploadRes.json();
|
|
520
|
+
|
|
521
|
+
let description = '';
|
|
522
|
+
try {
|
|
523
|
+
const descRes = await fetch(workspaceUrl('/api/describe-image'), {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
526
|
+
body: JSON.stringify({
|
|
527
|
+
base64: img.base64,
|
|
528
|
+
prompt:
|
|
529
|
+
'Create accurate retrieval notes for this rendered document page. Preserve captions, figure and table identifiers, equations and their grouping, readable labels, legends, headings, row/column relationships, and diagram connections. Do not invent unreadable text; say when text is unclear. Use concise labeled bullets so later questions can retrieve the correct page without re-reading the visual.',
|
|
530
|
+
}),
|
|
531
|
+
});
|
|
532
|
+
if (descRes.ok) {
|
|
533
|
+
const descData = await descRes.json();
|
|
534
|
+
if (descData.description) {
|
|
535
|
+
description = descData.description;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch (e) {
|
|
539
|
+
console.error('Failed to describe image:', e);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (description.trim()) {
|
|
543
|
+
uploadedImages.push({
|
|
544
|
+
imageUrl: url,
|
|
545
|
+
pageNumber: img.pageNumber,
|
|
546
|
+
index: img.index,
|
|
547
|
+
description,
|
|
548
|
+
});
|
|
549
|
+
} else {
|
|
550
|
+
skippedImageCount++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
// Ensure images remain in original order after concurrent processing
|
|
557
|
+
uploadedImages.sort((a, b) => a.index - b.index);
|
|
558
|
+
if (skippedImageCount > 0) {
|
|
559
|
+
toast.warning(
|
|
560
|
+
`${skippedImageCount} image${skippedImageCount === 1 ? '' : 's'} from ${f.name} could not be analyzed and were not indexed.`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.error('Failed to extract images from PDF:', err);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (hasText || uploadedImages.length > 0) {
|
|
569
|
+
let contentStr = hasText ? f.rawContent : `Images extracted from ${f.name}`;
|
|
570
|
+
if (uploadedImages.length > 0) {
|
|
571
|
+
const descriptions = uploadedImages
|
|
572
|
+
.map(
|
|
573
|
+
(img) =>
|
|
574
|
+
`Image ${img.index + 1} (Page ${img.pageNumber}): ${
|
|
575
|
+
img.description || 'No description'
|
|
576
|
+
}`,
|
|
577
|
+
)
|
|
578
|
+
.join('\n');
|
|
579
|
+
contentStr += `\n\nImage Descriptions:\n${descriptions}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
payloads.push({
|
|
583
|
+
title: f.name,
|
|
584
|
+
content: contentStr,
|
|
585
|
+
source: 'files',
|
|
586
|
+
url: fileUrl ?? f.sourceUrl,
|
|
587
|
+
metadata: {
|
|
588
|
+
...(f.name.toLowerCase().endsWith('.pdf')
|
|
589
|
+
? { mimeType: 'application/pdf', fileKind: 'pdf' }
|
|
590
|
+
: {}),
|
|
591
|
+
...(f.sourceUrl ? { originalUrl: f.sourceUrl } : {}),
|
|
592
|
+
...(uploadedImages.length > 0
|
|
593
|
+
? {
|
|
594
|
+
images: uploadedImages.map((img) => ({
|
|
595
|
+
imageUrl: img.imageUrl,
|
|
596
|
+
pageNumber: img.pageNumber,
|
|
597
|
+
index: img.index,
|
|
598
|
+
description: img.description,
|
|
599
|
+
})),
|
|
600
|
+
}
|
|
601
|
+
: {}),
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
} else if (f.format === 'lines' && f.rawContent) {
|
|
606
|
+
payloads.push({
|
|
607
|
+
title: f.name,
|
|
608
|
+
content: f.rawContent,
|
|
609
|
+
source: 'files',
|
|
610
|
+
metadata: {
|
|
611
|
+
fileName: f.name,
|
|
612
|
+
format: 'lines',
|
|
613
|
+
...(f.sourceUrl ? { originalUrl: f.sourceUrl } : {}),
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
} else if (f.format === 'structured' && f.rows) {
|
|
617
|
+
const rows = f.rows
|
|
618
|
+
.map((row) => {
|
|
619
|
+
const fields = f.contentKeys?.length
|
|
620
|
+
? f.contentKeys.map((key) => `${key}: ${String(row[key] ?? '')}`)
|
|
621
|
+
: Object.entries(row).map(([key, value]) => `${key}: ${String(value ?? '')}`);
|
|
622
|
+
return fields.join(f.contentSeparator || ' | ');
|
|
623
|
+
})
|
|
624
|
+
.filter(Boolean);
|
|
625
|
+
const metadata: Record<string, unknown> = {
|
|
626
|
+
fileName: f.name,
|
|
627
|
+
rowCount: f.rows.length,
|
|
628
|
+
...(tabularDatasetIds.get(f.id) ? { tabularDatasetId: tabularDatasetIds.get(f.id) } : {}),
|
|
629
|
+
...(f.sourceUrl ? { originalUrl: f.sourceUrl } : {}),
|
|
630
|
+
};
|
|
631
|
+
for (const item of f.globalMetadata ?? []) {
|
|
632
|
+
if (item.key.trim()) metadata[item.key.trim()] = item.value;
|
|
633
|
+
}
|
|
634
|
+
if (rows.length) {
|
|
635
|
+
payloads.push({ title: f.name, content: rows.join('\n'), metadata, source: 'files' });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
setProgress({ message: 'Uploading documents...', current: 0, total: payloads.length || 1 });
|
|
641
|
+
|
|
642
|
+
for (const [index, p] of payloads.entries()) {
|
|
643
|
+
try {
|
|
644
|
+
const res = await fetch(workspaceUrl('/api/documents'), {
|
|
645
|
+
method: 'POST',
|
|
646
|
+
headers: { 'Content-Type': 'application/json' },
|
|
647
|
+
body: JSON.stringify({ ...p, groupId: destinationGroupId }),
|
|
648
|
+
});
|
|
649
|
+
if (res.ok) ok++;
|
|
650
|
+
} catch {
|
|
651
|
+
/* continue */
|
|
652
|
+
}
|
|
653
|
+
setProgress({
|
|
654
|
+
message: 'Uploading documents...',
|
|
655
|
+
current: index + 1,
|
|
656
|
+
total: payloads.length || 1,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const stagedCount = filesToIngest.length;
|
|
661
|
+
savingRef.current = false;
|
|
662
|
+
setSaving(false);
|
|
663
|
+
setProgress(null);
|
|
664
|
+
const remaining = stagedRef.current.filter(
|
|
665
|
+
(pending) => !filesToIngest.some((ingested) => ingested.id === pending.id),
|
|
666
|
+
);
|
|
667
|
+
setStaged(remaining);
|
|
668
|
+
if (remaining.length === 0) sessionStorage.removeItem(queueKey);
|
|
669
|
+
|
|
670
|
+
// Also explicitly clear the input value just in case
|
|
671
|
+
if (inputRef.current) {
|
|
672
|
+
inputRef.current.value = '';
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (ok > 0) {
|
|
676
|
+
toast.success(
|
|
677
|
+
`Added ${ok} document${ok > 1 ? 's' : ''} from ${stagedCount} file${
|
|
678
|
+
stagedCount > 1 ? 's' : ''
|
|
679
|
+
}`,
|
|
680
|
+
);
|
|
681
|
+
onAdded();
|
|
682
|
+
} else {
|
|
683
|
+
toast.error('No text or images could be extracted from the uploaded files.');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!files && remaining.length > 0) window.setTimeout(() => void ingest(), 0);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
const actionBusy = saving || loadingRemoteFile;
|
|
691
|
+
onActionChange?.({
|
|
692
|
+
label: 'Add file',
|
|
693
|
+
onClick: isRemoteEntry ? () => void loadRemoteFile() : () => void ingest(),
|
|
694
|
+
disabled: actionBusy || (isRemoteEntry ? !remoteUrl.trim() : staged.length === 0),
|
|
695
|
+
loading: actionBusy,
|
|
696
|
+
});
|
|
697
|
+
return () => onActionChange?.(null);
|
|
698
|
+
}, [entryTab, staged, saving, remoteUrl, loadingRemoteFile, groupId, onActionChange]);
|
|
699
|
+
|
|
700
|
+
function updateEditingFile(patch: Partial<StagedFile>) {
|
|
701
|
+
setStaged((prev) => prev.map((f) => (f.id === editingFileId ? { ...f, ...patch } : f)));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<div className="space-y-4 cursor-pointer">
|
|
706
|
+
<DataEntryModeSwitch
|
|
707
|
+
label="File source"
|
|
708
|
+
value={entryTab}
|
|
709
|
+
onValueChange={(value) => setEntryTab(value as 'upload' | 'url')}
|
|
710
|
+
options={[
|
|
711
|
+
{ value: 'upload', label: 'Upload', icon: FileUp },
|
|
712
|
+
{ value: 'url', label: 'From URL', icon: Link2 },
|
|
713
|
+
]}
|
|
714
|
+
/>
|
|
715
|
+
|
|
716
|
+
{entryTab === 'upload' ? (
|
|
717
|
+
<SourceUploadDropzone
|
|
718
|
+
onClick={() => inputRef.current?.click()}
|
|
719
|
+
onDragOver={(e) => {
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
setDragging(true);
|
|
722
|
+
}}
|
|
723
|
+
onDragLeave={() => setDragging(false)}
|
|
724
|
+
onDrop={(e) => {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
setDragging(false);
|
|
727
|
+
if (e.dataTransfer.files?.length) readFiles(e.dataTransfer.files);
|
|
728
|
+
}}
|
|
729
|
+
dragging={dragging}
|
|
730
|
+
icon={<FileUp className="size-6 text-muted-foreground" />}
|
|
731
|
+
/>
|
|
732
|
+
) : (
|
|
733
|
+
<div className="space-y-1">
|
|
734
|
+
<Input
|
|
735
|
+
aria-label="Remote file URL"
|
|
736
|
+
type="url"
|
|
737
|
+
value={remoteUrl}
|
|
738
|
+
onChange={(event) => setRemoteUrl(event.target.value)}
|
|
739
|
+
onKeyDown={(event) => {
|
|
740
|
+
if (event.key === 'Enter') void loadRemoteFile();
|
|
741
|
+
}}
|
|
742
|
+
placeholder="https://example.com/my-report.pdf"
|
|
743
|
+
disabled={loadingRemoteFile}
|
|
744
|
+
className="bg-white"
|
|
745
|
+
/>
|
|
746
|
+
<p className="text-[11px] pl-1 leading-relaxed text-muted-foreground">
|
|
747
|
+
Paste a public file URL, GitHub file link, or Hugging Face file link. PDF, Word, CSV,
|
|
748
|
+
JSON, Excel, and text files up to 50 MB are supported.
|
|
749
|
+
</p>
|
|
750
|
+
{!onActionChange && (
|
|
751
|
+
<Button
|
|
752
|
+
type="button"
|
|
753
|
+
onClick={() => void loadRemoteFile()}
|
|
754
|
+
disabled={!remoteUrl.trim() || loadingRemoteFile}
|
|
755
|
+
>
|
|
756
|
+
{loadingRemoteFile ? (
|
|
757
|
+
<Loader2 className="size-4 animate-spin" />
|
|
758
|
+
) : (
|
|
759
|
+
<Link2 className="size-4" />
|
|
760
|
+
)}
|
|
761
|
+
Add file
|
|
762
|
+
</Button>
|
|
763
|
+
)}
|
|
764
|
+
</div>
|
|
765
|
+
)}
|
|
766
|
+
<input
|
|
767
|
+
ref={inputRef}
|
|
768
|
+
type="file"
|
|
769
|
+
multiple
|
|
770
|
+
accept={ACCEPT}
|
|
771
|
+
className="sr-only"
|
|
772
|
+
onChange={(e) => {
|
|
773
|
+
if (e.target.files?.length) readFiles(e.target.files);
|
|
774
|
+
e.target.value = '';
|
|
775
|
+
}}
|
|
776
|
+
/>
|
|
777
|
+
|
|
778
|
+
{staged.length > 0 && (
|
|
779
|
+
<div className="space-y-2 pb-5">
|
|
780
|
+
{staged.some((file) => file.name.toLowerCase().endsWith('.pdf')) &&
|
|
781
|
+
imageIndexingCapability &&
|
|
782
|
+
!imageIndexingAvailable && (
|
|
783
|
+
<Alert className="border-amber-500/30 bg-amber-500/5">
|
|
784
|
+
<AlertTriangle className="size-4 text-amber-600 dark:text-amber-400" />
|
|
785
|
+
<AlertTitle>PDF image indexing is unavailable</AlertTitle>
|
|
786
|
+
<AlertDescription>
|
|
787
|
+
{imageIndexingCapability.message} Configure a vision model in Settings → AI Models
|
|
788
|
+
to index PDF visuals.
|
|
789
|
+
</AlertDescription>
|
|
790
|
+
</Alert>
|
|
791
|
+
)}
|
|
792
|
+
<div className="flex items-center justify-between px-1 ">
|
|
793
|
+
<span className="text-[13px] font-medium text-foreground">
|
|
794
|
+
{staged.length} file{staged.length !== 1 ? 's' : ''} staged
|
|
795
|
+
</span>
|
|
796
|
+
<div className="flex items-center gap-4 mt-5">
|
|
797
|
+
{staged.some((f) => f.name.toLowerCase().endsWith('.pdf')) && (
|
|
798
|
+
<div className="flex items-center gap-1.5">
|
|
799
|
+
<Switch
|
|
800
|
+
checked={indexAllImages}
|
|
801
|
+
disabled={!imageIndexingAvailable}
|
|
802
|
+
onCheckedChange={(val) => {
|
|
803
|
+
if (!imageIndexingAvailable) return;
|
|
804
|
+
setIndexAllImages(val);
|
|
805
|
+
setStaged((p) =>
|
|
806
|
+
p.map((item) =>
|
|
807
|
+
item.name.toLowerCase().endsWith('.pdf')
|
|
808
|
+
? { ...item, indexImages: val }
|
|
809
|
+
: item,
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
}}
|
|
813
|
+
id="global-index-images"
|
|
814
|
+
className="scale-75 origin-right"
|
|
815
|
+
/>
|
|
816
|
+
<Label
|
|
817
|
+
htmlFor="global-index-images"
|
|
818
|
+
className="text-[11px] text-muted-foreground cursor-pointer"
|
|
819
|
+
>
|
|
820
|
+
Index all PDF images
|
|
821
|
+
</Label>
|
|
822
|
+
</div>
|
|
823
|
+
)}
|
|
824
|
+
<button
|
|
825
|
+
type="button"
|
|
826
|
+
onClick={() => setStaged([])}
|
|
827
|
+
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
828
|
+
>
|
|
829
|
+
Clear All
|
|
830
|
+
</button>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
<ul className="space-y-1.5">
|
|
834
|
+
{staged.map((f) => (
|
|
835
|
+
<li
|
|
836
|
+
key={f.id}
|
|
837
|
+
className="flex flex-col gap-2 rounded-md border border-border bg-card px-3 py-2 text-sm"
|
|
838
|
+
>
|
|
839
|
+
<div className="flex items-center justify-between gap-3">
|
|
840
|
+
<div className="min-w-0 flex-1 flex flex-col gap-0.5">
|
|
841
|
+
<span className="truncate font-mono text-xs">{f.name}</span>
|
|
842
|
+
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
843
|
+
{f.format === 'structured'
|
|
844
|
+
? 'STRUCTURED'
|
|
845
|
+
: f.format === 'lines'
|
|
846
|
+
? 'SPLIT BY LINE'
|
|
847
|
+
: 'PLAIN TEXT'}
|
|
848
|
+
{f.format === 'structured' && ` • ${f.rows?.length} ROWS`}
|
|
849
|
+
{f.indexAsTabular && ' • TABULAR'}
|
|
850
|
+
</span>
|
|
851
|
+
{f.sourceUrl && (
|
|
852
|
+
<span
|
|
853
|
+
className="truncate text-[10px] text-muted-foreground"
|
|
854
|
+
title={f.sourceUrl}
|
|
855
|
+
>
|
|
856
|
+
Imported from {new URL(f.sourceUrl).hostname.replace(/^www\./, '')}
|
|
857
|
+
</span>
|
|
858
|
+
)}
|
|
859
|
+
</div>
|
|
860
|
+
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
|
|
861
|
+
{(f.size / 1024).toFixed(1)} KB
|
|
862
|
+
</span>
|
|
863
|
+
|
|
864
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
865
|
+
{/* Tabular indexing toggle for structured files */}
|
|
866
|
+
{f.format === 'structured' && (
|
|
867
|
+
<button
|
|
868
|
+
type="button"
|
|
869
|
+
aria-label="Toggle tabular indexing"
|
|
870
|
+
title={
|
|
871
|
+
f.indexAsTabular
|
|
872
|
+
? 'Tabular indexing ON'
|
|
873
|
+
: 'Enable tabular indexing for data analysis'
|
|
874
|
+
}
|
|
875
|
+
onClick={() =>
|
|
876
|
+
setStaged((p) =>
|
|
877
|
+
p.map((item) =>
|
|
878
|
+
item.id === f.id
|
|
879
|
+
? { ...item, indexAsTabular: !item.indexAsTabular }
|
|
880
|
+
: item,
|
|
881
|
+
),
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
className={cn(
|
|
885
|
+
'p-1.5 border rounded-md transition-colors cursor-pointer',
|
|
886
|
+
f.indexAsTabular
|
|
887
|
+
? 'bg-primary/10 border-primary/30 text-primary'
|
|
888
|
+
: 'bg-secondary text-muted-foreground hover:bg-muted/50 hover:text-foreground',
|
|
889
|
+
)}
|
|
890
|
+
>
|
|
891
|
+
<Database className="size-3.5" />
|
|
892
|
+
</button>
|
|
893
|
+
)}
|
|
894
|
+
{/* Image indexing toggle for PDF files */}
|
|
895
|
+
{f.name.toLowerCase().endsWith('.pdf') && (
|
|
896
|
+
<TooltipProvider delay={0}>
|
|
897
|
+
<Tooltip>
|
|
898
|
+
<TooltipTrigger
|
|
899
|
+
type="button"
|
|
900
|
+
aria-label="Toggle image indexing"
|
|
901
|
+
aria-pressed={Boolean(f.indexImages)}
|
|
902
|
+
disabled={!imageIndexingAvailable}
|
|
903
|
+
onClick={() => {
|
|
904
|
+
if (!imageIndexingAvailable) return;
|
|
905
|
+
setStaged((p) =>
|
|
906
|
+
p.map((item) =>
|
|
907
|
+
item.id === f.id
|
|
908
|
+
? { ...item, indexImages: !item.indexImages }
|
|
909
|
+
: item,
|
|
910
|
+
),
|
|
911
|
+
);
|
|
912
|
+
}}
|
|
913
|
+
className={cn(
|
|
914
|
+
'p-1.5 border rounded-md transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50',
|
|
915
|
+
f.indexImages
|
|
916
|
+
? 'bg-blue-100 border-blue-300 text-blue-600 dark:bg-blue-900/30 dark:border-blue-700 dark:text-blue-400'
|
|
917
|
+
: 'bg-secondary text-muted-foreground hover:bg-muted/50 hover:text-foreground',
|
|
918
|
+
)}
|
|
919
|
+
>
|
|
920
|
+
<ImageIcon className="size-3.5" />
|
|
921
|
+
</TooltipTrigger>
|
|
922
|
+
<TooltipContent side="top">
|
|
923
|
+
<p className="text-xs">
|
|
924
|
+
{f.indexImages
|
|
925
|
+
? 'Image indexing ON'
|
|
926
|
+
: 'Extract and index images from PDF'}
|
|
927
|
+
</p>
|
|
928
|
+
</TooltipContent>
|
|
929
|
+
</Tooltip>
|
|
930
|
+
</TooltipProvider>
|
|
931
|
+
)}
|
|
932
|
+
{f.rawContent && (
|
|
933
|
+
<button
|
|
934
|
+
type="button"
|
|
935
|
+
aria-label={`Preview ${f.name}`}
|
|
936
|
+
title="Preview extracted content"
|
|
937
|
+
onClick={() => setPreviewingFileId(f.id)}
|
|
938
|
+
className="rounded-md border bg-secondary p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
|
939
|
+
>
|
|
940
|
+
<Eye className="size-3.5" />
|
|
941
|
+
</button>
|
|
942
|
+
)}
|
|
943
|
+
<button
|
|
944
|
+
type="button"
|
|
945
|
+
aria-label={`Configure ${f.name}`}
|
|
946
|
+
onClick={() => setEditingFileId(f.id)}
|
|
947
|
+
className="p-1.5 bg-secondary cursor-pointer border text-muted-foreground hover:bg-muted/50 hover:text-foreground rounded-md transition-colors"
|
|
948
|
+
>
|
|
949
|
+
<Settings2 className="size-3.5" />
|
|
950
|
+
</button>
|
|
951
|
+
<button
|
|
952
|
+
type="button"
|
|
953
|
+
aria-label={`Remove ${f.name}`}
|
|
954
|
+
onClick={() => setStaged((p) => p.filter((item) => item.id !== f.id))}
|
|
955
|
+
className="p-1.5 border bg-muted hover:bg-muted/50 cursor-pointer text-muted-foreground hover:text-foreground rounded-md transition-colors"
|
|
956
|
+
>
|
|
957
|
+
<X className="size-3.5 text-red-500" />
|
|
958
|
+
</button>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
|
|
962
|
+
{/* Tabular preview for structured files with tabular indexing */}
|
|
963
|
+
{f.format === 'structured' && f.indexAsTabular && f.rows && (
|
|
964
|
+
<div className="min-w-0 w-full mt-1">
|
|
965
|
+
<TabularPreview rows={f.rows} maxPreviewRows={5} />
|
|
966
|
+
</div>
|
|
967
|
+
)}
|
|
968
|
+
</li>
|
|
969
|
+
))}
|
|
970
|
+
</ul>
|
|
971
|
+
</div>
|
|
972
|
+
)}
|
|
973
|
+
|
|
974
|
+
{progress && (
|
|
975
|
+
<div className="space-y-2 mb-4">
|
|
976
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
977
|
+
<span>{progress.message || 'Processing...'}</span>
|
|
978
|
+
<span className="tabular-nums font-mono">
|
|
979
|
+
{progress.current} / {progress.total}
|
|
980
|
+
</span>
|
|
981
|
+
</div>
|
|
982
|
+
<Progress value={(progress.current / progress.total) * 100} />
|
|
983
|
+
</div>
|
|
984
|
+
)}
|
|
985
|
+
|
|
986
|
+
{!onActionChange && (
|
|
987
|
+
<Button
|
|
988
|
+
onClick={isRemoteEntry ? () => void loadRemoteFile() : () => void ingest()}
|
|
989
|
+
disabled={
|
|
990
|
+
loadingRemoteFile || saving || (isRemoteEntry ? !remoteUrl.trim() : staged.length === 0)
|
|
991
|
+
}
|
|
992
|
+
>
|
|
993
|
+
{loadingRemoteFile || saving ? (
|
|
994
|
+
<Loader2 className="size-4 animate-spin" />
|
|
995
|
+
) : (
|
|
996
|
+
<FileUp className="size-4" />
|
|
997
|
+
)}
|
|
998
|
+
Add file
|
|
999
|
+
</Button>
|
|
1000
|
+
)}
|
|
1001
|
+
|
|
1002
|
+
{/* Mapping Dialog */}
|
|
1003
|
+
<Dialog open={!!editingFile} onOpenChange={(open) => !open && setEditingFileId(null)}>
|
|
1004
|
+
<DialogContent className="sm:max-w-xl max-h-[80%]">
|
|
1005
|
+
<DialogHeader>
|
|
1006
|
+
<DialogTitle className="flex items-center gap-2">
|
|
1007
|
+
<Columns className="size-4 text-primary" />
|
|
1008
|
+
Configure Ingestion
|
|
1009
|
+
</DialogTitle>
|
|
1010
|
+
</DialogHeader>
|
|
1011
|
+
|
|
1012
|
+
{editingFile && (
|
|
1013
|
+
<div
|
|
1014
|
+
className="py-4 space-y-6 overflow-y-auto pr-2"
|
|
1015
|
+
style={{ maxHeight: 'calc(80vh - 120px)' }}
|
|
1016
|
+
>
|
|
1017
|
+
{editingFile.format === 'structured' ? (
|
|
1018
|
+
<>
|
|
1019
|
+
<div className="space-y-4">
|
|
1020
|
+
<div className="space-y-2">
|
|
1021
|
+
<Label>Title Column</Label>
|
|
1022
|
+
<Select
|
|
1023
|
+
value={editingFile.titleKey}
|
|
1024
|
+
onValueChange={(val) => updateEditingFile({ titleKey: val || undefined })}
|
|
1025
|
+
>
|
|
1026
|
+
<SelectTrigger>
|
|
1027
|
+
<SelectValue placeholder="Select column" />
|
|
1028
|
+
</SelectTrigger>
|
|
1029
|
+
<SelectContent>
|
|
1030
|
+
{editingFile.keys?.map((k) => (
|
|
1031
|
+
<SelectItem key={k} value={k}>
|
|
1032
|
+
{k}
|
|
1033
|
+
</SelectItem>
|
|
1034
|
+
))}
|
|
1035
|
+
</SelectContent>
|
|
1036
|
+
</Select>
|
|
1037
|
+
<p className="text-[11px] text-muted-foreground">
|
|
1038
|
+
Used as the document name.
|
|
1039
|
+
</p>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<div className="space-y-3 pt-2 border-t border-border">
|
|
1043
|
+
<div className="flex items-center justify-between">
|
|
1044
|
+
<Label>Content Columns</Label>
|
|
1045
|
+
<div className="flex items-center gap-2">
|
|
1046
|
+
<button
|
|
1047
|
+
type="button"
|
|
1048
|
+
className="text-[10px] uppercase font-medium text-primary hover:underline cursor-pointer"
|
|
1049
|
+
onClick={() => {
|
|
1050
|
+
updateEditingFile({
|
|
1051
|
+
contentKeys: editingFile.keys || [],
|
|
1052
|
+
});
|
|
1053
|
+
}}
|
|
1054
|
+
>
|
|
1055
|
+
Select All
|
|
1056
|
+
</button>
|
|
1057
|
+
<span className="text-muted-foreground text-[10px]">|</span>
|
|
1058
|
+
<button
|
|
1059
|
+
type="button"
|
|
1060
|
+
className="text-[10px] uppercase font-medium text-muted-foreground hover:underline cursor-pointer"
|
|
1061
|
+
onClick={() => {
|
|
1062
|
+
updateEditingFile({ contentKeys: [] });
|
|
1063
|
+
}}
|
|
1064
|
+
>
|
|
1065
|
+
Clear
|
|
1066
|
+
</button>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
<p className="text-[11px] text-muted-foreground mb-2 -mt-1">
|
|
1070
|
+
Selected columns will be combined to form the searchable content.
|
|
1071
|
+
</p>
|
|
1072
|
+
<div className="max-h-30 overflow-y-auto space-y-2 pr-2">
|
|
1073
|
+
{editingFile.keys?.map((k) => {
|
|
1074
|
+
const isSelected = editingFile.contentKeys?.includes(k);
|
|
1075
|
+
return (
|
|
1076
|
+
<div key={k} className="flex items-center space-x-2">
|
|
1077
|
+
<Checkbox
|
|
1078
|
+
id={`content-${k}`}
|
|
1079
|
+
checked={isSelected}
|
|
1080
|
+
onCheckedChange={(checked) => {
|
|
1081
|
+
const current = editingFile.contentKeys || [];
|
|
1082
|
+
if (checked) {
|
|
1083
|
+
updateEditingFile({
|
|
1084
|
+
contentKeys: [...current, k],
|
|
1085
|
+
});
|
|
1086
|
+
} else {
|
|
1087
|
+
updateEditingFile({
|
|
1088
|
+
contentKeys: current.filter((x) => x !== k),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}}
|
|
1092
|
+
/>
|
|
1093
|
+
<label
|
|
1094
|
+
htmlFor={`content-${k}`}
|
|
1095
|
+
className="text-sm font-medium leading-none"
|
|
1096
|
+
>
|
|
1097
|
+
{k}
|
|
1098
|
+
</label>
|
|
1099
|
+
</div>
|
|
1100
|
+
);
|
|
1101
|
+
})}
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
{editingFile.contentKeys && editingFile.contentKeys.length > 1 && (
|
|
1105
|
+
<div className="pt-2">
|
|
1106
|
+
<Label className="mb-2 block">Separator</Label>
|
|
1107
|
+
<Select
|
|
1108
|
+
value={editingFile.contentSeparator || ' '}
|
|
1109
|
+
onValueChange={(val) =>
|
|
1110
|
+
updateEditingFile({
|
|
1111
|
+
contentSeparator: val || undefined,
|
|
1112
|
+
})
|
|
1113
|
+
}
|
|
1114
|
+
>
|
|
1115
|
+
<SelectTrigger>
|
|
1116
|
+
<SelectValue placeholder="Select separator" />
|
|
1117
|
+
</SelectTrigger>
|
|
1118
|
+
<SelectContent>
|
|
1119
|
+
<SelectItem value=" ">Space</SelectItem>
|
|
1120
|
+
<SelectItem value=", ">Comma</SelectItem>
|
|
1121
|
+
<SelectItem value="\n">Newline</SelectItem>
|
|
1122
|
+
<SelectItem value=" - ">Hyphen</SelectItem>
|
|
1123
|
+
</SelectContent>
|
|
1124
|
+
</Select>
|
|
1125
|
+
</div>
|
|
1126
|
+
)}
|
|
1127
|
+
</div>
|
|
1128
|
+
|
|
1129
|
+
<div className="space-y-3 pt-2 border-t border-border">
|
|
1130
|
+
<div className="flex items-center justify-between">
|
|
1131
|
+
<Label>Metadata Columns</Label>
|
|
1132
|
+
<div className="flex items-center gap-2">
|
|
1133
|
+
<button
|
|
1134
|
+
type="button"
|
|
1135
|
+
className="text-[10px] uppercase font-medium text-primary hover:underline cursor-pointer"
|
|
1136
|
+
onClick={() => {
|
|
1137
|
+
const availableKeys =
|
|
1138
|
+
editingFile.keys?.filter((k) => k !== editingFile.titleKey) || [];
|
|
1139
|
+
updateEditingFile({
|
|
1140
|
+
metadataKeys: availableKeys,
|
|
1141
|
+
});
|
|
1142
|
+
}}
|
|
1143
|
+
>
|
|
1144
|
+
Select All
|
|
1145
|
+
</button>
|
|
1146
|
+
<span className="text-muted-foreground text-[10px]">|</span>
|
|
1147
|
+
<button
|
|
1148
|
+
type="button"
|
|
1149
|
+
className="text-[10px] uppercase font-medium text-muted-foreground hover:underline cursor-pointer"
|
|
1150
|
+
onClick={() => {
|
|
1151
|
+
updateEditingFile({ metadataKeys: [] });
|
|
1152
|
+
}}
|
|
1153
|
+
>
|
|
1154
|
+
Clear
|
|
1155
|
+
</button>
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
<p className="text-[11px] text-muted-foreground mb-2 -mt-1">
|
|
1159
|
+
These columns will be stored as searchable metadata.
|
|
1160
|
+
</p>
|
|
1161
|
+
<div className="max-h-30 overflow-y-auto space-y-2 pr-2">
|
|
1162
|
+
{editingFile.keys?.map((k) => {
|
|
1163
|
+
const isSelected = editingFile.metadataKeys?.includes(k);
|
|
1164
|
+
const disabled = k === editingFile.titleKey;
|
|
1165
|
+
return (
|
|
1166
|
+
<div key={k} className="flex items-center space-x-2">
|
|
1167
|
+
<Checkbox
|
|
1168
|
+
id={`meta-${k}`}
|
|
1169
|
+
checked={isSelected}
|
|
1170
|
+
disabled={disabled}
|
|
1171
|
+
onCheckedChange={(checked) => {
|
|
1172
|
+
const current = editingFile.metadataKeys || [];
|
|
1173
|
+
if (checked) {
|
|
1174
|
+
updateEditingFile({
|
|
1175
|
+
metadataKeys: [...current, k],
|
|
1176
|
+
});
|
|
1177
|
+
} else {
|
|
1178
|
+
updateEditingFile({
|
|
1179
|
+
metadataKeys: current.filter((x) => x !== k),
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}}
|
|
1183
|
+
/>
|
|
1184
|
+
<label
|
|
1185
|
+
htmlFor={`meta-${k}`}
|
|
1186
|
+
className={cn(
|
|
1187
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
1188
|
+
disabled && 'text-muted-foreground',
|
|
1189
|
+
)}
|
|
1190
|
+
>
|
|
1191
|
+
{k} {disabled && '(used)'}
|
|
1192
|
+
</label>
|
|
1193
|
+
</div>
|
|
1194
|
+
);
|
|
1195
|
+
})}
|
|
1196
|
+
</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
</div>
|
|
1199
|
+
</>
|
|
1200
|
+
) : (
|
|
1201
|
+
<div className="space-y-4">
|
|
1202
|
+
<div className="flex items-center justify-between rounded-lg border border-border p-3">
|
|
1203
|
+
<div className="space-y-0.5">
|
|
1204
|
+
<Label>Split by Line</Label>
|
|
1205
|
+
<p className="text-[11px] text-muted-foreground">
|
|
1206
|
+
Treat each line as a completely separate document.
|
|
1207
|
+
</p>
|
|
1208
|
+
</div>
|
|
1209
|
+
<Switch
|
|
1210
|
+
checked={editingFile.format === 'lines'}
|
|
1211
|
+
onCheckedChange={(checked) =>
|
|
1212
|
+
updateEditingFile({
|
|
1213
|
+
format: checked ? 'lines' : 'plain',
|
|
1214
|
+
})
|
|
1215
|
+
}
|
|
1216
|
+
/>
|
|
1217
|
+
</div>
|
|
1218
|
+
</div>
|
|
1219
|
+
)}
|
|
1220
|
+
|
|
1221
|
+
<div className="space-y-3 pt-4 border-t border-border">
|
|
1222
|
+
<div className="flex items-center justify-between">
|
|
1223
|
+
<Label>Global Custom Metadata</Label>
|
|
1224
|
+
<Button
|
|
1225
|
+
variant="ghost"
|
|
1226
|
+
size="sm"
|
|
1227
|
+
className="h-6 px-2 text-xs"
|
|
1228
|
+
onClick={() => {
|
|
1229
|
+
const current = editingFile.globalMetadata || [];
|
|
1230
|
+
updateEditingFile({
|
|
1231
|
+
globalMetadata: [...current, { key: '', value: '' }],
|
|
1232
|
+
});
|
|
1233
|
+
}}
|
|
1234
|
+
>
|
|
1235
|
+
<Plus className="size-3 mr-1" /> Add Field
|
|
1236
|
+
</Button>
|
|
1237
|
+
</div>
|
|
1238
|
+
<p className="text-[11px] text-muted-foreground mb-2">
|
|
1239
|
+
Apply custom key-value pairs to every document generated from this file.
|
|
1240
|
+
</p>
|
|
1241
|
+
<div className="space-y-2">
|
|
1242
|
+
{(editingFile.globalMetadata || []).map((gm, i) => (
|
|
1243
|
+
<div key={i} className="flex items-center gap-2">
|
|
1244
|
+
<Input
|
|
1245
|
+
placeholder="Key"
|
|
1246
|
+
value={gm.key}
|
|
1247
|
+
className="h-8 text-xs font-mono"
|
|
1248
|
+
onChange={(e) => {
|
|
1249
|
+
const next = [...(editingFile.globalMetadata || [])];
|
|
1250
|
+
next[i].key = e.target.value;
|
|
1251
|
+
updateEditingFile({ globalMetadata: next });
|
|
1252
|
+
}}
|
|
1253
|
+
/>
|
|
1254
|
+
<Input
|
|
1255
|
+
placeholder="Value"
|
|
1256
|
+
value={gm.value}
|
|
1257
|
+
className="h-8 text-xs font-mono"
|
|
1258
|
+
onChange={(e) => {
|
|
1259
|
+
const next = [...(editingFile.globalMetadata || [])];
|
|
1260
|
+
next[i].value = e.target.value;
|
|
1261
|
+
updateEditingFile({ globalMetadata: next });
|
|
1262
|
+
}}
|
|
1263
|
+
/>
|
|
1264
|
+
<Button
|
|
1265
|
+
variant="ghost"
|
|
1266
|
+
size="icon"
|
|
1267
|
+
className="h-8 w-8 text-muted-foreground shrink-0"
|
|
1268
|
+
onClick={() => {
|
|
1269
|
+
const next = [...(editingFile.globalMetadata || [])];
|
|
1270
|
+
next.splice(i, 1);
|
|
1271
|
+
updateEditingFile({ globalMetadata: next });
|
|
1272
|
+
}}
|
|
1273
|
+
>
|
|
1274
|
+
<X className="size-3.5" />
|
|
1275
|
+
</Button>
|
|
1276
|
+
</div>
|
|
1277
|
+
))}
|
|
1278
|
+
{(!editingFile.globalMetadata || editingFile.globalMetadata.length === 0) && (
|
|
1279
|
+
<p className="text-xs text-muted-foreground italic text-center py-2">
|
|
1280
|
+
No custom metadata.
|
|
1281
|
+
</p>
|
|
1282
|
+
)}
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
</div>
|
|
1286
|
+
)}
|
|
1287
|
+
|
|
1288
|
+
<DialogFooter className="pb-2! bg-muted ">
|
|
1289
|
+
<Button onClick={() => setEditingFileId(null)}>Done</Button>
|
|
1290
|
+
</DialogFooter>
|
|
1291
|
+
</DialogContent>
|
|
1292
|
+
</Dialog>
|
|
1293
|
+
|
|
1294
|
+
<Dialog open={!!previewingFile} onOpenChange={(open) => !open && setPreviewingFileId(null)}>
|
|
1295
|
+
<DialogContent className="max-w-7xl! ">
|
|
1296
|
+
<DialogHeader>
|
|
1297
|
+
<DialogTitle className="truncate">Preview: {previewingFile?.name}</DialogTitle>
|
|
1298
|
+
</DialogHeader>
|
|
1299
|
+
<div className="max-h-[65vh] overflow-auto rounded-md border bg-muted/30">
|
|
1300
|
+
<pre className="min-w-max p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap wrap-break-word">
|
|
1301
|
+
{previewingFile?.rawContent?.slice(0, CONTENT_PREVIEW_LIMIT)}
|
|
1302
|
+
</pre>
|
|
1303
|
+
</div>
|
|
1304
|
+
{(previewingFile?.rawContent?.length ?? 0) > CONTENT_PREVIEW_LIMIT && (
|
|
1305
|
+
<p className="text-xs text-muted-foreground">
|
|
1306
|
+
Preview is limited to the first {CONTENT_PREVIEW_LIMIT.toLocaleString()} characters.
|
|
1307
|
+
</p>
|
|
1308
|
+
)}
|
|
1309
|
+
</DialogContent>
|
|
1310
|
+
</Dialog>
|
|
1311
|
+
</div>
|
|
1312
|
+
);
|
|
1313
|
+
}
|