@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,1223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { workspaceUrl, workspaceFetch as fetch } from '../../host';
|
|
4
|
+
import { useEffect, useState, useRef, useMemo } from 'react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { formatErrorMessage } from '../../lib/error-formatter';
|
|
7
|
+
import {
|
|
8
|
+
FileText,
|
|
9
|
+
Trash2,
|
|
10
|
+
FileUp,
|
|
11
|
+
Globe,
|
|
12
|
+
Pencil,
|
|
13
|
+
Loader2,
|
|
14
|
+
ChevronLeft,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
Film,
|
|
17
|
+
Upload,
|
|
18
|
+
Type,
|
|
19
|
+
Image,
|
|
20
|
+
Plug,
|
|
21
|
+
ChevronDown,
|
|
22
|
+
Check,
|
|
23
|
+
X,
|
|
24
|
+
Eye,
|
|
25
|
+
EyeOff,
|
|
26
|
+
Monitor,
|
|
27
|
+
Cloud,
|
|
28
|
+
Info,
|
|
29
|
+
FolderInput,
|
|
30
|
+
} from 'lucide-react';
|
|
31
|
+
import {
|
|
32
|
+
Select,
|
|
33
|
+
SelectContent,
|
|
34
|
+
SelectItem,
|
|
35
|
+
SelectTrigger,
|
|
36
|
+
SelectValue,
|
|
37
|
+
} from '@larkup/workspace-ui/select';
|
|
38
|
+
import type { DocumentSource, SourceDocument, DataGroup } from '@larkup/core/types';
|
|
39
|
+
import { GenericAlert } from '../alerts/generic-alert';
|
|
40
|
+
import { Badge } from '../ui/badge';
|
|
41
|
+
import { Button } from '@larkup/workspace-ui/button';
|
|
42
|
+
import { Input } from '../ui/input';
|
|
43
|
+
import { Label } from '@larkup/workspace-ui/label';
|
|
44
|
+
import { Textarea } from '../ui/textarea';
|
|
45
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
|
46
|
+
import {
|
|
47
|
+
Dialog,
|
|
48
|
+
DialogContent,
|
|
49
|
+
DialogDescription,
|
|
50
|
+
DialogFooter,
|
|
51
|
+
DialogHeader,
|
|
52
|
+
DialogTitle,
|
|
53
|
+
} from '../ui/dialog';
|
|
54
|
+
import { ScrollArea } from '../ui/scroll-area';
|
|
55
|
+
import { Checkbox } from '../ui/checkbox';
|
|
56
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
57
|
+
import {
|
|
58
|
+
DropdownMenu,
|
|
59
|
+
DropdownMenuContent,
|
|
60
|
+
DropdownMenuItem,
|
|
61
|
+
DropdownMenuSeparator,
|
|
62
|
+
DropdownMenuSub,
|
|
63
|
+
DropdownMenuSubContent,
|
|
64
|
+
DropdownMenuSubTrigger,
|
|
65
|
+
DropdownMenuTrigger,
|
|
66
|
+
} from '../ui/dropdown-menu';
|
|
67
|
+
import { cn } from '../../lib/utils';
|
|
68
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
69
|
+
import {
|
|
70
|
+
videoRuntimeScopeFromMetadata,
|
|
71
|
+
videoRuntimeScopeLabel,
|
|
72
|
+
type VideoRuntimeScope,
|
|
73
|
+
} from '../../lib/media/video-runtime-scope';
|
|
74
|
+
|
|
75
|
+
const SOURCE_META: Record<DocumentSource, { label: string; icon: any }> = {
|
|
76
|
+
website: { label: 'Website', icon: Globe },
|
|
77
|
+
text: { label: 'Text', icon: Type },
|
|
78
|
+
files: { label: 'Files', icon: FileUp },
|
|
79
|
+
media: { label: 'Media', icon: Image },
|
|
80
|
+
integrations: { label: 'Integrations', icon: Plug },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function CorpusPanel({
|
|
84
|
+
documents,
|
|
85
|
+
groups = [],
|
|
86
|
+
onChanged,
|
|
87
|
+
isIndexing,
|
|
88
|
+
activeVideoRuntimeScope,
|
|
89
|
+
initialGroupId,
|
|
90
|
+
isLoading,
|
|
91
|
+
}: {
|
|
92
|
+
documents: SourceDocument[];
|
|
93
|
+
groups?: DataGroup[];
|
|
94
|
+
onChanged: () => void | Promise<unknown>;
|
|
95
|
+
isIndexing?: boolean;
|
|
96
|
+
activeVideoRuntimeScope: VideoRuntimeScope;
|
|
97
|
+
initialGroupId?: string;
|
|
98
|
+
isLoading?: boolean;
|
|
99
|
+
}) {
|
|
100
|
+
const [active, setActive] = useState<SourceDocument | null>(null);
|
|
101
|
+
const [deleteTask, setDeleteTask] = useState<
|
|
102
|
+
{ type: 'single'; doc: SourceDocument } | { type: 'all' } | { type: 'selected' } | null
|
|
103
|
+
>(null);
|
|
104
|
+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
105
|
+
const [movingGroupId, setMovingGroupId] = useState<string | null>(null);
|
|
106
|
+
const [inspectorDoc, setInspectorDoc] = useState<SourceDocument | null>(null);
|
|
107
|
+
|
|
108
|
+
const [page, setPage] = useState(0);
|
|
109
|
+
|
|
110
|
+
const [sourceFilter, setSourceFilter] = useState<DocumentSource | 'all'>('all');
|
|
111
|
+
const [statusFilter, setStatusFilter] = useState<'all' | 'indexed' | 'unindexed'>('all');
|
|
112
|
+
const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>(
|
|
113
|
+
() => new Set(initialGroupId ? [initialGroupId] : []),
|
|
114
|
+
);
|
|
115
|
+
const [groupDropdownOpen, setGroupDropdownOpen] = useState(false);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
setSelectedGroupIds(new Set(initialGroupId ? [initialGroupId] : []));
|
|
119
|
+
}, [initialGroupId]);
|
|
120
|
+
|
|
121
|
+
const groupedDocuments = useMemo(() => {
|
|
122
|
+
const grouped = new Map<string, SourceDocument>();
|
|
123
|
+
const others: SourceDocument[] = [];
|
|
124
|
+
|
|
125
|
+
for (const doc of documents) {
|
|
126
|
+
if (doc.source === 'media' && doc.metadata?.mediaAssetId) {
|
|
127
|
+
const id = doc.metadata.mediaAssetId;
|
|
128
|
+
if (!grouped.has(id)) {
|
|
129
|
+
grouped.set(id, {
|
|
130
|
+
...doc,
|
|
131
|
+
id: `media-group-${id}`,
|
|
132
|
+
title: doc.metadata.fileName || doc.title || 'Media Asset',
|
|
133
|
+
charCount: doc.charCount,
|
|
134
|
+
metadata: { ...doc.metadata, isGroup: true, childIds: [doc.id] },
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
const existing = grouped.get(id)!;
|
|
138
|
+
existing.charCount += doc.charCount;
|
|
139
|
+
existing.metadata!.childIds.push(doc.id);
|
|
140
|
+
if (doc.status !== 'indexed') existing.status = doc.status;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
others.push(doc);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...Array.from(grouped.values()), ...others].sort(
|
|
147
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
|
148
|
+
);
|
|
149
|
+
}, [documents]);
|
|
150
|
+
|
|
151
|
+
const filteredDocuments = groupedDocuments.filter(
|
|
152
|
+
(document) =>
|
|
153
|
+
(sourceFilter === 'all' || document.source === sourceFilter) &&
|
|
154
|
+
(statusFilter === 'all' || (document.status ?? 'unindexed') === statusFilter) &&
|
|
155
|
+
(selectedGroupIds.size === 0 || selectedGroupIds.has(document.groupId ?? 'default')),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
setPage(0);
|
|
160
|
+
}, [sourceFilter, statusFilter, selectedGroupIds]);
|
|
161
|
+
|
|
162
|
+
const PAGE_SIZE = 10;
|
|
163
|
+
const totalPages = Math.max(1, Math.ceil(filteredDocuments.length / PAGE_SIZE));
|
|
164
|
+
const safePage = Math.min(page, totalPages - 1);
|
|
165
|
+
const pageDocuments = filteredDocuments.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
|
|
166
|
+
const legacyVideoCount = groupedDocuments.filter(
|
|
167
|
+
(document) =>
|
|
168
|
+
document.source === 'media' &&
|
|
169
|
+
document.metadata?.isGroup &&
|
|
170
|
+
!videoRuntimeScopeFromMetadata(document.metadata),
|
|
171
|
+
).length;
|
|
172
|
+
|
|
173
|
+
const allPageSelected =
|
|
174
|
+
pageDocuments.length > 0 && pageDocuments.every((d) => selectedIds.has(d.id));
|
|
175
|
+
|
|
176
|
+
async function deleteRequest(url: string) {
|
|
177
|
+
const response = await fetch(url, { method: 'DELETE' });
|
|
178
|
+
if (response.ok) return;
|
|
179
|
+
|
|
180
|
+
const body = await response.json().catch(() => ({}));
|
|
181
|
+
throw new Error(body.error ?? 'Could not delete this document.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function del(id: string, doc?: SourceDocument) {
|
|
185
|
+
try {
|
|
186
|
+
if (doc?.metadata?.isGroup && doc?.metadata?.mediaAssetId) {
|
|
187
|
+
await deleteRequest(workspaceUrl(`/api/media?id=${doc.metadata.mediaAssetId}`));
|
|
188
|
+
} else {
|
|
189
|
+
await deleteRequest(workspaceUrl(`/api/documents?id=${id}`));
|
|
190
|
+
}
|
|
191
|
+
setSelectedIds((prev) => {
|
|
192
|
+
const next = new Set(prev);
|
|
193
|
+
next.delete(id);
|
|
194
|
+
return next;
|
|
195
|
+
});
|
|
196
|
+
await onChanged();
|
|
197
|
+
toast.success('Document deleted');
|
|
198
|
+
} catch (error) {
|
|
199
|
+
toast.error(formatErrorMessage(error) || 'Could not delete this document.');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function delSelected() {
|
|
203
|
+
if (selectedIds.size === 0) return;
|
|
204
|
+
const toDelete = groupedDocuments.filter((d) => selectedIds.has(d.id));
|
|
205
|
+
try {
|
|
206
|
+
for (const doc of toDelete) {
|
|
207
|
+
if (doc.metadata?.isGroup && doc.metadata?.mediaAssetId) {
|
|
208
|
+
await deleteRequest(workspaceUrl(`/api/media?id=${doc.metadata.mediaAssetId}`));
|
|
209
|
+
} else {
|
|
210
|
+
await deleteRequest(workspaceUrl(`/api/documents?id=${doc.id}`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
setSelectedIds(new Set());
|
|
214
|
+
await onChanged();
|
|
215
|
+
toast.success(`Deleted ${toDelete.length} item(s)`);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
toast.error(formatErrorMessage(error) || 'Could not delete the selected documents.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function clearAll() {
|
|
221
|
+
try {
|
|
222
|
+
await deleteRequest(workspaceUrl('/api/documents'));
|
|
223
|
+
setSelectedIds(new Set());
|
|
224
|
+
await onChanged();
|
|
225
|
+
toast.success('Corpus and vector index cleared');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
toast.error(formatErrorMessage(error) || 'Could not clear the corpus.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const selectedDocuments = useMemo(
|
|
232
|
+
() => groupedDocuments.filter((document) => selectedIds.has(document.id)),
|
|
233
|
+
[groupedDocuments, selectedIds],
|
|
234
|
+
);
|
|
235
|
+
const moveTargets = groups.filter((group) =>
|
|
236
|
+
selectedDocuments.some((document) => (document.groupId ?? 'default') !== group.id),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
async function moveSelectedToGroup(group: DataGroup) {
|
|
240
|
+
if (selectedDocuments.length === 0 || movingGroupId) return;
|
|
241
|
+
const documentIds = selectedDocuments.flatMap((document) => {
|
|
242
|
+
if (!document.metadata?.isGroup) return [document.id];
|
|
243
|
+
return (document.metadata.childIds ?? []).filter(
|
|
244
|
+
(id: unknown): id is string => typeof id === 'string',
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
const mediaAssetIds = selectedDocuments.flatMap((document) =>
|
|
248
|
+
document.metadata?.isGroup && typeof document.metadata.mediaAssetId === 'string'
|
|
249
|
+
? [document.metadata.mediaAssetId]
|
|
250
|
+
: [],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
setMovingGroupId(group.id);
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetch(workspaceUrl('/api/documents/move'), {
|
|
256
|
+
method: 'PATCH',
|
|
257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
258
|
+
body: JSON.stringify({ documentIds, mediaAssetIds, groupId: group.id }),
|
|
259
|
+
});
|
|
260
|
+
const body = await response.json().catch(() => ({}));
|
|
261
|
+
if (!response.ok) throw new Error(body.error ?? 'Could not move the selected data.');
|
|
262
|
+
setSelectedIds(new Set());
|
|
263
|
+
await onChanged();
|
|
264
|
+
toast.success(
|
|
265
|
+
`Moved ${selectedDocuments.length} source${selectedDocuments.length === 1 ? '' : 's'} to ${group.name}.`,
|
|
266
|
+
);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
toast.error(formatErrorMessage(error) || 'Could not move the selected data.');
|
|
269
|
+
} finally {
|
|
270
|
+
setMovingGroupId(null);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const isAllSelected = selectedGroupIds.size === 0;
|
|
275
|
+
const selectedGroupLabels = useMemo(() => {
|
|
276
|
+
if (isAllSelected) return 'All Groups';
|
|
277
|
+
return Array.from(selectedGroupIds)
|
|
278
|
+
.map((id) => {
|
|
279
|
+
const g = groups.find((g) => g.id === id);
|
|
280
|
+
return g ? (g.icon ? `${g.icon} ${g.name}` : g.name) : id;
|
|
281
|
+
})
|
|
282
|
+
.join(', ');
|
|
283
|
+
}, [selectedGroupIds, groups, isAllSelected]);
|
|
284
|
+
|
|
285
|
+
function toggleGroupFilter(groupId: string) {
|
|
286
|
+
setSelectedGroupIds((prev) => {
|
|
287
|
+
const next = new Set(prev);
|
|
288
|
+
if (next.has(groupId)) next.delete(groupId);
|
|
289
|
+
else next.add(groupId);
|
|
290
|
+
return next;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function clearGroupFilter() {
|
|
295
|
+
setSelectedGroupIds(new Set());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isLoading) {
|
|
299
|
+
return <CorpusSkeleton />;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (documents.length === 0) {
|
|
303
|
+
return (
|
|
304
|
+
<div className="flex flex-col items-center justify-center rounded-lg px-6 py-14 text-center">
|
|
305
|
+
<div className="size-10 p-1.5 bg-white/60 border border-border rounded-md flex items-center justify-center">
|
|
306
|
+
<FileText className="size-7 text-muted-foreground/60" />
|
|
307
|
+
</div>
|
|
308
|
+
<p className="mt-3 text-lg font-medium">Your corpus is empty</p>
|
|
309
|
+
<p className="mt-1 max-w-xl text-sm text-muted-foreground">
|
|
310
|
+
Scrape the web, paste text, or upload files. Everything you collect is stored locally and
|
|
311
|
+
ready to index in the next step.
|
|
312
|
+
</p>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<TooltipProvider>
|
|
319
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center justify-between pb-3">
|
|
320
|
+
<div className="flex items-center gap-3">
|
|
321
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
322
|
+
{[
|
|
323
|
+
{ id: 'all', label: 'All Sources' },
|
|
324
|
+
...Object.entries(SOURCE_META).map(([key, meta]) => ({ id: key, label: meta.label })),
|
|
325
|
+
].map((tab) => {
|
|
326
|
+
const isActive = sourceFilter === tab.id;
|
|
327
|
+
return (
|
|
328
|
+
<button
|
|
329
|
+
key={tab.id}
|
|
330
|
+
type="button"
|
|
331
|
+
onClick={() => setSourceFilter(tab.id as any)}
|
|
332
|
+
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
|
|
333
|
+
isActive
|
|
334
|
+
? 'bg-muted text-foreground'
|
|
335
|
+
: 'border border-border bg-transparent text-muted-foreground hover:bg-muted/50'
|
|
336
|
+
}`}
|
|
337
|
+
>
|
|
338
|
+
{tab.label}
|
|
339
|
+
</button>
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="flex items-center gap-2">
|
|
345
|
+
<Popover open={groupDropdownOpen} onOpenChange={setGroupDropdownOpen}>
|
|
346
|
+
<PopoverTrigger
|
|
347
|
+
render={
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
className={cn(
|
|
351
|
+
'inline-flex h-9 items-center gap-1.5 rounded-md border border-border bg-white px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted/50 shadow-none',
|
|
352
|
+
!isAllSelected && 'border-primary/40 bg-primary/5 text-primary',
|
|
353
|
+
)}
|
|
354
|
+
aria-label="Filter by group"
|
|
355
|
+
>
|
|
356
|
+
<span className="max-w-40 truncate">{selectedGroupLabels}</span>
|
|
357
|
+
<ChevronDown className="size-3 shrink-0 text-muted-foreground" />
|
|
358
|
+
</button>
|
|
359
|
+
}
|
|
360
|
+
/>
|
|
361
|
+
<PopoverContent align="start" className="w-56 p-1.5 shadow-none">
|
|
362
|
+
<div className="space-y-0.5">
|
|
363
|
+
<button
|
|
364
|
+
type="button"
|
|
365
|
+
onClick={clearGroupFilter}
|
|
366
|
+
className={cn(
|
|
367
|
+
'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs transition-colors hover:bg-accent',
|
|
368
|
+
isAllSelected && 'bg-accent/60 font-medium',
|
|
369
|
+
)}
|
|
370
|
+
>
|
|
371
|
+
<div
|
|
372
|
+
className={cn(
|
|
373
|
+
'flex size-4 shrink-0 items-center justify-center rounded border border-primary/40',
|
|
374
|
+
isAllSelected ? 'bg-primary border-primary' : 'bg-white',
|
|
375
|
+
)}
|
|
376
|
+
>
|
|
377
|
+
{isAllSelected && <Check className="size-2.5 text-white" strokeWidth={3} />}
|
|
378
|
+
</div>
|
|
379
|
+
All Groups
|
|
380
|
+
</button>
|
|
381
|
+
{groups.length > 0 && <div className="my-1 border-t border-border" />}
|
|
382
|
+
{groups.map((group) => {
|
|
383
|
+
const isChecked = selectedGroupIds.has(group.id);
|
|
384
|
+
return (
|
|
385
|
+
<button
|
|
386
|
+
key={group.id}
|
|
387
|
+
type="button"
|
|
388
|
+
onClick={() => toggleGroupFilter(group.id)}
|
|
389
|
+
className={cn(
|
|
390
|
+
'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs transition-colors hover:bg-accent',
|
|
391
|
+
isChecked && 'bg-accent/60',
|
|
392
|
+
)}
|
|
393
|
+
>
|
|
394
|
+
<div
|
|
395
|
+
className={cn(
|
|
396
|
+
'flex size-4 shrink-0 items-center justify-center rounded border border-primary/40',
|
|
397
|
+
isChecked ? 'bg-primary border-primary' : 'bg-white',
|
|
398
|
+
)}
|
|
399
|
+
>
|
|
400
|
+
{isChecked && <Check className="size-2.5 text-white" strokeWidth={3} />}
|
|
401
|
+
</div>
|
|
402
|
+
<span className="flex-1 truncate text-left">
|
|
403
|
+
{group.icon ? `${group.icon} ` : ''}
|
|
404
|
+
{group.name}
|
|
405
|
+
</span>
|
|
406
|
+
</button>
|
|
407
|
+
);
|
|
408
|
+
})}
|
|
409
|
+
</div>
|
|
410
|
+
</PopoverContent>
|
|
411
|
+
</Popover>
|
|
412
|
+
|
|
413
|
+
<Select
|
|
414
|
+
value={statusFilter}
|
|
415
|
+
onValueChange={(v) => v && setStatusFilter(v as 'all' | 'indexed' | 'unindexed')}
|
|
416
|
+
>
|
|
417
|
+
<SelectTrigger className="w-29 shadow-none border-border/70 bg-white h-9! text-xs">
|
|
418
|
+
<SelectValue placeholder="All status" />
|
|
419
|
+
</SelectTrigger>
|
|
420
|
+
<SelectContent>
|
|
421
|
+
<SelectItem value="all">All status</SelectItem>
|
|
422
|
+
<SelectItem value="indexed">Indexed</SelectItem>
|
|
423
|
+
<SelectItem value="unindexed">Unindexed</SelectItem>
|
|
424
|
+
</SelectContent>
|
|
425
|
+
</Select>
|
|
426
|
+
{selectedIds.size > 0 && (
|
|
427
|
+
<DropdownMenu>
|
|
428
|
+
<DropdownMenuTrigger
|
|
429
|
+
render={
|
|
430
|
+
<Button
|
|
431
|
+
size="sm"
|
|
432
|
+
variant="secondary"
|
|
433
|
+
className="h-8 gap-1.5 text-xs"
|
|
434
|
+
disabled={movingGroupId !== null}
|
|
435
|
+
>
|
|
436
|
+
<FolderInput className="size-3.5" />
|
|
437
|
+
{movingGroupId ? 'Moving…' : `Actions (${selectedIds.size})`}
|
|
438
|
+
<ChevronDown className="size-3.5" />
|
|
439
|
+
</Button>
|
|
440
|
+
}
|
|
441
|
+
/>
|
|
442
|
+
<DropdownMenuContent align="end" className="min-w-52">
|
|
443
|
+
<DropdownMenuSub>
|
|
444
|
+
<DropdownMenuSubTrigger>
|
|
445
|
+
<FolderInput className="size-3.5" />
|
|
446
|
+
Move to group
|
|
447
|
+
</DropdownMenuSubTrigger>
|
|
448
|
+
<DropdownMenuSubContent className="min-w-52">
|
|
449
|
+
{moveTargets.length > 0 ? (
|
|
450
|
+
moveTargets.map((group) => (
|
|
451
|
+
<DropdownMenuItem
|
|
452
|
+
key={group.id}
|
|
453
|
+
disabled={movingGroupId !== null}
|
|
454
|
+
onClick={() => void moveSelectedToGroup(group)}
|
|
455
|
+
>
|
|
456
|
+
<span className="w-5 text-center">{group.icon || '✦'}</span>
|
|
457
|
+
<span className="truncate">{group.name}</span>
|
|
458
|
+
</DropdownMenuItem>
|
|
459
|
+
))
|
|
460
|
+
) : (
|
|
461
|
+
<DropdownMenuItem disabled>Already in this group</DropdownMenuItem>
|
|
462
|
+
)}
|
|
463
|
+
</DropdownMenuSubContent>
|
|
464
|
+
</DropdownMenuSub>
|
|
465
|
+
<DropdownMenuSeparator />
|
|
466
|
+
<DropdownMenuItem
|
|
467
|
+
variant="destructive"
|
|
468
|
+
disabled={movingGroupId !== null}
|
|
469
|
+
onClick={() => setDeleteTask({ type: 'selected' })}
|
|
470
|
+
>
|
|
471
|
+
<Trash2 className="size-3.5" />
|
|
472
|
+
Delete selected
|
|
473
|
+
</DropdownMenuItem>
|
|
474
|
+
</DropdownMenuContent>
|
|
475
|
+
</DropdownMenu>
|
|
476
|
+
)}
|
|
477
|
+
<TooltipProvider>
|
|
478
|
+
<Tooltip>
|
|
479
|
+
<TooltipTrigger
|
|
480
|
+
render={
|
|
481
|
+
<Button
|
|
482
|
+
size="icon"
|
|
483
|
+
variant="destructive"
|
|
484
|
+
aria-label="Clear all data"
|
|
485
|
+
className="h-9 border border-border text-xs"
|
|
486
|
+
onClick={() => setDeleteTask({ type: 'all' })}
|
|
487
|
+
disabled={isIndexing}
|
|
488
|
+
>
|
|
489
|
+
<Trash2 className="size-3.5" />
|
|
490
|
+
</Button>
|
|
491
|
+
}
|
|
492
|
+
/>
|
|
493
|
+
<TooltipContent>Clear all data</TooltipContent>
|
|
494
|
+
</Tooltip>
|
|
495
|
+
</TooltipProvider>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
{legacyVideoCount > 0 && (
|
|
500
|
+
<div className="mb-3 flex items-start gap-2 rounded-lg border border-border bg-muted/25 px-3 py-2 text-xs text-muted-foreground">
|
|
501
|
+
<Info className="mt-0.5 size-3.5 shrink-0" />
|
|
502
|
+
<span>
|
|
503
|
+
{legacyVideoCount} existing video{legacyVideoCount === 1 ? '' : 's'} will stay available
|
|
504
|
+
until re-indexed. Re-index a video once to link it to{' '}
|
|
505
|
+
{videoRuntimeScopeLabel(activeVideoRuntimeScope)} and enable runtime-specific
|
|
506
|
+
visibility.
|
|
507
|
+
</span>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
512
|
+
<Table>
|
|
513
|
+
<TableHeader>
|
|
514
|
+
<TableRow className="bg-muted/50">
|
|
515
|
+
<TableHead className="w-12">
|
|
516
|
+
<Checkbox
|
|
517
|
+
checked={allPageSelected}
|
|
518
|
+
onCheckedChange={(checked) => {
|
|
519
|
+
if (checked) {
|
|
520
|
+
const next = new Set(selectedIds);
|
|
521
|
+
pageDocuments.forEach((d) => next.add(d.id));
|
|
522
|
+
setSelectedIds(next);
|
|
523
|
+
} else {
|
|
524
|
+
const next = new Set(selectedIds);
|
|
525
|
+
pageDocuments.forEach((d) => next.delete(d.id));
|
|
526
|
+
setSelectedIds(next);
|
|
527
|
+
}
|
|
528
|
+
}}
|
|
529
|
+
aria-label="Select all on page"
|
|
530
|
+
/>
|
|
531
|
+
</TableHead>
|
|
532
|
+
<TableHead>Title</TableHead>
|
|
533
|
+
<TableHead className="w-24">Status</TableHead>
|
|
534
|
+
<TableHead className="w-28">Source</TableHead>
|
|
535
|
+
<TableHead className="w-24 text-right">Chars</TableHead>
|
|
536
|
+
<TableHead className="w-20" />
|
|
537
|
+
</TableRow>
|
|
538
|
+
</TableHeader>
|
|
539
|
+
<TableBody className="bg-white/70">
|
|
540
|
+
{pageDocuments.map((doc) => {
|
|
541
|
+
const meta = SOURCE_META[doc.source as DocumentSource] || {
|
|
542
|
+
label: doc.source || 'Unknown',
|
|
543
|
+
icon: FileText,
|
|
544
|
+
};
|
|
545
|
+
const Icon = meta.icon;
|
|
546
|
+
const indexedRuntimeScope =
|
|
547
|
+
doc.source === 'media' ? videoRuntimeScopeFromMetadata(doc.metadata) : undefined;
|
|
548
|
+
const unavailableInActiveRuntime =
|
|
549
|
+
Boolean(indexedRuntimeScope) && indexedRuntimeScope !== activeVideoRuntimeScope;
|
|
550
|
+
const runtimeLabel = indexedRuntimeScope
|
|
551
|
+
? videoRuntimeScopeLabel(indexedRuntimeScope)
|
|
552
|
+
: undefined;
|
|
553
|
+
const RuntimeIcon = indexedRuntimeScope === 'local' ? Monitor : Cloud;
|
|
554
|
+
return (
|
|
555
|
+
<TableRow
|
|
556
|
+
key={doc.id}
|
|
557
|
+
className={cn(
|
|
558
|
+
'border-border/50 h-10.5! transition-opacity',
|
|
559
|
+
unavailableInActiveRuntime && 'opacity-45 hover:opacity-70',
|
|
560
|
+
)}
|
|
561
|
+
>
|
|
562
|
+
<TableCell>
|
|
563
|
+
<Checkbox
|
|
564
|
+
checked={selectedIds.has(doc.id)}
|
|
565
|
+
onCheckedChange={(checked) => {
|
|
566
|
+
const next = new Set(selectedIds);
|
|
567
|
+
if (checked) next.add(doc.id);
|
|
568
|
+
else next.delete(doc.id);
|
|
569
|
+
setSelectedIds(next);
|
|
570
|
+
}}
|
|
571
|
+
aria-label="Select document"
|
|
572
|
+
/>
|
|
573
|
+
</TableCell>
|
|
574
|
+
<TableCell>
|
|
575
|
+
<button
|
|
576
|
+
type="button"
|
|
577
|
+
onClick={() => !doc.metadata?.isGroup && setActive(doc)}
|
|
578
|
+
className={cn(
|
|
579
|
+
'group flex flex-col items-start text-left',
|
|
580
|
+
doc.metadata?.isGroup ? 'cursor-default' : 'cursor-pointer',
|
|
581
|
+
)}
|
|
582
|
+
>
|
|
583
|
+
<span
|
|
584
|
+
className={cn(
|
|
585
|
+
'line-clamp-1 text-sm font-medium',
|
|
586
|
+
!doc.metadata?.isGroup && 'group-hover:text-primary',
|
|
587
|
+
)}
|
|
588
|
+
>
|
|
589
|
+
{doc.title}
|
|
590
|
+
</span>
|
|
591
|
+
{doc.metadata?.isGroup && (
|
|
592
|
+
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
|
593
|
+
<span className="line-clamp-1">
|
|
594
|
+
Video ({doc.metadata.childIds?.length} chunks)
|
|
595
|
+
</span>
|
|
596
|
+
{unavailableInActiveRuntime && runtimeLabel && (
|
|
597
|
+
<Tooltip>
|
|
598
|
+
<TooltipTrigger
|
|
599
|
+
render={
|
|
600
|
+
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/5 px-1.5 py-0.5 text-[9px] font-medium text-amber-700 dark:text-amber-300">
|
|
601
|
+
<EyeOff className="size-2.5" />
|
|
602
|
+
{indexedRuntimeScope === 'local' ? 'Local only' : 'Cloud only'}
|
|
603
|
+
</span>
|
|
604
|
+
}
|
|
605
|
+
/>
|
|
606
|
+
<TooltipContent className="max-w-64 text-center">
|
|
607
|
+
This video was indexed with {runtimeLabel}. It is not available to
|
|
608
|
+
the agent while {videoRuntimeScopeLabel(activeVideoRuntimeScope)} is
|
|
609
|
+
selected. Switch runtimes or re-index the video.
|
|
610
|
+
</TooltipContent>
|
|
611
|
+
</Tooltip>
|
|
612
|
+
)}
|
|
613
|
+
{!indexedRuntimeScope && (
|
|
614
|
+
<span className="shrink-0 text-[9px] text-muted-foreground/70">
|
|
615
|
+
Legacy index
|
|
616
|
+
</span>
|
|
617
|
+
)}
|
|
618
|
+
</div>
|
|
619
|
+
)}
|
|
620
|
+
{/* {doc.url && (
|
|
621
|
+
<span className="line-clamp-1 text-xs text-muted-foreground">
|
|
622
|
+
{doc.url}
|
|
623
|
+
</span>
|
|
624
|
+
)} */}
|
|
625
|
+
</button>
|
|
626
|
+
</TableCell>
|
|
627
|
+
<TableCell>
|
|
628
|
+
{doc.status === 'indexed' ? (
|
|
629
|
+
<Badge
|
|
630
|
+
variant="outline"
|
|
631
|
+
className="text-green-600 bg-green-50/10 border-green-500 font-normal"
|
|
632
|
+
>
|
|
633
|
+
Indexed
|
|
634
|
+
</Badge>
|
|
635
|
+
) : isIndexing ? (
|
|
636
|
+
<Badge
|
|
637
|
+
variant="outline"
|
|
638
|
+
className="text-blue-500 bg-blue-50 border-blue-200 font-normal flex items-center gap-1.5"
|
|
639
|
+
>
|
|
640
|
+
<Loader2 className="size-3 animate-spin" />
|
|
641
|
+
Indexing
|
|
642
|
+
</Badge>
|
|
643
|
+
) : (
|
|
644
|
+
<Badge
|
|
645
|
+
variant="outline"
|
|
646
|
+
className="text-orange-500 bg-orange-50 border-orange-200 font-normal"
|
|
647
|
+
>
|
|
648
|
+
Unindexed
|
|
649
|
+
</Badge>
|
|
650
|
+
)}
|
|
651
|
+
</TableCell>
|
|
652
|
+
<TableCell>
|
|
653
|
+
<div className="flex items-center gap-1.5">
|
|
654
|
+
<Badge variant="secondary" className="gap-1 font-normal">
|
|
655
|
+
<Icon className="size-3" />
|
|
656
|
+
{meta.label}
|
|
657
|
+
</Badge>
|
|
658
|
+
{doc.metadata?.isGroup &&
|
|
659
|
+
indexedRuntimeScope &&
|
|
660
|
+
!unavailableInActiveRuntime && (
|
|
661
|
+
<span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
662
|
+
<RuntimeIcon className="size-2.5" />
|
|
663
|
+
{indexedRuntimeScope === 'local' ? 'Local' : 'Cloud'}
|
|
664
|
+
</span>
|
|
665
|
+
)}
|
|
666
|
+
</div>
|
|
667
|
+
</TableCell>
|
|
668
|
+
<TableCell className="text-right text-xs text-muted-foreground tabular-nums">
|
|
669
|
+
{doc.charCount.toLocaleString()}
|
|
670
|
+
</TableCell>
|
|
671
|
+
<TableCell>
|
|
672
|
+
<div className="flex items-center justify-end gap-4">
|
|
673
|
+
{!doc.metadata?.isGroup && (
|
|
674
|
+
<button
|
|
675
|
+
type="button"
|
|
676
|
+
aria-label="Edit document"
|
|
677
|
+
onClick={() => setActive(doc)}
|
|
678
|
+
className="text-muted-foreground transition-colors hover:text-primary"
|
|
679
|
+
>
|
|
680
|
+
<Pencil className="size-4" />
|
|
681
|
+
</button>
|
|
682
|
+
)}
|
|
683
|
+
{doc.metadata?.isGroup && (
|
|
684
|
+
<Tooltip>
|
|
685
|
+
<TooltipTrigger
|
|
686
|
+
render={
|
|
687
|
+
<button
|
|
688
|
+
type="button"
|
|
689
|
+
aria-label="View indexed video evidence"
|
|
690
|
+
onClick={() => setInspectorDoc(doc)}
|
|
691
|
+
className="text-muted-foreground transition-colors hover:text-primary"
|
|
692
|
+
>
|
|
693
|
+
<Eye className="size-4" />
|
|
694
|
+
</button>
|
|
695
|
+
}
|
|
696
|
+
/>
|
|
697
|
+
<TooltipContent>View indexed video evidence</TooltipContent>
|
|
698
|
+
</Tooltip>
|
|
699
|
+
)}
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
aria-label="Delete document"
|
|
703
|
+
onClick={() => setDeleteTask({ type: 'single', doc })}
|
|
704
|
+
className="text-muted-foreground transition-colors hover:text-destructive"
|
|
705
|
+
>
|
|
706
|
+
<Trash2 className="size-4" />
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
709
|
+
</TableCell>
|
|
710
|
+
</TableRow>
|
|
711
|
+
);
|
|
712
|
+
})}
|
|
713
|
+
</TableBody>
|
|
714
|
+
</Table>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
{/* Pagination */}
|
|
718
|
+
{totalPages > 1 && (
|
|
719
|
+
<div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
|
|
720
|
+
<span>
|
|
721
|
+
{filteredDocuments.length === 0 ? 0 : safePage * PAGE_SIZE + 1}–
|
|
722
|
+
{Math.min((safePage + 1) * PAGE_SIZE, filteredDocuments.length)} of{' '}
|
|
723
|
+
{filteredDocuments.length}
|
|
724
|
+
</span>
|
|
725
|
+
<div className="flex items-center gap-1">
|
|
726
|
+
<button
|
|
727
|
+
disabled={safePage === 0}
|
|
728
|
+
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
729
|
+
className="rounded p-1 hover:bg-muted disabled:opacity-40 transition-colors cursor-pointer"
|
|
730
|
+
>
|
|
731
|
+
<ChevronLeft className="size-3.5" />
|
|
732
|
+
</button>
|
|
733
|
+
<span className="tabular-nums">
|
|
734
|
+
{safePage + 1} / {totalPages}
|
|
735
|
+
</span>
|
|
736
|
+
<button
|
|
737
|
+
disabled={safePage >= totalPages - 1}
|
|
738
|
+
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
|
739
|
+
className="rounded p-1 hover:bg-muted disabled:opacity-40 transition-colors cursor-pointer"
|
|
740
|
+
>
|
|
741
|
+
<ChevronRight className="size-3.5" />
|
|
742
|
+
</button>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
)}
|
|
746
|
+
|
|
747
|
+
<DocumentDialog
|
|
748
|
+
doc={active}
|
|
749
|
+
onOpenChange={(o) => !o && setActive(null)}
|
|
750
|
+
onSaved={() => {
|
|
751
|
+
setActive(null);
|
|
752
|
+
onChanged();
|
|
753
|
+
}}
|
|
754
|
+
onDelete={(id) => {
|
|
755
|
+
if (active) {
|
|
756
|
+
setDeleteTask({ type: 'single', doc: active });
|
|
757
|
+
}
|
|
758
|
+
}}
|
|
759
|
+
/>
|
|
760
|
+
|
|
761
|
+
<VideoKnowledgeInspector
|
|
762
|
+
doc={inspectorDoc}
|
|
763
|
+
allDocuments={documents}
|
|
764
|
+
onClose={() => setInspectorDoc(null)}
|
|
765
|
+
/>
|
|
766
|
+
|
|
767
|
+
<GenericAlert
|
|
768
|
+
open={deleteTask !== null}
|
|
769
|
+
onOpenChange={(open) => {
|
|
770
|
+
if (!open) setDeleteTask(null);
|
|
771
|
+
}}
|
|
772
|
+
title={
|
|
773
|
+
deleteTask?.type === 'all'
|
|
774
|
+
? 'Clear corpus'
|
|
775
|
+
: deleteTask?.type === 'selected'
|
|
776
|
+
? 'Delete selected documents'
|
|
777
|
+
: 'Delete document'
|
|
778
|
+
}
|
|
779
|
+
description={
|
|
780
|
+
deleteTask?.type === 'all' ? (
|
|
781
|
+
'Delete every uploaded file and document from this knowledge base? This also clears the vector index and cannot be undone.'
|
|
782
|
+
) : deleteTask?.type === 'selected' ? (
|
|
783
|
+
`Are you sure you want to delete ${selectedIds.size} selected documents?`
|
|
784
|
+
) : (
|
|
785
|
+
<span className="wrap-break-word">
|
|
786
|
+
Are you sure you want to delete "
|
|
787
|
+
<span className="break-all font-medium">
|
|
788
|
+
{deleteTask?.type === 'single' ? deleteTask.doc.title : ''}
|
|
789
|
+
</span>
|
|
790
|
+
"?
|
|
791
|
+
</span>
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
actionText="Delete"
|
|
795
|
+
variant="destructive"
|
|
796
|
+
onAction={() => {
|
|
797
|
+
if (deleteTask?.type === 'all') {
|
|
798
|
+
void clearAll();
|
|
799
|
+
} else if (deleteTask?.type === 'selected') {
|
|
800
|
+
void delSelected();
|
|
801
|
+
} else if (deleteTask?.type === 'single') {
|
|
802
|
+
void del(deleteTask.doc.id, deleteTask.doc);
|
|
803
|
+
if (active?.id === deleteTask.doc.id) {
|
|
804
|
+
setActive(null);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}}
|
|
808
|
+
/>
|
|
809
|
+
</TooltipProvider>
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function VideoKnowledgeInspector({
|
|
814
|
+
doc,
|
|
815
|
+
allDocuments,
|
|
816
|
+
onClose,
|
|
817
|
+
}: {
|
|
818
|
+
doc: SourceDocument | null;
|
|
819
|
+
allDocuments: SourceDocument[];
|
|
820
|
+
onClose: () => void;
|
|
821
|
+
}) {
|
|
822
|
+
if (!doc || !doc.metadata?.isGroup) return null;
|
|
823
|
+
|
|
824
|
+
const childIds: string[] = doc.metadata?.childIds ?? [];
|
|
825
|
+
const children = childIds
|
|
826
|
+
.map((id: string) => allDocuments.find((d) => d.id === id))
|
|
827
|
+
.filter((d): d is SourceDocument => d !== undefined)
|
|
828
|
+
.sort((a, b) => {
|
|
829
|
+
const aStart = a.metadata?.startSecs ?? 0;
|
|
830
|
+
const bStart = b.metadata?.startSecs ?? 0;
|
|
831
|
+
return aStart - bStart;
|
|
832
|
+
});
|
|
833
|
+
const summaryDocument = children.find((child) => child.metadata?.isMediaSummary);
|
|
834
|
+
const mediaUrl = summaryDocument?.url || doc.url;
|
|
835
|
+
const previewUrl = mediaUrl ? workspaceUrl(mediaUrl) : undefined;
|
|
836
|
+
const mediaType = summaryDocument?.metadata?.mediaType || doc.metadata?.mediaType;
|
|
837
|
+
|
|
838
|
+
return (
|
|
839
|
+
<Dialog open={!!doc} onOpenChange={(open) => !open && onClose()}>
|
|
840
|
+
<DialogContent className="w-full pb-3 max-w-5xl! max-h-[90%]! h-full!">
|
|
841
|
+
<DialogHeader>
|
|
842
|
+
<DialogTitle className="flex items-center gap-2">
|
|
843
|
+
<Film className="size-4 text-muted-foreground" />
|
|
844
|
+
{doc.title}
|
|
845
|
+
</DialogTitle>
|
|
846
|
+
<DialogDescription>
|
|
847
|
+
{children.length} indexed segment{children.length !== 1 ? 's' : ''} ·{' '}
|
|
848
|
+
{doc.charCount.toLocaleString()} total characters
|
|
849
|
+
</DialogDescription>
|
|
850
|
+
</DialogHeader>
|
|
851
|
+
{/* {previewUrl && (mediaType === 'video' || mediaType === 'audio') && (
|
|
852
|
+
<div className="overflow-hidden rounded-lg border border-border bg-black/95">
|
|
853
|
+
{mediaType === 'video' ? (
|
|
854
|
+
<video
|
|
855
|
+
src={previewUrl}
|
|
856
|
+
controls
|
|
857
|
+
playsInline
|
|
858
|
+
preload="metadata"
|
|
859
|
+
className="max-h-[42vh] w-full object-contain"
|
|
860
|
+
/>
|
|
861
|
+
) : (
|
|
862
|
+
<audio src={previewUrl} controls preload="metadata" className="w-full" />
|
|
863
|
+
)}
|
|
864
|
+
</div>
|
|
865
|
+
)} */}
|
|
866
|
+
<ScrollArea className={previewUrl ? 'max-h-[70vh] h-full' : 'max-h-[70vh]'}>
|
|
867
|
+
<div className="space-y-4 pr-4">
|
|
868
|
+
{children.map((child, index) => {
|
|
869
|
+
const startSecs = child.metadata?.startSecs;
|
|
870
|
+
const hasTimestamp = typeof startSecs === 'number';
|
|
871
|
+
return (
|
|
872
|
+
<div
|
|
873
|
+
key={child.id}
|
|
874
|
+
className="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-1.5"
|
|
875
|
+
>
|
|
876
|
+
<div className="flex items-center justify-between">
|
|
877
|
+
<span className="text-[11px] font-medium text-muted-foreground">
|
|
878
|
+
{hasTimestamp
|
|
879
|
+
? `${Math.floor(startSecs / 60)}:${String(
|
|
880
|
+
Math.floor(startSecs % 60),
|
|
881
|
+
).padStart(2, '0')}`
|
|
882
|
+
: `Segment ${index + 1}`}
|
|
883
|
+
</span>
|
|
884
|
+
<span className="text-[10px] text-muted-foreground/60 tabular-nums">
|
|
885
|
+
{child.charCount.toLocaleString()} chars
|
|
886
|
+
</span>
|
|
887
|
+
</div>
|
|
888
|
+
<pre className="whitespace-pre-wrap wrap-break-word text-xs leading-relaxed text-foreground font-mono">
|
|
889
|
+
{child.content}
|
|
890
|
+
</pre>
|
|
891
|
+
</div>
|
|
892
|
+
);
|
|
893
|
+
})}
|
|
894
|
+
</div>
|
|
895
|
+
</ScrollArea>
|
|
896
|
+
<DialogFooter className="h-full">
|
|
897
|
+
<Button variant="outline" onClick={onClose} className={''}>
|
|
898
|
+
Close
|
|
899
|
+
</Button>
|
|
900
|
+
</DialogFooter>
|
|
901
|
+
</DialogContent>
|
|
902
|
+
</Dialog>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function DocumentDialog({
|
|
907
|
+
doc,
|
|
908
|
+
onOpenChange,
|
|
909
|
+
onSaved,
|
|
910
|
+
onDelete,
|
|
911
|
+
}: {
|
|
912
|
+
doc: SourceDocument | null;
|
|
913
|
+
onOpenChange: (open: boolean) => void;
|
|
914
|
+
onSaved: () => void;
|
|
915
|
+
onDelete: (id: string) => void;
|
|
916
|
+
}) {
|
|
917
|
+
const [editing, setEditing] = useState(false);
|
|
918
|
+
const [title, setTitle] = useState('');
|
|
919
|
+
const [url, setUrl] = useState('');
|
|
920
|
+
const [content, setContent] = useState('');
|
|
921
|
+
const [saving, setSaving] = useState(false);
|
|
922
|
+
const [uploadingMedia, setUploadingMedia] = useState(false);
|
|
923
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
924
|
+
|
|
925
|
+
useEffect(() => {
|
|
926
|
+
if (doc) {
|
|
927
|
+
setEditing(false);
|
|
928
|
+
setTitle(doc.title);
|
|
929
|
+
setUrl(doc.url ?? '');
|
|
930
|
+
setContent(doc.content);
|
|
931
|
+
}
|
|
932
|
+
}, [doc]);
|
|
933
|
+
|
|
934
|
+
async function save() {
|
|
935
|
+
if (!doc) return;
|
|
936
|
+
if (!content.trim()) {
|
|
937
|
+
toast.error("Content can't be empty.");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
setSaving(true);
|
|
941
|
+
try {
|
|
942
|
+
const res = await fetch(workspaceUrl('/api/documents'), {
|
|
943
|
+
method: 'PATCH',
|
|
944
|
+
headers: { 'Content-Type': 'application/json' },
|
|
945
|
+
body: JSON.stringify({
|
|
946
|
+
id: doc.id,
|
|
947
|
+
title: title.trim() || 'Untitled',
|
|
948
|
+
url: url.trim(),
|
|
949
|
+
content,
|
|
950
|
+
}),
|
|
951
|
+
});
|
|
952
|
+
if (!res.ok) {
|
|
953
|
+
const body = await res.json().catch(() => ({}));
|
|
954
|
+
toast.error(
|
|
955
|
+
body.error ? formatErrorMessage(new Error(body.error)) : 'Failed to save document.',
|
|
956
|
+
);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
toast.success('Document saved');
|
|
960
|
+
onSaved();
|
|
961
|
+
} catch (err) {
|
|
962
|
+
toast.error(formatErrorMessage(err) || 'Could not reach the server.');
|
|
963
|
+
} finally {
|
|
964
|
+
setSaving(false);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
|
969
|
+
const file = e.target.files?.[0];
|
|
970
|
+
if (!file) return;
|
|
971
|
+
|
|
972
|
+
setUploadingMedia(true);
|
|
973
|
+
try {
|
|
974
|
+
const formData = new FormData();
|
|
975
|
+
formData.append('file', file);
|
|
976
|
+
|
|
977
|
+
const res = await fetch(workspaceUrl('/api/media'), {
|
|
978
|
+
method: 'POST',
|
|
979
|
+
body: formData,
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (!res.ok) {
|
|
983
|
+
const err = await res.json().catch(() => ({}));
|
|
984
|
+
throw new Error(err.error || 'Failed to upload file');
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const data = await res.json();
|
|
988
|
+
if (data.assets && data.assets.length > 0) {
|
|
989
|
+
setUrl(workspaceUrl(`/api/media/${data.assets[0].id}`));
|
|
990
|
+
toast.success('File uploaded and URL updated');
|
|
991
|
+
}
|
|
992
|
+
} catch (err) {
|
|
993
|
+
toast.error(formatErrorMessage(err) || 'Failed to upload');
|
|
994
|
+
} finally {
|
|
995
|
+
setUploadingMedia(false);
|
|
996
|
+
if (fileInputRef.current) {
|
|
997
|
+
fileInputRef.current.value = '';
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<Dialog open={!!doc} onOpenChange={onOpenChange}>
|
|
1004
|
+
<DialogContent className="w-full max-w-5xl! ">
|
|
1005
|
+
<DialogHeader>
|
|
1006
|
+
<DialogTitle className="text-balance">
|
|
1007
|
+
{editing ? 'Edit document' : doc?.title}
|
|
1008
|
+
</DialogTitle>
|
|
1009
|
+
{!editing && doc?.url && (
|
|
1010
|
+
<DialogDescription className="break-all">{doc.url}</DialogDescription>
|
|
1011
|
+
)}
|
|
1012
|
+
{editing && (
|
|
1013
|
+
<DialogDescription>
|
|
1014
|
+
Changes are saved to this server's corpus. Re-index to apply them to search.
|
|
1015
|
+
</DialogDescription>
|
|
1016
|
+
)}
|
|
1017
|
+
</DialogHeader>
|
|
1018
|
+
|
|
1019
|
+
{editing ? (
|
|
1020
|
+
<div
|
|
1021
|
+
className="grid gap-4 overflow-y-auto pr-2 pb-2 "
|
|
1022
|
+
style={{ maxHeight: 'calc(80vh - 150px)' }}
|
|
1023
|
+
>
|
|
1024
|
+
<div className="grid gap-2">
|
|
1025
|
+
<Label htmlFor="doc-title">Title</Label>
|
|
1026
|
+
<Input
|
|
1027
|
+
id="doc-title"
|
|
1028
|
+
value={title}
|
|
1029
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
1030
|
+
placeholder="Untitled"
|
|
1031
|
+
/>
|
|
1032
|
+
</div>
|
|
1033
|
+
<div className="grid gap-2">
|
|
1034
|
+
<div className="flex items-center justify-between">
|
|
1035
|
+
<Label htmlFor="doc-url">URL (optional)</Label>
|
|
1036
|
+
<Button
|
|
1037
|
+
type="button"
|
|
1038
|
+
variant="ghost"
|
|
1039
|
+
size="sm"
|
|
1040
|
+
className="h-6 text-[10px] uppercase font-medium text-primary hover:bg-primary/10"
|
|
1041
|
+
onClick={() => fileInputRef.current?.click()}
|
|
1042
|
+
disabled={uploadingMedia}
|
|
1043
|
+
>
|
|
1044
|
+
{uploadingMedia ? (
|
|
1045
|
+
<Loader2 className="size-3 mr-1 animate-spin" />
|
|
1046
|
+
) : (
|
|
1047
|
+
<Upload className="size-3 mr-1" />
|
|
1048
|
+
)}
|
|
1049
|
+
Upload File
|
|
1050
|
+
</Button>
|
|
1051
|
+
<input
|
|
1052
|
+
type="file"
|
|
1053
|
+
ref={fileInputRef}
|
|
1054
|
+
className="hidden"
|
|
1055
|
+
onChange={handleFileUpload}
|
|
1056
|
+
accept="image/*,video/*,audio/*"
|
|
1057
|
+
/>
|
|
1058
|
+
</div>
|
|
1059
|
+
<Input
|
|
1060
|
+
id="doc-url"
|
|
1061
|
+
value={url}
|
|
1062
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
1063
|
+
placeholder="https://…"
|
|
1064
|
+
/>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div className="grid gap-2">
|
|
1067
|
+
<div className="flex items-center justify-between">
|
|
1068
|
+
<Label htmlFor="doc-content">Content</Label>
|
|
1069
|
+
<span className="text-xs text-muted-foreground tabular-nums ">
|
|
1070
|
+
{content.length.toLocaleString()} chars
|
|
1071
|
+
</span>
|
|
1072
|
+
</div>
|
|
1073
|
+
<Textarea
|
|
1074
|
+
id="doc-content"
|
|
1075
|
+
value={content}
|
|
1076
|
+
onChange={(e) => setContent(e.target.value)}
|
|
1077
|
+
rows={14}
|
|
1078
|
+
className="resize-none font-mono text-xs leading-relaxed"
|
|
1079
|
+
/>
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
) : (
|
|
1083
|
+
<div className="flex flex-col gap-4">
|
|
1084
|
+
{doc?.source === 'media' && doc?.url && (
|
|
1085
|
+
<div className="rounded-md border border-border bg-muted/30 overflow-hidden flex items-center justify-center p-4">
|
|
1086
|
+
{doc.metadata?.mediaType === 'video' ? (
|
|
1087
|
+
<video src={doc.url} controls className="max-h-[40vh] max-w-full" />
|
|
1088
|
+
) : doc.metadata?.mediaType === 'audio' ? (
|
|
1089
|
+
<audio src={doc.url} controls className="w-full" />
|
|
1090
|
+
) : (
|
|
1091
|
+
<img
|
|
1092
|
+
src={doc.url}
|
|
1093
|
+
alt={doc.title}
|
|
1094
|
+
className="max-h-[40vh] max-w-full object-contain"
|
|
1095
|
+
/>
|
|
1096
|
+
)}
|
|
1097
|
+
</div>
|
|
1098
|
+
)}
|
|
1099
|
+
<ScrollArea className="max-h-[60vh] rounded-md border border-border bg-muted/30">
|
|
1100
|
+
<pre
|
|
1101
|
+
className={cn(
|
|
1102
|
+
'whitespace-pre-wrap break-all px-4 py-3 font-mono text-xs leading-relaxed text-foreground',
|
|
1103
|
+
)}
|
|
1104
|
+
>
|
|
1105
|
+
{doc?.content}
|
|
1106
|
+
</pre>
|
|
1107
|
+
</ScrollArea>
|
|
1108
|
+
</div>
|
|
1109
|
+
)}
|
|
1110
|
+
|
|
1111
|
+
<DialogFooter className="flex-row items-center justify-between gap-2 sm:justify-between">
|
|
1112
|
+
{editing ? (
|
|
1113
|
+
<>
|
|
1114
|
+
<Button variant="ghost" onClick={() => setEditing(false)} disabled={saving}>
|
|
1115
|
+
<X className="size-4" />
|
|
1116
|
+
Cancel
|
|
1117
|
+
</Button>
|
|
1118
|
+
<Button onClick={() => void save()} disabled={saving}>
|
|
1119
|
+
{saving && <Loader2 className="size-4 animate-spin" />}
|
|
1120
|
+
Save changes
|
|
1121
|
+
</Button>
|
|
1122
|
+
</>
|
|
1123
|
+
) : (
|
|
1124
|
+
<>
|
|
1125
|
+
<Button
|
|
1126
|
+
variant="destructive"
|
|
1127
|
+
className="text-white hover:text-white hover:bg-destructive/80"
|
|
1128
|
+
onClick={() => doc && onDelete(doc.id)}
|
|
1129
|
+
>
|
|
1130
|
+
<Trash2 className="size-4 mr-2" />
|
|
1131
|
+
Delete
|
|
1132
|
+
</Button>
|
|
1133
|
+
<Button onClick={() => setEditing(true)}>
|
|
1134
|
+
<Pencil className="size-4 mr-2" />
|
|
1135
|
+
Edit
|
|
1136
|
+
</Button>
|
|
1137
|
+
</>
|
|
1138
|
+
)}
|
|
1139
|
+
</DialogFooter>
|
|
1140
|
+
</DialogContent>
|
|
1141
|
+
</Dialog>
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function CorpusSkeleton() {
|
|
1146
|
+
return (
|
|
1147
|
+
<div className="space-y-4 animate-in fade-in duration-150">
|
|
1148
|
+
{/* Filter bar skeleton */}
|
|
1149
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center justify-between pb-3">
|
|
1150
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1151
|
+
{['All Sources', 'Website', 'Text', 'Files', 'Media', 'Integrations'].map((label, i) => (
|
|
1152
|
+
<div
|
|
1153
|
+
key={label}
|
|
1154
|
+
className={cn(
|
|
1155
|
+
'h-7 rounded-full px-3 text-xs font-medium flex items-center',
|
|
1156
|
+
i === 0 ? 'bg-muted/80 text-transparent w-22' : 'bg-muted/40 text-transparent w-16',
|
|
1157
|
+
'animate-pulse',
|
|
1158
|
+
)}
|
|
1159
|
+
>
|
|
1160
|
+
|
|
1161
|
+
</div>
|
|
1162
|
+
))}
|
|
1163
|
+
</div>
|
|
1164
|
+
<div className="flex items-center gap-2">
|
|
1165
|
+
<div className="h-9 w-32 rounded-md bg-muted/50 animate-pulse border border-border/50" />
|
|
1166
|
+
<div className="h-9 w-28 rounded-md bg-muted/50 animate-pulse border border-border/50" />
|
|
1167
|
+
<div className="size-9 rounded-md bg-muted/50 animate-pulse border border-border/50" />
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
|
|
1171
|
+
{/* Table skeleton */}
|
|
1172
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
1173
|
+
<Table>
|
|
1174
|
+
<TableHeader>
|
|
1175
|
+
<TableRow className="bg-muted/50">
|
|
1176
|
+
<TableHead className="w-12">
|
|
1177
|
+
<div className="size-4 rounded bg-muted/70 animate-pulse" />
|
|
1178
|
+
</TableHead>
|
|
1179
|
+
<TableHead>Title</TableHead>
|
|
1180
|
+
<TableHead className="w-24">Status</TableHead>
|
|
1181
|
+
<TableHead className="w-28">Source</TableHead>
|
|
1182
|
+
<TableHead className="w-24 text-right">Chars</TableHead>
|
|
1183
|
+
<TableHead className="w-20" />
|
|
1184
|
+
</TableRow>
|
|
1185
|
+
</TableHeader>
|
|
1186
|
+
<TableBody className="bg-card">
|
|
1187
|
+
{Array.from({ length: 5 }).map((_, index) => (
|
|
1188
|
+
<TableRow key={index} className="border-t border-border/60">
|
|
1189
|
+
<TableCell className="w-12 py-3.5">
|
|
1190
|
+
<div className="size-4 rounded bg-muted/60 animate-pulse" />
|
|
1191
|
+
</TableCell>
|
|
1192
|
+
<TableCell className="py-3.5">
|
|
1193
|
+
<div className="space-y-1.5">
|
|
1194
|
+
<div
|
|
1195
|
+
className="h-4 rounded bg-muted/80 animate-pulse"
|
|
1196
|
+
style={{ width: `${Math.max(35, 75 - index * 10)}%` }}
|
|
1197
|
+
/>
|
|
1198
|
+
<div className="h-3 w-40 rounded bg-muted/40 animate-pulse" />
|
|
1199
|
+
</div>
|
|
1200
|
+
</TableCell>
|
|
1201
|
+
<TableCell className="w-24 py-3.5">
|
|
1202
|
+
<div className="h-5 w-16 rounded-full bg-muted/60 animate-pulse" />
|
|
1203
|
+
</TableCell>
|
|
1204
|
+
<TableCell className="w-28 py-3.5">
|
|
1205
|
+
<div className="flex items-center gap-2">
|
|
1206
|
+
<div className="size-4 rounded bg-muted/60 animate-pulse" />
|
|
1207
|
+
<div className="h-3 w-14 rounded bg-muted/60 animate-pulse" />
|
|
1208
|
+
</div>
|
|
1209
|
+
</TableCell>
|
|
1210
|
+
<TableCell className="w-24 py-3.5 text-right">
|
|
1211
|
+
<div className="h-3 w-12 ml-auto rounded bg-muted/60 animate-pulse" />
|
|
1212
|
+
</TableCell>
|
|
1213
|
+
<TableCell className="w-20 py-3.5 text-right">
|
|
1214
|
+
<div className="size-6 ml-auto rounded bg-muted/40 animate-pulse" />
|
|
1215
|
+
</TableCell>
|
|
1216
|
+
</TableRow>
|
|
1217
|
+
))}
|
|
1218
|
+
</TableBody>
|
|
1219
|
+
</Table>
|
|
1220
|
+
</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
);
|
|
1223
|
+
}
|