Merge remote-tracking branch 'upstream/dev' into fix/connector

This commit is contained in:
Anish Sarkar 2026-01-07 15:16:24 +05:30
commit 8b398bb9f2
54 changed files with 1471 additions and 400 deletions

View file

@ -37,14 +37,11 @@ COPY surfsense_web/ ./
# Run fumadocs-mdx postinstall now that source files are available
RUN pnpm fumadocs-mdx
# Build args for frontend
ARG NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
ARG NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
ARG NEXT_PUBLIC_ETL_SERVICE=DOCLING
ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=$NEXT_PUBLIC_FASTAPI_BACKEND_URL
ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=$NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE
ENV NEXT_PUBLIC_ETL_SERVICE=$NEXT_PUBLIC_ETL_SERVICE
# Build with placeholder values that will be replaced at runtime
# These unique strings allow runtime substitution via entrypoint script
ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__
ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
ENV NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__
# Build
RUN pnpm run build
@ -233,6 +230,12 @@ ENV AUTH_TYPE=LOCAL
ENV ETL_SERVICE=DOCLING
ENV EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
# Frontend configuration (can be overridden at runtime)
# These are injected into the Next.js build at container startup
ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
ENV NEXT_PUBLIC_ETL_SERVICE=DOCLING
# Data volume
VOLUME ["/data"]

View file

@ -96,6 +96,30 @@ if [ -d /app/frontend/.next/standalone ]; then
cp -r /app/frontend/.next/static /app/frontend/.next/static 2>/dev/null || true
fi
# ================================================
# Runtime Environment Variable Replacement
# ================================================
# Next.js NEXT_PUBLIC_* vars are baked in at build time.
# This replaces placeholder values with actual runtime env vars.
echo "🔧 Applying runtime environment configuration..."
# Set defaults if not provided
NEXT_PUBLIC_FASTAPI_BACKEND_URL="${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}"
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE="${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}"
NEXT_PUBLIC_ETL_SERVICE="${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}"
# Replace placeholders in all JS files
find /app/frontend -type f \( -name "*.js" -o -name "*.json" \) -exec sed -i \
-e "s|__NEXT_PUBLIC_FASTAPI_BACKEND_URL__|${NEXT_PUBLIC_FASTAPI_BACKEND_URL}|g" \
-e "s|__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__|${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}|g" \
-e "s|__NEXT_PUBLIC_ETL_SERVICE__|${NEXT_PUBLIC_ETL_SERVICE}|g" \
{} +
echo "✅ Environment configuration applied"
echo " Backend URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}"
echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}"
echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}"
# ================================================
# Run database migrations
# ================================================
@ -135,10 +159,10 @@ echo "==========================================="
echo " 📋 Configuration"
echo "==========================================="
echo " Frontend URL: http://localhost:3000"
echo " Backend API: http://localhost:8000"
echo " API Docs: http://localhost:8000/docs"
echo " Auth Type: ${AUTH_TYPE:-LOCAL}"
echo " ETL Service: ${ETL_SERVICE:-DOCLING}"
echo " Backend API: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}"
echo " API Docs: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}/docs"
echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}"
echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}"
echo " TTS Service: ${TTS_SERVICE}"
echo " STT Service: ${STT_SERVICE}"
echo "==========================================="

View file

@ -17,6 +17,7 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer
import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup";
import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { OnboardingTour } from "@/components/onboarding-tour";
import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
@ -242,6 +243,7 @@ export function DashboardClientLayout({
return (
<DocumentUploadDialogProvider>
<OnboardingTour />
<SidebarProvider className="h-full overflow-hidden" open={open} onOpenChange={setOpen}>
{/* Use AppSidebarProvider which fetches user, search space, and recent chats */}
<AppSidebarProvider

View file

@ -162,6 +162,7 @@ export const ConnectorIndicator: FC = () => {
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<TooltipIconButton
data-joyride="connector-icon"
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
side="bottom"
className={cn(

View file

@ -0,0 +1,742 @@
"use client";
import { useAtomValue } from "jotai";
import { useQuery } from "@tanstack/react-query";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import { fetchThreads } from "@/lib/chat/thread-persistence";
interface TourStep {
target: string;
title: string;
content: string;
placement: "top" | "bottom" | "left" | "right";
}
const TOUR_STEPS: TourStep[] = [
{
target: '[data-joyride="connector-icon"]',
title: "Connect your data sources",
content: "Connect and sync data from Gmail, Drive, Slack, Notion, Jira, Confluence, and more.",
placement: "bottom",
},
{
target: '[data-joyride="documents-sidebar"]',
title: "Manage your documents",
content: "Access and manage all your uploaded documents.",
placement: "right",
},
];
interface TooltipPosition {
top: number;
left: number;
pointerPosition: "top" | "bottom" | "left" | "right";
}
function calculatePosition(targetEl: Element, placement: TourStep["placement"]): TooltipPosition {
const rect = targetEl.getBoundingClientRect();
const scrollTop = window.scrollY;
const scrollLeft = window.scrollX;
const tooltipWidth = 280;
const tooltipHeight = 120;
const offset = 16;
let top = 0;
let left = 0;
let pointerPosition: TooltipPosition["pointerPosition"] = "left";
switch (placement) {
case "bottom":
top = rect.bottom + scrollTop + offset;
left = rect.left + scrollLeft + rect.width / 2 - tooltipWidth / 2;
pointerPosition = "top";
break;
case "top":
top = rect.top + scrollTop - tooltipHeight - offset;
left = rect.left + scrollLeft + rect.width / 2 - tooltipWidth / 2;
pointerPosition = "bottom";
break;
case "right":
top = rect.top + scrollTop + rect.height / 2 - tooltipHeight / 2;
left = rect.right + scrollLeft + offset;
pointerPosition = "left";
break;
case "left":
top = rect.top + scrollTop + rect.height / 2 - tooltipHeight / 2;
left = rect.left + scrollLeft - tooltipWidth - offset;
pointerPosition = "right";
break;
}
// Ensure tooltip stays within viewport
left = Math.max(10, Math.min(left, window.innerWidth - tooltipWidth - 10));
top = Math.max(10, top);
return { top, left, pointerPosition };
}
function Spotlight({
targetEl,
isDarkMode,
currentStepTarget,
}: {
targetEl: Element;
isDarkMode: boolean;
currentStepTarget: string;
}) {
const rect = targetEl.getBoundingClientRect();
const padding = 6;
const shadowColor = isDarkMode ? "#172554" : "#3b82f6";
// Check if this is the connector icon step - verify both the selector matches AND the element matches
// This prevents the shape from changing before targetEl updates
const isConnectorSelector = currentStepTarget === '[data-joyride="connector-icon"]';
const isConnectorElement = targetEl.matches('[data-joyride="connector-icon"]');
const isConnectorStep = isConnectorSelector && isConnectorElement;
// For circle, use the larger dimension to ensure it's a perfect circle
const circleSize = isConnectorStep ? Math.max(rect.width, rect.height) : 0;
const circleTop = isConnectorStep ? rect.top + (rect.height - circleSize) / 2 : rect.top;
const circleLeft = isConnectorStep ? rect.left + (rect.width - circleSize) / 2 : rect.left;
return (
<>
{/* Dark overlay with cutout using box-shadow technique */}
<div
className="fixed pointer-events-none"
style={{
top: isConnectorStep ? circleTop - padding : rect.top - padding,
left: isConnectorStep ? circleLeft - padding : rect.left - padding,
width: isConnectorStep ? circleSize + padding * 2 : rect.width + padding * 2,
height: isConnectorStep ? circleSize + padding * 2 : rect.height + padding * 2,
borderRadius: isConnectorStep ? "50%" : 8,
boxShadow: isDarkMode
? `0 0 0 9999px rgba(0, 0, 0, 0.6)`
: `0 0 0 9999px rgba(0, 0, 0, 0.3)`,
backgroundColor: "transparent",
zIndex: 99996,
}}
/>
{/* Blue shadow behind the button - starts from button border */}
<div
className="fixed pointer-events-none"
style={{
top: isConnectorStep ? circleTop : rect.top,
left: isConnectorStep ? circleLeft : rect.left,
width: isConnectorStep ? circleSize : rect.width,
height: isConnectorStep ? circleSize : rect.height,
borderRadius: isConnectorStep ? "50%" : 8,
boxShadow: isDarkMode
? `0 0 10px 2px ${shadowColor}CC, 0 0 20px 6px ${shadowColor}99, 0 0 40px 12px ${shadowColor}66`
: `0 0 6px 1px ${shadowColor}80, 0 0 12px 3px ${shadowColor}50, 0 0 20px 6px ${shadowColor}30`,
backgroundColor: "transparent",
zIndex: 99997,
}}
/>
</>
);
}
function TourTooltip({
step,
stepIndex,
totalSteps,
position,
onNext,
onPrev,
onSkip,
isDarkMode,
}: {
step: TourStep;
stepIndex: number;
totalSteps: number;
position: TooltipPosition;
targetRect: DOMRect;
onNext: () => void;
onPrev: () => void;
onSkip: () => void;
isDarkMode: boolean;
}) {
const [contentKey, setContentKey] = useState(stepIndex);
const [shouldAnimate, setShouldAnimate] = useState(false);
const prevStepIndexRef = useRef(stepIndex);
const isLastStep = stepIndex === totalSteps - 1;
const isFirstStep = stepIndex === 0;
// Update content key when step changes to trigger animation
// Only animate if stepIndex actually changes (not on initial mount)
useEffect(() => {
if (prevStepIndexRef.current !== stepIndex) {
setShouldAnimate(true);
setContentKey(stepIndex);
prevStepIndexRef.current = stepIndex;
}
}, [stepIndex]);
const bgColor = isDarkMode ? "#18181b" : "#ffffff";
const textColor = isDarkMode ? "#ffffff" : "#18181b";
const mutedTextColor = isDarkMode ? "#a1a1aa" : "#71717a";
// Calculate pointer line position
const getPointerStyles = (): React.CSSProperties => {
const lineLength = 16;
const dotSize = 6;
// Check if this is the documents step (stepIndex === 1)
const isDocumentsStep = stepIndex === 1;
if (position.pointerPosition === "left") {
return {
position: "absolute",
left: -lineLength - dotSize,
top: isDocumentsStep ? "calc(50% - 8px)" : "50%",
transform: "translateY(-50%)",
display: "flex",
alignItems: "center",
};
}
if (position.pointerPosition === "top") {
return {
position: "absolute",
top: -lineLength - dotSize,
left: "50%",
transform: "translateX(-50%)",
display: "flex",
flexDirection: "column",
alignItems: "center",
};
}
return {};
};
const renderPointer = () => {
const lineColor = isDarkMode ? "#18181B" : "#ffffff";
if (position.pointerPosition === "left") {
return (
<div style={getPointerStyles()}>
<div
style={{
width: 6,
height: 6,
borderRadius: "50%",
backgroundColor: lineColor,
}}
/>
<div
style={{
width: 16,
height: 2,
backgroundColor: lineColor,
}}
/>
</div>
);
}
if (position.pointerPosition === "top") {
return (
<div style={getPointerStyles()}>
<div
style={{
width: 6,
height: 6,
borderRadius: "50%",
backgroundColor: lineColor,
}}
/>
<div
style={{
width: 2,
height: 16,
backgroundColor: lineColor,
}}
/>
</div>
);
}
return null;
};
// Render step dots
const renderStepDots = () => {
return (
<div className="flex items-center gap-1.5">
{Array.from({ length: totalSteps }).map((_, i) => (
<div
key={TOUR_STEPS[i]?.target ?? `step-${i}`}
style={{
width: 6,
height: 6,
borderRadius: "50%",
backgroundColor:
i === stepIndex
? isDarkMode
? "#ffffff"
: "#18181b"
: isDarkMode
? "#52525b"
: "#d4d4d8",
transition: "background-color 0.2s",
}}
/>
))}
</div>
);
};
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="tour-title"
className="fixed z-[99999]"
style={{
top: position.top,
left: position.left,
width: 280,
transition: "top 0.4s cubic-bezier(0.4, 0, 0.2, 1), left 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{/* Pointer line */}
{renderPointer()}
<div
className="relative rounded-lg p-4"
style={{
backgroundColor: bgColor,
color: textColor,
boxShadow: isDarkMode
? "0 25px 50px -12px rgba(0, 0, 0, 0.5)"
: "0 25px 50px -12px rgba(0, 0, 0, 0.15)",
}}
>
{/* Content */}
<div
key={contentKey}
style={{
animation: shouldAnimate ? "fadeInSlide 0.3s ease-out" : "none",
}}
onAnimationEnd={() => setShouldAnimate(false)}
>
<h3 id="tour-title" className="text-sm font-semibold mb-1.5" style={{ color: textColor }}>
{step.title}
</h3>
<p className="text-sm leading-relaxed" style={{ color: mutedTextColor }}>
{step.content}
</p>
</div>
{/* Footer */}
<div className="flex items-center justify-between mt-4">
{/* Step dots */}
{renderStepDots()}
{/* Navigation buttons */}
<div className="flex items-center gap-3">
{!isFirstStep && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onPrev();
}}
className="text-sm font-medium transition-opacity hover:opacity-80"
style={{ color: mutedTextColor }}
>
Back
</button>
)}
{isFirstStep && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onSkip();
}}
className="text-sm font-medium transition-opacity hover:opacity-80"
style={{ color: mutedTextColor }}
>
Skip
</button>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onNext();
}}
className="text-sm font-medium transition-opacity hover:opacity-80"
style={{ color: textColor }}
>
{isLastStep ? "Done" : "Next"}
</button>
</div>
</div>
</div>
</div>
);
}
export function OnboardingTour() {
const [isActive, setIsActive] = useState(false);
const [stepIndex, setStepIndex] = useState(0);
const [targetEl, setTargetEl] = useState<Element | null>(null);
const [spotlightTargetEl, setSpotlightTargetEl] = useState<Element | null>(null);
const [spotlightStepTarget, setSpotlightStepTarget] = useState<string | null>(null);
const [position, setPosition] = useState<TooltipPosition | null>(null);
const [targetRect, setTargetRect] = useState<DOMRect | null>(null);
const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme();
const pathname = usePathname();
const retryCountRef = useRef(0);
const maxRetries = 10;
// Track previous user ID to detect user changes
const previousUserIdRef = useRef<string | null>(null);
// Get user data
const { data: user } = useAtomValue(currentUserAtom);
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
// Fetch threads data
const { data: threadsData } = useQuery({
queryKey: ["threads", searchSpaceId],
queryFn: () => fetchThreads(Number(searchSpaceId), 1), // Only need to check if any exist
enabled: !!searchSpaceId,
});
// Get document type counts
const { data: documentTypeCounts } = useAtomValue(documentTypeCountsAtom);
// Get connectors
const { data: connectors = [] } = useAtomValue(connectorsAtom);
const isDarkMode = resolvedTheme === "dark";
const currentStep = TOUR_STEPS[stepIndex];
// Handle mounting for portal
useEffect(() => {
setMounted(true);
}, []);
// Find and track target element with retry logic
const updateTarget = useCallback(() => {
if (!currentStep) return;
const el = document.querySelector(currentStep.target);
if (el) {
setTargetEl(el);
setTargetRect(el.getBoundingClientRect());
setPosition(calculatePosition(el, currentStep.placement));
retryCountRef.current = 0;
} else if (retryCountRef.current < maxRetries) {
retryCountRef.current++;
setTimeout(() => {
const retryEl = document.querySelector(currentStep.target);
if (retryEl) {
setTargetEl(retryEl);
setTargetRect(retryEl.getBoundingClientRect());
setPosition(calculatePosition(retryEl, currentStep.placement));
retryCountRef.current = 0;
}
}, 200);
}
}, [currentStep]);
// Check if tour should run: localStorage + data validation with user ID tracking
useEffect(() => {
// Don't check if not mounted or no user
if (!mounted || !user?.id || !searchSpaceId) return;
// Check if on new-chat page
const isNewChatPage = pathname?.includes("/new-chat");
if (!isNewChatPage) return;
// Wait for all data to be loaded before making decision
// Data is considered loaded when:
// - threadsData is defined (query completed, even if empty)
// - documentTypeCounts is defined (query completed, even if empty object)
// - connectors is an array (always defined with default [])
// If searchSpaceId is not set, connectors query won't run, but that's okay
const dataLoaded = threadsData !== undefined && documentTypeCounts !== undefined;
if (!dataLoaded) return;
const currentUserId = user.id;
const previousUserId = previousUserIdRef.current;
// Detect user change - if user ID changed, reset tour state
if (previousUserId !== null && previousUserId !== currentUserId) {
// User changed - reset tour state and re-evaluate for new user
setIsActive(false);
setStepIndex(0);
setTargetEl(null);
setSpotlightTargetEl(null);
setSpotlightStepTarget(null);
setPosition(null);
setTargetRect(null);
retryCountRef.current = 0;
}
// Update previous user ID ref
previousUserIdRef.current = currentUserId;
// Check localStorage for CURRENT user ID (not stale cache)
// This ensures we check the correct user's tour status
const tourKey = `surfsense-tour-${currentUserId}`;
const hasSeenTour = localStorage.getItem(tourKey);
if (hasSeenTour === "true") {
return; // Current user has seen tour, don't show
}
// Validate user is actually new (reliable check)
const threads = threadsData?.threads ?? [];
const hasThreads = threads.length > 0;
// Check document counts - sum all document type counts
const totalDocuments = documentTypeCounts
? Object.values(documentTypeCounts).reduce((sum, count) => sum + count, 0)
: 0;
const hasDocuments = totalDocuments > 0;
const hasConnectors = connectors.length > 0;
// User is new if they have no threads, documents, or connectors
const isNewUser = !hasThreads && !hasDocuments && !hasConnectors;
// Only show tour if user is new and hasn't seen it
// Don't auto-mark as seen if user has data - let them explicitly dismiss it
if (!isNewUser) {
return; // User has data, don't show tour
}
// User is new and hasn't seen tour - wait for DOM elements and start tour
const checkAndStartTour = () => {
// Check if both required elements exist
const connectorEl = document.querySelector(TOUR_STEPS[0].target);
const documentsEl = document.querySelector(TOUR_STEPS[1].target);
if (connectorEl && documentsEl) {
// Both elements found, start tour
setIsActive(true);
setTargetEl(connectorEl);
setSpotlightTargetEl(connectorEl);
setSpotlightStepTarget(TOUR_STEPS[0].target);
setTargetRect(connectorEl.getBoundingClientRect());
setPosition(calculatePosition(connectorEl, TOUR_STEPS[0].placement));
} else {
// Retry after delay
setTimeout(checkAndStartTour, 200);
}
};
// Start checking after initial delay
const timer = setTimeout(checkAndStartTour, 500);
return () => clearTimeout(timer);
}, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]);
// Update position on resize/scroll
useEffect(() => {
if (!isActive || !targetEl) return;
const handleUpdate = () => {
const rect = targetEl.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
setTargetRect(rect);
setPosition(calculatePosition(targetEl, currentStep?.placement || "bottom"));
}
};
window.addEventListener("resize", handleUpdate);
window.addEventListener("scroll", handleUpdate, true);
return () => {
window.removeEventListener("resize", handleUpdate);
window.removeEventListener("scroll", handleUpdate, true);
};
}, [isActive, targetEl, currentStep?.placement]);
// Update target when step changes
useEffect(() => {
if (isActive && currentStep) {
// Try to find element synchronously first to prevent any delay
const el = document.querySelector(currentStep.target);
if (el) {
// Found immediately - update state synchronously to prevent flicker
const rect = el.getBoundingClientRect();
const newPosition = calculatePosition(el, currentStep.placement);
// React 18+ automatically batches these updates
setTargetEl(el);
setTargetRect(rect);
setPosition(newPosition);
retryCountRef.current = 0;
} else {
// Not found immediately, use updateTarget with retry logic
// Use requestAnimationFrame to batch with next paint
const frameId = requestAnimationFrame(() => {
updateTarget();
});
return () => cancelAnimationFrame(frameId);
}
}
}, [isActive, updateTarget, currentStep]);
// Delay spotlight update to sync with tooltip animation
useEffect(() => {
if (targetEl && currentStep) {
const timer = setTimeout(() => {
setSpotlightTargetEl(targetEl);
setSpotlightStepTarget(currentStep.target);
}, 100);
return () => clearTimeout(timer);
}
}, [targetEl, currentStep]);
// Ensure target element is above overlay layers so content is fully visible
useEffect(() => {
if (!targetEl || !isActive) return;
const originalZIndex = (targetEl as HTMLElement).style.zIndex;
const originalPosition = (targetEl as HTMLElement).style.position;
// Ensure the element has a position that allows z-index
if (getComputedStyle(targetEl).position === "static") {
(targetEl as HTMLElement).style.position = "relative";
}
(targetEl as HTMLElement).style.zIndex = "99999";
return () => {
(targetEl as HTMLElement).style.zIndex = originalZIndex;
if (originalPosition) {
(targetEl as HTMLElement).style.position = originalPosition;
} else if (getComputedStyle(targetEl).position === "relative" && originalPosition === "") {
(targetEl as HTMLElement).style.position = "";
}
};
}, [targetEl, isActive]);
const handleNext = useCallback(() => {
if (stepIndex < TOUR_STEPS.length - 1) {
retryCountRef.current = 0;
setStepIndex(stepIndex + 1);
} else {
// Tour completed - save to localStorage
if (user?.id) {
const tourKey = `surfsense-tour-${user.id}`;
localStorage.setItem(tourKey, "true");
}
setIsActive(false);
}
}, [stepIndex, user?.id]);
const handlePrev = useCallback(() => {
if (stepIndex > 0) {
retryCountRef.current = 0;
setStepIndex(stepIndex - 1);
}
}, [stepIndex]);
const handleSkip = useCallback(() => {
// Tour skipped - save to localStorage
if (user?.id) {
const tourKey = `surfsense-tour-${user.id}`;
localStorage.setItem(tourKey, "true");
}
setIsActive(false);
}, [user?.id]);
// Handle overlay click to close
const handleOverlayClick = useCallback(() => {
// Tour closed - save to localStorage
if (user?.id) {
const tourKey = `surfsense-tour-${user.id}`;
localStorage.setItem(tourKey, "true");
}
setIsActive(false);
}, [user?.id]);
// Handle escape key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isActive) {
// Tour closed via escape - save to localStorage
if (user?.id) {
const tourKey = `surfsense-tour-${user.id}`;
localStorage.setItem(tourKey, "true");
}
setIsActive(false);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isActive, user?.id]);
// Don't render if not active or not mounted
if (!mounted || !isActive) {
return null;
}
return createPortal(
<>
<style>{`
@keyframes fadeInSlide {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`}</style>
<div className="fixed inset-0 z-[99995]">
{/* Clickable backdrop to close */}
<button
type="button"
className="fixed inset-0 w-full h-full bg-transparent border-0 cursor-default"
onClick={handleOverlayClick}
aria-label="Close tour"
/>
{/* Only render Spotlight and TourTooltip when we have target data */}
{targetEl && position && currentStep && targetRect && (
<>
{spotlightTargetEl && spotlightStepTarget && (
<Spotlight
targetEl={spotlightTargetEl}
isDarkMode={isDarkMode}
currentStepTarget={spotlightStepTarget}
/>
)}
<TourTooltip
step={currentStep}
stepIndex={stepIndex}
totalSteps={TOUR_STEPS.length}
position={position}
targetRect={targetRect}
onNext={handleNext}
onPrev={handlePrev}
onSkip={handleSkip}
isDarkMode={isDarkMode}
/>
</>
)}
</div>
</>,
document.body
);
}

View file

@ -131,7 +131,11 @@ export function NavMain({ items }: NavMainProps) {
isActive={isActive}
aria-label={`${translatedTitle} with submenu`}
>
<button type="button" className="flex items-center gap-2 w-full text-left">
<button
type="button"
className="flex items-center gap-2 w-full text-left"
{...(item.title === "Sources" ? { "data-joyride": "sources-menu" } : {})}
>
<item.icon />
<span>{translatedTitle}</span>
</button>
@ -152,10 +156,18 @@ export function NavMain({ items }: NavMainProps) {
<SidebarMenuSub>
{item.items?.map((subItem, subIndex) => {
const translatedSubTitle = translateTitle(subItem.title);
const isDocumentsLink =
subItem.title === "Manage Documents" ||
translatedSubTitle.toLowerCase().includes("documents");
return (
<SidebarMenuSubItem key={`${subItem.title}-${subIndex}`}>
<SidebarMenuSubButton asChild aria-label={translatedSubTitle}>
<a href={subItem.url}>
<a
href={subItem.url}
{...(isDocumentsLink
? { "data-joyride": "documents-sidebar" }
: {})}
>
<span>{translatedSubTitle}</span>
</a>
</SidebarMenuSubButton>
@ -173,7 +185,13 @@ export function NavMain({ items }: NavMainProps) {
isActive={isActive}
aria-label={translatedTitle}
>
<a href={item.url}>
<a
href={item.url}
{...(item.title === "Documents" ||
translatedTitle.toLowerCase() === "documents"
? { "data-joyride": "documents-sidebar" }
: {})}
>
<item.icon />
<span>{translatedTitle}</span>
</a>

View file

@ -3,31 +3,4 @@ title: Airtable
description: Connect your Airtable bases to SurfSense
---
# Airtable Connector
Index your Airtable bases, tables, and records.
## Prerequisites
- An Airtable account
- API access to the bases you want to connect
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Airtable** from the list
4. Enter your Airtable API key
5. Select the bases and tables you want to index
## What Gets Indexed
- Table records
- Field values
- Attachments
- Linked records
## Sync Frequency
The Airtable connector supports scheduled syncing to keep your data up to date.
# Documentation in progress

View file

@ -3,31 +3,4 @@ title: Bookstack
description: Connect your Bookstack instance to SurfSense
---
# Bookstack Connector
Index your Bookstack books, chapters, and pages.
## Prerequisites
- A Bookstack instance
- API access credentials
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Bookstack** from the list
4. Enter your Bookstack instance URL and API credentials
5. Select the shelves and books you want to index
## What Gets Indexed
- Books and chapters
- Pages and content
- Attachments
- Tags and metadata
## Sync Frequency
The Bookstack connector supports scheduled syncing to keep your content up to date.
# Documentation in progress

View file

@ -0,0 +1,8 @@
---
title: Circleback
description: Connect your circleback to SurfSense
---
# Documentation in progress

View file

@ -3,31 +3,4 @@ title: ClickUp
description: Connect your ClickUp workspace to SurfSense
---
# ClickUp Connector
Sync your ClickUp tasks, docs, and content to SurfSense.
## Prerequisites
- A ClickUp account
- Access to the workspaces you want to connect
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **ClickUp** from the list
4. Authorize SurfSense to access your ClickUp workspace
5. Select the spaces and folders you want to index
## What Gets Indexed
- Tasks and subtasks
- Task descriptions and comments
- ClickUp Docs
- Custom fields
## Sync Frequency
The ClickUp connector supports scheduled syncing to keep your content up to date.
# Documentation in progress

View file

@ -3,32 +3,104 @@ title: Confluence
description: Connect your Confluence spaces to SurfSense
---
# Confluence Connector
# Confluence OAuth Integration Setup Guide
Index your Confluence pages, spaces, and documentation.
This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration for SurfSense to connect your Confluence spaces.
## Prerequisites
## Step 1: Access the Developer Console
- A Confluence account (Cloud or Data Center)
- Access to the spaces you want to connect
1. Navigate to [developer.atlassian.com](https://developer.atlassian.com)
2. Click your profile icon in the top-right corner
3. Select **"Developer console"** from the dropdown
## Setup
![Atlassian Developer Console Access](/docs/connectors/atlassian/atlassian-dev-console-access.png)
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Confluence** from the list
4. Enter your Confluence instance URL and credentials
5. Select the spaces you want to index
## Step 2: Create a New OAuth 2.0 Integration
## What Gets Indexed
1. In the Developer Console, under **My apps**, click the **"Create"** button
2. Select **"OAuth 2.0 integration"** from the dropdown
- Pages and blog posts
- Page comments
- Attachments
- Space documentation
- Page hierarchy
![Create OAuth 2.0 Integration](/docs/connectors/atlassian/atlassian-create-app.png)
## Sync Frequency
## Step 3: Name Your Integration
The Confluence connector supports scheduled syncing to keep your content up to date.
1. Enter **Name**: `SurfSense`
2. Check the box to agree to Atlassian's developer terms
3. Click **"Create"**
> New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse.
![Create New Integration Form](/docs/connectors/atlassian/atlassian-name-integration.png)
## Step 4: Configure Callback URL
1. In the left sidebar, click **"Authorization"**
2. Under **Callback URLs**, enter the redirect URI:
```
http://localhost:8000/api/v1/auth/confluence/connector/callback
```
3. Click **"Save changes"**
> You can enter up to 10 redirect URIs, one per line.
![Authorization Callback URLs](/docs/connectors/atlassian/atlassian-authorization.png)
## Step 5: Configure API Permissions
1. In the left sidebar, click **"Permissions"**
2. You'll see a list of available APIs including Confluence API
![Permissions Overview](/docs/connectors/atlassian/atlassian-permissions.png)
## Step 6: Configure Confluence API Scopes
1. Click **"Configure"** next to **Confluence API**
### Classic Scopes
Select the **"Classic scopes"** tab and enable:
| Scope Name | Code | Description |
|------------|------|-------------|
| ✅ Read user | `read:confluence-user` | View user information in Confluence that you have access to, including usernames, email addresses and profile pictures |
![Confluence API Classic Scopes](/docs/connectors/atlassian/confluence/atlassian-confluence-classic-scopes.png)
### Granular Scopes
Select the **"Granular scopes"** tab and enable:
| Scope Name | Code | Description |
|------------|------|-------------|
| ✅ View pages | `read:page:confluence` | View page content |
| ✅ View comments | `read:comment:confluence` | View comments on pages or blogposts |
| ✅ View spaces | `read:space:confluence` | View space details |
4. Click **"Save"**
![Confluence API Granular Scopes](/docs/connectors/atlassian/confluence/atlassian-confluence-granular-scopes.png)
## Step 7: Get OAuth Credentials
1. In the left sidebar, click **"Settings"**
2. Copy your **Client ID** and **Client Secret**
> ⚠️ Never share your client secret publicly or include it in code repositories.
---
## Running SurfSense with Confluence Connector
Add the Atlassian environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Confluence Connector
-e ATLASSIAN_CLIENT_ID=your_atlassian_client_id \
-e ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret \
-e CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,30 +3,74 @@ title: Discord
description: Connect your Discord servers to SurfSense
---
# Discord Connector
# Discord OAuth Integration Setup Guide
Index your Discord server conversations and content.
This guide walks you through setting up a Discord OAuth integration for SurfSense.
## Prerequisites
## Step 1: Create a New Discord Application
- A Discord account
- Server admin permissions
1. Navigate to [discord.com/developers/applications](https://discord.com/developers/applications)
2. Click **"New Application"**
3. Enter the application name: `SurfSense`
4. Click **"Create"**
## Setup
## Step 2: Configure General Information
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Discord** from the list
4. Authorize SurfSense to access your Discord server
5. Select the channels you want to index
On the **General Information** page, fill in the details:
## What Gets Indexed
| Field | Value |
|-------|-------|
| **App Icon** | Upload an icon (1024x1024px, PNG/GIF/JPG/WEBP, max 10MB) |
| **Name** | `SurfSense` |
| **Description** | Connect any LLM to your internal knowledge sources and chat with it in real time alongside your team. |
| **Tags** | Add up to 5 tags (optional) |
- Text channel messages
- Thread messages
- Shared files and links
You'll also see your **Application ID** and **Public Key** on this page.
## Sync Frequency
![General Information](/docs/connectors/discord/discord-general-info.png)
The Discord connector supports scheduled syncing to keep your content up to date.
## Step 3: Configure OAuth2 Settings
1. In the left sidebar, click **"OAuth2"**
2. Copy your **Client ID** and **Client Secret** (click to reveal)
3. Under **Redirects**, click **"Add Another"** and enter:
```
http://localhost:8000/api/v1/auth/discord/connector/callback
```
> ⚠️ Keep **Public Client** disabled (off) since SurfSense uses a server to make requests.
![OAuth2 Configuration](/docs/connectors/discord/discord-oauth2.png)
## Step 4: Configure Bot Settings
1. In the left sidebar, click **"Bot"**
2. Configure the **Authorization Flow**:
- ✅ **Public Bot** - Enable to allow anyone to add the bot to servers
3. Enable **Privileged Gateway Intents**:
- ✅ **Server Members Intent** - Required to receive GUILD_MEMBERS events
- ✅ **Message Content Intent** - Required to receive message content
> ⚠️ Once your bot reaches 100+ servers, these intents will require verification and approval.
![Bot Settings](/docs/connectors/discord/discord-bot-settings.png)
---
## Running SurfSense with Discord Connector
Add the Discord environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Discord Connector
-e DISCORD_CLIENT_ID=your_discord_client_id \
-e DISCORD_CLIENT_SECRET=your_discord_client_secret \
-e DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback \
-e DISCORD_BOT_TOKEN=http://localhost:8000/api/v1/auth/discord/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,30 +3,4 @@ title: Elasticsearch
description: Connect your Elasticsearch cluster to SurfSense
---
# Elasticsearch Connector
Index data from your Elasticsearch cluster.
## Prerequisites
- An Elasticsearch cluster
- Access credentials
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Elasticsearch** from the list
4. Enter your Elasticsearch cluster URL and credentials
5. Configure the indices you want to index
## What Gets Indexed
- Documents from specified indices
- Custom field mappings
- Metadata
## Sync Frequency
The Elasticsearch connector supports scheduled syncing to keep your data up to date.
# Documentation in progress

View file

@ -3,32 +3,4 @@ title: GitHub
description: Connect your GitHub repositories to SurfSense
---
# GitHub Connector
Index your GitHub repositories, issues, pull requests, and documentation.
## Prerequisites
- A GitHub account
- Access to the repositories you want to connect
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **GitHub** from the list
4. Authorize SurfSense to access your GitHub account
5. Select the repositories you want to index
## What Gets Indexed
- Repository README and documentation
- Issues and issue comments
- Pull requests and PR comments
- Code files (configurable)
- Discussions
## Sync Frequency
The GitHub connector supports scheduled syncing to keep your content up to date.
# Documentation in progress

View file

@ -3,32 +3,81 @@ title: Gmail
description: Connect your Gmail to SurfSense
---
# Gmail Connector
# Gmail OAuth Integration Setup Guide
Index your Gmail emails and make them searchable.
This guide walks you through setting up a Google OAuth 2.0 integration for SurfSense to connect your Gmail account.
## Prerequisites
## Step 1: Access the Google Cloud Console
- A Google account
- Google OAuth configured in SurfSense (see [Prerequisites](/docs))
- Gmail API enabled in Google Cloud Console
1. Navigate to [Google Cloud Console](https://console.cloud.google.com/)
2. Select an existing project or create a new one
## Setup
## Step 2: Enable Required APIs
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Gmail** from the list
4. Authorize SurfSense to access your Gmail
5. Configure which labels/folders to index
1. Go to **APIs & Services** > **Library**
2. Search for and enable the following APIs:
- **People API** (required for Google OAuth)
- **Gmail API** (required for Gmail connector)
## What Gets Indexed
![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png)
- Email content
- Email attachments
- Thread conversations
- Labels and categories
## Step 3: Configure OAuth Consent Screen
## Sync Frequency
1. Go to **APIs & Services** > **OAuth consent screen**
2. Select **External** user type (or Internal if using Google Workspace)
3. Fill in the required information:
- **App name**: `SurfSense`
- **User support email**: Your email address
- **Developer contact information**: Your email address
4. Click **Save and Continue**
The Gmail connector supports scheduled syncing to keep your emails indexed.
![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png)
### Add Scopes
1. Click **Add or Remove Scopes**
2. Add the following scopes:
- `https://www.googleapis.com/auth/gmail.readonly` - Read Gmail messages
- `https://www.googleapis.com/auth/userinfo.email` - View user email address
3. Click **Update** and then **Save and Continue**
## Step 4: Create OAuth Client ID
1. Go to **APIs & Services** > **Credentials**
2. Click **Create Credentials** > **OAuth client ID**
3. Select **Web application** as the application type
4. Enter **Name**: `SurfSense`
5. Under **Authorized redirect URIs**, add:
```
http://localhost:8000/api/v1/auth/google/gmail/connector/callback
```
6. Click **Create**
![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png)
## Step 5: Get OAuth Credentials
1. After creating the OAuth client, you'll see a dialog with your credentials
2. Copy your **Client ID** and **Client Secret**
> ⚠️ Never share your client secret publicly or include it in code repositories.
![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png)
---
## Running SurfSense with Gmail Connector
Add the Google OAuth environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Gmail Connector
-e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
-e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
-e GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,32 +3,80 @@ title: Google Calendar
description: Connect your Google Calendar to SurfSense
---
# Google Calendar Connector
# Google Calendar OAuth Integration Setup Guide
Index your Google Calendar events and make them searchable.
This guide walks you through setting up a Google OAuth 2.0 integration for SurfSense to connect your Google Calendar.
## Prerequisites
## Step 1: Access the Google Cloud Console
- A Google account
- Google OAuth configured in SurfSense (see [Prerequisites](/docs))
- Google Calendar API enabled in Google Cloud Console
1. Navigate to [Google Cloud Console](https://console.cloud.google.com/)
2. Select an existing project or create a new one
## Setup
## Step 2: Enable Required APIs
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Google Calendar** from the list
4. Authorize SurfSense to access your Google Calendar
5. Select which calendars to index
1. Go to **APIs & Services** > **Library**
2. Search for and enable the following APIs:
- **People API** (required for Google OAuth)
- **Google Calendar API** (required for Calendar connector)
## What Gets Indexed
![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png)
- Event titles and descriptions
- Event attendees
- Meeting notes
- Recurring events
## Step 3: Configure OAuth Consent Screen
## Sync Frequency
1. Go to **APIs & Services** > **OAuth consent screen**
2. Select **External** user type (or Internal if using Google Workspace)
3. Fill in the required information:
- **App name**: `SurfSense`
- **User support email**: Your email address
- **Developer contact information**: Your email address
4. Click **Save and Continue**
The Google Calendar connector supports scheduled syncing to keep your events indexed.
![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png)
### Add Scopes
1. Click **Add or Remove Scopes**
2. Add the following scope:
- `https://www.googleapis.com/auth/calendar.readonly` - Read Google Calendar events
3. Click **Update** and then **Save and Continue**
## Step 4: Create OAuth Client ID
1. Go to **APIs & Services** > **Credentials**
2. Click **Create Credentials** > **OAuth client ID**
3. Select **Web application** as the application type
4. Enter **Name**: `SurfSense`
5. Under **Authorized redirect URIs**, add:
```
http://localhost:8000/api/v1/auth/google/calendar/connector/callback
```
6. Click **Create**
![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png)
## Step 5: Get OAuth Credentials
1. After creating the OAuth client, you'll see a dialog with your credentials
2. Copy your **Client ID** and **Client Secret**
> ⚠️ Never share your client secret publicly or include it in code repositories.
![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png)
---
## Running SurfSense with Google Calendar Connector
Add the Google OAuth environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Google Calendar Connector
-e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
-e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
-e GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,32 +3,81 @@ title: Google Drive
description: Connect your Google Drive to SurfSense
---
# Google Drive Connector
# Google Drive OAuth Integration Setup Guide
Index your Google Drive files, documents, and shared content.
This guide walks you through setting up a Google OAuth 2.0 integration for SurfSense to connect your Google Drive.
## Prerequisites
## Step 1: Access the Google Cloud Console
- A Google account
- Google OAuth configured in SurfSense (see [Prerequisites](/docs))
1. Navigate to [Google Cloud Console](https://console.cloud.google.com/)
2. Select an existing project or create a new one
## Setup
## Step 2: Enable Required APIs
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Google Drive** from the list
4. Authorize SurfSense to access your Google Drive
5. Select the folders you want to index
1. Go to **APIs & Services** > **Library**
2. Search for and enable the following APIs:
- **People API** (required for Google OAuth)
- **Google Drive API** (required for Drive connector)
## What Gets Indexed
![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png)
- Google Docs
- Google Sheets
- Google Slides
- PDFs and other documents
- Shared files
## Step 3: Configure OAuth Consent Screen
## Sync Frequency
1. Go to **APIs & Services** > **OAuth consent screen**
2. Select **External** user type (or Internal if using Google Workspace)
3. Fill in the required information:
- **App name**: `SurfSense`
- **User support email**: Your email address
- **Developer contact information**: Your email address
4. Click **Save and Continue**
The Google Drive connector supports scheduled syncing to keep your content up to date.
![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png)
### Add Scopes
1. Click **Add or Remove Scopes**
2. Add the following scopes:
- `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive
- `https://www.googleapis.com/auth/userinfo.email` - View user email address
3. Click **Update** and then **Save and Continue**
## Step 4: Create OAuth Client ID
1. Go to **APIs & Services** > **Credentials**
2. Click **Create Credentials** > **OAuth client ID**
3. Select **Web application** as the application type
4. Enter **Name**: `SurfSense`
5. Under **Authorized redirect URIs**, add:
```
http://localhost:8000/api/v1/auth/google/drive/connector/callback
```
6. Click **Create**
![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png)
## Step 5: Get OAuth Credentials
1. After creating the OAuth client, you'll see a dialog with your credentials
2. Copy your **Client ID** and **Client Secret**
> ⚠️ Never share your client secret publicly or include it in code repositories.
![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png)
---
## Running SurfSense with Google Drive Connector
Add the Google OAuth environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Google Drive Connector
-e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
-e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
-e GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,32 +3,91 @@ title: Jira
description: Connect your Jira projects to SurfSense
---
# Jira Connector
# Jira OAuth Integration Setup Guide
Sync your Jira issues, projects, and documentation to SurfSense.
This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration for SurfSense to connect your Jira projects.
## Prerequisites
## Step 1: Access the Developer Console
- A Jira account (Cloud or Data Center)
- Access to the projects you want to connect
1. Navigate to [developer.atlassian.com](https://developer.atlassian.com)
2. Click your profile icon in the top-right corner
3. Select **"Developer console"** from the dropdown
## Setup
![Atlassian Developer Console Access](/docs/connectors/atlassian/atlassian-dev-console-access.png)
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Jira** from the list
4. Enter your Jira instance URL and credentials
5. Select the projects you want to index
## Step 2: Create a New OAuth 2.0 Integration
## What Gets Indexed
1. In the Developer Console, under **My apps**, click the **"Create"** button
2. Select **"OAuth 2.0 integration"** from the dropdown
- Issues and subtasks
- Issue descriptions and comments
- Attachments
- Custom fields
- Project documentation
![Create OAuth 2.0 Integration](/docs/connectors/atlassian/atlassian-create-app.png)
## Sync Frequency
## Step 3: Name Your Integration
The Jira connector supports scheduled syncing to keep your content up to date.
1. Enter **Name**: `SurfSense`
2. Check the box to agree to Atlassian's developer terms
3. Click **"Create"**
> New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse.
![Create New Integration Form](/docs/connectors/atlassian/atlassian-name-integration.png)
## Step 4: Configure Callback URL
1. In the left sidebar, click **"Authorization"**
2. Under **Callback URLs**, enter the redirect URI:
```
http://localhost:8000/api/v1/auth/jira/connector/callback
```
3. Click **"Save changes"**
> You can enter up to 10 redirect URIs, one per line.
![Authorization Callback URLs](/docs/connectors/atlassian/atlassian-authorization.png)
## Step 5: Configure API Permissions
1. In the left sidebar, click **"Permissions"**
2. You'll see a list of available APIs including Jira API
![Permissions Overview](/docs/connectors/atlassian/atlassian-permissions.png)
## Step 6: Configure Jira API Scopes
1. Click **"Configure"** next to **Jira API**
2. Select the **"Classic scopes"** tab
3. Under **Jira platform REST API**, select the following scopes:
| Scope Name | Code | Description |
|------------|------|-------------|
| ✅ View Jira issue data | `read:jira-work` | Read Jira project and issue data, search for issues, and objects associated with issues like attachments and worklogs |
| ✅ View user profiles | `read:jira-user` | View user information in Jira that the user has access to, including usernames, email addresses, and avatars |
4. Click **"Save"**
![Jira API Scopes](/docs/connectors/atlassian/jira/atlassian-jira-scopes.png)
## Step 7: Get OAuth Credentials
1. In the left sidebar, click **"Settings"**
2. Copy your **Client ID** and **Client Secret**
> ⚠️ Never share your client secret publicly or include it in code repositories.
---
## Running SurfSense with Jira Connector
Add the Atlassian environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Jira Connector
-e ATLASSIAN_CLIENT_ID=your_atlassian_client_id \
-e ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret \
-e JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,31 +3,65 @@ title: Linear
description: Connect your Linear workspace to SurfSense
---
# Linear Connector
# Linear OAuth Integration Setup Guide
Sync your Linear issues, projects, and documentation to SurfSense.
This guide walks you through setting up a Linear OAuth integration for SurfSense.
## Prerequisites
## Step 1: Access Linear API Settings
- A Linear account
- Access to the teams you want to connect
1. Navigate to your workspace's API settings at `linear.app/<your-workspace>/settings/api`
2. Under **OAuth Applications**, click **"+ New OAuth application"**
## Setup
![Linear API Settings Page](/docs/connectors/linear/linear-api-settings.png)
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Linear** from the list
4. Authorize SurfSense to access your Linear workspace
5. Select the teams and projects you want to index
## Step 2: Create New Application
## What Gets Indexed
Fill in the application details:
- Issues and sub-issues
- Issue descriptions and comments
- Project documentation
- Roadmap items
| Field | Value |
|-------|-------|
| **Application icon** | Upload an icon (at least 256x256px) |
| **Application name** | `SurfSense` |
| **Developer name** | `SurfSense` |
| **Developer URL** | `https://www.surfsense.com/` |
| **Description** | Connect any LLM to your internal knowledge sources and chat with it in real time alongside your team. |
| **Callback URLs** | `http://localhost:8000/api/v1/auth/linear/connector/callback` |
| **GitHub username** | Your GitHub username (optional) |
## Sync Frequency
### Settings
The Linear connector supports scheduled syncing to keep your content up to date.
- ✅ **Public** - Enable this to allow the application to be installed by other workspaces
Click **Create** to create the application.
![Create New Application Form](/docs/connectors/linear/linear-new-application.png)
## Step 3: Get OAuth Credentials
After creating the application, you'll see your OAuth credentials:
1. Copy your **Client ID**
2. Copy your **Client Secret**
> ⚠️ Never share your client secret publicly.
![OAuth Credentials](/docs/connectors/linear/linear-oauth-credentials.png)
---
## Running SurfSense with Linear Connector
Add the Linear environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Linear Connector
-e LINEAR_CLIENT_ID=your_linear_client_id \
-e LINEAR_CLIENT_SECRET=your_linear_client_secret \
-e LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,31 +3,4 @@ title: Luma
description: Connect your Luma events to SurfSense
---
# Luma Connector
Index your Luma events and event content.
## Prerequisites
- A Luma account
- API access
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Luma** from the list
4. Authorize SurfSense to access your Luma account
5. Select the events you want to index
## What Gets Indexed
- Event details and descriptions
- Event schedules
- Attendee information (if authorized)
- Event updates
## Sync Frequency
The Luma connector supports scheduled syncing to keep your events up to date.
# Documentation in progress

View file

@ -1,22 +1,22 @@
{
"title": "Connectors",
"pages": [
"notion",
"slack",
"discord",
"clickup",
"github",
"jira",
"linear",
"google-drive",
"gmail",
"google-calendar",
"notion",
"slack",
"discord",
"jira",
"linear",
"confluence",
"bookstack",
"airtable",
"clickup",
"github",
"luma",
"circleback",
"elasticsearch",
"web-crawler",
"luma"
"bookstack"
],
"defaultOpen": true
}

View file

@ -3,31 +3,82 @@ title: Notion
description: Connect your Notion workspaces to SurfSense
---
# Notion Connector
# Notion OAuth Integration Setup Guide
Connect your Notion workspaces to index pages, databases, and content.
This guide walks you through setting up a Notion OAuth integration for SurfSense.
## Prerequisites
## Step 1: Access Notion Integrations
- A Notion account
- Access to the workspaces you want to connect
1. Navigate to [notion.so/profile/integrations](https://notion.so/profile/integrations)
2. Click the **"New integration"** button
## Setup
![Notion Integrations Page](/docs/connectors/notion/notion-integrations-page.png)
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Notion** from the list
4. Authorize SurfSense to access your Notion workspace
5. Select the pages and databases you want to index
## Step 2: Configure New Integration
## What Gets Indexed
Fill in the integration details:
- Pages and subpages
- Database entries
- Comments and discussions
- Embedded content
| Field | Value |
|-------|-------|
| **Integration Name** | `SurfSense` |
| **Associated workspace** | Select your workspace |
| **Type** | `Public` |
| **Company name** | Your company name |
| **Website** | Your website URL |
| **Tagline** | Brief description |
| **Privacy Policy URL** | Your privacy policy URL |
| **Terms of Use URL** | Your terms of use URL |
| **Email** | Your developer email |
| **Logo** | Upload a 512x512 logo |
## Sync Frequency
### OAuth Redirect URI
The Notion connector supports scheduled syncing to keep your content up to date.
Under **OAuth domains & URIs**, set the **Redirect URI** to:
```
http://localhost:8000/api/v1/auth/notion/connector/callback
```
Click **Save** to create the integration.
![New Integration Form](/docs/connectors/notion/notion-new-integration-form.png)
## Step 3: Get OAuth Credentials & Configure Capabilities
After creating the integration, you'll see the configuration page with your credentials:
1. Copy your **OAuth Client ID**
2. Copy your **OAuth Client Secret** (click Refresh if needed)
### Set Required Capabilities
Under **Content Capabilities**, enable:
- ✅ Read content
Under **Comment Capabilities**, enable:
- ✅ Read comments
Under **User Capabilities**, select:
- 🔘 Read user information including email addresses
Click **Save** to apply the capabilities.
![Integration Configuration](/docs/connectors/notion/notion-integration-config.png)
---
## Running SurfSense with Notion Connector
Add the Notion environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Notion Connector
-e NOTION_OAUTH_CLIENT_ID=your_notion_client_id \
-e NOTION_OAUTH_CLIENT_SECRET=your_notion_client_secret \
-e NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,31 +3,89 @@ title: Slack
description: Connect your Slack workspace to SurfSense
---
# Slack Connector
# Slack OAuth Integration Setup Guide
Index your Slack conversations and make them searchable.
This guide walks you through setting up a Slack OAuth integration for SurfSense.
## Prerequisites
## Step 1: Create a New Slack App
- A Slack workspace
- Admin permissions to install apps
1. Navigate to [api.slack.com/apps](https://api.slack.com/apps)
2. Click **"Create New App"**
3. Select **"From scratch"** to manually configure your app
## Setup
![Create an App Dialog](/docs/connectors/slack/slack-create-app.png)
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Slack** from the list
4. Authorize SurfSense to access your Slack workspace
5. Select the channels you want to index
## Step 2: Name App & Choose Workspace
## What Gets Indexed
1. Enter **App Name**: `SurfSense`
2. Select the workspace to develop your app in
3. Click **"Create App"**
- Public channel messages
- Private channels (if authorized)
- Thread replies
- Shared files and links
> ⚠️ You won't be able to change the workspace later. The workspace will control the app even if you leave it.
## Sync Frequency
![Name App & Choose Workspace](/docs/connectors/slack/slack-name-workspace.png)
The Slack connector supports scheduled syncing to keep your conversations indexed.
## Step 3: Get App Credentials
After creating the app, you'll be taken to the **Basic Information** page. Here you'll find your credentials:
1. Copy your **Client ID**
2. Copy your **Client Secret** (click Show to reveal)
> ⚠️ Never share your app credentials publicly or include them in code repositories.
![Basic Information - App Credentials](/docs/connectors/slack/slack-app-credentials.png)
## Step 4: Configure Redirect URLs
1. In the left sidebar, click **"OAuth & Permissions"**
2. Scroll down to **Redirect URLs**
3. Click **"Add New Redirect URL"**
4. Enter: `https://localhost:8000/api/v1/auth/slack/connector/callback`
5. Click **"Add"**, then **"Save URLs"**
![Redirect URLs Configuration](/docs/connectors/slack/slack-redirect-urls.png)
## Step 5: Configure Bot Token Scopes
On the same **OAuth & Permissions** page, scroll to **Scopes** and add the following **Bot Token Scopes**:
| OAuth Scope | Description |
|-------------|-------------|
| `channels:history` | View messages and other content in public channels |
| `channels:read` | View basic information about public channels |
| `groups:history` | View messages and other content in private channels |
| `groups:read` | View basic information about private channels |
| `im:history` | View messages and other content in direct messages |
| `mpim:history` | View messages and other content in group direct messages |
| `users:read` | View people in a workspace |
Click **"Add an OAuth Scope"** to add each scope.
![Bot Token Scopes](/docs/connectors/slack/slack-scopes.png)
## Step 6: Enable Public Distribution
1. In the left sidebar, click **"Manage Distribution"**
2. Under **Share Your App with Other Workspaces**, ensure distribution is enabled
3. You can use the **"Add to Slack"** button or **Sharable URL** to install the app
![Manage Distribution](/docs/connectors/slack/slack-distribution.png)
---
## Running SurfSense with Slack Connector
Add the Slack environment variables to your Docker run command:
```bash
docker run -d -p 3000:3000 -p 8000:8000 \
-v surfsense-data:/data \
# Slack Connector
-e SLACK_CLIENT_ID=your_slack_client_id \
-e SLACK_CLIENT_SECRET=your_slack_client_secret \
-e SLACK_REDIRECT_URI=https://localhost:8000/api/v1/auth/slack/connector/callback \
--name surfsense \
--restart unless-stopped \
ghcr.io/modsetter/surfsense:latest
```

View file

@ -3,36 +3,4 @@ title: Web Crawler
description: Crawl and index websites with SurfSense
---
# Web Crawler Connector
Crawl and index public websites to make them searchable.
## Prerequisites
- Firecrawl API key (see [Prerequisites](/docs))
## Setup
1. Navigate to your Search Space settings
2. Click on **Add Connector**
3. Select **Web Crawler** from the list
4. Enter the URL(s) you want to crawl
5. Configure crawl depth and settings
## What Gets Indexed
- Web page content
- Page titles and metadata
- Links and navigation
- Images and media (configurable)
## Configuration Options
- **Crawl Depth**: How many levels deep to crawl
- **Include/Exclude Patterns**: Filter which URLs to index
- **Rate Limiting**: Control crawl speed
## Sync Frequency
The Web Crawler connector supports scheduled re-crawling to keep your content up to date.
# Documentation in progress

View file

@ -15,15 +15,13 @@ To set up Google OAuth:
1. Login to your [Google Developer Console](https://console.cloud.google.com/)
2. Enable the required APIs:
- **People API** (required for basic Google OAuth)
- **Gmail API** (required if you want to use the Gmail connector)
- **Google Calendar API** (required if you want to use the Google Calendar connector)
![Google Developer Console People API](/docs/google_oauth_people_api.png)
![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png)
3. Set up OAuth consent screen.
![Google Developer Console OAuth consent screen](/docs/google_oauth_screen.png)
![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png)
4. Create OAuth client ID and secret.
![Google Developer Console OAuth client ID](/docs/google_oauth_client.png)
![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png)
5. It should look like this.
![Google Developer Console Config](/docs/google_oauth_config.png)
![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png)
---

View file

@ -7,12 +7,17 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { cn } from "@/lib/utils";
import Image, { type ImageProps } from "next/image";
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
img: ({ className, ...props }: React.ComponentProps<"img">) => (
<img className={cn("rounded-md border", className)} {...props} />
img: ({ className, alt, ...props }: React.ComponentProps<"img">) => (
<Image
className={cn("rounded-md border", className)}
alt={alt ?? ""}
{...(props as ImageProps)}
/>
),
Video: ({ className, ...props }: React.ComponentProps<"video">) => (
<video className={cn("rounded-md border", className)} controls loop {...props} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View file

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB