Merge remote-tracking branch 'upstream/dev' into pr-611

This commit is contained in:
Anish Sarkar 2025-12-23 15:45:28 +05:30
commit 6f330e7b8d
92 changed files with 5331 additions and 6029 deletions

View file

@ -3,10 +3,8 @@
import { CTAHomepage } from "@/components/homepage/cta";
import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid";
import { FeaturesCards } from "@/components/homepage/features-card";
import { Footer } from "@/components/homepage/footer";
import { HeroSection } from "@/components/homepage/hero-section";
import ExternalIntegrations from "@/components/homepage/integrations";
import { Navbar } from "@/components/homepage/navbar";
export default function HomePage() {
return (

View file

@ -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" />

View file

@ -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>
);

View file

@ -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;
}

View file

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

View file

@ -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" />