@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,1290 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { workspaceStorageKey, workspaceUrl, workspaceFetch as fetch } from '../../host';
|
|
4
|
+
import { useMemo, useState, useEffect, useRef } from 'react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import {
|
|
7
|
+
RECENT_ADD_HISTORY_CLEARED_EVENT,
|
|
8
|
+
SCRAPE_RECENT_QUERIES_STORAGE_KEY,
|
|
9
|
+
} from '../../lib/browser-cache';
|
|
10
|
+
import { formatErrorMessage } from '../../lib/shared/error-formatter';
|
|
11
|
+
import {
|
|
12
|
+
Globe,
|
|
13
|
+
Loader2,
|
|
14
|
+
Search,
|
|
15
|
+
ChevronDown,
|
|
16
|
+
ChevronsDown,
|
|
17
|
+
Info,
|
|
18
|
+
Clock,
|
|
19
|
+
X,
|
|
20
|
+
Trash2,
|
|
21
|
+
Settings,
|
|
22
|
+
Plus,
|
|
23
|
+
ChevronLeft,
|
|
24
|
+
ChevronRight,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { useScrapeStore, type SearchState } from '../../store/scrape-store';
|
|
27
|
+
import type { CrawlJob, CrawlScope, SearchResultItem } from '@larkup/core/types';
|
|
28
|
+
import { Button } from '@larkup/workspace-ui/button';
|
|
29
|
+
import { Input } from '../ui/input';
|
|
30
|
+
import { Label } from '@larkup/workspace-ui/label';
|
|
31
|
+
import { Checkbox } from '../ui/checkbox';
|
|
32
|
+
|
|
33
|
+
import { Switch } from '../ui/switch';
|
|
34
|
+
import {
|
|
35
|
+
Select,
|
|
36
|
+
SelectContent,
|
|
37
|
+
SelectItem,
|
|
38
|
+
SelectTrigger,
|
|
39
|
+
SelectValue,
|
|
40
|
+
} from '@larkup/workspace-ui/select';
|
|
41
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
42
|
+
import { cn } from '../../lib/utils';
|
|
43
|
+
import useSWR from 'swr';
|
|
44
|
+
import { useRouter } from 'next/navigation';
|
|
45
|
+
import type { RagConfig } from '@larkup/core/types';
|
|
46
|
+
import type { DataPrimaryAction } from './data-primary-action';
|
|
47
|
+
import { DataEntryModeSwitch } from './data-entry-mode-switch';
|
|
48
|
+
|
|
49
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json() as Promise<{ config: RagConfig }>);
|
|
50
|
+
|
|
51
|
+
type LocalCrawlerState = {
|
|
52
|
+
running: boolean;
|
|
53
|
+
mode?: 'native' | 'firecrawl';
|
|
54
|
+
lastError?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type LocalCrawlerResponse = {
|
|
58
|
+
state: LocalCrawlerState;
|
|
59
|
+
starting?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function domainOf(url: string) {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(url).hostname;
|
|
65
|
+
} catch {
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatDuration(seconds: number): string {
|
|
71
|
+
if (seconds < 60) return `~${Math.ceil(seconds)}s`;
|
|
72
|
+
if (seconds < 3600) return `~${Math.ceil(seconds / 60)}min`;
|
|
73
|
+
const h = Math.floor(seconds / 3600);
|
|
74
|
+
const m = Math.ceil((seconds % 3600) / 60);
|
|
75
|
+
return `~${h}h ${m}min`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Estimate ETL duration based on URL count, scope, and page limit */
|
|
79
|
+
function estimateEtlDuration(
|
|
80
|
+
urlCount: number,
|
|
81
|
+
scope: CrawlScope,
|
|
82
|
+
pageLimit: number,
|
|
83
|
+
): { totalPages: number; estimatedSeconds: number } {
|
|
84
|
+
const pagesPerUrl = scope === 'domain' ? pageLimit : 1;
|
|
85
|
+
const totalPages = urlCount * pagesPerUrl;
|
|
86
|
+
const perPageSeconds = scope === 'domain' ? 5 : 3;
|
|
87
|
+
const estimatedSeconds = totalPages * perPageSeconds;
|
|
88
|
+
return { totalPages, estimatedSeconds };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function ScrapePanel({
|
|
92
|
+
disabled,
|
|
93
|
+
onStarted,
|
|
94
|
+
onActionChange,
|
|
95
|
+
groupId,
|
|
96
|
+
}: {
|
|
97
|
+
disabled?: boolean;
|
|
98
|
+
onStarted: (job: CrawlJob) => void;
|
|
99
|
+
onActionChange?: (action: DataPrimaryAction | null) => void;
|
|
100
|
+
groupId?: string;
|
|
101
|
+
}) {
|
|
102
|
+
const groupIdRef = useRef(groupId ?? 'default');
|
|
103
|
+
groupIdRef.current = groupId ?? 'default';
|
|
104
|
+
const router = useRouter();
|
|
105
|
+
const {
|
|
106
|
+
query,
|
|
107
|
+
setQuery,
|
|
108
|
+
searchState,
|
|
109
|
+
setSearchState,
|
|
110
|
+
selected,
|
|
111
|
+
setSelected,
|
|
112
|
+
scope,
|
|
113
|
+
setScope,
|
|
114
|
+
pageLimit,
|
|
115
|
+
setPageLimit,
|
|
116
|
+
searchLimit,
|
|
117
|
+
setSearchLimit,
|
|
118
|
+
showAdvanced,
|
|
119
|
+
setShowAdvanced,
|
|
120
|
+
serperTotalForQuery,
|
|
121
|
+
setSerperTotalForQuery,
|
|
122
|
+
flush: flushSearch,
|
|
123
|
+
} = useScrapeStore();
|
|
124
|
+
|
|
125
|
+
const [searching, setSearching] = useState(false);
|
|
126
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
127
|
+
const [gatheringAll, setGatheringAll] = useState(false);
|
|
128
|
+
const [gatherProgress, setGatherProgress] = useState(0);
|
|
129
|
+
const [manualUrl, setManualUrl] = useState('');
|
|
130
|
+
const [manualUrlList, setManualUrlList] = useState<string[]>([]);
|
|
131
|
+
|
|
132
|
+
// Search results can contain private source URLs. Clear the module-level
|
|
133
|
+
// store whenever this project-scoped panel unmounts so a project switch
|
|
134
|
+
// cannot briefly render another project's search state.
|
|
135
|
+
useEffect(() => () => flushSearch(), [flushSearch]);
|
|
136
|
+
const [starting, setStarting] = useState(false);
|
|
137
|
+
/** When true: only the exact custom URLs are scraped — no deep crawl/pagination */
|
|
138
|
+
const [specificUrls, setSpecificUrls] = useState(false);
|
|
139
|
+
const [firecrawlConfigured, setFirecrawlConfigured] = useState<boolean | null>(null);
|
|
140
|
+
const [crawlerState, setCrawlerState] = useState<LocalCrawlerState | null>(null);
|
|
141
|
+
const [crawlerStarting, setCrawlerStarting] = useState(false);
|
|
142
|
+
const crawlerPolling = useRef<Promise<boolean> | null>(null);
|
|
143
|
+
const { data: configData } = useSWR(workspaceUrl('/api/config'), fetcher);
|
|
144
|
+
const activeProvider = configData?.config?.webSearchProvider || 'tavily';
|
|
145
|
+
const crawlerProvider = configData?.config?.webCrawlerProvider || 'local';
|
|
146
|
+
const [cachedQueries, setCachedQueries] = useState<string[]>([]);
|
|
147
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
148
|
+
const [displayPage, setDisplayPage] = useState(1);
|
|
149
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
150
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
151
|
+
|
|
152
|
+
function showCrawlerSettings() {
|
|
153
|
+
toast.error('Web crawler is not available. Choose a crawler provider in Settings.', {
|
|
154
|
+
action: {
|
|
155
|
+
label: 'Open settings',
|
|
156
|
+
onClick: () => router.push('/settings?section=general#web-crawler'),
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readCrawlerStatus(): Promise<LocalCrawlerResponse> {
|
|
162
|
+
const res = await fetch(workspaceUrl('/api/firecrawl/local'), { cache: 'no-store' });
|
|
163
|
+
const body = (await res.json()) as LocalCrawlerResponse & { error?: string };
|
|
164
|
+
if (!res.ok) throw new Error(body.error || 'Could not read crawler status.');
|
|
165
|
+
setCrawlerState(body.state);
|
|
166
|
+
setCrawlerStarting(Boolean(body.starting));
|
|
167
|
+
return body;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function pollCrawlerUntilReady(): Promise<boolean> {
|
|
171
|
+
if (crawlerPolling.current) return crawlerPolling.current;
|
|
172
|
+
|
|
173
|
+
crawlerPolling.current = (async () => {
|
|
174
|
+
for (let attempt = 0; attempt < 45; attempt++) {
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
176
|
+
const status = await readCrawlerStatus();
|
|
177
|
+
if (status.state.running) return true;
|
|
178
|
+
if (!status.starting) return false;
|
|
179
|
+
}
|
|
180
|
+
setCrawlerStarting(false);
|
|
181
|
+
setCrawlerState((current) => ({
|
|
182
|
+
...(current ?? { running: false }),
|
|
183
|
+
lastError: 'The website crawler is still starting. Try again in a moment.',
|
|
184
|
+
}));
|
|
185
|
+
return false;
|
|
186
|
+
})().finally(() => {
|
|
187
|
+
crawlerPolling.current = null;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return crawlerPolling.current;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function warmCrawler(): Promise<LocalCrawlerResponse | null> {
|
|
194
|
+
if (crawlerProvider === 'cloud') return null;
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(workspaceUrl('/api/firecrawl/local'), {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
body: JSON.stringify({ action: 'start' }),
|
|
200
|
+
});
|
|
201
|
+
const body = (await res.json()) as LocalCrawlerResponse & { error?: string };
|
|
202
|
+
if (!res.ok) throw new Error(body.error || 'Could not prepare the crawler.');
|
|
203
|
+
setCrawlerState(body.state);
|
|
204
|
+
setCrawlerStarting(Boolean(body.starting));
|
|
205
|
+
if (body.starting) void pollCrawlerUntilReady();
|
|
206
|
+
return body;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
setCrawlerStarting(false);
|
|
209
|
+
setCrawlerState((current) => ({
|
|
210
|
+
...(current ?? { running: false }),
|
|
211
|
+
lastError: formatErrorMessage(error),
|
|
212
|
+
}));
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function ensureCrawlerReady() {
|
|
218
|
+
if (crawlerProvider === 'cloud') return true;
|
|
219
|
+
if (crawlerState?.running) return true;
|
|
220
|
+
|
|
221
|
+
const started = await warmCrawler();
|
|
222
|
+
if (!started) return false;
|
|
223
|
+
if (started.state.running) return true;
|
|
224
|
+
// Keep a slow runtime startup in the background without making the Add
|
|
225
|
+
// website action look frozen. The built-in crawler normally starts at once.
|
|
226
|
+
const ready = started.starting
|
|
227
|
+
? await Promise.race([
|
|
228
|
+
pollCrawlerUntilReady(),
|
|
229
|
+
new Promise<false>((resolve) => setTimeout(() => resolve(false), 8_000)),
|
|
230
|
+
])
|
|
231
|
+
: false;
|
|
232
|
+
if (!ready && started.starting) return true;
|
|
233
|
+
if (!ready) {
|
|
234
|
+
toast.error(started.state.lastError || 'The website crawler is not ready yet.');
|
|
235
|
+
}
|
|
236
|
+
return ready;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
function handleClickOutside(event: MouseEvent) {
|
|
241
|
+
if (
|
|
242
|
+
inputRef.current &&
|
|
243
|
+
!inputRef.current.contains(event.target as Node) &&
|
|
244
|
+
dropdownRef.current &&
|
|
245
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
246
|
+
) {
|
|
247
|
+
setShowDropdown(false);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
251
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
try {
|
|
256
|
+
const saved = localStorage.getItem(workspaceStorageKey(SCRAPE_RECENT_QUERIES_STORAGE_KEY));
|
|
257
|
+
if (saved) setCachedQueries(JSON.parse(saved));
|
|
258
|
+
} catch {
|
|
259
|
+
// ignore
|
|
260
|
+
}
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const clearRecentQueries = () => {
|
|
265
|
+
setCachedQueries([]);
|
|
266
|
+
setShowDropdown(false);
|
|
267
|
+
};
|
|
268
|
+
const handleStorage = (event: StorageEvent) => {
|
|
269
|
+
if (
|
|
270
|
+
event.key === workspaceStorageKey(SCRAPE_RECENT_QUERIES_STORAGE_KEY) ||
|
|
271
|
+
event.key === null
|
|
272
|
+
) {
|
|
273
|
+
clearRecentQueries();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
window.addEventListener(RECENT_ADD_HISTORY_CLEARED_EVENT, clearRecentQueries);
|
|
277
|
+
window.addEventListener('storage', handleStorage);
|
|
278
|
+
return () => {
|
|
279
|
+
window.removeEventListener(RECENT_ADD_HISTORY_CLEARED_EVENT, clearRecentQueries);
|
|
280
|
+
window.removeEventListener('storage', handleStorage);
|
|
281
|
+
};
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
284
|
+
function saveQuery(q: string) {
|
|
285
|
+
const trimmed = q.trim();
|
|
286
|
+
if (!trimmed) return;
|
|
287
|
+
setCachedQueries((prev) => {
|
|
288
|
+
const newCache = [trimmed, ...prev.filter((item) => item !== trimmed)].slice(0, 10);
|
|
289
|
+
try {
|
|
290
|
+
localStorage.setItem(
|
|
291
|
+
workspaceStorageKey(SCRAPE_RECENT_QUERIES_STORAGE_KEY),
|
|
292
|
+
JSON.stringify(newCache),
|
|
293
|
+
);
|
|
294
|
+
} catch {
|
|
295
|
+
// ignore
|
|
296
|
+
}
|
|
297
|
+
return newCache;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function removeQuery(q: string, e: React.MouseEvent) {
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
setCachedQueries((prev) => {
|
|
304
|
+
const newCache = prev.filter((item) => item !== q);
|
|
305
|
+
try {
|
|
306
|
+
localStorage.setItem(
|
|
307
|
+
workspaceStorageKey(SCRAPE_RECENT_QUERIES_STORAGE_KEY),
|
|
308
|
+
JSON.stringify(newCache),
|
|
309
|
+
);
|
|
310
|
+
} catch {
|
|
311
|
+
// ignore
|
|
312
|
+
}
|
|
313
|
+
return newCache;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
// Check the selected web-search provider.
|
|
319
|
+
fetch(workspaceUrl('/api/search'))
|
|
320
|
+
.then((r) => r.json())
|
|
321
|
+
.then((d) => setFirecrawlConfigured(d.configured ?? false))
|
|
322
|
+
.catch(() => setFirecrawlConfigured(false));
|
|
323
|
+
}, []);
|
|
324
|
+
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (crawlerProvider !== 'local') return;
|
|
327
|
+
void (async () => {
|
|
328
|
+
try {
|
|
329
|
+
const status = await readCrawlerStatus();
|
|
330
|
+
if (!status.state.running) await warmCrawler();
|
|
331
|
+
} catch {
|
|
332
|
+
await warmCrawler();
|
|
333
|
+
}
|
|
334
|
+
})();
|
|
335
|
+
}, [crawlerProvider]);
|
|
336
|
+
|
|
337
|
+
const selectedUrls = useMemo(() => Object.keys(selected).filter((u) => selected[u]), [selected]);
|
|
338
|
+
|
|
339
|
+
/** Search through the configured crawler endpoint. */
|
|
340
|
+
async function searchFirecrawl(q: string, isMulti: boolean) {
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch(workspaceUrl('/api/search'), {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers: { 'Content-Type': 'application/json' },
|
|
345
|
+
body: JSON.stringify({ query: q, limit: searchLimit }),
|
|
346
|
+
});
|
|
347
|
+
const data = await res.json();
|
|
348
|
+
if (!res.ok) throw new Error(data.error ?? 'Search failed');
|
|
349
|
+
const newItems = (data.results as SearchResultItem[]) ?? [];
|
|
350
|
+
|
|
351
|
+
setSearchState((prev) => {
|
|
352
|
+
const existingResults = prev ? prev.results : [];
|
|
353
|
+
const existingUrls = new Set(existingResults.map((r) => r.url));
|
|
354
|
+
const fresh = newItems.filter((r) => !existingUrls.has(r.url));
|
|
355
|
+
return {
|
|
356
|
+
results: [...existingResults, ...fresh],
|
|
357
|
+
totalResults: isMulti ? (prev?.totalResults ?? 0) + fresh.length : fresh.length,
|
|
358
|
+
totalResultsIsEstimate: false,
|
|
359
|
+
currentPage: 1,
|
|
360
|
+
totalPages: 1,
|
|
361
|
+
hasMore: false,
|
|
362
|
+
query: isMulti ? 'Multiple queries' : q,
|
|
363
|
+
searchProvider: 'firecrawl',
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const newSelected = Object.fromEntries(newItems.map((r) => [r.url, true]));
|
|
368
|
+
setSelected((prev) => ({ ...prev, ...newSelected }));
|
|
369
|
+
|
|
370
|
+
if (newItems.length === 0 && !isMulti) toast.message('No results. Try different keywords.');
|
|
371
|
+
|
|
372
|
+
if (activeProvider && !isMulti) {
|
|
373
|
+
fetchProviderTotalCount(q, activeProvider);
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
const message = formatErrorMessage(err);
|
|
377
|
+
if (/crawler|firecrawl|not running|api key/i.test(message)) {
|
|
378
|
+
showCrawlerSettings();
|
|
379
|
+
} else {
|
|
380
|
+
toast.error(message);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Search using generic provider. */
|
|
386
|
+
async function searchGeneric(
|
|
387
|
+
q: string,
|
|
388
|
+
provider: string,
|
|
389
|
+
page: number,
|
|
390
|
+
isMulti: boolean,
|
|
391
|
+
appendPagination = false,
|
|
392
|
+
) {
|
|
393
|
+
if (appendPagination) {
|
|
394
|
+
setLoadingMore(true);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const endpoint =
|
|
398
|
+
provider === 'serper' || provider === 'google'
|
|
399
|
+
? workspaceUrl('/api/search/google')
|
|
400
|
+
: provider === 'brave'
|
|
401
|
+
? workspaceUrl('/api/search/brave')
|
|
402
|
+
: provider === 'bing'
|
|
403
|
+
? workspaceUrl('/api/search/bing')
|
|
404
|
+
: provider === 'tavily'
|
|
405
|
+
? workspaceUrl('/api/search/tavily')
|
|
406
|
+
: provider === 'exa'
|
|
407
|
+
? workspaceUrl('/api/search/exa')
|
|
408
|
+
: null;
|
|
409
|
+
|
|
410
|
+
if (!endpoint) {
|
|
411
|
+
toast.error('Invalid search provider');
|
|
412
|
+
if (appendPagination) setLoadingMore(false);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const res = await fetch(endpoint, {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: { 'Content-Type': 'application/json' },
|
|
420
|
+
body: JSON.stringify({ query: q, page }),
|
|
421
|
+
});
|
|
422
|
+
const data = await res.json();
|
|
423
|
+
|
|
424
|
+
if (!res.ok) throw new Error(data.error ?? 'Search failed');
|
|
425
|
+
|
|
426
|
+
const newItems: SearchResultItem[] = (data.items ?? []).map(
|
|
427
|
+
(item: { url: string; title: string; description?: string }) => ({
|
|
428
|
+
url: item.url,
|
|
429
|
+
title: item.title || item.url,
|
|
430
|
+
description: item.description,
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
setSearchState((prev) => {
|
|
435
|
+
const existingResults = prev ? prev.results : [];
|
|
436
|
+
const existingUrls = new Set(existingResults.map((r) => r.url));
|
|
437
|
+
const fresh = newItems.filter((r) => !existingUrls.has(r.url));
|
|
438
|
+
return {
|
|
439
|
+
results: [...existingResults, ...fresh],
|
|
440
|
+
totalResults: (prev?.totalResults ?? 0) + (data.totalResults ?? 0),
|
|
441
|
+
totalResultsIsEstimate: data.totalResultsIsEstimate ?? true,
|
|
442
|
+
currentPage: data.currentPage ?? page,
|
|
443
|
+
totalPages: data.totalPages ?? 1,
|
|
444
|
+
hasMore: data.hasMore ?? false,
|
|
445
|
+
query: data.query ?? q,
|
|
446
|
+
searchProvider: provider as any,
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const newSelected = Object.fromEntries(newItems.map((r) => [r.url, true]));
|
|
451
|
+
setSelected((prev) => ({ ...prev, ...newSelected }));
|
|
452
|
+
|
|
453
|
+
if (!isMulti) {
|
|
454
|
+
// Update total
|
|
455
|
+
setSerperTotalForQuery({
|
|
456
|
+
query: q,
|
|
457
|
+
total: data.totalResults ?? 0,
|
|
458
|
+
totalPages: data.totalPages ?? 1,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (page === 1 && newItems.length === 0 && !isMulti) {
|
|
463
|
+
toast.message('No results. Try different keywords.');
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (page === 1) throw err;
|
|
467
|
+
toast.error(formatErrorMessage(err));
|
|
468
|
+
} finally {
|
|
469
|
+
if (appendPagination) setLoadingMore(false);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Fetch total count from provider without using it for results (saves on Firecrawl credits) */
|
|
474
|
+
async function fetchProviderTotalCount(q: string, provider: string) {
|
|
475
|
+
const endpoint =
|
|
476
|
+
provider === 'serper' || provider === 'google'
|
|
477
|
+
? workspaceUrl('/api/search/google')
|
|
478
|
+
: provider === 'brave'
|
|
479
|
+
? workspaceUrl('/api/search/brave')
|
|
480
|
+
: provider === 'bing'
|
|
481
|
+
? workspaceUrl('/api/search/bing')
|
|
482
|
+
: provider === 'tavily'
|
|
483
|
+
? workspaceUrl('/api/search/tavily')
|
|
484
|
+
: provider === 'exa'
|
|
485
|
+
? workspaceUrl('/api/search/exa')
|
|
486
|
+
: null;
|
|
487
|
+
|
|
488
|
+
if (!endpoint) return;
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const res = await fetch(endpoint, {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: { 'Content-Type': 'application/json' },
|
|
494
|
+
body: JSON.stringify({ query: q, page: 1 }),
|
|
495
|
+
});
|
|
496
|
+
const data = await res.json();
|
|
497
|
+
if (res.ok) {
|
|
498
|
+
setSerperTotalForQuery({
|
|
499
|
+
query: q,
|
|
500
|
+
total: data.totalResults ?? 0,
|
|
501
|
+
totalPages: data.totalPages ?? 1,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
// Silent fail — count is optional
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function runSearch() {
|
|
510
|
+
setShowDropdown(false);
|
|
511
|
+
if (!query.trim()) {
|
|
512
|
+
toast.error('Enter one or more keywords to search.');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
setSerperTotalForQuery(null);
|
|
517
|
+
saveQuery(query);
|
|
518
|
+
|
|
519
|
+
const queries = query
|
|
520
|
+
.split(',')
|
|
521
|
+
.map((q) => q.trim())
|
|
522
|
+
.filter(Boolean);
|
|
523
|
+
if (queries.length === 0) return;
|
|
524
|
+
|
|
525
|
+
setSearching(true);
|
|
526
|
+
setDisplayPage(1);
|
|
527
|
+
// Removed setSearchState(null) so we append instead of clear
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
if (activeProvider === 'local' || !activeProvider) {
|
|
531
|
+
if (!(await ensureCrawlerReady())) return;
|
|
532
|
+
await Promise.all(queries.map((q) => searchFirecrawl(q, queries.length > 1)));
|
|
533
|
+
} else {
|
|
534
|
+
try {
|
|
535
|
+
await Promise.all(
|
|
536
|
+
queries.map((q) => searchGeneric(q, activeProvider, 1, queries.length > 1, false)),
|
|
537
|
+
);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
toast.info('Using local crawler…');
|
|
540
|
+
if (!(await ensureCrawlerReady())) return;
|
|
541
|
+
await Promise.all(queries.map((q) => searchFirecrawl(q, queries.length > 1)));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} finally {
|
|
545
|
+
setSearching(false);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function loadNextPage() {
|
|
550
|
+
if (!searchState?.hasMore || loadingMore) return;
|
|
551
|
+
if (searchState.searchProvider !== 'firecrawl') {
|
|
552
|
+
await searchGeneric(
|
|
553
|
+
searchState.query,
|
|
554
|
+
searchState.searchProvider,
|
|
555
|
+
searchState.currentPage + 1,
|
|
556
|
+
false,
|
|
557
|
+
true,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Automatically paginate through ALL available provider results. */
|
|
563
|
+
async function gatherAll() {
|
|
564
|
+
if (!searchState || searchState.searchProvider === 'firecrawl') return;
|
|
565
|
+
setGatheringAll(true);
|
|
566
|
+
setGatherProgress(searchState.currentPage);
|
|
567
|
+
|
|
568
|
+
let page = searchState.currentPage + 1;
|
|
569
|
+
const total = searchState.totalPages;
|
|
570
|
+
const provider = searchState.searchProvider;
|
|
571
|
+
|
|
572
|
+
const endpoint =
|
|
573
|
+
provider === 'serper' || provider === 'google'
|
|
574
|
+
? workspaceUrl('/api/search/google')
|
|
575
|
+
: provider === 'brave'
|
|
576
|
+
? workspaceUrl('/api/search/brave')
|
|
577
|
+
: provider === 'bing'
|
|
578
|
+
? workspaceUrl('/api/search/bing')
|
|
579
|
+
: provider === 'tavily'
|
|
580
|
+
? workspaceUrl('/api/search/tavily')
|
|
581
|
+
: provider === 'exa'
|
|
582
|
+
? workspaceUrl('/api/search/exa')
|
|
583
|
+
: null;
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
while (page <= total) {
|
|
587
|
+
setGatherProgress(page);
|
|
588
|
+
if (!endpoint) break;
|
|
589
|
+
const res = await fetch(endpoint, {
|
|
590
|
+
method: 'POST',
|
|
591
|
+
headers: { 'Content-Type': 'application/json' },
|
|
592
|
+
body: JSON.stringify({ query: query.trim(), page }),
|
|
593
|
+
});
|
|
594
|
+
const data = await res.json();
|
|
595
|
+
if (!res.ok) throw new Error(data.error ?? 'Search failed');
|
|
596
|
+
|
|
597
|
+
const newItems: SearchResultItem[] = (data.items ?? []).map(
|
|
598
|
+
(item: { url: string; title: string; description?: string }) => ({
|
|
599
|
+
url: item.url,
|
|
600
|
+
title: item.title || item.url,
|
|
601
|
+
description: item.description,
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
setSearchState((prev) => {
|
|
606
|
+
if (!prev) return prev;
|
|
607
|
+
const existingUrls = new Set(prev.results.map((r) => r.url));
|
|
608
|
+
const fresh = newItems.filter((r) => !existingUrls.has(r.url));
|
|
609
|
+
return {
|
|
610
|
+
...prev,
|
|
611
|
+
results: [...prev.results, ...fresh],
|
|
612
|
+
totalResultsIsEstimate: data.totalResultsIsEstimate ?? prev.totalResultsIsEstimate,
|
|
613
|
+
currentPage: data.currentPage ?? page,
|
|
614
|
+
totalPages: data.totalPages ?? prev.totalPages,
|
|
615
|
+
hasMore: data.hasMore ?? false,
|
|
616
|
+
};
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const newSelected = Object.fromEntries(newItems.map((r) => [r.url, true]));
|
|
620
|
+
setSelected((prev) => ({ ...prev, ...newSelected }));
|
|
621
|
+
|
|
622
|
+
if (!data.hasMore) break;
|
|
623
|
+
page++;
|
|
624
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
625
|
+
}
|
|
626
|
+
toast.success('All available results gathered!');
|
|
627
|
+
} catch (err) {
|
|
628
|
+
toast.error(formatErrorMessage(err));
|
|
629
|
+
} finally {
|
|
630
|
+
setGatheringAll(false);
|
|
631
|
+
setGatherProgress(0);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function appendManualUrl() {
|
|
636
|
+
const urls = manualUrl
|
|
637
|
+
.split(/[\s,]+/)
|
|
638
|
+
.map((u) => u.trim())
|
|
639
|
+
.filter((u) => /^https?:\/\//i.test(u));
|
|
640
|
+
|
|
641
|
+
if (urls.length > 0) {
|
|
642
|
+
setManualUrlList((prev) => Array.from(new Set([...prev, ...urls])));
|
|
643
|
+
setManualUrl('');
|
|
644
|
+
} else {
|
|
645
|
+
toast.error('Enter at least one full URL starting with http(s)://');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function addManual() {
|
|
650
|
+
const currentUrls = manualUrl
|
|
651
|
+
.split(/[\s,]+/)
|
|
652
|
+
.map((u) => u.trim())
|
|
653
|
+
.filter((u) => /^https?:\/\//i.test(u));
|
|
654
|
+
|
|
655
|
+
const allUrls = Array.from(new Set([...manualUrlList, ...currentUrls]));
|
|
656
|
+
|
|
657
|
+
if (allUrls.length === 0) {
|
|
658
|
+
toast.error('Enter at least one full URL starting with http(s)://');
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
void startJob(allUrls, true).then((success) => {
|
|
663
|
+
if (success) {
|
|
664
|
+
setManualUrl('');
|
|
665
|
+
setManualUrlList([]);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
671
|
+
const file = e.target.files?.[0];
|
|
672
|
+
if (!file) return;
|
|
673
|
+
const reader = new FileReader();
|
|
674
|
+
reader.onload = (e) => {
|
|
675
|
+
const text = e.target?.result as string;
|
|
676
|
+
if (text) {
|
|
677
|
+
setManualUrl((prev) => prev + (prev ? '\n' : '') + text);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
reader.readAsText(file);
|
|
681
|
+
e.target.value = ''; // reset input
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/** Open confirm modal before starting ETL */
|
|
685
|
+
function handleStartClick() {
|
|
686
|
+
if (selectedUrls.length === 0) {
|
|
687
|
+
toast.error('Select at least one URL to scrape.');
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
void startJob();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function startJob(urls = selectedUrls, exactUrls = false) {
|
|
694
|
+
setStarting(true);
|
|
695
|
+
const effectiveScope: CrawlScope = specificUrls || exactUrls ? 'page' : scope;
|
|
696
|
+
try {
|
|
697
|
+
if (!(await ensureCrawlerReady())) return;
|
|
698
|
+
const res = await fetch(workspaceUrl('/api/jobs'), {
|
|
699
|
+
method: 'POST',
|
|
700
|
+
headers: { 'Content-Type': 'application/json' },
|
|
701
|
+
body: JSON.stringify({
|
|
702
|
+
keywords: query || urls[0],
|
|
703
|
+
pageLimit: specificUrls || exactUrls ? 1 : pageLimit,
|
|
704
|
+
targets: urls.map((url) => ({ url, scope: effectiveScope })),
|
|
705
|
+
groupId: groupIdRef.current,
|
|
706
|
+
}),
|
|
707
|
+
});
|
|
708
|
+
const data = await res.json();
|
|
709
|
+
if (!res.ok) {
|
|
710
|
+
if (res.status === 409 && data.code === 'already_scraped') {
|
|
711
|
+
toast.info('Already in your knowledge base', {
|
|
712
|
+
description: 'This URL has already been scraped and added.',
|
|
713
|
+
});
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
throw new Error(data.error ?? 'Could not start job');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
toast.success('Website added', {
|
|
720
|
+
description: 'We’ll finish it in the background.',
|
|
721
|
+
duration: 8000,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
setSelected({});
|
|
725
|
+
setSearchState((prev) => {
|
|
726
|
+
if (!prev) return prev;
|
|
727
|
+
const urlSet = new Set(urls);
|
|
728
|
+
return {
|
|
729
|
+
...prev,
|
|
730
|
+
results: prev.results.filter((r) => !urlSet.has(r.url)),
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
onStarted(data.job as CrawlJob);
|
|
734
|
+
return true;
|
|
735
|
+
} catch (err) {
|
|
736
|
+
toast.error(formatErrorMessage(err));
|
|
737
|
+
return false;
|
|
738
|
+
} finally {
|
|
739
|
+
setStarting(false);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const results = searchState?.results ?? [];
|
|
744
|
+
const gatherAllAvailable =
|
|
745
|
+
searchState?.searchProvider !== 'firecrawl' && searchState?.hasMore && !gatheringAll;
|
|
746
|
+
|
|
747
|
+
const ITEMS_PER_PAGE = 5;
|
|
748
|
+
const totalDisplayPages = Math.max(1, Math.ceil(results.length / ITEMS_PER_PAGE));
|
|
749
|
+
const currentDisplayResults = results.slice(
|
|
750
|
+
(displayPage - 1) * ITEMS_PER_PAGE,
|
|
751
|
+
displayPage * ITEMS_PER_PAGE,
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
useEffect(() => {
|
|
755
|
+
if (displayPage > totalDisplayPages && totalDisplayPages > 0) {
|
|
756
|
+
setDisplayPage(totalDisplayPages);
|
|
757
|
+
}
|
|
758
|
+
}, [totalDisplayPages, displayPage]);
|
|
759
|
+
|
|
760
|
+
const [manualUrlPage, setManualUrlPage] = useState(1);
|
|
761
|
+
const MANUAL_URL_PAGE_SIZE = 10;
|
|
762
|
+
const totalManualPages = Math.max(1, Math.ceil(manualUrlList.length / MANUAL_URL_PAGE_SIZE));
|
|
763
|
+
const currentManualUrls = manualUrlList.slice(
|
|
764
|
+
(manualUrlPage - 1) * MANUAL_URL_PAGE_SIZE,
|
|
765
|
+
manualUrlPage * MANUAL_URL_PAGE_SIZE,
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
if (manualUrlPage > totalManualPages && totalManualPages > 0) {
|
|
770
|
+
setManualUrlPage(totalManualPages);
|
|
771
|
+
}
|
|
772
|
+
}, [totalManualPages, manualUrlPage]);
|
|
773
|
+
|
|
774
|
+
// Estimate for confirmation modal
|
|
775
|
+
const effectiveScopeForEstimate: CrawlScope = specificUrls ? 'page' : scope;
|
|
776
|
+
const estimate = useMemo(
|
|
777
|
+
() =>
|
|
778
|
+
estimateEtlDuration(
|
|
779
|
+
selectedUrls.length,
|
|
780
|
+
effectiveScopeForEstimate,
|
|
781
|
+
specificUrls ? 1 : pageLimit,
|
|
782
|
+
),
|
|
783
|
+
[selectedUrls.length, effectiveScopeForEstimate, specificUrls, pageLimit],
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const [inputMode, setInputMode] = useState<'search' | 'url'>('url');
|
|
787
|
+
|
|
788
|
+
useEffect(() => {
|
|
789
|
+
const hasSelection = selectedUrls.length > 0;
|
|
790
|
+
const isUrl = inputMode === 'url';
|
|
791
|
+
const currentUrls = manualUrl.split(/[\s,]+/).filter((u) => /^https?:\/\//i.test(u));
|
|
792
|
+
const allUrls = Array.from(new Set([...manualUrlList, ...currentUrls]));
|
|
793
|
+
const urlCount = allUrls.length;
|
|
794
|
+
|
|
795
|
+
onActionChange?.({
|
|
796
|
+
label: isUrl
|
|
797
|
+
? urlCount > 1
|
|
798
|
+
? 'Add websites'
|
|
799
|
+
: 'Add website'
|
|
800
|
+
: hasSelection
|
|
801
|
+
? 'Add selected'
|
|
802
|
+
: 'Search topic',
|
|
803
|
+
onClick: () => {
|
|
804
|
+
if (isUrl) void addManual();
|
|
805
|
+
else if (hasSelection) handleStartClick();
|
|
806
|
+
else void runSearch();
|
|
807
|
+
},
|
|
808
|
+
disabled: disabled || (isUrl ? urlCount === 0 : !hasSelection && !query.trim()),
|
|
809
|
+
loading: searching || starting,
|
|
810
|
+
});
|
|
811
|
+
return () => onActionChange?.(null);
|
|
812
|
+
}, [
|
|
813
|
+
disabled,
|
|
814
|
+
inputMode,
|
|
815
|
+
manualUrl,
|
|
816
|
+
manualUrlList,
|
|
817
|
+
query,
|
|
818
|
+
searching,
|
|
819
|
+
starting,
|
|
820
|
+
selectedUrls.length,
|
|
821
|
+
groupId,
|
|
822
|
+
onActionChange,
|
|
823
|
+
]);
|
|
824
|
+
|
|
825
|
+
return (
|
|
826
|
+
<div className="space-y-4">
|
|
827
|
+
{/* Unified input area */}
|
|
828
|
+
<div className="space-y-3">
|
|
829
|
+
{/* Mode toggle + input */}
|
|
830
|
+
<div className="flex items-center gap-2">
|
|
831
|
+
<DataEntryModeSwitch
|
|
832
|
+
label="Website source"
|
|
833
|
+
value={inputMode}
|
|
834
|
+
onValueChange={(value) => setInputMode(value as 'search' | 'url')}
|
|
835
|
+
options={[
|
|
836
|
+
{ value: 'url', label: 'Direct URL', icon: Globe },
|
|
837
|
+
{ value: 'search', label: 'Search', icon: Search },
|
|
838
|
+
]}
|
|
839
|
+
/>
|
|
840
|
+
|
|
841
|
+
{/* Input field */}
|
|
842
|
+
<div className="relative flex-1 flex items-center gap-2">
|
|
843
|
+
{inputMode === 'search' ? (
|
|
844
|
+
<div className="relative flex-1">
|
|
845
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground z-10 pointer-events-none" />
|
|
846
|
+
<Input
|
|
847
|
+
ref={inputRef}
|
|
848
|
+
id="kw"
|
|
849
|
+
autoComplete="off"
|
|
850
|
+
placeholder='Search for websites, e.g. "Who is the best player in the world?"'
|
|
851
|
+
value={query}
|
|
852
|
+
disabled={disabled}
|
|
853
|
+
className="pl-9 h-11 pr-22 bg-white"
|
|
854
|
+
onChange={(e) => {
|
|
855
|
+
setQuery(e.target.value);
|
|
856
|
+
setShowDropdown(true);
|
|
857
|
+
}}
|
|
858
|
+
onFocus={() => {
|
|
859
|
+
setShowDropdown(true);
|
|
860
|
+
void warmCrawler();
|
|
861
|
+
}}
|
|
862
|
+
onKeyDown={(e) => {
|
|
863
|
+
if (e.key === 'Enter') {
|
|
864
|
+
setShowDropdown(false);
|
|
865
|
+
runSearch();
|
|
866
|
+
}
|
|
867
|
+
}}
|
|
868
|
+
/>
|
|
869
|
+
{showDropdown && cachedQueries.length > 0 && (
|
|
870
|
+
<div
|
|
871
|
+
ref={dropdownRef}
|
|
872
|
+
className="absolute z-10 top-full left-0 mt-1 w-full rounded-md border border-border bg-popover overflow-hidden"
|
|
873
|
+
>
|
|
874
|
+
<div className="py-1">
|
|
875
|
+
{cachedQueries.map((cq) => (
|
|
876
|
+
<div
|
|
877
|
+
key={cq}
|
|
878
|
+
className="flex items-center justify-between px-3 py-1.5 text-sm hover:bg-muted cursor-pointer group"
|
|
879
|
+
onClick={() => {
|
|
880
|
+
setQuery(cq);
|
|
881
|
+
setShowDropdown(false);
|
|
882
|
+
}}
|
|
883
|
+
>
|
|
884
|
+
<div className="flex items-center gap-2 truncate mr-2">
|
|
885
|
+
<Clock className="size-3 text-muted-foreground shrink-0" />
|
|
886
|
+
<span className="truncate text-xs">{cq}</span>
|
|
887
|
+
</div>
|
|
888
|
+
<button
|
|
889
|
+
className="text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity p-0.5"
|
|
890
|
+
onClick={(e) => removeQuery(cq, e)}
|
|
891
|
+
>
|
|
892
|
+
<X className="size-3" />
|
|
893
|
+
</button>
|
|
894
|
+
</div>
|
|
895
|
+
))}
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
)}
|
|
899
|
+
</div>
|
|
900
|
+
) : (
|
|
901
|
+
<>
|
|
902
|
+
<div className="relative flex-1 flex flex-col gap-2">
|
|
903
|
+
<div className="flex items-center gap-2">
|
|
904
|
+
<div className="relative flex-1">
|
|
905
|
+
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground z-10 pointer-events-none" />
|
|
906
|
+
<Input
|
|
907
|
+
placeholder="Paste URLs (comma or space separated)"
|
|
908
|
+
value={manualUrl}
|
|
909
|
+
disabled={disabled}
|
|
910
|
+
className="pl-9 h-11 pr-16 bg-white"
|
|
911
|
+
onChange={(e) => setManualUrl(e.target.value)}
|
|
912
|
+
onFocus={() => void warmCrawler()}
|
|
913
|
+
onKeyDown={(e) => e.key === 'Enter' && appendManualUrl()}
|
|
914
|
+
/>
|
|
915
|
+
</div>
|
|
916
|
+
<Button
|
|
917
|
+
size="icon"
|
|
918
|
+
className="h-11 w-11 shrink-0"
|
|
919
|
+
variant="outline"
|
|
920
|
+
onClick={() => appendManualUrl()}
|
|
921
|
+
disabled={disabled || !manualUrl.trim()}
|
|
922
|
+
>
|
|
923
|
+
<Plus className="size-4" />
|
|
924
|
+
</Button>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
</>
|
|
928
|
+
)}
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
|
|
933
|
+
{/* No provider configured hint */}
|
|
934
|
+
{firecrawlConfigured === false && !activeProvider && (
|
|
935
|
+
<div className="flex items-start gap-2 rounded-md border border-border bg-muted/30 px-3 py-2.5 text-xs text-muted-foreground">
|
|
936
|
+
<Info className="mt-0.5 size-3.5 shrink-0" />
|
|
937
|
+
<span>
|
|
938
|
+
<strong className="text-foreground">No search provider configured.</strong> Set up Use
|
|
939
|
+
Larkup’s built-in crawler or configure a Web Search provider in Settings.
|
|
940
|
+
</span>
|
|
941
|
+
</div>
|
|
942
|
+
)}
|
|
943
|
+
|
|
944
|
+
{/* Minimal Action Row */}
|
|
945
|
+
<div className="flex items-center justify-between mt-2">
|
|
946
|
+
<div className="flex items-center gap-2">
|
|
947
|
+
<button
|
|
948
|
+
type="button"
|
|
949
|
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
950
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
951
|
+
>
|
|
952
|
+
{showAdvanced ? (
|
|
953
|
+
<ChevronDown className="size-3.5" />
|
|
954
|
+
) : (
|
|
955
|
+
<ChevronsDown className="size-3.5" />
|
|
956
|
+
)}
|
|
957
|
+
{showAdvanced ? 'Hide advanced settings' : 'Advanced settings'}
|
|
958
|
+
</button>
|
|
959
|
+
</div>
|
|
960
|
+
|
|
961
|
+
{/* Estimation inline */}
|
|
962
|
+
{selectedUrls.length > 0 && (
|
|
963
|
+
<span className="text-xs text-muted-foreground flex items-center gap-1.5 ml-auto">
|
|
964
|
+
<Clock className="size-3" />
|
|
965
|
+
{specificUrls
|
|
966
|
+
? `${selectedUrls.length} URL${selectedUrls.length !== 1 ? 's' : ''}`
|
|
967
|
+
: `~${estimate.totalPages.toLocaleString()} pages · ${formatDuration(
|
|
968
|
+
estimate.estimatedSeconds,
|
|
969
|
+
)}`}
|
|
970
|
+
</span>
|
|
971
|
+
)}
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
{/* Advanced Settings Collapsible */}
|
|
975
|
+
{showAdvanced && (
|
|
976
|
+
<div className="flex flex-col gap-4 w-full rounded-lg border border-border bg-muted/30 p-3 mt-2 animate-in slide-in-from-top-2 fade-in duration-200">
|
|
977
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
978
|
+
<div className="flex ">
|
|
979
|
+
<div className="flex items-center gap-2">
|
|
980
|
+
<Label className="text-xs text-muted-foreground">Scope</Label>
|
|
981
|
+
<Select
|
|
982
|
+
defaultValue={scope}
|
|
983
|
+
value={scope}
|
|
984
|
+
onValueChange={(v) => setScope(v as CrawlScope)}
|
|
985
|
+
disabled={specificUrls || disabled}
|
|
986
|
+
>
|
|
987
|
+
<SelectTrigger className="w-52 bg-white h-8 text-xs">
|
|
988
|
+
<SelectValue />
|
|
989
|
+
</SelectTrigger>
|
|
990
|
+
<SelectContent>
|
|
991
|
+
<SelectItem value="page">Scrape pages only</SelectItem>
|
|
992
|
+
<SelectItem value="domain">Crawl whole domain</SelectItem>
|
|
993
|
+
</SelectContent>
|
|
994
|
+
</Select>
|
|
995
|
+
</div>
|
|
996
|
+
|
|
997
|
+
{scope === 'domain' && !specificUrls && (
|
|
998
|
+
<div className="flex items-center gap-2">
|
|
999
|
+
<Label htmlFor="limit" className="text-xs text-muted-foreground ml-4">
|
|
1000
|
+
Max pages
|
|
1001
|
+
</Label>
|
|
1002
|
+
<Input
|
|
1003
|
+
id="limit"
|
|
1004
|
+
type="number"
|
|
1005
|
+
min={1}
|
|
1006
|
+
max={500}
|
|
1007
|
+
value={pageLimit}
|
|
1008
|
+
disabled={specificUrls || disabled}
|
|
1009
|
+
onChange={(e) => setPageLimit(Number(e.target.value) || 1)}
|
|
1010
|
+
className="w-20 h-8 tabular-nums text-xs ml-2"
|
|
1011
|
+
/>
|
|
1012
|
+
</div>
|
|
1013
|
+
)}
|
|
1014
|
+
|
|
1015
|
+
{inputMode === 'search' && (
|
|
1016
|
+
<div className="flex items-center gap-2">
|
|
1017
|
+
<Label htmlFor="search-limit" className="text-xs text-muted-foreground ml-4">
|
|
1018
|
+
Max results
|
|
1019
|
+
</Label>
|
|
1020
|
+
<Input
|
|
1021
|
+
id="search-limit"
|
|
1022
|
+
type="number"
|
|
1023
|
+
min={1}
|
|
1024
|
+
max={100}
|
|
1025
|
+
value={searchLimit}
|
|
1026
|
+
disabled={disabled}
|
|
1027
|
+
onChange={(e) => setSearchLimit(Number(e.target.value) || 1)}
|
|
1028
|
+
className="w-20 h-8 tabular-nums text-xs ml-2"
|
|
1029
|
+
/>
|
|
1030
|
+
</div>
|
|
1031
|
+
)}
|
|
1032
|
+
|
|
1033
|
+
<div className="flex items-center gap-2 ml-4">
|
|
1034
|
+
<Switch
|
|
1035
|
+
id="specific-urls-inline"
|
|
1036
|
+
checked={specificUrls}
|
|
1037
|
+
onCheckedChange={setSpecificUrls}
|
|
1038
|
+
disabled={disabled}
|
|
1039
|
+
size="sm"
|
|
1040
|
+
/>
|
|
1041
|
+
<Label
|
|
1042
|
+
htmlFor="specific-urls-inline"
|
|
1043
|
+
className="text-xs font-medium cursor-pointer flex items-center gap-1"
|
|
1044
|
+
>
|
|
1045
|
+
Exact URLs only
|
|
1046
|
+
<TooltipProvider delay={0}>
|
|
1047
|
+
<Tooltip>
|
|
1048
|
+
<TooltipTrigger
|
|
1049
|
+
type="button"
|
|
1050
|
+
className="text-muted-foreground hover:text-foreground"
|
|
1051
|
+
onClick={(e) => e.preventDefault()}
|
|
1052
|
+
>
|
|
1053
|
+
<Info className="size-3" />
|
|
1054
|
+
</TooltipTrigger>
|
|
1055
|
+
<TooltipContent>
|
|
1056
|
+
<p className="max-w-50 text-center">
|
|
1057
|
+
Scrape exact URLs only — no deep crawl or pagination.
|
|
1058
|
+
</p>
|
|
1059
|
+
</TooltipContent>
|
|
1060
|
+
</Tooltip>
|
|
1061
|
+
</TooltipProvider>
|
|
1062
|
+
</Label>
|
|
1063
|
+
</div>
|
|
1064
|
+
</div>
|
|
1065
|
+
<TooltipProvider delay={0}>
|
|
1066
|
+
<Tooltip>
|
|
1067
|
+
<TooltipTrigger
|
|
1068
|
+
type="button"
|
|
1069
|
+
className="text-muted-foreground hover:text-foreground p-1 rounded-md hover:bg-muted transition-colors"
|
|
1070
|
+
onClick={() => router.push('/settings?section=search-web')}
|
|
1071
|
+
>
|
|
1072
|
+
<Settings className="size-3.5" />
|
|
1073
|
+
</TooltipTrigger>
|
|
1074
|
+
<TooltipContent className="max-w-xs text-xs">
|
|
1075
|
+
<p>
|
|
1076
|
+
Customize your web search provider, or set up a custom proxy to help bypass
|
|
1077
|
+
scraping blocks.
|
|
1078
|
+
</p>
|
|
1079
|
+
</TooltipContent>
|
|
1080
|
+
</Tooltip>
|
|
1081
|
+
</TooltipProvider>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
|
|
1086
|
+
{manualUrlList.length > 0 && inputMode === 'url' && (
|
|
1087
|
+
<div className="rounded-md border border-border bg-card overflow-hidden mt-2 animate-in slide-in-from-top-2 fade-in duration-200">
|
|
1088
|
+
<table className="w-full text-xs">
|
|
1089
|
+
<thead>
|
|
1090
|
+
<tr className="border-b border-border bg-muted/50">
|
|
1091
|
+
<th className="py-2 pl-3 text-left font-medium text-muted-foreground">URL</th>
|
|
1092
|
+
<th className="py-2 pr-3 text-right font-medium text-muted-foreground w-12">
|
|
1093
|
+
Action
|
|
1094
|
+
</th>
|
|
1095
|
+
</tr>
|
|
1096
|
+
</thead>
|
|
1097
|
+
<tbody>
|
|
1098
|
+
{currentManualUrls.map((url) => (
|
|
1099
|
+
<tr
|
|
1100
|
+
key={url}
|
|
1101
|
+
className="border-b border-border/50 last:border-0 hover:bg-muted/30 transition-colors"
|
|
1102
|
+
>
|
|
1103
|
+
<td className="py-2 pl-3">
|
|
1104
|
+
<span className="truncate max-w-100 inline-block">{url}</span>
|
|
1105
|
+
</td>
|
|
1106
|
+
<td className="py-2 pr-3 text-right">
|
|
1107
|
+
<button
|
|
1108
|
+
type="button"
|
|
1109
|
+
onClick={() => setManualUrlList((prev) => prev.filter((u) => u !== url))}
|
|
1110
|
+
className="text-muted-foreground hover:text-destructive transition-colors rounded p-1"
|
|
1111
|
+
title="Remove URL"
|
|
1112
|
+
>
|
|
1113
|
+
<Trash2 className="size-3.5" />
|
|
1114
|
+
</button>
|
|
1115
|
+
</td>
|
|
1116
|
+
</tr>
|
|
1117
|
+
))}
|
|
1118
|
+
</tbody>
|
|
1119
|
+
</table>
|
|
1120
|
+
{totalManualPages > 1 && (
|
|
1121
|
+
<div className="flex items-center justify-between border-t border-border px-3 py-2 text-xs text-muted-foreground bg-muted/20">
|
|
1122
|
+
<span>
|
|
1123
|
+
{(manualUrlPage - 1) * MANUAL_URL_PAGE_SIZE + 1}–
|
|
1124
|
+
{Math.min(manualUrlPage * MANUAL_URL_PAGE_SIZE, manualUrlList.length)} of{' '}
|
|
1125
|
+
{manualUrlList.length}
|
|
1126
|
+
</span>
|
|
1127
|
+
<div className="flex items-center gap-1">
|
|
1128
|
+
<button
|
|
1129
|
+
disabled={manualUrlPage === 1}
|
|
1130
|
+
onClick={() => setManualUrlPage((p) => Math.max(1, p - 1))}
|
|
1131
|
+
className="rounded p-1 hover:bg-muted disabled:opacity-40 transition-colors cursor-pointer"
|
|
1132
|
+
>
|
|
1133
|
+
<ChevronLeft className="size-3.5" />
|
|
1134
|
+
</button>
|
|
1135
|
+
<span className="tabular-nums font-medium mx-1">
|
|
1136
|
+
{manualUrlPage} / {totalManualPages}
|
|
1137
|
+
</span>
|
|
1138
|
+
<button
|
|
1139
|
+
disabled={manualUrlPage >= totalManualPages}
|
|
1140
|
+
onClick={() => setManualUrlPage((p) => Math.min(totalManualPages, p + 1))}
|
|
1141
|
+
className="rounded p-1 hover:bg-muted disabled:opacity-40 transition-colors cursor-pointer"
|
|
1142
|
+
>
|
|
1143
|
+
<ChevronRight className="size-3.5" />
|
|
1144
|
+
</button>
|
|
1145
|
+
</div>
|
|
1146
|
+
</div>
|
|
1147
|
+
)}
|
|
1148
|
+
</div>
|
|
1149
|
+
)}
|
|
1150
|
+
|
|
1151
|
+
{/* Results list */}
|
|
1152
|
+
{results.length > 0 && (
|
|
1153
|
+
<div className="rounded-lg border border-border mb-9">
|
|
1154
|
+
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
|
1155
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
1156
|
+
{results.length} source{results.length === 1 ? '' : 's'} · {selectedUrls.length}{' '}
|
|
1157
|
+
selected
|
|
1158
|
+
</span>
|
|
1159
|
+
<div className="flex items-center gap-4">
|
|
1160
|
+
<button
|
|
1161
|
+
type="button"
|
|
1162
|
+
className="text-xs text-primary hover:underline"
|
|
1163
|
+
onClick={() => {
|
|
1164
|
+
const all = selectedUrls.length !== results.length;
|
|
1165
|
+
setSelected(all ? Object.fromEntries(results.map((r) => [r.url, true])) : {});
|
|
1166
|
+
}}
|
|
1167
|
+
>
|
|
1168
|
+
{selectedUrls.length === results.length ? 'Deselect all' : 'Select all'}
|
|
1169
|
+
</button>
|
|
1170
|
+
<button
|
|
1171
|
+
type="button"
|
|
1172
|
+
className="flex items-center gap-1 text-xs text-destructive hover:underline"
|
|
1173
|
+
onClick={flushSearch}
|
|
1174
|
+
>
|
|
1175
|
+
<Trash2 className="size-3.5" />
|
|
1176
|
+
Clear
|
|
1177
|
+
</button>
|
|
1178
|
+
</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
<ul className="divide-y divide-border overflow-y-auto">
|
|
1181
|
+
{currentDisplayResults.map((r) => {
|
|
1182
|
+
const checked = !!selected[r.url];
|
|
1183
|
+
return (
|
|
1184
|
+
<li key={r.url}>
|
|
1185
|
+
<label
|
|
1186
|
+
className={cn(
|
|
1187
|
+
'flex cursor-pointer items-start gap-3 px-3 py-2.5 transition-colors hover:bg-muted/50',
|
|
1188
|
+
'bg-white',
|
|
1189
|
+
checked && 'bg-accent/50',
|
|
1190
|
+
)}
|
|
1191
|
+
>
|
|
1192
|
+
<Checkbox
|
|
1193
|
+
checked={checked}
|
|
1194
|
+
onCheckedChange={(v) => setSelected((prev) => ({ ...prev, [r.url]: !!v }))}
|
|
1195
|
+
className="mt-0.5"
|
|
1196
|
+
/>
|
|
1197
|
+
<span className="min-w-0 flex-1">
|
|
1198
|
+
<span className="block truncate text-sm font-medium">{r.title}</span>
|
|
1199
|
+
<span className="flex items-center gap-1 truncate text-xs text-muted-foreground">
|
|
1200
|
+
<Globe className="size-3 shrink-0" />
|
|
1201
|
+
{domainOf(r.url)}
|
|
1202
|
+
</span>
|
|
1203
|
+
{r.description && (
|
|
1204
|
+
<span className="mt-0.5 line-clamp-1 block text-xs text-muted-foreground/80">
|
|
1205
|
+
{r.description}
|
|
1206
|
+
</span>
|
|
1207
|
+
)}
|
|
1208
|
+
</span>
|
|
1209
|
+
</label>
|
|
1210
|
+
</li>
|
|
1211
|
+
);
|
|
1212
|
+
})}
|
|
1213
|
+
</ul>
|
|
1214
|
+
|
|
1215
|
+
{/* Local Pagination footer */}
|
|
1216
|
+
{results.length > ITEMS_PER_PAGE && (
|
|
1217
|
+
<div className="flex items-center justify-between border-t border-border px-3 py-2 bg-muted/20">
|
|
1218
|
+
<span className="text-xs text-muted-foreground">
|
|
1219
|
+
Showing {(displayPage - 1) * ITEMS_PER_PAGE + 1} to{' '}
|
|
1220
|
+
{Math.min(displayPage * ITEMS_PER_PAGE, results.length)} of {results.length} sources
|
|
1221
|
+
</span>
|
|
1222
|
+
<div className="flex items-center gap-1">
|
|
1223
|
+
<Button
|
|
1224
|
+
variant="outline"
|
|
1225
|
+
size="sm"
|
|
1226
|
+
className="h-7 px-2 text-xs"
|
|
1227
|
+
disabled={displayPage === 1}
|
|
1228
|
+
onClick={() => setDisplayPage((p) => Math.max(1, p - 1))}
|
|
1229
|
+
>
|
|
1230
|
+
Previous
|
|
1231
|
+
</Button>
|
|
1232
|
+
<div className="flex items-center gap-2 px-2 text-xs font-medium">
|
|
1233
|
+
{displayPage} / {totalDisplayPages}
|
|
1234
|
+
</div>
|
|
1235
|
+
<Button
|
|
1236
|
+
variant="outline"
|
|
1237
|
+
size="sm"
|
|
1238
|
+
className="h-7 px-2 text-xs"
|
|
1239
|
+
disabled={displayPage === totalDisplayPages}
|
|
1240
|
+
onClick={() => setDisplayPage((p) => Math.min(totalDisplayPages, p + 1))}
|
|
1241
|
+
>
|
|
1242
|
+
Next
|
|
1243
|
+
</Button>
|
|
1244
|
+
</div>
|
|
1245
|
+
</div>
|
|
1246
|
+
)}
|
|
1247
|
+
|
|
1248
|
+
{/* API Pagination footer */}
|
|
1249
|
+
{searchState?.searchProvider === 'serper' &&
|
|
1250
|
+
(searchState.hasMore || loadingMore || gatheringAll) && (
|
|
1251
|
+
<div className="flex items-center justify-between border-t border-border px-3 py-2">
|
|
1252
|
+
<span className="text-xs text-muted-foreground">
|
|
1253
|
+
{gatheringAll
|
|
1254
|
+
? `Gathering page ${gatherProgress} of ${searchState.totalPages}…`
|
|
1255
|
+
: `Page ${searchState.currentPage} of ${searchState.totalPages}`}
|
|
1256
|
+
</span>
|
|
1257
|
+
<div className="flex items-center gap-2">
|
|
1258
|
+
{gatherAllAvailable && (
|
|
1259
|
+
<Button
|
|
1260
|
+
size="sm"
|
|
1261
|
+
variant="ghost"
|
|
1262
|
+
className="h-7 gap-1 text-xs"
|
|
1263
|
+
onClick={gatherAll}
|
|
1264
|
+
>
|
|
1265
|
+
<ChevronsDown className="size-3.5" />
|
|
1266
|
+
Gather all ({searchState.totalPages - searchState.currentPage} more pages)
|
|
1267
|
+
</Button>
|
|
1268
|
+
)}
|
|
1269
|
+
<Button
|
|
1270
|
+
size="sm"
|
|
1271
|
+
variant="outline"
|
|
1272
|
+
className="h-7 gap-1 text-xs"
|
|
1273
|
+
onClick={loadNextPage}
|
|
1274
|
+
disabled={!searchState?.hasMore || loadingMore || gatheringAll}
|
|
1275
|
+
>
|
|
1276
|
+
{loadingMore ? (
|
|
1277
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
1278
|
+
) : (
|
|
1279
|
+
<ChevronDown className="size-3.5" />
|
|
1280
|
+
)}
|
|
1281
|
+
Load next page
|
|
1282
|
+
</Button>
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
)}
|
|
1286
|
+
</div>
|
|
1287
|
+
)}
|
|
1288
|
+
</div>
|
|
1289
|
+
);
|
|
1290
|
+
}
|