diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 70d7fb07e..000000000 --- a/.dockerignore +++ /dev/null @@ -1,97 +0,0 @@ -# Git -.git -.gitignore -.gitattributes - -# Documentation -*.md -!README.md -docs/ -CONTRIBUTING.md -CODE_OF_CONDUCT.md -LICENSE - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -.cursor/ - -# Node -**/node_modules/ -**/.next/ -**/dist/ -**/.turbo/ -**/.cache/ -**/coverage/ - -# Python -**/__pycache__/ -**/*.pyc -**/*.pyo -**/*.pyd -**/.Python -**/build/ -**/develop-eggs/ -**/downloads/ -**/eggs/ -**/.eggs/ -# Python venv lib folders (but not frontend lib folders) -surfsense_backend/lib/ -surfsense_backend/lib64/ -**/parts/ -**/sdist/ -**/var/ -**/wheels/ -**/*.egg-info/ -**/.installed.cfg -**/*.egg -**/pip-log.txt -**/.tox/ -**/.coverage -**/htmlcov/ -**/.pytest_cache/ -**/nosetests.xml -**/coverage.xml - -# Environment -**/.env -**/.env.* -!**/.env.example -**/*.local - -# Docker -**/Dockerfile -**/docker-compose*.yml -**/.docker/ - -# Testing -**/tests/ -**/test/ -**/__tests__/ -**/*.test.* -**/*.spec.* - -# Logs -**/*.log - -# Temporary files -**/tmp/ -**/temp/ -**/.tmp/ -**/.temp/ - -# Build artifacts from backend -surfsense_backend/podcasts/ -surfsense_backend/temp_audio/ -surfsense_backend/*.bak -surfsense_backend/*.dat -surfsense_backend/*.dir - -# GitHub -.github/ - -# Browser extension (not needed for main deployment) -surfsense_browser_extension/ - diff --git a/.github/workflows/docker_build.yaml b/.github/workflows/docker-build.yml similarity index 66% rename from .github/workflows/docker_build.yaml rename to .github/workflows/docker-build.yml index 15b89198e..a53a4b414 100644 --- a/.github/workflows/docker_build.yaml +++ b/.github/workflows/docker-build.yml @@ -26,6 +26,7 @@ permissions: jobs: tag_release: runs-on: ubuntu-latest + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event_name == 'workflow_dispatch' outputs: new_tag: ${{ steps.tag_version.outputs.next_version }} steps: @@ -86,6 +87,7 @@ jobs: build: needs: tag_release + if: always() && (needs.tag_release.result == 'success' || needs.tag_release.result == 'skipped') runs-on: ${{ matrix.os }} permissions: packages: write @@ -121,6 +123,12 @@ jobs: id: image run: echo "name=${REGISTRY_IMAGE,,}" >> $GITHUB_OUTPUT + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.image.outputs.name }} + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: @@ -139,14 +147,15 @@ jobs: sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true docker system prune -af - - name: Build and push ${{ matrix.name }} (${{ matrix.suffix }}) + - name: Build and push by digest ${{ matrix.name }} (${{ matrix.suffix }}) id: build uses: docker/build-push-action@v6 with: context: ${{ matrix.context }} file: ${{ matrix.file }} - push: true - tags: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}-${{ matrix.suffix }} + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.image.outputs.name }} + outputs: type=image,push-by-digest=true,name-canonical=true,push=true platforms: ${{ matrix.platform }} cache-from: type=gha,scope=${{ matrix.image }}-${{ matrix.suffix }} cache-to: type=gha,mode=max,scope=${{ matrix.image }}-${{ matrix.suffix }} @@ -159,9 +168,24 @@ jobs: ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ELECTRIC_AUTH_MODE=__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__' || '' }} ${{ matrix.image == 'web' && 'NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__' || '' }} + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.image }}-${{ matrix.suffix }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + create_manifest: runs-on: ubuntu-latest needs: [tag_release, build] + if: always() && needs.build.result == 'success' permissions: packages: write contents: read @@ -170,7 +194,9 @@ jobs: matrix: include: - name: surfsense-backend + image: backend - name: surfsense-web + image: web env: REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.name }} @@ -179,6 +205,21 @@ jobs: id: image run: echo "name=${REGISTRY_IMAGE,,}" >> $GITHUB_OUTPUT + - name: Download amd64 digest + uses: actions/download-artifact@v4 + with: + name: digests-${{ matrix.image }}-amd64 + path: /tmp/digests + + - name: Download arm64 digest + uses: actions/download-artifact@v4 + with: + name: digests-${{ matrix.image }}-arm64 + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: @@ -186,35 +227,41 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create and push multi-arch manifest + - name: Compute app version + id: appver run: | VERSION_TAG="${{ needs.tag_release.outputs.new_tag }}" - IMAGE="${{ steps.image.outputs.name }}" - APP_VERSION=$(echo "$VERSION_TAG" | rev | cut -d. -f2- | rev) - - docker manifest create ${IMAGE}:${VERSION_TAG} \ - ${IMAGE}:${VERSION_TAG}-amd64 \ - ${IMAGE}:${VERSION_TAG}-arm64 - - docker manifest push ${IMAGE}:${VERSION_TAG} - - if [[ "${{ github.ref }}" == "refs/heads/${{ github.event.repository.default_branch }}" ]] || [[ "${{ github.event.inputs.branch }}" == "${{ github.event.repository.default_branch }}" ]]; then - docker manifest create ${IMAGE}:${APP_VERSION} \ - ${IMAGE}:${VERSION_TAG}-amd64 \ - ${IMAGE}:${VERSION_TAG}-arm64 - - docker manifest push ${IMAGE}:${APP_VERSION} - - docker manifest create ${IMAGE}:latest \ - ${IMAGE}:${VERSION_TAG}-amd64 \ - ${IMAGE}:${VERSION_TAG}-arm64 - - docker manifest push ${IMAGE}:latest + if [ -n "$VERSION_TAG" ]; then + APP_VERSION=$(echo "$VERSION_TAG" | rev | cut -d. -f2- | rev) + else + APP_VERSION="" fi + echo "app_version=$APP_VERSION" >> $GITHUB_OUTPUT + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.image.outputs.name }} + tags: | + type=raw,value=${{ needs.tag_release.outputs.new_tag }},enable=${{ needs.tag_release.outputs.new_tag != '' }} + type=raw,value=${{ steps.appver.outputs.app_version }},enable=${{ needs.tag_release.outputs.new_tag != '' && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event.inputs.branch == github.event.repository.default_branch) }} + type=ref,event=branch + type=sha,prefix=git- + flavor: | + latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event.inputs.branch == github.event.repository.default_branch }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ steps.image.outputs.name }}@sha256:%s ' *) + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ steps.image.outputs.name }}:${{ steps.meta.outputs.version }} - name: Summary - run: | + run: | echo "Multi-arch manifest created for ${{ matrix.name }}!" - echo "Versioned: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}" - echo "App version: ${{ steps.image.outputs.name }}:$(echo '${{ needs.tag_release.outputs.new_tag }}' | rev | cut -d. -f2- | rev)" - echo "Latest: ${{ steps.image.outputs.name }}:latest" + echo "Tags: $(jq -cr '.tags | join(", ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")" diff --git a/docker/.env.example b/docker/.env.example index 7025cac52..c31b87185 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -33,9 +33,9 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # Ports (change to avoid conflicts with other services on your machine) # ------------------------------------------------------------------------------ -# BACKEND_PORT=8000 -# FRONTEND_PORT=3000 -# ELECTRIC_PORT=5133 +# BACKEND_PORT=8929 +# FRONTEND_PORT=3929 +# ELECTRIC_PORT=5929 # FLOWER_PORT=5555 # ============================================================================== diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index b76f26b2d..4d602f584 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -8,7 +8,7 @@ # For production with prebuilt images, use docker/docker-compose.yml instead. # ============================================================================= -name: surfsense +name: surfsense-dev services: db: @@ -162,8 +162,9 @@ services: image: electricsql/electric:1.4.10 ports: - "${ELECTRIC_PORT:-5133}:3000" - # depends_on: - # - db + depends_on: + db: + condition: service_healthy environment: - DATABASE_URL=${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} - ELECTRIC_INSECURE=true @@ -197,10 +198,10 @@ services: volumes: postgres_data: - name: surfsense-postgres + name: surfsense-dev-postgres pgadmin_data: - name: surfsense-pgadmin + name: surfsense-dev-pgadmin redis_data: - name: surfsense-redis + name: surfsense-dev-redis shared_temp: - name: surfsense-shared-temp + name: surfsense-dev-shared-temp diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9fca4dfb5..ca20e3ed4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -45,7 +45,7 @@ services: backend: image: ghcr.io/modsetter/surfsense-backend:${SURFSENSE_VERSION:-latest} ports: - - "${BACKEND_PORT:-8000}:8000" + - "${BACKEND_PORT:-8929}:8000" volumes: - shared_temp:/shared_tmp env_file: @@ -61,7 +61,7 @@ services: UNSTRUCTURED_HAS_PATCHED_LOOP: "1" ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric} ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password} - NEXT_FRONTEND_URL: ${NEXT_FRONTEND_URL:-http://localhost:${FRONTEND_PORT:-3000}} + NEXT_FRONTEND_URL: ${NEXT_FRONTEND_URL:-http://localhost:${FRONTEND_PORT:-3929}} # Daytona Sandbox – uncomment and set credentials to enable cloud code execution # DAYTONA_SANDBOX_ENABLED: "TRUE" # DAYTONA_API_KEY: ${DAYTONA_API_KEY:-} @@ -151,7 +151,7 @@ services: electric: image: electricsql/electric:1.4.10 ports: - - "${ELECTRIC_PORT:-5133}:3000" + - "${ELECTRIC_PORT:-5929}:3000" environment: DATABASE_URL: ${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} ELECTRIC_INSECURE: "true" @@ -169,10 +169,10 @@ services: frontend: image: ghcr.io/modsetter/surfsense-web:${SURFSENSE_VERSION:-latest} ports: - - "${FRONTEND_PORT:-3000}:3000" + - "${FRONTEND_PORT:-3929}:3000" environment: - NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8000}} - NEXT_PUBLIC_ELECTRIC_URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:${ELECTRIC_PORT:-5133}} + NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8929}} + NEXT_PUBLIC_ELECTRIC_URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:${ELECTRIC_PORT:-5929}} NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL} NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING} NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted} diff --git a/docker/scripts/install.ps1 b/docker/scripts/install.ps1 index fc9c75a28..5f41ef7d6 100644 --- a/docker/scripts/install.ps1 +++ b/docker/scripts/install.ps1 @@ -321,9 +321,9 @@ Write-Host " OSS Alternative to NotebookLM for Teams [$versionDisplay]" Write-Host ("=" * 62) -ForegroundColor Cyan Write-Host "" -Write-Info " Frontend: http://localhost:3000" -Write-Info " Backend: http://localhost:8000" -Write-Info " API Docs: http://localhost:8000/docs" +Write-Info " Frontend: http://localhost:3929" +Write-Info " Backend: http://localhost:8929" +Write-Info " API Docs: http://localhost:8929/docs" Write-Info "" Write-Info " Config: $InstallDir\.env" Write-Info " Logs: cd $InstallDir; docker compose logs -f" diff --git a/docker/scripts/install.sh b/docker/scripts/install.sh index c4a0d5c9f..eb6aeb83d 100644 --- a/docker/scripts/install.sh +++ b/docker/scripts/install.sh @@ -304,9 +304,9 @@ _version_display="${_version_display:-latest}" printf " OSS Alternative to NotebookLM for Teams ${YELLOW}[%s]${NC}\n" "${_version_display}" printf "${CYAN}══════════════════════════════════════════════════════════════${NC}\n\n" -info " Frontend: http://localhost:3000" -info " Backend: http://localhost:8000" -info " API Docs: http://localhost:8000/docs" +info " Frontend: http://localhost:3929" +info " Backend: http://localhost:8929" +info " API Docs: http://localhost:8929/docs" info "" info " Config: ${INSTALL_DIR}/.env" info " Logs: cd ${INSTALL_DIR} && ${DC} logs -f" diff --git a/surfsense_web/app/dashboard/[search_space_id]/more-pages/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/more-pages/page.tsx index 6779dc5d0..27c451d2f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/more-pages/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/more-pages/page.tsx @@ -9,7 +9,14 @@ import { useEffect } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Dialog, DialogContent, @@ -108,37 +115,26 @@ export default function MorePagesPage() {
- {task.completed ? ( - - ) : ( - - )} + {task.completed ? : }

{task.title}

-

- +{task.pages_reward} pages -

+

+{task.pages_reward} pages

- - - )} + {/* Content */} +
+
+
+ {/* LLM Configuration Warning */} + {!llmConfigLoading && !hasDocumentSummaryLLM && ( + + + LLM Configuration Required + +

+ {isAutoMode && !hasGlobalConfigs + ? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources." + : "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."} +

+ +
+
+ )} - - + {}} + onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}} + onCreateWebcrawler={ + hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {} + } + onCreateYouTubeCrawler={ + hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {} + } + onManage={handleStartEdit} + onViewAccountsList={handleViewAccountsList} + /> + + + {}} - onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}} - onCreateWebcrawler={hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}} - onCreateYouTubeCrawler={ - hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {} - } + onTabChange={handleTabChange} onManage={handleStartEdit} onViewAccountsList={handleViewAccountsList} /> - - - +
+ {/* Bottom fade shadow */} +
- {/* Bottom fade shadow */} -
-
- - )} - - - ); -}); + + )} + + + ); + } +); ConnectorIndicator.displayName = "ConnectorIndicator"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx index 044bb5565..a4f145e06 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx @@ -70,23 +70,21 @@ export const BaiduSearchApiConnectForm: FC = ({ onSubmit, isSu return (
- - -
- API Key Required - - You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing - up at{" "} - - qianfan.cloud.baidu.com - - -
+ + + API Key Required + + You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing + up at{" "} + + qianfan.cloud.baidu.com + +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx index 65edb095c..b2a6b0b25 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx @@ -96,15 +96,13 @@ export const BookStackConnectForm: FC = ({ onSubmit, isSubmitt return (
- - -
- API Token Required - - You'll need a BookStack API Token to use this connector. You can create one from your - BookStack instance settings. - -
+ + + API Token Required + + You'll need a BookStack API Token to use this connector. You can create one from your + BookStack instance settings. +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx index 625856e46..c23dfcf03 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx @@ -64,15 +64,13 @@ export const CirclebackConnectForm: FC = ({ onSubmit, isSubmit return (
- - -
- Webhook-Based Integration - - Circleback uses webhooks to automatically send meeting data. After connecting, you'll - receive a webhook URL to configure in your Circleback settings. - -
+ + + Webhook-Based Integration + + Circleback uses webhooks to automatically send meeting data. After connecting, you'll + receive a webhook URL to configure in your Circleback settings. +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx index 95b93cd20..b3298cbe0 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx @@ -172,14 +172,12 @@ export const ElasticsearchConnectForm: FC = ({ onSubmit, isSub return (
- - -
- API Key Required - - Enter your Elasticsearch cluster endpoint URL and authentication credentials to connect. - -
+ + + API Key Required + + Enter your Elasticsearch cluster endpoint URL and authentication credentials to connect. +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx index 9d0ef2c45..68d5ef5fb 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx @@ -105,24 +105,21 @@ export const GithubConnectForm: FC = ({ onSubmit, isSubmitting return (
- - -
- Personal Access Token (Optional) - - A GitHub PAT is only required for private repositories. Public repos work without a - token.{" "} - - Get your token - - {" "} - -
+ + + Personal Access Token (Optional) + + A GitHub PAT is only required for private repositories. Public repos work without a token.{" "} + + Get your token + + {" "} +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx index 7c4ff0a46..b9771e95a 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx @@ -70,22 +70,20 @@ export const LinkupApiConnectForm: FC = ({ onSubmit, isSubmitt return (
- - -
- API Key Required - - You'll need a Linkup API key to use this connector. You can get one by signing up at{" "} - - linkup.so - - -
+ + + API Key Required + + You'll need a Linkup API key to use this connector. You can get one by signing up at{" "} + + linkup.so + +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx index 63be3cc93..13cc7efe1 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx @@ -88,22 +88,20 @@ export const LumaConnectForm: FC = ({ onSubmit, isSubmitting } return (
- - -
- API Key Required - - You'll need a Luma API Key to use this connector. You can create one from{" "} - - Luma API Settings - - -
+ + + API Key Required + + You'll need a Luma API Key to use this connector. You can create one from{" "} + + Luma API Settings + +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx index 9ece079f3..2a5bb2121 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx @@ -136,7 +136,7 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) return (
- + Connect to an MCP (Model Context Protocol) server. Each MCP server is added as a separate @@ -230,55 +230,51 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) ) : ( )} -
-
- - {testResult.status === "success" - ? "Connection Successful" - : "Connection Failed"} - - {testResult.tools.length > 0 && ( - - )} -
- - {testResult.message} - {showDetails && testResult.tools.length > 0 && ( -
-

Available tools:

-
    - {testResult.tools.map((tool, i) => ( -
  • {tool.name}
  • - ))} -
-
- )} -
+
+ + {testResult.status === "success" ? "Connection Successful" : "Connection Failed"} + + {testResult.tools.length > 0 && ( + + )}
+ + {testResult.message} + {showDetails && testResult.tools.length > 0 && ( +
+

Available tools:

+
    + {testResult.tools.map((tool, i) => ( +
  • {tool.name}
  • + ))} +
+
+ )} +
)}
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx index 3c4b64090..08c1dd30c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx @@ -102,15 +102,13 @@ export const ObsidianConnectForm: FC = ({ onSubmit, isSubmitti return (
- - -
- Self-Hosted Only - - This connector requires direct file system access and only works with self-hosted - SurfSense installations. - -
+ + + Self-Hosted Only + + This connector requires direct file system access and only works with self-hosted + SurfSense installations. +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx index ecf219924..5ff54fb9e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx @@ -123,23 +123,21 @@ export const SearxngConnectForm: FC = ({ onSubmit, isSubmittin return (
- - -
- SearxNG Instance Required - - You need access to a running SearxNG instance. Refer to the{" "} - - SearxNG installation guide - {" "} - for setup instructions. If your instance requires an API key, include it below. - -
+ + + SearxNG Instance Required + + You need access to a running SearxNG instance. Refer to the{" "} + + SearxNG installation guide + {" "} + for setup instructions. If your instance requires an API key, include it below. +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx index a8032e11a..57d183d44 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx @@ -70,22 +70,20 @@ export const TavilyApiConnectForm: FC = ({ onSubmit, isSubmitt return (
- - -
- API Key Required - - You'll need a Tavily API key to use this connector. You can get one by signing up at{" "} - - tavily.com - - -
+ + + API Key Required + + You'll need a Tavily API key to use this connector. You can get one by signing up at{" "} + + tavily.com + +
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx index 568b47d09..3ab9cba53 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx @@ -166,7 +166,7 @@ export const CirclebackConfig: FC = ({ connector, onNameC Configuration Instructions - + Configure this URL in Circleback Settings → Automations → Create automation → Send webhook request. The webhook will automatically send meeting notes, transcripts, and action items to this search space. diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx index ac450677e..38d60d7bd 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx @@ -235,55 +235,51 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam ) : ( )} -
-
- - {testResult.status === "success" - ? "Connection Successful" - : "Connection Failed"} - - {testResult.tools.length > 0 && ( - - )} -
- - {testResult.message} - {showDetails && testResult.tools.length > 0 && ( -
-

Available tools:

-
    - {testResult.tools.map((tool) => ( -
  • {tool.name}
  • - ))} -
-
- )} -
+
+ + {testResult.status === "success" ? "Connection Successful" : "Connection Failed"} + + {testResult.tools.length > 0 && ( + + )}
+ + {testResult.message} + {showDetails && testResult.tools.length > 0 && ( +
+

Available tools:

+
    + {testResult.tools.map((tool) => ( +
  • {tool.name}
  • + ))} +
+
+ )} +
)}
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/webcrawler-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/webcrawler-config.tsx index 22b14842b..164d78e09 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/webcrawler-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/webcrawler-config.tsx @@ -63,7 +63,8 @@ export const WebcrawlerConfig: FC = ({ connector, onConfig

- Want a quick answer from a webpage without indexing it? Just paste the URL directly into the chat instead. + Want a quick answer from a webpage without indexing it? Just paste the URL directly into + the chat instead.

@@ -123,9 +124,9 @@ export const WebcrawlerConfig: FC = ({ connector, onConfig
{/* Info Alert */} - - - + + + Configuration is saved when you start indexing. You can update these settings anytime from the connector management page. diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx index c8e565df5..7ec85f4d3 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx @@ -280,9 +280,7 @@ export const YouTubeCrawlerView: FC = ({ searchSpaceId,
-

- {t("chat_tip")} -

+

{t("chat_tip")}

diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 4281ca2a7..51a9b9275 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -14,7 +14,6 @@ import { AlertCircle, ArrowDownIcon, ArrowUpIcon, - Cable, CheckIcon, ChevronLeftIcon, ChevronRightIcon, @@ -23,17 +22,20 @@ import { PlusIcon, RefreshCwIcon, SquareIcon, - SquareLibrary, + Unplug, + Upload, + X, } from "lucide-react"; import { useParams } from "next/navigation"; import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; -import { showCommentsGutterAtom } from "@/atoms/chat/current-thread.atom"; import { mentionedDocumentsAtom, sidebarSelectedDocumentsAtom, } from "@/atoms/chat/mentioned-documents.atom"; +import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; +import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms"; import { membersAtom } from "@/atoms/members/members-query.atoms"; import { @@ -48,6 +50,7 @@ import { ConnectorIndicator, type ConnectorIndicatorHandle, } from "@/components/assistant-ui/connector-popup"; +import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { InlineMentionEditor, type InlineMentionEditorRef, @@ -60,13 +63,21 @@ import { import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { UserMessage } from "@/components/assistant-ui/user-message"; +import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel"; import { DocumentMentionPicker, type DocumentMentionPickerRef, } from "@/components/new-chat/document-mention-picker"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; +import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { Document } from "@/contracts/types/document.types"; import { useBatchCommentsPreload } from "@/hooks/use-comments"; import { useCommentsElectric } from "@/hooks/use-comments-electric"; @@ -95,8 +106,6 @@ export const Thread: FC = ({ messageThinkingSteps = new Map() }) => }; const ThreadContent: FC = () => { - const showGutter = useAtomValue(showCommentsGutterAtom); - return ( { > thread.isEmpty}> @@ -228,6 +234,72 @@ const ThreadWelcome: FC = () => { ); }; +const BANNER_CONNECTORS = [ + { type: "GOOGLE_DRIVE_CONNECTOR", label: "Google Drive" }, + { type: "GOOGLE_GMAIL_CONNECTOR", label: "Gmail" }, + { type: "NOTION_CONNECTOR", label: "Notion" }, + { type: "YOUTUBE_CONNECTOR", label: "YouTube" }, + { type: "SLACK_CONNECTOR", label: "Slack" }, +] as const; + +const BANNER_DISMISSED_KEY = "surfsense-connect-tools-banner-dismissed"; + +const ConnectToolsBanner: FC = () => { + const { data: connectors } = useAtomValue(connectorsAtom); + const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom); + const [dismissed, setDismissed] = useState(() => { + if (typeof window === "undefined") return false; + return localStorage.getItem(BANNER_DISMISSED_KEY) === "true"; + }); + + const hasConnectors = (connectors?.length ?? 0) > 0; + + if (dismissed || hasConnectors) return null; + + const handleDismiss = (e: React.MouseEvent) => { + e.stopPropagation(); + setDismissed(true); + localStorage.setItem(BANNER_DISMISSED_KEY, "true"); + }; + + return ( +
+ +
+ ); +}; + const Composer: FC = () => { // Document mention state (atoms persist across component remounts) const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom); @@ -312,6 +384,16 @@ const Composer: FC = () => { } }, [isThreadEmpty]); + // Close document picker when a slide-out panel (inbox, shared/private chats) opens + useEffect(() => { + const handler = () => { + setShowDocumentPopover(false); + setMentionQuery(""); + }; + window.addEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler); + return () => window.removeEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler); + }, []); + // Sync editor text with assistant-ui composer runtime const handleEditorChange = useCallback( (text: string) => { @@ -425,9 +507,9 @@ const Composer: FC = () => { currentUserId={currentUser?.id ?? null} members={members ?? []} /> -
+
{/* Inline editor with @mention support */} -
+
{ document.body )} +
); @@ -481,7 +564,9 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false const setDocumentsSidebarOpen = useSetAtom(documentsSidebarOpenAtom); const connectorRef = useRef(null); const [addMenuOpen, setAddMenuOpen] = useState(false); - + const { openDialog: openUploadDialog } = useDocumentUploadDialog(); + const { data: connectors } = useAtomValue(connectorsAtom); + const connectorCount = connectors?.length ?? 0; const isComposerTextEmpty = useAssistantState(({ composer }) => { const text = composer.text?.trim() || ""; return text.length === 0; @@ -506,55 +591,61 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser; return ( -
+
- - + + - - + -
- - -
-
-
+ { + setAddMenuOpen(false); + openUploadDialog(); + }} + > + + Upload files + + { + setAddMenuOpen(false); + connectorRef.current?.open(); + }} + > + + {connectorCount > 0 ? "Manage tools" : "Connect your tools"} + {connectorCount > 0 && ( + {connectorCount} + )} + + + + {sidebarDocs.length > 0 && ( + + )}
{!hasModelConfigured && ( @@ -565,16 +656,6 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false )}
- {sidebarDocs.length > 0 && ( - - )} - !thread.isRunning}> ([]); @@ -257,44 +258,46 @@ export function CommentComposer({ }, [adjustTextareaHeight]); return ( -
- !open && closeMentionPicker()} - modal={false} - > - -