mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-09 15:52:40 +02:00
Merge remote-tracking branch 'upstream/dev' into pr-611
This commit is contained in:
commit
6f330e7b8d
92 changed files with 5331 additions and 6029 deletions
|
|
@ -5,9 +5,14 @@ import { Loader2 } from "lucide-react";
|
|||
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { llmPreferencesAtom } from "@/atoms/llm-config/llm-config-query.atoms";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { myAccessAtom } from "@/atoms/members/members-query.atoms";
|
||||
import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
|
||||
import {
|
||||
globalNewLLMConfigsAtom,
|
||||
llmPreferencesAtom,
|
||||
} from "@/atoms/new-llm-config/new-llm-config-query.atoms";
|
||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
|
||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||
|
|
@ -33,18 +38,24 @@ export function DashboardClientLayout({
|
|||
const { search_space_id } = useParams();
|
||||
const setActiveSearchSpaceIdState = useSetAtom(activeSearchSpaceIdAtom);
|
||||
|
||||
const { data: preferences = {}, isFetching: loading, error } = useAtomValue(llmPreferencesAtom);
|
||||
const {
|
||||
data: preferences = {},
|
||||
isFetching: loading,
|
||||
error,
|
||||
refetch: refetchPreferences,
|
||||
} = useAtomValue(llmPreferencesAtom);
|
||||
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
||||
useAtomValue(globalNewLLMConfigsAtom);
|
||||
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
|
||||
|
||||
const isOnboardingComplete = useCallback(() => {
|
||||
return !!(
|
||||
preferences.long_context_llm_id &&
|
||||
preferences.fast_llm_id &&
|
||||
preferences.strategic_llm_id
|
||||
);
|
||||
return !!(preferences.agent_llm_id && preferences.document_summary_llm_id);
|
||||
}, [preferences]);
|
||||
|
||||
const { data: access = null, isLoading: accessLoading } = useAtomValue(myAccessAtom);
|
||||
const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false);
|
||||
const [isAutoConfiguring, setIsAutoConfiguring] = useState(false);
|
||||
const hasAttemptedAutoConfig = useRef(false);
|
||||
|
||||
// Skip onboarding check if we're already on the onboarding page
|
||||
const isOnboardingPage = pathname?.includes("/onboard");
|
||||
|
|
@ -89,27 +100,82 @@ export function DashboardClientLayout({
|
|||
return;
|
||||
}
|
||||
|
||||
// Wait for both preferences and access data to load
|
||||
if (!loading && !accessLoading && !hasCheckedOnboarding) {
|
||||
// Wait for all data to load
|
||||
if (
|
||||
!loading &&
|
||||
!accessLoading &&
|
||||
!globalConfigsLoading &&
|
||||
!hasCheckedOnboarding &&
|
||||
!isAutoConfiguring
|
||||
) {
|
||||
const onboardingComplete = isOnboardingComplete();
|
||||
|
||||
// Only redirect to onboarding if user is the owner and onboarding is not complete
|
||||
// Invited members (non-owners) should skip onboarding and use existing config
|
||||
if (!onboardingComplete && isOwner) {
|
||||
router.push(`/dashboard/${searchSpaceId}/onboard`);
|
||||
// If onboarding is complete, nothing to do
|
||||
if (onboardingComplete) {
|
||||
setHasCheckedOnboarding(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle onboarding for owners
|
||||
if (!isOwner) {
|
||||
setHasCheckedOnboarding(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If global configs available, auto-configure without going to onboard page
|
||||
if (globalConfigs.length > 0 && !hasAttemptedAutoConfig.current) {
|
||||
hasAttemptedAutoConfig.current = true;
|
||||
setIsAutoConfiguring(true);
|
||||
|
||||
const autoConfigureWithGlobal = async () => {
|
||||
try {
|
||||
const firstGlobalConfig = globalConfigs[0];
|
||||
await updatePreferences({
|
||||
search_space_id: Number(searchSpaceId),
|
||||
data: {
|
||||
agent_llm_id: firstGlobalConfig.id,
|
||||
document_summary_llm_id: firstGlobalConfig.id,
|
||||
},
|
||||
});
|
||||
|
||||
await refetchPreferences();
|
||||
|
||||
toast.success("AI configured automatically!", {
|
||||
description: `Using ${firstGlobalConfig.name}. Customize in Settings.`,
|
||||
});
|
||||
|
||||
setHasCheckedOnboarding(true);
|
||||
} catch (error) {
|
||||
console.error("Auto-configuration failed:", error);
|
||||
// Fall back to onboard page
|
||||
router.push(`/dashboard/${searchSpaceId}/onboard`);
|
||||
} finally {
|
||||
setIsAutoConfiguring(false);
|
||||
}
|
||||
};
|
||||
|
||||
autoConfigureWithGlobal();
|
||||
return;
|
||||
}
|
||||
|
||||
// No global configs - redirect to onboard page
|
||||
router.push(`/dashboard/${searchSpaceId}/onboard`);
|
||||
setHasCheckedOnboarding(true);
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
accessLoading,
|
||||
globalConfigsLoading,
|
||||
isOnboardingComplete,
|
||||
isOnboardingPage,
|
||||
isOwner,
|
||||
isAutoConfiguring,
|
||||
globalConfigs,
|
||||
router,
|
||||
searchSpaceId,
|
||||
hasCheckedOnboarding,
|
||||
updatePreferences,
|
||||
refetchPreferences,
|
||||
]);
|
||||
|
||||
// Synchronize active search space and chat IDs with URL
|
||||
|
|
@ -124,14 +190,25 @@ export function DashboardClientLayout({
|
|||
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
|
||||
}, [search_space_id, setActiveSearchSpaceIdState]);
|
||||
|
||||
// Show loading screen while checking onboarding status (only on first load)
|
||||
if (!hasCheckedOnboarding && (loading || accessLoading) && !isOnboardingPage) {
|
||||
// Show loading screen while checking onboarding status or auto-configuring
|
||||
if (
|
||||
(!hasCheckedOnboarding &&
|
||||
(loading || accessLoading || globalConfigsLoading) &&
|
||||
!isOnboardingPage) ||
|
||||
isAutoConfiguring
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
|
||||
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-xl font-medium">{t("loading_config")}</CardTitle>
|
||||
<CardDescription>{t("checking_llm_prefs")}</CardDescription>
|
||||
<CardTitle className="text-xl font-medium">
|
||||
{isAutoConfiguring ? "Setting up AI..." : t("loading_config")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{isAutoConfiguring
|
||||
? "Auto-configuring with available settings"
|
||||
: t("checking_llm_prefs")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center py-6">
|
||||
<Loader2 className="h-12 w-12 text-primary animate-spin" />
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import { toast } from "sonner";
|
||||
import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, messageDocumentsMapAtom, type MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { ChatHeader } from "@/components/new-chat/chat-header";
|
||||
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||
import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview";
|
||||
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
||||
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
|
||||
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { createAttachmentAdapter, extractAttachmentContent } from "@/lib/chat/attachment-adapter";
|
||||
import {
|
||||
|
|
@ -36,15 +37,15 @@ import {
|
|||
*/
|
||||
function extractThinkingSteps(content: unknown): ThinkingStep[] {
|
||||
if (!Array.isArray(content)) return [];
|
||||
|
||||
|
||||
const thinkingPart = content.find(
|
||||
(part: unknown) =>
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
(part: unknown) =>
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
(part as { type: string }).type === "thinking-steps"
|
||||
) as { type: "thinking-steps"; steps: ThinkingStep[] } | undefined;
|
||||
|
||||
|
||||
return thinkingPart?.steps || [];
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ function extractMentionedDocuments(content: unknown): MentionedDocumentInfo[] {
|
|||
|
||||
/**
|
||||
* Convert backend message to assistant-ui ThreadMessageLike format
|
||||
* Filters out 'thinking-steps' part as it's handled separately
|
||||
* Filters out 'thinking-steps' part as it's handled separately via messageThinkingSteps
|
||||
*/
|
||||
function convertToThreadMessage(msg: MessageRecord): ThreadMessageLike {
|
||||
let content: ThreadMessageLike["content"];
|
||||
|
|
@ -77,16 +78,18 @@ function convertToThreadMessage(msg: MessageRecord): ThreadMessageLike {
|
|||
} else if (Array.isArray(msg.content)) {
|
||||
// Filter out custom metadata parts - they're handled separately
|
||||
const filteredContent = msg.content.filter(
|
||||
(part: unknown) => {
|
||||
if (typeof part !== "object" || part === null || !("type" in part)) return true;
|
||||
const partType = (part as { type: string }).type;
|
||||
// Filter out thinking-steps and mentioned-documents
|
||||
return partType !== "thinking-steps" && partType !== "mentioned-documents";
|
||||
}
|
||||
(part: unknown) =>
|
||||
!(
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
(part as { type: string }).type === "thinking-steps"
|
||||
)
|
||||
);
|
||||
content = filteredContent.length > 0
|
||||
? (filteredContent as ThreadMessageLike["content"])
|
||||
: [{ type: "text", text: "" }];
|
||||
content =
|
||||
filteredContent.length > 0
|
||||
? (filteredContent as ThreadMessageLike["content"])
|
||||
: [{ type: "text", text: "" }];
|
||||
} else {
|
||||
content = [{ type: "text", text: String(msg.content) }];
|
||||
}
|
||||
|
|
@ -102,7 +105,12 @@ function convertToThreadMessage(msg: MessageRecord): ThreadMessageLike {
|
|||
/**
|
||||
* Tools that should render custom UI in the chat.
|
||||
*/
|
||||
const TOOLS_WITH_UI = new Set(["generate_podcast", "link_preview", "display_image", "scrape_webpage"]);
|
||||
const TOOLS_WITH_UI = new Set([
|
||||
"generate_podcast",
|
||||
"link_preview",
|
||||
"display_image",
|
||||
"scrape_webpage",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Type for thinking step data from the backend
|
||||
|
|
@ -121,10 +129,11 @@ export default function NewChatPage() {
|
|||
const [threadId, setThreadId] = useState<number | null>(null);
|
||||
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
// Store thinking steps per message ID
|
||||
const [messageThinkingSteps, setMessageThinkingSteps] = useState<
|
||||
Map<string, ThinkingStep[]>
|
||||
>(new Map());
|
||||
// Store thinking steps per message ID - kept separate from content to avoid
|
||||
// "unsupported part type" errors from assistant-ui
|
||||
const [messageThinkingSteps, setMessageThinkingSteps] = useState<Map<string, ThinkingStep[]>>(
|
||||
new Map()
|
||||
);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Get mentioned document IDs from the composer
|
||||
|
|
@ -168,7 +177,7 @@ export default function NewChatPage() {
|
|||
if (response.messages && response.messages.length > 0) {
|
||||
const loadedMessages = response.messages.map(convertToThreadMessage);
|
||||
setMessages(loadedMessages);
|
||||
|
||||
|
||||
// Extract and restore thinking steps from persisted messages
|
||||
const restoredThinkingSteps = new Map<string, ThinkingStep[]>();
|
||||
// Extract and restore mentioned documents from persisted messages
|
||||
|
|
@ -309,10 +318,10 @@ export default function NewChatPage() {
|
|||
// Prepare assistant message
|
||||
const assistantMsgId = `msg-assistant-${Date.now()}`;
|
||||
const currentThinkingSteps = new Map<string, ThinkingStepData>();
|
||||
|
||||
|
||||
// Ordered content parts to preserve inline tool call positions
|
||||
// Each part is either a text segment or a tool call
|
||||
type ContentPart =
|
||||
type ContentPart =
|
||||
| { type: "text"; text: string }
|
||||
| {
|
||||
type: "tool-call";
|
||||
|
|
@ -322,13 +331,13 @@ export default function NewChatPage() {
|
|||
result?: unknown;
|
||||
};
|
||||
const contentParts: ContentPart[] = [];
|
||||
|
||||
|
||||
// Track the current text segment index (for appending text deltas)
|
||||
let currentTextPartIndex = -1;
|
||||
|
||||
|
||||
// Map to track tool call indices for updating results
|
||||
const toolCallIndices = new Map<string, number>();
|
||||
|
||||
|
||||
// Helper to get or create the current text part for appending text
|
||||
const appendText = (delta: string) => {
|
||||
if (currentTextPartIndex >= 0 && contentParts[currentTextPartIndex]?.type === "text") {
|
||||
|
|
@ -340,7 +349,7 @@ export default function NewChatPage() {
|
|||
currentTextPartIndex = contentParts.length - 1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Helper to add a tool call (this "breaks" the current text segment)
|
||||
const addToolCall = (toolCallId: string, toolName: string, args: Record<string, unknown>) => {
|
||||
if (TOOLS_WITH_UI.has(toolName)) {
|
||||
|
|
@ -355,9 +364,12 @@ export default function NewChatPage() {
|
|||
currentTextPartIndex = -1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Helper to update a tool call's args or result
|
||||
const updateToolCall = (toolCallId: string, update: { args?: Record<string, unknown>; result?: unknown }) => {
|
||||
const updateToolCall = (
|
||||
toolCallId: string,
|
||||
update: { args?: Record<string, unknown>; result?: unknown }
|
||||
) => {
|
||||
const index = toolCallIndices.get(toolCallId);
|
||||
if (index !== undefined && contentParts[index]?.type === "tool-call") {
|
||||
const tc = contentParts[index] as ContentPart & { type: "tool-call" };
|
||||
|
|
@ -366,7 +378,7 @@ export default function NewChatPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Helper to build content for UI (without thinking-steps)
|
||||
// Helper to build content for UI (without thinking-steps to avoid assistant-ui errors)
|
||||
const buildContentForUI = (): ThreadMessageLike["content"] => {
|
||||
// Filter to only include text parts with content and tool-calls with UI
|
||||
const filtered = contentParts.filter((part) => {
|
||||
|
|
@ -379,10 +391,10 @@ export default function NewChatPage() {
|
|||
: [{ type: "text", text: "" }];
|
||||
};
|
||||
|
||||
// Helper to build content for persistence (includes thinking-steps)
|
||||
// Helper to build content for persistence (includes thinking-steps for restoration)
|
||||
const buildContentForPersistence = (): unknown[] => {
|
||||
const parts: unknown[] = [];
|
||||
|
||||
|
||||
// Include thinking steps for persistence
|
||||
if (currentThinkingSteps.size > 0) {
|
||||
parts.push({
|
||||
|
|
@ -390,7 +402,7 @@ export default function NewChatPage() {
|
|||
steps: Array.from(currentThinkingSteps.values()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Add content parts (filtered)
|
||||
for (const part of contentParts) {
|
||||
if (part.type === "text" && part.text.length > 0) {
|
||||
|
|
@ -399,7 +411,7 @@ export default function NewChatPage() {
|
|||
parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return parts.length > 0 ? parts : [{ type: "text", text: "" }];
|
||||
};
|
||||
|
||||
|
|
@ -554,13 +566,12 @@ export default function NewChatPage() {
|
|||
const stepData = parsed.data as ThinkingStepData;
|
||||
if (stepData?.id) {
|
||||
currentThinkingSteps.set(stepData.id, stepData);
|
||||
// Update message-specific thinking steps
|
||||
// Update thinking steps state for rendering
|
||||
// The ThinkingStepsScrollHandler in Thread component
|
||||
// will handle auto-scrolling when this state changes
|
||||
setMessageThinkingSteps((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(
|
||||
assistantMsgId,
|
||||
Array.from(currentThinkingSteps.values())
|
||||
);
|
||||
newMap.set(assistantMsgId, Array.from(currentThinkingSteps.values()));
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
|
|
@ -686,8 +697,11 @@ export default function NewChatPage() {
|
|||
<LinkPreviewToolUI />
|
||||
<DisplayImageToolUI />
|
||||
<ScrapeWebpageToolUI />
|
||||
<div className="h-[calc(100vh-64px)] max-h-[calc(100vh-64px)] overflow-hidden">
|
||||
<Thread messageThinkingSteps={messageThinkingSteps} />
|
||||
<div className="flex flex-col h-[calc(100vh-64px)] max-h-[calc(100vh-64px)] overflow-hidden">
|
||||
<ChatHeader searchSpaceId={searchSpaceId} />
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<Thread messageThinkingSteps={messageThinkingSteps} />
|
||||
</div>
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,312 +1,268 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { FileText, MessageSquare, UserPlus, Users } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { updateLLMPreferencesMutationAtom } from "@/atoms/llm-config/llm-config-mutation.atoms";
|
||||
import {
|
||||
globalLLMConfigsAtom,
|
||||
llmConfigsAtom,
|
||||
createNewLLMConfigMutationAtom,
|
||||
updateLLMPreferencesMutationAtom,
|
||||
} from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
|
||||
import {
|
||||
globalNewLLMConfigsAtom,
|
||||
llmPreferencesAtom,
|
||||
} from "@/atoms/llm-config/llm-config-query.atoms";
|
||||
import { OnboardActionCard } from "@/components/onboard/onboard-action-card";
|
||||
import { OnboardAdvancedSettings } from "@/components/onboard/onboard-advanced-settings";
|
||||
import { OnboardHeader } from "@/components/onboard/onboard-header";
|
||||
import { OnboardLLMSetup } from "@/components/onboard/onboard-llm-setup";
|
||||
import { OnboardLoading } from "@/components/onboard/onboard-loading";
|
||||
import { OnboardStats } from "@/components/onboard/onboard-stats";
|
||||
} from "@/atoms/new-llm-config/new-llm-config-query.atoms";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
|
||||
const OnboardPage = () => {
|
||||
const t = useTranslations("onboard");
|
||||
export default function OnboardPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchSpaceId = Number(params.search_space_id);
|
||||
|
||||
// Queries
|
||||
const {
|
||||
data: llmConfigs = [],
|
||||
isFetching: configsLoading,
|
||||
refetch: refreshConfigs,
|
||||
} = useAtomValue(llmConfigsAtom);
|
||||
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
||||
useAtomValue(globalLLMConfigsAtom);
|
||||
const {
|
||||
data: preferences = {},
|
||||
isFetching: preferencesLoading,
|
||||
refetch: refreshPreferences,
|
||||
} = useAtomValue(llmPreferencesAtom);
|
||||
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
|
||||
data: globalConfigs = [],
|
||||
isFetching: globalConfigsLoading,
|
||||
isSuccess: globalConfigsLoaded,
|
||||
} = useAtomValue(globalNewLLMConfigsAtom);
|
||||
const { data: preferences = {}, isFetching: preferencesLoading } =
|
||||
useAtomValue(llmPreferencesAtom);
|
||||
|
||||
// Compute isOnboardingComplete
|
||||
const isOnboardingComplete = useMemo(() => {
|
||||
return !!(
|
||||
preferences.long_context_llm_id &&
|
||||
preferences.fast_llm_id &&
|
||||
preferences.strategic_llm_id
|
||||
);
|
||||
}, [preferences]);
|
||||
// Mutations
|
||||
const { mutateAsync: createConfig, isPending: isCreating } = useAtomValue(
|
||||
createNewLLMConfigMutationAtom
|
||||
);
|
||||
const { mutateAsync: updatePreferences, isPending: isUpdatingPreferences } = useAtomValue(
|
||||
updateLLMPreferencesMutationAtom
|
||||
);
|
||||
|
||||
// State
|
||||
const [isAutoConfiguring, setIsAutoConfiguring] = useState(false);
|
||||
const [autoConfigComplete, setAutoConfigComplete] = useState(false);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [showPromptSettings, setShowPromptSettings] = useState(false);
|
||||
|
||||
const handleRefreshPreferences = useCallback(async () => {
|
||||
await refreshPreferences();
|
||||
}, []);
|
||||
|
||||
// Track if we've already attempted auto-configuration
|
||||
const hasAttemptedAutoConfig = useRef(false);
|
||||
|
||||
// Track if onboarding was complete on initial mount
|
||||
const wasCompleteOnMount = useRef<boolean | null>(null);
|
||||
const hasCheckedInitialState = useRef(false);
|
||||
|
||||
// Check if user is authenticated
|
||||
// Check authentication
|
||||
useEffect(() => {
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
// Save current path and redirect to login
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Capture onboarding state on first load
|
||||
// Check if onboarding is already complete
|
||||
const isOnboardingComplete = preferences.agent_llm_id && preferences.document_summary_llm_id;
|
||||
|
||||
// If onboarding is already complete, redirect immediately
|
||||
useEffect(() => {
|
||||
if (
|
||||
!hasCheckedInitialState.current &&
|
||||
!preferencesLoading &&
|
||||
!configsLoading &&
|
||||
!globalConfigsLoading
|
||||
) {
|
||||
wasCompleteOnMount.current = isOnboardingComplete;
|
||||
hasCheckedInitialState.current = true;
|
||||
if (!preferencesLoading && isOnboardingComplete) {
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
}
|
||||
}, [preferencesLoading, configsLoading, globalConfigsLoading, isOnboardingComplete]);
|
||||
}, [preferencesLoading, isOnboardingComplete, router, searchSpaceId]);
|
||||
|
||||
// Redirect to dashboard if onboarding was already complete
|
||||
// Auto-configure if global configs are available
|
||||
useEffect(() => {
|
||||
if (
|
||||
wasCompleteOnMount.current === true &&
|
||||
!preferencesLoading &&
|
||||
!configsLoading &&
|
||||
!globalConfigsLoading
|
||||
) {
|
||||
const timer = setTimeout(() => {
|
||||
router.push(`/dashboard/${searchSpaceId}`);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [preferencesLoading, configsLoading, globalConfigsLoading, router, searchSpaceId]);
|
||||
const autoConfigureWithGlobal = async () => {
|
||||
if (hasAttemptedAutoConfig.current) return;
|
||||
if (globalConfigsLoading || preferencesLoading) return;
|
||||
if (!globalConfigsLoaded) return;
|
||||
if (isOnboardingComplete) return;
|
||||
|
||||
// Auto-configure LLM roles if global configs are available
|
||||
const autoConfigureLLMs = useCallback(async () => {
|
||||
if (hasAttemptedAutoConfig.current) return;
|
||||
if (globalConfigs.length === 0) return;
|
||||
if (isOnboardingComplete) {
|
||||
setAutoConfigComplete(true);
|
||||
return;
|
||||
}
|
||||
// Only auto-configure if we have global configs
|
||||
if (globalConfigs.length > 0) {
|
||||
hasAttemptedAutoConfig.current = true;
|
||||
setIsAutoConfiguring(true);
|
||||
|
||||
hasAttemptedAutoConfig.current = true;
|
||||
setIsAutoConfiguring(true);
|
||||
try {
|
||||
const firstGlobalConfig = globalConfigs[0];
|
||||
|
||||
try {
|
||||
const allConfigs = [...globalConfigs, ...llmConfigs];
|
||||
await updatePreferences({
|
||||
search_space_id: searchSpaceId,
|
||||
data: {
|
||||
agent_llm_id: firstGlobalConfig.id,
|
||||
document_summary_llm_id: firstGlobalConfig.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (allConfigs.length === 0) {
|
||||
setIsAutoConfiguring(false);
|
||||
return;
|
||||
toast.success("AI configured automatically!", {
|
||||
description: `Using ${firstGlobalConfig.name}. You can customize this later in Settings.`,
|
||||
});
|
||||
|
||||
// Redirect to new-chat
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
} catch (error) {
|
||||
console.error("Auto-configuration failed:", error);
|
||||
toast.error("Auto-configuration failed. Please add a configuration manually.");
|
||||
setIsAutoConfiguring(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use first available config for all roles
|
||||
const defaultConfigId = allConfigs[0].id;
|
||||
autoConfigureWithGlobal();
|
||||
}, [
|
||||
globalConfigs,
|
||||
globalConfigsLoading,
|
||||
globalConfigsLoaded,
|
||||
preferencesLoading,
|
||||
isOnboardingComplete,
|
||||
updatePreferences,
|
||||
searchSpaceId,
|
||||
router,
|
||||
]);
|
||||
|
||||
const newPreferences = {
|
||||
long_context_llm_id: defaultConfigId,
|
||||
fast_llm_id: defaultConfigId,
|
||||
strategic_llm_id: defaultConfigId,
|
||||
};
|
||||
// Handle form submission
|
||||
const handleSubmit = async (formData: LLMConfigFormData) => {
|
||||
try {
|
||||
// Create the config
|
||||
const newConfig = await createConfig(formData);
|
||||
|
||||
// Auto-assign to all roles
|
||||
await updatePreferences({
|
||||
search_space_id: searchSpaceId,
|
||||
data: newPreferences,
|
||||
data: {
|
||||
agent_llm_id: newConfig.id,
|
||||
document_summary_llm_id: newConfig.id,
|
||||
},
|
||||
});
|
||||
await refreshPreferences();
|
||||
setAutoConfigComplete(true);
|
||||
toast.success("AI models configured automatically!", {
|
||||
description: "You can customize these in advanced settings.",
|
||||
|
||||
toast.success("Configuration created!", {
|
||||
description: "Redirecting to chat...",
|
||||
});
|
||||
|
||||
// Redirect to new-chat
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
} catch (error) {
|
||||
console.error("Auto-configuration failed:", error);
|
||||
} finally {
|
||||
setIsAutoConfiguring(false);
|
||||
console.error("Failed to create config:", error);
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message || "Failed to create configuration");
|
||||
}
|
||||
}
|
||||
}, [globalConfigs, llmConfigs, isOnboardingComplete, updatePreferences, refreshPreferences]);
|
||||
};
|
||||
|
||||
// Trigger auto-configuration once data is loaded
|
||||
useEffect(() => {
|
||||
if (!configsLoading && !globalConfigsLoading && !preferencesLoading) {
|
||||
autoConfigureLLMs();
|
||||
}
|
||||
}, [configsLoading, globalConfigsLoading, preferencesLoading, autoConfigureLLMs]);
|
||||
|
||||
const allConfigs = [...globalConfigs, ...llmConfigs];
|
||||
const isReady = autoConfigComplete || isOnboardingComplete;
|
||||
const isSubmitting = isCreating || isUpdatingPreferences;
|
||||
|
||||
// Loading state
|
||||
if (configsLoading || preferencesLoading || globalConfigsLoading || isAutoConfiguring) {
|
||||
if (globalConfigsLoading || preferencesLoading || isAutoConfiguring) {
|
||||
return (
|
||||
<OnboardLoading
|
||||
title={isAutoConfiguring ? "Setting up your AI assistant..." : t("loading_config")}
|
||||
subtitle={
|
||||
isAutoConfiguring
|
||||
? "Auto-configuring optimal settings for you"
|
||||
: "Please wait while we load your configuration"
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show LLM setup if no configs available OR if roles are not assigned yet
|
||||
// This forces users to complete role assignment before seeing the final screen
|
||||
if (allConfigs.length === 0 || !isOnboardingComplete) {
|
||||
return (
|
||||
<OnboardLLMSetup
|
||||
searchSpaceId={searchSpaceId}
|
||||
title={t("welcome_title")}
|
||||
configTitle={
|
||||
allConfigs.length === 0 ? t("setup_llm_configuration") : t("assign_llm_roles_title")
|
||||
}
|
||||
configDescription={
|
||||
allConfigs.length === 0
|
||||
? t("configure_providers_and_assign_roles")
|
||||
: t("complete_role_assignment")
|
||||
}
|
||||
onConfigCreated={() => refreshConfigs()}
|
||||
onConfigDeleted={() => refreshConfigs()}
|
||||
onPreferencesUpdated={handleRefreshPreferences}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Main onboarding view
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="flex items-center justify-center min-h-screen p-4 md:p-8">
|
||||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20 flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="w-full max-w-5xl"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center space-y-6"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 blur-3xl bg-gradient-to-r from-violet-500/20 to-cyan-500/20 rounded-full" />
|
||||
<div className="relative flex items-center justify-center w-24 h-24 mx-auto rounded-2xl bg-gradient-to-br from-violet-500 to-purple-600 shadow-2xl shadow-violet-500/25">
|
||||
<Loader2 className="h-12 w-12 text-white animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
{isAutoConfiguring ? "Setting up your AI..." : "Loading..."}
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{isAutoConfiguring
|
||||
? "Auto-configuring with available settings"
|
||||
: "Please wait while we check your configuration"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center gap-1">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="w-2 h-2 rounded-full bg-violet-500"
|
||||
animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 1, repeat: Infinity, delay: i * 0.2 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If global configs exist but auto-config failed, show simple message
|
||||
if (globalConfigs.length > 0 && !isAutoConfiguring) {
|
||||
return null; // Will redirect via useEffect
|
||||
}
|
||||
|
||||
// No global configs - show the config form
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-background via-background to-muted/30">
|
||||
<div className="container mx-auto px-4 py-8 md:py-12 max-w-3xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="space-y-8"
|
||||
>
|
||||
{/* Header */}
|
||||
<OnboardHeader
|
||||
title={t("welcome_title")}
|
||||
subtitle={
|
||||
isReady ? "You're all set! Choose what you'd like to do next." : t("welcome_subtitle")
|
||||
}
|
||||
isReady={isReady}
|
||||
/>
|
||||
<div className="text-center space-y-4">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", delay: 0.2 }}
|
||||
className="relative inline-block"
|
||||
>
|
||||
<Logo className="w-20 h-20 mx-auto rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<OnboardStats
|
||||
globalConfigsCount={globalConfigs.length}
|
||||
userConfigsCount={llmConfigs.length}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Configure Your AI</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Add your LLM provider to get started with SurfSense
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Cards */}
|
||||
{/* Config Form */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<OnboardActionCard
|
||||
title="Start Chatting"
|
||||
description="Jump right into the AI researcher and start asking questions"
|
||||
icon={MessageSquare}
|
||||
features={[
|
||||
"AI-powered conversations",
|
||||
"Research and explore topics",
|
||||
"Get instant insights",
|
||||
]}
|
||||
buttonText="Start Chatting"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/researcher`)}
|
||||
colorScheme="violet"
|
||||
delay={0.9}
|
||||
/>
|
||||
|
||||
<OnboardActionCard
|
||||
title="Add Sources"
|
||||
description="Connect your data sources to start building your knowledge base"
|
||||
icon={FileText}
|
||||
features={[
|
||||
"Connect documents and files",
|
||||
"Import from various sources",
|
||||
"Build your knowledge base",
|
||||
]}
|
||||
buttonText="Add Sources"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/sources/add`)}
|
||||
colorScheme="blue"
|
||||
delay={0.8}
|
||||
/>
|
||||
|
||||
<OnboardActionCard
|
||||
title="Manage Team"
|
||||
description="Invite team members and collaborate on your search space"
|
||||
icon={Users}
|
||||
features={[
|
||||
"Invite team members",
|
||||
"Assign roles & permissions",
|
||||
"Collaborate together",
|
||||
]}
|
||||
buttonText="Manage Team"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/team`)}
|
||||
colorScheme="emerald"
|
||||
delay={0.7}
|
||||
/>
|
||||
<Card className="border-2 border-muted shadow-xl overflow-hidden">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl">LLM Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LLMConfigForm
|
||||
searchSpaceId={searchSpaceId}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
mode="create"
|
||||
showAdvanced={true}
|
||||
submitLabel="Start Using SurfSense"
|
||||
initialData={{
|
||||
citations_enabled: true,
|
||||
use_default_system_instructions: true,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Advanced Settings */}
|
||||
<OnboardAdvancedSettings
|
||||
searchSpaceId={searchSpaceId}
|
||||
showLLMSettings={showAdvancedSettings}
|
||||
setShowLLMSettings={setShowAdvancedSettings}
|
||||
showPromptSettings={showPromptSettings}
|
||||
setShowPromptSettings={setShowPromptSettings}
|
||||
onConfigCreated={() => refreshConfigs()}
|
||||
onConfigDeleted={() => refreshConfigs()}
|
||||
onPreferencesUpdated={handleRefreshPreferences}
|
||||
/>
|
||||
|
||||
{/* Footer */}
|
||||
<motion.div
|
||||
{/* Footer note */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.1 }}
|
||||
className="text-center mt-10 text-muted-foreground text-sm"
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-center text-sm text-muted-foreground"
|
||||
>
|
||||
<p>
|
||||
You can always adjust these settings later in{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings`)}
|
||||
className="text-primary hover:underline underline-offset-2 transition-colors"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</p>
|
||||
</motion.div>
|
||||
You can add more configurations and customize settings anytime in{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings`)}
|
||||
className="text-violet-500 hover:underline"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardPage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,20 +30,20 @@ interface SettingsNavItem {
|
|||
const settingsNavItems: SettingsNavItem[] = [
|
||||
{
|
||||
id: "models",
|
||||
label: "Model Configs",
|
||||
description: "Configure AI models and providers",
|
||||
label: "Agent Configs",
|
||||
description: "LLM models with prompts & citations",
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
id: "roles",
|
||||
label: "LLM Roles",
|
||||
description: "Manage language model roles",
|
||||
label: "Role Assignments",
|
||||
description: "Assign configs to agent roles",
|
||||
icon: Brain,
|
||||
},
|
||||
{
|
||||
id: "prompts",
|
||||
label: "System Instructions",
|
||||
description: "Customize system prompts",
|
||||
description: "SearchSpace-wide AI instructions",
|
||||
icon: MessageSquare,
|
||||
},
|
||||
];
|
||||
|
|
@ -236,9 +236,6 @@ function SettingsContent({
|
|||
<h1 className="text-xl md:text-2xl font-bold tracking-tight truncate">
|
||||
{activeItem?.label}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5 truncate">
|
||||
{activeItem?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -275,7 +272,7 @@ export default function SettingsPage() {
|
|||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const handleBackToApp = useCallback(() => {
|
||||
router.push(`/dashboard/${searchSpaceId}/researcher`);
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
}, [router, searchSpaceId]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -807,7 +807,6 @@ function RolesTab({
|
|||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
// TODO: Implement edit role dialog/modal
|
||||
console.log("Edit role not yet implemented", role);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue