2025-08-08 21:12:28 -07:00
|
|
|
"use client";
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-12-21 22:26:33 -08:00
|
|
|
import { useAtomValue, useSetAtom } from "jotai";
|
2025-11-12 12:32:04 +02:00
|
|
|
import { useParams, usePathname, useRouter } from "next/navigation";
|
feat(i18n): Add next-intl framework with full bilingual support (EN/ZH)
- Implement next-intl framework for scalable i18n
- Add complete Chinese (Simplified) localization
- Support 400+ translated strings across all pages
- Add language switcher with persistent preference
- Zero breaking changes to existing functionality
Framework additions:
- i18n routing and middleware
- LocaleContext for client-side state
- LanguageSwitcher component
- Translation files (en.json, zh.json)
Translated components:
- Homepage: Hero, features, CTA, navbar
- Auth: Login, register
- Dashboard: Main page, layout
- Connectors: Management, add page (all categories)
- Documents: Upload, manage, filters
- Settings: LLM configs, role assignments
- Onboarding: Add provider, assign roles
- Logs: Task logs viewer
Adding a new language now requires only:
1. Create messages/<locale>.json
2. Add locale to i18n/routing.ts
2025-10-26 14:05:46 +08:00
|
|
|
import { useTranslations } from "next-intl";
|
2025-10-27 20:30:10 -07:00
|
|
|
import type React from "react";
|
2026-01-08 19:11:07 +02:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
2025-12-23 01:16:25 -08:00
|
|
|
import { toast } from "sonner";
|
2025-12-19 00:45:29 -08:00
|
|
|
import { myAccessAtom } from "@/atoms/members/members-query.atoms";
|
2025-12-23 01:16:25 -08:00
|
|
|
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";
|
2025-12-11 21:19:29 +00:00
|
|
|
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
2026-01-01 22:56:37 -08:00
|
|
|
import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup";
|
2026-01-08 19:11:07 +02:00
|
|
|
import { LayoutDataProvider } from "@/components/layout";
|
2026-01-06 21:37:49 +05:30
|
|
|
import { OnboardingTour } from "@/components/onboarding-tour";
|
2025-11-14 00:42:19 +02:00
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
2026-01-25 16:15:25 +05:30
|
|
|
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-08-11 10:49:36 -07:00
|
|
|
export function DashboardClientLayout({
|
2025-11-14 00:42:19 +02:00
|
|
|
children,
|
|
|
|
|
searchSpaceId,
|
2025-04-07 23:47:06 -07:00
|
|
|
}: {
|
2025-11-14 00:42:19 +02:00
|
|
|
children: React.ReactNode;
|
|
|
|
|
searchSpaceId: string;
|
2025-04-07 23:47:06 -07:00
|
|
|
}) {
|
2025-11-14 00:42:19 +02:00
|
|
|
const t = useTranslations("dashboard");
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const pathname = usePathname();
|
2025-12-21 22:26:33 -08:00
|
|
|
const { search_space_id } = useParams();
|
2025-11-14 00:42:19 +02:00
|
|
|
const setActiveSearchSpaceIdState = useSetAtom(activeSearchSpaceIdAtom);
|
2025-11-11 18:07:32 -08:00
|
|
|
|
2025-12-23 01:16:25 -08:00
|
|
|
const {
|
|
|
|
|
data: preferences = {},
|
|
|
|
|
isFetching: loading,
|
|
|
|
|
error,
|
|
|
|
|
refetch: refetchPreferences,
|
|
|
|
|
} = useAtomValue(llmPreferencesAtom);
|
|
|
|
|
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
|
|
|
|
useAtomValue(globalNewLLMConfigsAtom);
|
|
|
|
|
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
|
2025-12-11 13:42:33 +02:00
|
|
|
|
2025-12-10 10:31:26 +00:00
|
|
|
const isOnboardingComplete = useCallback(() => {
|
2026-01-29 15:28:31 -08:00
|
|
|
// Check that both LLM IDs are set (including 0 for Auto mode)
|
|
|
|
|
return (
|
|
|
|
|
preferences.agent_llm_id !== null &&
|
|
|
|
|
preferences.agent_llm_id !== undefined &&
|
|
|
|
|
preferences.document_summary_llm_id !== null &&
|
|
|
|
|
preferences.document_summary_llm_id !== undefined
|
|
|
|
|
);
|
2025-12-10 10:31:26 +00:00
|
|
|
}, [preferences]);
|
2025-12-11 13:42:33 +02:00
|
|
|
|
2025-12-17 20:44:07 +00:00
|
|
|
const { data: access = null, isLoading: accessLoading } = useAtomValue(myAccessAtom);
|
2025-11-14 00:42:19 +02:00
|
|
|
const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false);
|
2025-12-23 01:16:25 -08:00
|
|
|
const [isAutoConfiguring, setIsAutoConfiguring] = useState(false);
|
|
|
|
|
const hasAttemptedAutoConfig = useRef(false);
|
2025-11-11 18:07:32 -08:00
|
|
|
|
2025-11-14 00:42:19 +02:00
|
|
|
const isOnboardingPage = pathname?.includes("/onboard");
|
2025-11-27 22:45:04 -08:00
|
|
|
const isOwner = access?.is_owner ?? false;
|
|
|
|
|
|
2025-11-14 00:42:19 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOnboardingPage) {
|
|
|
|
|
setHasCheckedOnboarding(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-08 11:17:43 -07:00
|
|
|
|
2025-12-23 01:16:25 -08:00
|
|
|
if (
|
|
|
|
|
!loading &&
|
|
|
|
|
!accessLoading &&
|
|
|
|
|
!globalConfigsLoading &&
|
|
|
|
|
!hasCheckedOnboarding &&
|
|
|
|
|
!isAutoConfiguring
|
|
|
|
|
) {
|
2025-11-14 00:42:19 +02:00
|
|
|
const onboardingComplete = isOnboardingComplete();
|
2025-10-10 00:50:29 -07:00
|
|
|
|
2025-12-23 01:16:25 -08:00
|
|
|
if (onboardingComplete) {
|
|
|
|
|
setHasCheckedOnboarding(true);
|
|
|
|
|
return;
|
2025-11-14 00:42:19 +02:00
|
|
|
}
|
2025-10-10 00:50:29 -07:00
|
|
|
|
2025-12-23 01:16:25 -08:00
|
|
|
if (!isOwner) {
|
|
|
|
|
setHasCheckedOnboarding(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
router.push(`/dashboard/${searchSpaceId}/onboard`);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsAutoConfiguring(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
autoConfigureWithGlobal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.push(`/dashboard/${searchSpaceId}/onboard`);
|
2025-11-14 00:42:19 +02:00
|
|
|
setHasCheckedOnboarding(true);
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
loading,
|
2025-11-27 22:45:04 -08:00
|
|
|
accessLoading,
|
2025-12-23 01:16:25 -08:00
|
|
|
globalConfigsLoading,
|
2025-11-14 00:42:19 +02:00
|
|
|
isOnboardingComplete,
|
|
|
|
|
isOnboardingPage,
|
2025-11-27 22:45:04 -08:00
|
|
|
isOwner,
|
2025-12-23 01:16:25 -08:00
|
|
|
isAutoConfiguring,
|
|
|
|
|
globalConfigs,
|
2025-11-14 00:42:19 +02:00
|
|
|
router,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
hasCheckedOnboarding,
|
2025-12-23 01:16:25 -08:00
|
|
|
updatePreferences,
|
|
|
|
|
refetchPreferences,
|
2025-11-14 00:42:19 +02:00
|
|
|
]);
|
2025-10-10 00:50:29 -07:00
|
|
|
|
2025-11-14 00:42:19 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
const activeSeacrhSpaceId =
|
|
|
|
|
typeof search_space_id === "string"
|
|
|
|
|
? search_space_id
|
|
|
|
|
: Array.isArray(search_space_id) && search_space_id.length > 0
|
|
|
|
|
? search_space_id[0]
|
|
|
|
|
: "";
|
|
|
|
|
if (!activeSeacrhSpaceId) return;
|
|
|
|
|
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
|
2025-12-21 22:26:33 -08:00
|
|
|
}, [search_space_id, setActiveSearchSpaceIdState]);
|
2025-10-10 00:50:29 -07:00
|
|
|
|
2026-01-25 16:15:25 +05:30
|
|
|
// Determine if we should show loading
|
|
|
|
|
const shouldShowLoading =
|
2026-01-25 16:16:09 +05:30
|
|
|
(!hasCheckedOnboarding &&
|
2025-12-23 01:16:25 -08:00
|
|
|
(loading || accessLoading || globalConfigsLoading) &&
|
|
|
|
|
!isOnboardingPage) ||
|
2026-01-25 16:16:09 +05:30
|
|
|
isAutoConfiguring;
|
2026-01-25 16:15:25 +05:30
|
|
|
|
|
|
|
|
// Use global loading screen - spinner animation won't reset
|
2026-01-27 15:28:30 +05:30
|
|
|
useGlobalLoadingEffect(shouldShowLoading);
|
2026-01-25 16:15:25 +05:30
|
|
|
|
|
|
|
|
if (shouldShowLoading) {
|
|
|
|
|
return null;
|
2025-11-14 00:42:19 +02:00
|
|
|
}
|
2025-10-10 00:50:29 -07:00
|
|
|
|
2025-11-14 00:42:19 +02:00
|
|
|
if (error && !hasCheckedOnboarding && !isOnboardingPage) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
|
|
|
|
|
<Card className="w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-xl font-medium text-destructive">
|
|
|
|
|
{t("config_error")}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>{t("failed_load_llm_config")}</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
2025-12-14 22:32:13 -08:00
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{error instanceof Error ? error.message : String(error)}
|
|
|
|
|
</p>
|
2025-11-14 00:42:19 +02:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-11-12 12:32:04 +02:00
|
|
|
|
2025-11-14 00:42:19 +02:00
|
|
|
return (
|
2026-01-02 04:07:13 +05:30
|
|
|
<DocumentUploadDialogProvider>
|
2026-01-06 21:37:49 +05:30
|
|
|
<OnboardingTour />
|
2026-03-07 04:46:48 +05:30
|
|
|
<LayoutDataProvider searchSpaceId={searchSpaceId}>{children}</LayoutDataProvider>
|
2026-01-02 04:07:13 +05:30
|
|
|
</DocumentUploadDialogProvider>
|
2025-07-27 10:05:37 -07:00
|
|
|
);
|
|
|
|
|
}
|