feat: update dependencies and enhance account settings dialog with password management and improved UI elements

This commit is contained in:
Junyan Qin
2025-12-28 22:38:11 +08:00
parent 24c15b4479
commit 07ad846e96
12 changed files with 549 additions and 340 deletions

View File

@@ -34,7 +34,7 @@
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",

2
web/pnpm-lock.yaml generated
View File

@@ -51,7 +51,7 @@ dependencies:
specifier: ^2.2.4
version: 2.2.6(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-separator':
specifier: ^1.1.7
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.1)(react@19.2.1)
'@radix-ui/react-slot':
specifier: ^1.2.3

View File

@@ -33,6 +33,7 @@ export default function SpaceOAuthCallback() {
const [isBindMode, setIsBindMode] = useState(false);
const [code, setCode] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [localEmail, setLocalEmail] = useState<string>('');
const handleOAuthCallback = useCallback(
async (authCode: string) => {
@@ -115,6 +116,7 @@ export default function SpaceOAuthCallback() {
}
setBindState(state);
setIsBindMode(true);
setLocalEmail(localStorage.getItem('userEmail') || '');
setStatus('confirm');
} else {
// Normal login/register mode
@@ -169,7 +171,9 @@ export default function SpaceOAuthCallback() {
<>
<AlertTriangle className="h-12 w-12 text-yellow-500" />
<p className="text-sm text-center text-muted-foreground px-4">
{t('account.bindSpaceWarning')}
{t('account.bindSpaceWarning', {
localEmail: localEmail || '-',
})}
</p>
<div className="flex gap-3 w-full">
<Button

View File

@@ -2,9 +2,6 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import {
@@ -16,17 +13,16 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
Item,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
} from '@/components/ui/item';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Loader2, ExternalLink } from 'lucide-react';
import { Loader2, ExternalLink, KeyRound } from 'lucide-react';
import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog';
interface AccountSettingsDialogProps {
open: boolean;
@@ -38,46 +34,12 @@ export default function AccountSettingsDialog({
onOpenChange,
}: AccountSettingsDialogProps) {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
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);
// Schema with optional currentPassword
const formSchema = z
.object({
currentPassword: z.string().optional(),
newPassword: z
.string()
.min(1, { message: t('common.newPasswordRequired') }),
confirmNewPassword: z
.string()
.min(1, { message: t('common.confirmPasswordRequired') }),
})
.refine((data) => data.newPassword === data.confirmNewPassword, {
message: t('common.passwordsDoNotMatch'),
path: ['confirmNewPassword'],
})
.refine(
(data) =>
!hasPassword ||
(data.currentPassword && data.currentPassword.length > 0),
{
message: t('common.currentPasswordRequired'),
path: ['currentPassword'],
},
);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
},
});
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
useEffect(() => {
if (open) {
@@ -85,14 +47,6 @@ export default function AccountSettingsDialog({
}
}, [open]);
useEffect(() => {
form.reset({
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
});
}, [hasPassword, form]);
async function loadUserInfo() {
setLoading(true);
try {
@@ -107,20 +61,6 @@ export default function AccountSettingsDialog({
}
}
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsSubmitting(true);
try {
await httpClient.setPassword(values.newPassword, values.currentPassword);
toast.success(t('account.passwordSetSuccess'));
form.reset();
setHasPassword(true);
} catch {
toast.error(t('common.changePasswordFailed'));
} finally {
setIsSubmitting(false);
}
};
const handleBindSpace = async () => {
setSpaceBindLoading(true);
try {
@@ -144,7 +84,16 @@ export default function AccountSettingsDialog({
}
};
const handlePasswordDialogClose = (dialogOpen: boolean) => {
setPasswordDialogOpen(dialogOpen);
if (!dialogOpen) {
// Reload user info to update password status
loadUserInfo();
}
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
@@ -157,102 +106,78 @@ export default function AccountSettingsDialog({
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="space-y-6">
{/* Password Section */}
<div className="space-y-4">
<h4 className="text-sm font-medium">
<div className="space-y-2">
{/* Password Item */}
<Item size="sm" variant="muted" className="rounded-lg">
<ItemMedia variant="icon">
<KeyRound className="h-4 w-4" />
</ItemMedia>
<ItemContent>
<ItemTitle>{t('account.passwordStatus')}</ItemTitle>
<ItemDescription>
{hasPassword
? t('account.passwordSetDescription')
: t('account.setPasswordHint')}
</ItemDescription>
</ItemContent>
<ItemActions>
<Button
variant="outline"
size="sm"
onClick={() => setPasswordDialogOpen(true)}
>
{hasPassword
? t('common.changePassword')
: t('account.setPassword')}
</h4>
{!hasPassword && (
<p className="text-sm text-muted-foreground">
{t('account.setPasswordHint')}
</p>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
{hasPassword && (
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.currentPassword')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('common.enterCurrentPassword')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.newPassword')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('common.enterNewPassword')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmNewPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.confirmNewPassword')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('common.enterConfirmPassword')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={isSubmitting}
className="w-full"
>
{isSubmitting ? t('common.saving') : t('common.save')}
</Button>
</form>
</Form>
</div>
</ItemActions>
</Item>
{/* Bind Space Account - only for local accounts */}
{/* Space Account Item */}
<Item size="sm" variant="muted" className="rounded-lg">
<ItemMedia variant="icon">
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2L2 7L12 12L22 7L12 2Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 17L12 22L22 17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 12L12 17L22 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</ItemMedia>
<ItemContent>
<ItemTitle>{t('account.spaceStatus')}</ItemTitle>
<ItemDescription>
{accountType === 'space'
? t('account.spaceBoundDescription')
: t('account.bindSpaceDescription')}
</ItemDescription>
</ItemContent>
{accountType === 'local' && (
<>
<Separator />
<div className="space-y-4">
<h4 className="text-sm font-medium">
{t('account.bindSpace')}
</h4>
<p className="text-sm text-muted-foreground">
{t('account.bindSpaceDescription')}
</p>
<ItemActions>
<Button
variant="outline"
className="w-full"
size="sm"
onClick={handleBindSpace}
disabled={spaceBindLoading}
>
@@ -263,12 +188,19 @@ export default function AccountSettingsDialog({
)}
{t('account.bindSpaceButton')}
</Button>
</div>
</>
</ItemActions>
)}
</Item>
</div>
)}
</DialogContent>
</Dialog>
<PasswordChangeDialog
open={passwordDialogOpen}
onOpenChange={handlePasswordDialogClose}
hasPassword={hasPassword}
/>
</>
);
}

View File

@@ -5,6 +5,7 @@ import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Copy, Trash2, Plus } from 'lucide-react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import {
Dialog,
DialogContent,
@@ -66,6 +67,9 @@ export default function ApiIntegrationDialog({
onOpenChange,
}: ApiIntegrationDialogProps) {
const { t } = useTranslation();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState('apikeys');
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [webhooks, setWebhooks] = useState<Webhook[]>([]);
@@ -84,6 +88,30 @@ export default function ApiIntegrationDialog({
const [newWebhookEnabled, setNewWebhookEnabled] = useState(true);
const [deleteWebhookId, setDeleteWebhookId] = useState<number | null>(null);
// Sync URL with dialog state
useEffect(() => {
if (open) {
const params = new URLSearchParams(searchParams.toString());
params.set('action', 'showApiIntegrationSettings');
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
}
}, [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;
router.replace(newUrl, { scroll: false });
}
onOpenChange(newOpen);
};
// 清理 body 样式,防止对话框关闭后页面无法交互
useEffect(() => {
if (!deleteKeyId && !deleteWebhookId) {
@@ -231,16 +259,7 @@ export default function ApiIntegrationDialog({
return (
<>
<Dialog
open={open}
onOpenChange={(newOpen) => {
// 如果删除确认框是打开的,不允许关闭主对话框
if (!newOpen && (deleteKeyId || deleteWebhookId)) {
return;
}
onOpenChange(newOpen);
}}
>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[800px] h-[26rem] flex flex-col">
<DialogHeader>
<DialogTitle>{t('common.manageApiIntegration')}</DialogTitle>

View File

@@ -20,7 +20,6 @@ import {
Lightbulb,
LogOut,
KeyRound,
User,
} from 'lucide-react';
import { useTheme } from 'next-themes';
@@ -79,6 +78,12 @@ export default function HomeSidebar({
if (searchParams.get('action') === 'showModelSettings') {
setModelsDialogOpen(true);
}
if (searchParams.get('action') === 'showAccountSettings') {
setAccountSettingsOpen(true);
}
if (searchParams.get('action') === 'showApiIntegrationSettings') {
setApiKeyDialogOpen(true);
}
}, [searchParams]);
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
@@ -114,6 +119,23 @@ export default function HomeSidebar({
}
}
// 处理账户设置对话框的打开和关闭,同时更新 URL
function handleAccountSettingsChange(open: boolean) {
setAccountSettingsOpen(open);
if (open) {
const params = new URLSearchParams(searchParams.toString());
params.set('action', 'showAccountSettings');
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
} else {
const params = new URLSearchParams(searchParams.toString());
params.delete('action');
const newUrl = params.toString()
? `${pathname}?${params.toString()}`
: pathname;
router.replace(newUrl, { scroll: false });
}
}
useEffect(() => {
initSelect();
if (!localStorage.getItem('token')) {
@@ -337,17 +359,34 @@ export default function HomeSidebar({
<PopoverContent
side="right"
align="end"
className="w-auto p-4 flex flex-col gap-4"
className="w-auto p-2 flex flex-col gap-2"
>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">{t('common.theme')}</span>
<div
className="flex items-center gap-3 p-2 rounded-lg hover:bg-accent cursor-pointer"
onClick={() => {
handleAccountSettingsChange(true);
setPopoverOpen(false);
}}
>
<div className="w-10 h-10 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-sm font-medium">
{userEmail ? userEmail.charAt(0).toUpperCase() : 'U'}
</div>
<span className="text-sm truncate max-w-[180px]">
{userEmail || t('account.settings')}
</span>
</div>
<LanguageSelector
triggerClassName="w-full"
onOpenChange={setLanguageSelectorOpen}
/>
<ToggleGroup
type="single"
value={theme}
onValueChange={(value) => {
if (value) setTheme(value);
}}
className="justify-start"
className="w-full justify-start"
>
<ToggleGroupItem value="light" size="sm">
<Sun className="h-4 w-4" />
@@ -359,22 +398,8 @@ export default function HomeSidebar({
<Monitor className="h-4 w-4" />
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">
{t('common.language')}
</span>
<LanguageSelector
triggerClassName="w-full"
onOpenChange={setLanguageSelectorOpen}
/>
</div>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">
{t('common.integration')}
</span>
<div className="flex flex-col gap-1">
<Button
variant="ghost"
className="w-full justify-start font-normal"
@@ -386,36 +411,12 @@ export default function HomeSidebar({
<KeyRound className="w-4 h-4 mr-2" />
{t('common.apiIntegration')}
</Button>
</div>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">{t('common.account')}</span>
{/* User email display */}
<Button
variant="ghost"
className="w-full justify-start font-normal"
onClick={() => {
setAccountSettingsOpen(true);
setPopoverOpen(false);
}}
>
<User className="w-4 h-4 mr-2" />
<span className="truncate max-w-[180px]">
{userEmail || t('account.settings')}
</span>
</Button>
<Button
variant="ghost"
className="w-full justify-start font-normal"
onClick={() => {
// open docs.langbot.app
const language = localStorage.getItem('langbot_language');
if (language === 'zh-Hans') {
window.open(
'https://docs.langbot.app/zh/insight/guide.html',
'_blank',
);
} else if (language === 'zh-Hant') {
if (language === 'zh-Hans' || language === 'zh-Hant') {
window.open(
'https://docs.langbot.app/zh/insight/guide.html',
'_blank',
@@ -449,9 +450,7 @@ export default function HomeSidebar({
<Button
variant="ghost"
className="w-full justify-start font-normal"
onClick={() => {
handleLogout();
}}
onClick={() => handleLogout()}
>
<LogOut className="w-4 h-4 mr-2" />
{t('common.logout')}
@@ -462,7 +461,7 @@ export default function HomeSidebar({
</div>
<AccountSettingsDialog
open={accountSettingsOpen}
onOpenChange={setAccountSettingsOpen}
onOpenChange={handleAccountSettingsChange}
/>
<ApiIntegrationDialog
open={apiKeyDialogOpen}

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -26,12 +26,26 @@ import {
import { Input } from '@/components/ui/input';
import { httpClient } from '@/app/infra/http/HttpClient';
const getFormSchema = (t: (key: string) => string) =>
interface PasswordChangeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
hasPassword?: boolean;
}
export default function PasswordChangeDialog({
open,
onOpenChange,
hasPassword = true,
}: PasswordChangeDialogProps) {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const getFormSchema = () =>
z
.object({
currentPassword: z
.string()
.min(1, { message: t('common.currentPasswordRequired') }),
currentPassword: hasPassword
? z.string().min(1, { message: t('common.currentPasswordRequired') })
: z.string().optional(),
newPassword: z
.string()
.min(1, { message: t('common.newPasswordRequired') }),
@@ -44,18 +58,7 @@ const getFormSchema = (t: (key: string) => string) =>
path: ['confirmNewPassword'],
});
interface PasswordChangeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export default function PasswordChangeDialog({
open,
onOpenChange,
}: PasswordChangeDialogProps) {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const formSchema = getFormSchema(t);
const formSchema = getFormSchema();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -66,14 +69,30 @@ export default function PasswordChangeDialog({
},
});
// Reset form when dialog opens/closes or hasPassword changes
useEffect(() => {
if (open) {
form.reset({
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
});
}
}, [open, hasPassword, form]);
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsSubmitting(true);
try {
if (hasPassword) {
await httpClient.changePassword(
values.currentPassword,
values.currentPassword!,
values.newPassword,
);
toast.success(t('common.changePasswordSuccess'));
} else {
await httpClient.setPassword(values.newPassword, undefined);
toast.success(t('account.passwordSetSuccess'));
}
form.reset();
onOpenChange(false);
} catch {
@@ -87,10 +106,15 @@ export default function PasswordChangeDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('common.changePassword')}</DialogTitle>
<DialogTitle>
{hasPassword
? t('common.changePassword')
: t('account.setPassword')}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{hasPassword && (
<FormField
control={form.control}
name="currentPassword"
@@ -108,6 +132,7 @@ export default function PasswordChangeDialog({
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="newPassword"

View File

@@ -0,0 +1,193 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Separator } from '@/components/ui/separator';
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
role="list"
data-slot="item-group"
className={cn('group/item-group flex flex-col', className)}
{...props}
/>
);
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn('my-0', className)}
{...props}
/>
);
}
const itemVariants = cva(
'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
{
variants: {
variant: {
default: 'bg-transparent',
outline: 'border-border',
muted: 'bg-muted/50',
},
size: {
default: 'p-4 gap-4 ',
sm: 'py-3 px-4 gap-2.5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
function Item({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'div'> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'div';
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
);
}
const itemMediaVariants = cva(
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
{
variants: {
variant: {
default: 'bg-transparent',
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
},
},
defaultVariants: {
variant: 'default',
},
},
);
function ItemMedia({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
}
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-content"
className={cn(
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
className,
)}
{...props}
/>
);
}
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-title"
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
className,
)}
{...props}
/>
);
}
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="item-description"
className={cn(
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
);
}
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-actions"
className={cn('flex items-center gap-2', className)}
{...props}
/>
);
}
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-header"
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
);
}
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-footer"
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
);
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
};

View File

@@ -49,7 +49,8 @@ const enUS = {
loading: 'Loading...',
or: 'or',
loginWithSpace: 'Login with Space',
spaceLoginRecommended: 'Recommended: Sync models and credits from Space',
spaceLoginRecommended:
'Recommended: Use official stable model APIs and cloud services',
loginLocal: 'Login with local account',
loginWithPassword: 'Login with password',
spaceLoginTitle: 'Login with Space',
@@ -707,7 +708,8 @@ const enUS = {
'The email and password you fill in will be used as the initial administrator account',
register: 'Register',
initWithSpace: 'Initialize with Space',
spaceRecommended: 'Recommended: Sync models and credits from Space',
spaceRecommended:
'Recommended: Use official stable model APIs and cloud services',
registerLocal: 'Register local account',
registerWithPassword: 'Register with email and password',
initSuccess: 'Initialization successful, please login',
@@ -756,21 +758,29 @@ const enUS = {
settings: 'Account Settings',
setPassword: 'Set Password',
passwordSetSuccess: 'Password set successfully',
passwordStatus: 'Local Password',
passwordSet: 'Set',
passwordNotSet: 'Not Set',
passwordSetDescription:
'Password is set, you can login with email and password',
spaceStatus: 'Space Account',
spaceBound: 'Bound',
spaceNotBound: 'Not Bound',
spaceBoundDescription:
'Space account bound, official model APIs and cloud services available',
bindSpace: 'Bind Space Account',
bindSpaceDescription:
'Link your local account to a Space account to sync models and credits',
bindSpaceButton: 'Bind Space Account',
bindSpaceDescription: 'Bind to use official model APIs and cloud services',
bindSpaceButton: 'Bind',
bindSpaceConfirmTitle: 'Confirm Binding',
bindSpaceConfirmDescription:
'You are about to bind your local account to a Space account',
'You are about to bind your local instance to a Space account',
bindSpaceWarning:
'After binding, your login email will be changed to the Space account email. You can still use email/password login if you have set a password.',
'After binding, your login email will be changed from {{localEmail}} to the Space account email.',
bindSpaceSuccess: 'Space account bound successfully',
bindSpaceFailed: 'Failed to bind Space account',
bindSpaceInvalidState:
'Invalid bind request. Please try again from account settings.',
setPasswordHint:
'After setting a password, you can login with email and password',
setPasswordHint: 'Set a password to login with email and password',
},
};

View File

@@ -50,7 +50,8 @@ const jaJP = {
loading: '読み込み中...',
or: 'または',
loginWithSpace: 'Space でログイン',
spaceLoginRecommended: 'おすすめSpace からモデルとクレジットを同期',
spaceLoginRecommended:
'おすすめ:公式の安定したモデル API とクラウドサービスを利用',
loginLocal: 'ローカルアカウントでログイン',
loginWithPassword: 'パスワードでログイン',
spaceLoginTitle: 'Space でログイン',
@@ -715,7 +716,8 @@ const jaJP = {
'入力したメールアドレスとパスワードが初期管理者アカウントになります',
register: '登録',
initWithSpace: 'Space で初期化',
spaceRecommended: 'おすすめSpace からモデルとクレジットを同期',
spaceRecommended:
'おすすめ:公式の安定したモデル API とクラウドサービスを利用',
registerLocal: 'ローカルアカウントを登録',
registerWithPassword: 'メールアドレスとパスワードで登録',
initSuccess: '初期化に成功しました。ログインしてください',
@@ -764,21 +766,30 @@ const jaJP = {
settings: 'アカウント設定',
setPassword: 'パスワードを設定',
passwordSetSuccess: 'パスワードの設定に成功しました',
passwordStatus: 'ローカルパスワード',
passwordSet: '設定済み',
passwordNotSet: '未設定',
passwordSetDescription:
'パスワードが設定されています。メールとパスワードでログインできます',
spaceStatus: 'Space アカウント',
spaceBound: '連携済み',
spaceNotBound: '未連携',
spaceBoundDescription:
'Space アカウントと連携済み、公式モデル API とクラウドサービスが利用可能',
bindSpace: 'Space アカウントを連携',
bindSpaceDescription:
'ローカルアカウントを Space アカウントに連携して、モデルとクレジットを同期します',
bindSpaceButton: 'Space アカウントを連携',
bindSpaceDescription: '連携して公式モデル API とクラウドサービスを利用',
bindSpaceButton: '連携',
bindSpaceConfirmTitle: '連携を確認',
bindSpaceConfirmDescription:
'ローカルアカウントを Space アカウントに連携しようとしています',
'ローカルインスタンスを Space アカウントに連携しようとしています',
bindSpaceWarning:
'連携後、ログインメールアドレスは Space アカウントのメールアドレスに変更されます。パスワードを設定している場合は、引き続きメールアドレスとパスワードでログインできます。',
'連携後、ログインメールアドレスは {{localEmail}} から Space アカウントのメールアドレスに変更されます。',
bindSpaceSuccess: 'Space アカウントの連携に成功しました',
bindSpaceFailed: 'Space アカウントの連携に失敗しました',
bindSpaceInvalidState:
'無効な連携リクエストです。アカウント設定から再度お試しください。',
setPasswordHint:
'パスワードを設定するとメールアドレスとパスワードでログインできます',
'パスワードを設定するとメールとパスワードでログインできます',
},
};

View File

@@ -49,7 +49,7 @@ const zhHans = {
loading: '加载中...',
or: '或',
loginWithSpace: '通过 Space 登录',
spaceLoginRecommended: '推荐:从 Space 同步模型和点数',
spaceLoginRecommended: '推荐:使用官方提供的稳定模型 API 和云服务',
loginLocal: '使用本地账号登录',
loginWithPassword: '通过密码登录',
spaceLoginTitle: '通过 Space 登录',
@@ -680,7 +680,7 @@ const zhHans = {
adminAccountNote: '您填写的邮箱和密码将作为初始管理员账号',
register: '注册',
initWithSpace: '通过 Space 初始化',
spaceRecommended: '推荐:从 Space 同步模型和点数',
spaceRecommended: '推荐:使用官方提供的稳定模型 API 和云服务',
registerLocal: '注册本地账号',
registerWithPassword: '通过邮箱密码组合注册',
initSuccess: '初始化成功 请登录',
@@ -727,17 +727,25 @@ const zhHans = {
settings: '账户设置',
setPassword: '设置密码',
passwordSetSuccess: '密码设置成功',
passwordStatus: '本地密码',
passwordSet: '已设置',
passwordNotSet: '未设置',
passwordSetDescription: '您已设置本地密码,可使用邮箱密码登录',
spaceStatus: 'Space 账户',
spaceBound: '已绑定',
spaceNotBound: '未绑定',
spaceBoundDescription: '已绑定 Space 账户,可使用官方模型 API 和云服务',
bindSpace: '绑定 Space 账户',
bindSpaceDescription: '将本地账户关联到 Space 账户,以同步模型和点数',
bindSpaceButton: '绑定 Space 账户',
bindSpaceDescription: '绑定后可使用官方模型 API 和云服务',
bindSpaceButton: '绑定',
bindSpaceConfirmTitle: '确认绑定',
bindSpaceConfirmDescription: '您即将把本地账户绑定到 Space 账户',
bindSpaceConfirmDescription: '您即将把本地实例绑定到 Space 账户',
bindSpaceWarning:
'绑定后,您的登录邮箱将更改为 Space 账户的邮箱。如果您已设置密码,仍可使用邮箱密码登录。',
'绑定后,您的登录邮箱将从 {{localEmail}} 更改为 Space 账户的邮箱。',
bindSpaceSuccess: 'Space 账户绑定成功',
bindSpaceFailed: '绑定 Space 账户失败',
bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起',
setPasswordHint: '设置密码后,您可使用邮箱密码组合登录',
setPasswordHint: '设置密码后可使用邮箱密码登录',
},
};

View File

@@ -49,7 +49,7 @@ const zhHant = {
loading: '載入中...',
or: '或',
loginWithSpace: '透過 Space 登入',
spaceLoginRecommended: '推薦:從 Space 同步模型和點數',
spaceLoginRecommended: '推薦:使用官方提供的穩定模型 API 和雲服務',
loginLocal: '使用本地帳號登入',
loginWithPassword: '透過密碼登入',
spaceLoginTitle: '透過 Space 登入',
@@ -677,7 +677,7 @@ const zhHant = {
adminAccountNote: '您填寫的電子郵件和密碼將作為初始管理員帳號',
register: '註冊',
initWithSpace: '透過 Space 初始化',
spaceRecommended: '推薦:從 Space 同步模型和點數',
spaceRecommended: '推薦:使用官方提供的穩定模型 API 和雲服務',
registerLocal: '註冊本地帳號',
registerWithPassword: '透過電子郵件密碼組合註冊',
initSuccess: '初始化成功 請登入',
@@ -724,17 +724,25 @@ const zhHant = {
settings: '帳戶設定',
setPassword: '設定密碼',
passwordSetSuccess: '密碼設定成功',
passwordStatus: '本地密碼',
passwordSet: '已設定',
passwordNotSet: '未設定',
passwordSetDescription: '您已設定本地密碼,可使用電子郵件密碼登入',
spaceStatus: 'Space 帳戶',
spaceBound: '已綁定',
spaceNotBound: '未綁定',
spaceBoundDescription: '已綁定 Space 帳戶,可使用官方模型 API 和雲服務',
bindSpace: '綁定 Space 帳戶',
bindSpaceDescription: '將本地帳戶關聯到 Space 帳戶,以同步模型和點數',
bindSpaceButton: '綁定 Space 帳戶',
bindSpaceDescription: '綁定後可使用官方模型 API 和雲服務',
bindSpaceButton: '綁定',
bindSpaceConfirmTitle: '確認綁定',
bindSpaceConfirmDescription: '您即將把本地帳戶綁定到 Space 帳戶',
bindSpaceConfirmDescription: '您即將把本地實例綁定到 Space 帳戶',
bindSpaceWarning:
'綁定後,您的登入電子郵件將更改為 Space 帳戶的電子郵件。如果您已設定密碼,仍可使用電子郵件密碼登入。',
'綁定後,您的登入電子郵件將從 {{localEmail}} 更改為 Space 帳戶的電子郵件。',
bindSpaceSuccess: 'Space 帳戶綁定成功',
bindSpaceFailed: '綁定 Space 帳戶失敗',
bindSpaceInvalidState: '無效的綁定請求,請從帳戶設定重新發起',
setPasswordHint: '設定密碼後,您可使用電子郵件密碼組合登入',
setPasswordHint: '設定密碼後可使用電子郵件密碼登入',
},
};