diff --git a/web/src/app/home/components/account-settings-dialog/AccountSettingsDialog.tsx b/web/src/app/home/components/account-settings-dialog/AccountSettingsDialog.tsx
deleted file mode 100644
index b658c9fa..00000000
--- a/web/src/app/home/components/account-settings-dialog/AccountSettingsDialog.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import * as React from 'react';
-import { useState, useEffect } from 'react';
-import { toast } from 'sonner';
-import { useTranslation } from 'react-i18next';
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
-} from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import {
- Item,
- ItemMedia,
- ItemContent,
- ItemTitle,
- ItemDescription,
- ItemActions,
-} from '@/components/ui/item';
-import { httpClient } from '@/app/infra/http/HttpClient';
-import { systemInfo } from '@/app/infra/http';
-import { Loader2, ExternalLink, KeyRound, Layers } from 'lucide-react';
-import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog';
-
-interface AccountSettingsDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-export default function AccountSettingsDialog({
- open,
- onOpenChange,
-}: AccountSettingsDialogProps) {
- const { t } = useTranslation();
- const [accountType, setAccountType] = useState<'local' | 'space'>('local');
- const [hasPassword, setHasPassword] = useState(false);
- const [userEmail, setUserEmail] = useState('');
- const [loading, setLoading] = useState(true);
- const [spaceBindLoading, setSpaceBindLoading] = useState(false);
- const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
-
- useEffect(() => {
- if (open) {
- loadUserInfo();
- }
- }, [open]);
-
- async function loadUserInfo() {
- setLoading(true);
- try {
- const info = await httpClient.getUserInfo();
- setAccountType(info.account_type);
- setHasPassword(info.has_password);
- setUserEmail(info.user);
- } catch {
- toast.error(t('common.error'));
- } finally {
- setLoading(false);
- }
- }
-
- const handleBindSpace = async () => {
- setSpaceBindLoading(true);
- try {
- const token = localStorage.getItem('token');
- if (!token) {
- toast.error(t('common.error'));
- setSpaceBindLoading(false);
- return;
- }
- const currentOrigin = window.location.origin;
- const redirectUri = `${currentOrigin}/auth/space/callback?mode=bind`;
- // Pass token as state for security verification
- const response = await httpClient.getSpaceAuthorizeUrl(
- redirectUri,
- token,
- );
- window.location.href = response.authorize_url;
- } catch {
- toast.error(t('common.spaceLoginFailed'));
- setSpaceBindLoading(false);
- }
- };
-
- const handlePasswordDialogClose = (dialogOpen: boolean) => {
- setPasswordDialogOpen(dialogOpen);
- if (!dialogOpen) {
- // Reload user info to update password status
- loadUserInfo();
- }
- };
-
- return (
- <>
-
-
-
- >
- );
-}
diff --git a/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx b/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx
new file mode 100644
index 00000000..5cf7e4c8
--- /dev/null
+++ b/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx
@@ -0,0 +1,170 @@
+import { useState, useEffect } from 'react';
+import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
+import { Button } from '@/components/ui/button';
+import {
+ Item,
+ ItemMedia,
+ ItemContent,
+ ItemTitle,
+ ItemDescription,
+ ItemActions,
+} from '@/components/ui/item';
+import { httpClient } from '@/app/infra/http/HttpClient';
+import { systemInfo } from '@/app/infra/http';
+import { Loader2, ExternalLink, KeyRound, Layers } from 'lucide-react';
+import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog';
+
+interface AccountSettingsPanelProps {
+ // True when this panel is the active section and the dialog is open.
+ active: boolean;
+ onEmailResolved?: (email: string) => void;
+}
+
+export default function AccountSettingsPanel({
+ active,
+ onEmailResolved,
+}: AccountSettingsPanelProps) {
+ const { t } = useTranslation();
+ const [accountType, setAccountType] = useState<'local' | 'space'>('local');
+ const [hasPassword, setHasPassword] = useState(false);
+ const [userEmail, setUserEmail] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [spaceBindLoading, setSpaceBindLoading] = useState(false);
+ const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
+
+ useEffect(() => {
+ if (active) {
+ loadUserInfo();
+ }
+ }, [active]);
+
+ async function loadUserInfo() {
+ setLoading(true);
+ try {
+ const info = await httpClient.getUserInfo();
+ setAccountType(info.account_type);
+ setHasPassword(info.has_password);
+ setUserEmail(info.user);
+ onEmailResolved?.(info.user);
+ } catch {
+ toast.error(t('common.error'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ const handleBindSpace = async () => {
+ setSpaceBindLoading(true);
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ toast.error(t('common.error'));
+ setSpaceBindLoading(false);
+ return;
+ }
+ const currentOrigin = window.location.origin;
+ const redirectUri = `${currentOrigin}/auth/space/callback?mode=bind`;
+ // Pass token as state for security verification
+ const response = await httpClient.getSpaceAuthorizeUrl(
+ redirectUri,
+ token,
+ );
+ window.location.href = response.authorize_url;
+ } catch {
+ toast.error(t('common.spaceLoginFailed'));
+ setSpaceBindLoading(false);
+ }
+ };
+
+ const handlePasswordDialogClose = (dialogOpen: boolean) => {
+ setPasswordDialogOpen(dialogOpen);
+ if (!dialogOpen) {
+ // Reload user info to update password status
+ loadUserInfo();
+ }
+ };
+
+ return (
+
+ {userEmail && (
+
{userEmail}
+ )}
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {/* Password Item */}
+ -
+
+
+
+
+ {t('account.passwordStatus')}
+
+ {hasPassword
+ ? t('account.passwordSetDescription')
+ : t('account.setPasswordHint')}
+
+
+
+
+
+
+
+ {/* Space Account Item */}
+ -
+
+
+
+
+ {t('account.spaceStatus')}
+
+ {accountType === 'space'
+ ? t('account.spaceBoundDescription')
+ : t('account.bindSpaceDescription')}
+
+
+ {accountType === 'local' && (
+
+
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx b/web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx
similarity index 61%
rename from web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx
rename to web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx
index 8ac3f496..e45d5f50 100644
--- a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx
+++ b/web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx
@@ -3,7 +3,6 @@ import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Copy, Check, Trash2, Plus } from 'lucide-react';
-import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import {
Dialog,
DialogContent,
@@ -55,20 +54,15 @@ interface Webhook {
created_at: string;
}
-interface ApiIntegrationDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
+interface ApiIntegrationPanelProps {
+ // True when this panel is the active section and the dialog is open.
+ active: boolean;
}
-export default function ApiIntegrationDialog({
- open,
- onOpenChange,
-}: ApiIntegrationDialogProps) {
+export default function ApiIntegrationPanel({
+ active,
+}: ApiIntegrationPanelProps) {
const { t } = useTranslation();
- const navigate = useNavigate();
- const location = useLocation();
- const pathname = location.pathname;
- const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState('apikeys');
const [apiKeys, setApiKeys] = useState([]);
const [webhooks, setWebhooks] = useState([]);
@@ -91,33 +85,7 @@ export default function ApiIntegrationDialog({
);
const [copiedKey, setCopiedKey] = useState(null);
- // Sync URL with dialog state
- useEffect(() => {
- if (open) {
- const params = new URLSearchParams(searchParams.toString());
- params.set('action', 'showApiIntegrationSettings');
- navigate(`${pathname}?${params.toString()}`, {
- preventScrollReset: true,
- });
- }
- }, [open]);
-
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen && (deleteKeyId || deleteWebhookId)) {
- return;
- }
- if (!newOpen) {
- const params = new URLSearchParams(searchParams.toString());
- params.delete('action');
- const newUrl = params.toString()
- ? `${pathname}?${params.toString()}`
- : pathname;
- navigate(newUrl, { preventScrollReset: true });
- }
- onOpenChange(newOpen);
- };
-
- // 清理 body 样式,防止对话框关闭后页面无法交互
+ // 清理 body 样式,防止嵌套对话框关闭后页面无法交互
useEffect(() => {
if (!deleteKeyId && !deleteWebhookId) {
const cleanup = () => {
@@ -131,11 +99,11 @@ export default function ApiIntegrationDialog({
}, [deleteKeyId, deleteWebhookId]);
useEffect(() => {
- if (open) {
+ if (active) {
loadApiKeys();
loadWebhooks();
}
- }, [open]);
+ }, [active]);
const loadApiKeys = async () => {
setLoading(true);
@@ -284,233 +252,216 @@ export default function ApiIntegrationDialog({
return (
<>
-