feat: add authenticated settings page.
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useCharacterDialog } from './character-dialog-context';
|
||||
import { Character, characterSchema } from '../data/schema';
|
||||
import { createCharacter, updateCharacter } from '../data/api';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function CharacterDialog() {
|
||||
const { open, setOpen, character } = useCharacterDialog();
|
||||
const queryClient = useQueryClient();
|
||||
const isEdit = !!character;
|
||||
|
||||
const form = useForm<any>({
|
||||
resolver: zodResolver(characterSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
status: 'online',
|
||||
compatibility: 0,
|
||||
is_active: true,
|
||||
is_locked: false,
|
||||
sort_order: 0,
|
||||
tagline: '',
|
||||
description: '',
|
||||
ai_system_prompt: '',
|
||||
ai_greeting: '',
|
||||
ai_personality: {},
|
||||
ai_voice_config: {},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (character) {
|
||||
form.reset(character);
|
||||
} else {
|
||||
form.reset({
|
||||
name: '',
|
||||
status: 'online',
|
||||
compatibility: 0,
|
||||
is_active: true,
|
||||
is_locked: false,
|
||||
sort_order: 0,
|
||||
tagline: '',
|
||||
description: '',
|
||||
ai_system_prompt: '',
|
||||
ai_greeting: '',
|
||||
});
|
||||
}
|
||||
}, [character, form, open]);
|
||||
|
||||
const onSubmit = async (values: Character) => {
|
||||
console.log('🚀 表单提交触发');
|
||||
console.log('📝 表单值:', values);
|
||||
console.log('✅ 表单验证状态:', form.formState.isValid);
|
||||
console.log('❌ 表单错误:', form.formState.errors);
|
||||
|
||||
try {
|
||||
// 确保 JSONB 字段有默认值
|
||||
const sanitizedValues = {
|
||||
...values,
|
||||
ai_personality: values.ai_personality || {},
|
||||
ai_voice_config: values.ai_voice_config || {},
|
||||
};
|
||||
|
||||
console.log('🔧 处理后的数据:', sanitizedValues);
|
||||
|
||||
if (isEdit) {
|
||||
console.log('🔄 更新角色,ID:', character.id);
|
||||
await updateCharacter({ ...sanitizedValues, id: character.id });
|
||||
toast.success('角色更新成功');
|
||||
} else {
|
||||
console.log('➕ 创建新角色');
|
||||
await createCharacter(sanitizedValues);
|
||||
toast.success('角色创建成功');
|
||||
}
|
||||
setOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['characters'] });
|
||||
} catch (error) {
|
||||
console.error('❌ 保存角色失败,详细错误:', error);
|
||||
|
||||
// 改进错误信息显示
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: JSON.stringify(error);
|
||||
|
||||
toast.error(`保存角色失败: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className='max-h-[85vh] overflow-y-auto sm:max-w-2xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? '编辑角色' : '创建角色'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? '在下方修改角色详情。'
|
||||
: '填写详情以创建新角色。'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
console.log('📋 Form onSubmit 事件触发', e);
|
||||
console.log('📊 当前表单状态:', form.formState);
|
||||
console.log('❌ 表单错误:', form.formState.errors);
|
||||
console.log('📝 当前表单值:', form.getValues());
|
||||
form.handleSubmit(
|
||||
onSubmit,
|
||||
(errors) => {
|
||||
console.error('🚫 表单验证失败:', errors);
|
||||
toast.error('请检查表单输入');
|
||||
}
|
||||
)(e);
|
||||
}}
|
||||
className='space-y-4'
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='角色名称' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='status'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>状态</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='选择状态' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='online'>在线</SelectItem>
|
||||
<SelectItem value='busy'>忙碌</SelectItem>
|
||||
<SelectItem value='offline'>离线</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='tagline'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>简述</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='简短描述' {...field} value={field.value || ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='description'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder='完整角色描述' {...field} value={field.value || ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='compatibility'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>契合度 (%)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
onChange={e => field.onChange(parseInt(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='sort_order'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>排序</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='number'
|
||||
{...field}
|
||||
onChange={e => field.onChange(parseInt(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='is_active'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4'>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='space-y-1 leading-none'>
|
||||
<FormLabel>启用</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='is_locked'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4'>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='space-y-1 leading-none'>
|
||||
<FormLabel>锁定 (会员)</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ai_system_prompt'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>系统提示词 (System Prompt)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder='你是一个乐于助人的助手...'
|
||||
className='min-h-[100px]'
|
||||
{...field}
|
||||
value={field.value || ''}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ai_greeting'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>问候语</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='你好!' {...field} value={field.value || ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button type='button' variant='outline' onClick={() => setOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={form.formState.isSubmitting}
|
||||
onClick={() => console.log('💾 保存按钮被点击')}
|
||||
>
|
||||
{form.formState.isSubmitting ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user