Merge remote-tracking branch 'upstream/dev' into fix/connector
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "==========================================="
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
742
surfsense_web/components/onboarding-tour.tsx
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
8
surfsense_web/content/docs/connectors/circleback.mdx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: Circleback
|
||||
description: Connect your circleback to SurfSense
|
||||
---
|
||||
|
||||
# Documentation in progress
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## Step 5: Configure API Permissions
|
||||
|
||||
1. In the left sidebar, click **"Permissions"**
|
||||
2. You'll see a list of available APIs including Confluence API
|
||||
|
||||

|
||||
|
||||
## 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 |
|
||||
|
||||

|
||||
|
||||
### 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"**
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||

|
||||
|
||||
- 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.
|
||||

|
||||
|
||||
### 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**
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||

|
||||
|
||||
- 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.
|
||||

|
||||
|
||||
### 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**
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 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.
|
||||

|
||||
|
||||
### 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**
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## Step 5: Configure API Permissions
|
||||
|
||||
1. In the left sidebar, click **"Permissions"**
|
||||
2. You'll see a list of available APIs including Jira API
|
||||
|
||||

|
||||
|
||||
## 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"**
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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"**
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||

|
||||

|
||||
3. Set up OAuth consent screen.
|
||||

|
||||

|
||||
4. Create OAuth client ID and secret.
|
||||

|
||||

|
||||
5. It should look like this.
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 161 KiB |
BIN
surfsense_web/public/docs/connectors/discord/discord-oauth2.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 167 KiB |
BIN
surfsense_web/public/docs/connectors/slack/slack-create-app.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 35 KiB |
BIN
surfsense_web/public/docs/connectors/slack/slack-scopes.png
Normal file
|
After Width: | Height: | Size: 84 KiB |