-
@@ -68,12 +66,6 @@ export default function ChatModel() {
Stay tuned for more models
-
-
-
-
- )
+ );
}
diff --git a/src/app/(main)/character/[id]/chat/Sider/Font.tsx b/src/app/(main)/chat/[id]/Sider/Font.tsx
similarity index 58%
rename from src/app/(main)/character/[id]/chat/Sider/Font.tsx
rename to src/app/(main)/chat/[id]/Sider/Font.tsx
index 758a2e4..f3a18f7 100644
--- a/src/app/(main)/character/[id]/chat/Sider/Font.tsx
+++ b/src/app/(main)/chat/[id]/Sider/Font.tsx
@@ -1,19 +1,18 @@
-'use client'
-import { SiderHeader } from '.'
-import { useChatStore } from '../store'
-import { useState } from 'react'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Button } from '@/components/ui/button'
-import { cn } from '@/lib/utils'
+'use client';
+import { useChatStore } from '../store';
+import { useState } from 'react';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
type FontOption = {
- value: number
- label: string
- isStandard?: boolean
-}
+ value: number;
+ label: string;
+ isStandard?: boolean;
+};
export default function Font() {
- const setSideBar = useChatStore((store) => store.setSideBar)
+ const setSideBar = useChatStore((store) => store.setSideBar);
// 字体大小选项
const fontOptions: FontOption[] = [
@@ -22,33 +21,31 @@ export default function Font() {
{ value: 16, label: 'A 16', isStandard: true },
{ value: 18, label: 'A 18' },
{ value: 20, label: 'A 20' },
- ]
+ ];
- const [selectedFont, setSelectedFont] = useState(16)
- const [loading, setLoading] = useState(false)
+ const [selectedFont, setSelectedFont] = useState(16);
+ const [loading, setLoading] = useState(false);
const handleConfirm = async () => {
- setLoading(true)
+ setLoading(true);
try {
// TODO: 调用实际的 API 保存字体设置
// await updateFontSize({ fontSize: selectedFont })
- console.log('Selected font size:', selectedFont)
+ console.log('Selected font size:', selectedFont);
// 模拟延迟
- await new Promise((resolve) => setTimeout(resolve, 500))
+ await new Promise((resolve) => setTimeout(resolve, 500));
- setSideBar('profile')
+ setSideBar('profile');
} catch (error) {
- console.error(error)
+ console.error(error);
} finally {
- setLoading(false)
+ setLoading(false);
}
- }
+ };
return (
-
-
{fontOptions.map((option) => (
@@ -71,15 +68,6 @@ export default function Font() {
))}
-
-
-
-
-
- )
+ );
}
diff --git a/src/app/(main)/chat/[id]/Sider/Language.tsx b/src/app/(main)/chat/[id]/Sider/Language.tsx
new file mode 100644
index 0000000..1d84e31
--- /dev/null
+++ b/src/app/(main)/chat/[id]/Sider/Language.tsx
@@ -0,0 +1,58 @@
+'use client';
+import { useChatStore } from '../store';
+import { useState } from 'react';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+export default function Language() {
+ const setSideBar = useChatStore((store) => store.setSideBar);
+
+ const tokenOptions = [
+ { value: 'zh-CN', label: 'Chinese' },
+ { value: 'en-US', label: 'English' },
+ ];
+
+ const [selectedToken, setSelectedToken] = useState('zh-CN');
+ const [loading, setLoading] = useState(false);
+
+ const handleConfirm = async () => {
+ setLoading(true);
+ try {
+ // TODO: 调用实际的 API 保存最大回复数设置
+ // await updateMaxToken({ maxToken: selectedToken })
+ console.log('Selected max token:', selectedToken);
+
+ // 模拟延迟
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
+ setSideBar('profile');
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ {tokenOptions.map((option) => (
+
setSelectedToken(option.value)}
+ >
+
{option.label}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/app/(main)/character/[id]/chat/Sider/MaxToken.tsx b/src/app/(main)/chat/[id]/Sider/MaxToken.tsx
similarity index 55%
rename from src/app/(main)/character/[id]/chat/Sider/MaxToken.tsx
rename to src/app/(main)/chat/[id]/Sider/MaxToken.tsx
index 77963d6..572577c 100644
--- a/src/app/(main)/character/[id]/chat/Sider/MaxToken.tsx
+++ b/src/app/(main)/chat/[id]/Sider/MaxToken.tsx
@@ -1,18 +1,17 @@
-'use client'
-import { SiderHeader } from '.'
-import { useChatStore } from '../store'
-import { useState } from 'react'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Button } from '@/components/ui/button'
-import { cn } from '@/lib/utils'
+'use client';
+import { useChatStore } from '../store';
+import { useState } from 'react';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
type TokenOption = {
- value: number
- label: string
-}
+ value: number;
+ label: string;
+};
export default function MaxToken() {
- const setSideBar = useChatStore((store) => store.setSideBar)
+ const setSideBar = useChatStore((store) => store.setSideBar);
// 最大回复数选项
const tokenOptions: TokenOption[] = [
@@ -20,33 +19,31 @@ export default function MaxToken() {
{ value: 1000, label: '1000' },
{ value: 1200, label: '1200' },
{ value: 1500, label: '1500' },
- ]
+ ];
- const [selectedToken, setSelectedToken] = useState(800)
- const [loading, setLoading] = useState(false)
+ const [selectedToken, setSelectedToken] = useState(800);
+ const [loading, setLoading] = useState(false);
const handleConfirm = async () => {
- setLoading(true)
+ setLoading(true);
try {
// TODO: 调用实际的 API 保存最大回复数设置
// await updateMaxToken({ maxToken: selectedToken })
- console.log('Selected max token:', selectedToken)
+ console.log('Selected max token:', selectedToken);
// 模拟延迟
- await new Promise((resolve) => setTimeout(resolve, 500))
+ await new Promise((resolve) => setTimeout(resolve, 500));
- setSideBar('profile')
+ setSideBar('profile');
} catch (error) {
- console.error(error)
+ console.error(error);
} finally {
- setLoading(false)
+ setLoading(false);
}
- }
+ };
return (
-
-
{tokenOptions.map((option) => (
@@ -64,15 +61,6 @@ export default function MaxToken() {
))}
-
-
-
-
-
- )
+ );
}
diff --git a/src/app/(main)/character/[id]/chat/Sider/Personal.tsx b/src/app/(main)/chat/[id]/Sider/Personal.tsx
similarity index 84%
rename from src/app/(main)/character/[id]/chat/Sider/Personal.tsx
rename to src/app/(main)/chat/[id]/Sider/Personal.tsx
index c472ccb..558e9f6 100644
--- a/src/app/(main)/character/[id]/chat/Sider/Personal.tsx
+++ b/src/app/(main)/chat/[id]/Sider/Personal.tsx
@@ -1,9 +1,8 @@
-'use client'
-import { useEffect, useState, useCallback } from 'react'
-import { SiderHeader } from '.'
-import { useChatStore } from '../store'
-import { z } from 'zod'
-import dayjs from 'dayjs'
+'use client';
+import { useEffect, useState, useCallback } from 'react';
+import { useChatStore } from '../store';
+import { z } from 'zod';
+import dayjs from 'dayjs';
import {
Form,
FormControl,
@@ -11,22 +10,22 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from '@/components/ui/form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { useForm } from 'react-hook-form'
-import { Gender } from '@/types/user'
-import { Input } from '@/components/ui/input'
+} from '@/components/ui/form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { Gender } from '@/types/user';
+import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select'
-import { Label } from '@/components/ui/label'
-import { calculateAge, getDaysInMonth } from '@/lib/utils'
-import { Textarea } from '@/components/ui/textarea'
-import { Button } from '@/components/ui/button'
+} from '@/components/ui/select';
+import { Label } from '@/components/ui/label';
+import { calculateAge, getDaysInMonth } from '@/lib/utils';
+import { Textarea } from '@/components/ui/textarea';
+import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
@@ -36,12 +35,12 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from '@/components/ui/alert-dialog'
+} from '@/components/ui/alert-dialog';
-const currentYear = dayjs().year()
-const years = Array.from({ length: currentYear - 1950 + 1 }, (_, i) => `${1950 + i}`)
-const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`.padStart(2, '0'))
-const monthTexts = Array.from({ length: 12 }, (_, i) => dayjs().month(i).format('MMM'))
+const currentYear = dayjs().year();
+const years = Array.from({ length: currentYear - 1950 + 1 }, (_, i) => `${1950 + i}`);
+const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`.padStart(2, '0'));
+const monthTexts = Array.from({ length: 12 }, (_, i) => dayjs().month(i).format('MMM'));
const characterFormSchema = z
.object({
@@ -59,8 +58,8 @@ const characterFormSchema = z
})
.refine(
(data) => {
- const age = calculateAge(data.year, data.month, data.day)
- return age >= 18
+ const age = calculateAge(data.year, data.month, data.day);
+ return age >= 18;
},
{
message: 'Character age must be at least 18 years old',
@@ -71,22 +70,22 @@ const characterFormSchema = z
(data) => {
if (data.profile) {
if (data.profile.trim().length > 300) {
- return false
+ return false;
}
- return data.profile.trim().length >= 10
+ return data.profile.trim().length >= 10;
}
- return true
+ return true;
},
{
message: 'At least 10 characters',
path: ['profile'],
}
- )
+ );
export default function Personal() {
- const setSideBar = useChatStore((store) => store.setSideBar)
- const [loading, setLoading] = useState(false)
- const [showConfirmDialog, setShowConfirmDialog] = useState(false)
+ const setSideBar = useChatStore((store) => store.setSideBar);
+ const [loading, setLoading] = useState(false);
+ const [showConfirmDialog, setShowConfirmDialog] = useState(false);
// 静态数据,模拟从接口获取的数据
const chatSettingData = {
@@ -94,9 +93,9 @@ export default function Personal() {
sex: Gender.MALE,
birthday: dayjs('1995-06-15').valueOf(),
whoAmI: 'A creative and passionate developer',
- }
+ };
- const birthday = chatSettingData?.birthday ? dayjs(chatSettingData.birthday) : undefined
+ const birthday = chatSettingData?.birthday ? dayjs(chatSettingData.birthday) : undefined;
const form = useForm
>({
resolver: zodResolver(characterFormSchema),
@@ -111,40 +110,40 @@ export default function Personal() {
day: birthday?.date().toString().padStart(2, '0') || undefined,
profile: chatSettingData?.whoAmI || '',
},
- })
+ });
// 处理返回的逻辑
const handleGoBack = useCallback(() => {
if (form.formState.isDirty) {
- setShowConfirmDialog(true)
+ setShowConfirmDialog(true);
} else {
- setSideBar('profile')
+ setSideBar('profile');
}
- }, [form.formState.isDirty, setSideBar])
+ }, [form.formState.isDirty, setSideBar]);
// 确认放弃修改
const handleConfirmDiscard = useCallback(() => {
- form.reset()
- setShowConfirmDialog(false)
- setSideBar('profile')
- }, [form, setSideBar])
+ form.reset();
+ setShowConfirmDialog(false);
+ setSideBar('profile');
+ }, [form, setSideBar]);
async function onSubmit(data: z.infer) {
if (!form.formState.isDirty) {
- setSideBar('profile')
- return
+ setSideBar('profile');
+ return;
}
- setLoading(true)
+ setLoading(true);
try {
// TODO: 这里应该调用实际的 API
// 模拟检查昵称是否存在
- const isExist = false // await checkNickname({ nickname: data.nickname.trim() })
+ const isExist = false; // await checkNickname({ nickname: data.nickname.trim() })
if (isExist) {
form.setError('nickname', {
message: 'This nickname is already taken',
- })
- return
+ });
+ return;
}
// TODO: 这里应该调用实际的保存 API
@@ -159,19 +158,19 @@ export default function Personal() {
nickname: data.nickname,
birthday: new Date(`${data.year}-${data.month}-${data.day}`).getTime(),
whoAmI: data.profile || '',
- })
+ });
- setSideBar('profile')
+ setSideBar('profile');
} catch (error) {
- console.error(error)
+ console.error(error);
} finally {
- setLoading(false)
+ setLoading(false);
}
}
- const selectedYear = form.watch('year')
- const selectedMonth = form.watch('month')
- const days = selectedYear && selectedMonth ? getDaysInMonth(selectedYear, selectedMonth) : []
+ const selectedYear = form.watch('year');
+ const selectedMonth = form.watch('month');
+ const days = selectedYear && selectedMonth ? getDaysInMonth(selectedYear, selectedMonth) : [];
const genderTexts = [
{
@@ -186,16 +185,14 @@ export default function Personal() {
value: Gender.OTHER,
label: 'Other',
},
- ]
+ ];
- const gender = form.watch('sex')
- const genderText = genderTexts.find((text) => text.value === gender)?.label
+ const gender = form.watch('sex');
+ const genderText = genderTexts.find((text) => text.value === gender)?.label;
return (
<>
-
-
-
+ {/*
@@ -344,7 +341,7 @@ export default function Personal() {
>
Save
-
+
*/}
{/* 确认放弃修改的对话框 */}
@@ -368,5 +365,5 @@ export default function Personal() {
>
- )
+ );
}
diff --git a/src/app/(main)/character/[id]/chat/Sider/Profile.tsx b/src/app/(main)/chat/[id]/Sider/Profile.tsx
similarity index 54%
rename from src/app/(main)/character/[id]/chat/Sider/Profile.tsx
rename to src/app/(main)/chat/[id]/Sider/Profile.tsx
index 1ba0b50..1971767 100644
--- a/src/app/(main)/character/[id]/chat/Sider/Profile.tsx
+++ b/src/app/(main)/chat/[id]/Sider/Profile.tsx
@@ -1,24 +1,26 @@
-'use client'
+'use client';
-import { getAge } from '@/lib/utils'
-import { CharacterAvatorAndName } from '../CharacterHeader'
-import { Tag } from '@/components/ui/tag'
-import Image from 'next/image'
-import { cn } from '@/lib/utils'
-import { useChatStore } from '../store'
-import { IconButton } from '@/components/ui/button'
-import React from 'react'
-import { Switch } from '@/components/ui/switch'
+import { getAge } from '@/lib/utils';
+import { CharacterAvatorAndName } from '../CharacterHeader';
+import { Tag } from '@/components/ui/tag';
+import Image from 'next/image';
+import { cn } from '@/lib/utils';
+import { useChatStore } from '../store';
+import { Button, IconButton } from '@/components/ui/button';
+import React from 'react';
+import { Switch } from '@/components/ui/switch';
+import { useCharacter } from '@/hooks/services/character';
+import { useParams } from 'next/navigation';
+import { ActiveTabType } from './index';
const genderMap = {
0: '/icons/male.svg',
1: '/icons/female.svg',
2: '/icons/gender-neutral.svg',
-}
+};
-const ChatProfilePersona = React.memo(() => {
- const setSideBar = useChatStore((store) => store.setSideBar)
- const whoAmI = 'whoAmI'
+const ChatProfilePersona = React.memo(({ onActiveTab }: ProfileProps) => {
+ const whoAmI = 'whoAmI';
return (
@@ -26,13 +28,13 @@ const ChatProfilePersona = React.memo(() => {
My Chat Persona
setSideBar('personal')}
+ onClick={() => onActiveTab('personal')}
>
Edit
-
+
Nickname
{''}
@@ -60,22 +62,37 @@ const ChatProfilePersona = React.memo(() => {
- )
-})
+ );
+});
type SettingItem = {
- onClick: () => void
- label: string
- value?: React.ReactNode
-}
+ onClick: () => void;
+ label: string;
+ value?: React.ReactNode;
+};
-export default function Profile() {
- const setSideBar = useChatStore((store) => store.setSideBar)
+type ProfileProps = {
+ onActiveTab: (tab: ActiveTabType) => void;
+};
+
+export default function Profile({ onActiveTab }: ProfileProps) {
+ const { id } = useParams<{ id: string }>();
+ const { data: character = {} } = useCharacter(id.split('-')[2]);
+
+ const preferenceItems: SettingItem[][] = [
+ [
+ {
+ onClick: () => onActiveTab('language'),
+ label: 'Language',
+ value: 'zh-CN',
+ },
+ ],
+ ];
const chatSettingItems: SettingItem[][] = [
[
{
- onClick: () => setSideBar('model'),
+ onClick: () => onActiveTab('model'),
label: 'Chat Model',
value: 'Role-Playing',
},
@@ -87,34 +104,34 @@ export default function Profile() {
],
[
{
- onClick: () => setSideBar('max_token'),
+ onClick: () => onActiveTab('max_token'),
label: 'Maximum Replies',
value: '1200',
},
],
[
{
- onClick: () => setSideBar('font'),
+ onClick: () => onActiveTab('font'),
label: 'Font',
value: '17px',
},
{
- onClick: () => setSideBar('background'),
+ onClick: () => onActiveTab('background'),
label: 'Chat Background',
value: '17px',
},
],
- ]
+ ];
const voiceSettingItems: SettingItem[][] = [
[
{
- onClick: () => setSideBar('voice_actor'),
+ onClick: () => onActiveTab('voice_actor'),
label: 'Voice Artist',
value: 'Default',
},
],
- ]
+ ];
const bundleRender = (title: string, items: SettingItem[][]) => {
return (
@@ -122,7 +139,7 @@ export default function Profile() {
{title}
{items.map((list, index) => (
-
+
{list.map((item, itemIndex) => (
- )
- }
+ );
+ };
return (
-
-
+
+ {/*
+ setSideBar('profile')}>
+
+
+ setSideBar('profile')}>
+
+
+
*/}
+
+
- {/* Tags */}
-
-
-
-
- {getAge(Number(24))}
-
-
{'Sensibility'}
-
{'Romantic'}
+ {/* Tags */}
+
+
+
+
+ {getAge(Number(24))}
+
+
{'Sensibility'}
+
{'Romantic'}
+
+
+
+
+ {bundleRender('Preference', preferenceItems)}
+
+ {bundleRender('Chat Setting', chatSettingItems)}
+
+ {bundleRender('Voice Setting', voiceSettingItems)}
+
+
+
+
-
-
-
- {bundleRender('Chat Setting', chatSettingItems)}
-
- {bundleRender('Voice Setting', voiceSettingItems)}
- )
+ );
}
diff --git a/src/app/(main)/character/[id]/chat/Sider/VoiceActor.tsx b/src/app/(main)/chat/[id]/Sider/VoiceActor.tsx
similarity index 80%
rename from src/app/(main)/character/[id]/chat/Sider/VoiceActor.tsx
rename to src/app/(main)/chat/[id]/Sider/VoiceActor.tsx
index 3974280..986f617 100644
--- a/src/app/(main)/character/[id]/chat/Sider/VoiceActor.tsx
+++ b/src/app/(main)/chat/[id]/Sider/VoiceActor.tsx
@@ -1,24 +1,23 @@
-'use client'
-import { SiderHeader } from '.'
-import { useChatStore } from '../store'
-import { useState } from 'react'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Button } from '@/components/ui/button'
-import { cn } from '@/lib/utils'
-import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
+'use client';
+import { useChatStore } from '../store';
+import { useState } from 'react';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
-type VoiceGender = 'all' | 'male' | 'female'
+type VoiceGender = 'all' | 'male' | 'female';
type VoiceActorItem = {
- id: number
- name: string
- description: string
- avatarUrl: string
- gender: 'male' | 'female'
-}
+ id: number;
+ name: string;
+ description: string;
+ avatarUrl: string;
+ gender: 'male' | 'female';
+};
export default function VoiceActor() {
- const setSideBar = useChatStore((store) => store.setSideBar)
+ const setSideBar = useChatStore((store) => store.setSideBar);
// 语音演员列表(静态数据)
const voiceActors: VoiceActorItem[] = [
@@ -71,40 +70,38 @@ export default function VoiceActor() {
avatarUrl: 'https://i.pravatar.cc/150?img=7',
gender: 'male',
},
- ]
+ ];
- const [selectedGender, setSelectedGender] = useState
('all')
- const [selectedActorId, setSelectedActorId] = useState(1)
- const [loading, setLoading] = useState(false)
+ const [selectedGender, setSelectedGender] = useState('all');
+ const [selectedActorId, setSelectedActorId] = useState(1);
+ const [loading, setLoading] = useState(false);
// 根据性别过滤演员列表
const filteredActors = voiceActors.filter((actor) => {
- if (selectedGender === 'all') return true
- return actor.gender === selectedGender
- })
+ if (selectedGender === 'all') return true;
+ return actor.gender === selectedGender;
+ });
const handleConfirm = async () => {
- setLoading(true)
+ setLoading(true);
try {
// TODO: 调用实际的 API 保存语音演员设置
// await updateVoiceActor({ voiceActorId: selectedActorId })
- console.log('Selected voice actor:', selectedActorId)
+ console.log('Selected voice actor:', selectedActorId);
// 模拟延迟
- await new Promise((resolve) => setTimeout(resolve, 500))
+ await new Promise((resolve) => setTimeout(resolve, 500));
- setSideBar('profile')
+ setSideBar('profile');
} catch (error) {
- console.error(error)
+ console.error(error);
} finally {
- setLoading(false)
+ setLoading(false);
}
- }
+ };
return (
-
-
{/* Gender Tabs */}
{[
@@ -157,14 +154,14 @@ export default function VoiceActor() {
{/* Footer Buttons */}
-
+ {/*
-
+
*/}
- )
+ );
}
diff --git a/src/app/(main)/chat/[id]/Sider/index.tsx b/src/app/(main)/chat/[id]/Sider/index.tsx
new file mode 100644
index 0000000..da53ad4
--- /dev/null
+++ b/src/app/(main)/chat/[id]/Sider/index.tsx
@@ -0,0 +1,81 @@
+'use client';
+import { useChatStore } from '../store';
+import Profile from './Profile';
+import Personal from './Personal';
+import VoiceActor from './VoiceActor';
+import Font from './Font';
+import MaxToken from './MaxToken';
+import Background from './Background';
+import ChatModel from './ChatModel';
+import Language from './Language';
+import { IconButton } from '@/components/ui/button';
+import React, { useState } from 'react';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog';
+
+type SettingProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+};
+export type ActiveTabType =
+ | 'profile'
+ | 'personal'
+ | 'history'
+ | 'voice_actor'
+ | 'font'
+ | 'max_token'
+ | 'background'
+ | 'model'
+ | 'language';
+
+const titleMap = {
+ personal: 'Personal',
+ history: 'History',
+ voice_actor: 'Voice Actor',
+ font: 'Font',
+ max_token: 'Max Token',
+ background: 'Background',
+ model: 'Chat Model',
+ language: 'Language',
+};
+
+export default function SettingDialog({ open, onOpenChange }: SettingProps) {
+ const [activeTab, setActiveTab] = useState('profile');
+
+ return (
+
+
+
+ {activeTab === 'profile' ? (
+
+ ) : (
+ titleMap[activeTab]
+ )}
+ {activeTab !== 'profile' && (
+ setActiveTab('profile')}>
+
+
+ )}
+
+
+ {activeTab === 'profile' &&
}
+ {activeTab === 'personal' &&
}
+ {activeTab === 'voice_actor' &&
}
+ {activeTab === 'font' &&
}
+ {activeTab === 'max_token' &&
}
+ {activeTab === 'background' &&
}
+ {activeTab === 'model' &&
}
+ {activeTab === 'language' &&
}
+
+
+
+ );
+}
diff --git a/src/app/(main)/character/[id]/chat/UserMessage.tsx b/src/app/(main)/chat/[id]/UserMessage.tsx
similarity index 85%
rename from src/app/(main)/character/[id]/chat/UserMessage.tsx
rename to src/app/(main)/chat/[id]/UserMessage.tsx
index 880b310..be41629 100644
--- a/src/app/(main)/character/[id]/chat/UserMessage.tsx
+++ b/src/app/(main)/chat/[id]/UserMessage.tsx
@@ -1,10 +1,10 @@
-'use client'
+'use client';
export default function UserMessage({ data }: { data: any }) {
return (
- {data.text}
+ {data.content}
- )
+ );
}
diff --git a/src/app/(main)/character/[id]/chat/page.tsx b/src/app/(main)/chat/[id]/page.tsx
similarity index 50%
rename from src/app/(main)/character/[id]/chat/page.tsx
rename to src/app/(main)/chat/[id]/page.tsx
index bb911a1..219183f 100644
--- a/src/app/(main)/character/[id]/chat/page.tsx
+++ b/src/app/(main)/chat/[id]/page.tsx
@@ -3,12 +3,22 @@
import { IconButton } from '@/components/ui/button';
import Input from './Input';
import MessageList from './MessageList';
-import { useChatStore } from './store';
-import Sider from './Sider';
+import SettingDialog from './Sider';
+import { useStreamChatStore } from '@/stores/stream-chat';
+import { useParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
export default function ChatPage() {
- const isSidebarOpen = useChatStore((store) => store.isSidebarOpen);
- const setIsSidebarOpen = useChatStore((store) => store.setIsSidebarOpen);
+ const { id } = useParams();
+ const [settingOpen, setSettingOpen] = useState(false);
+ const switchToChannel = useStreamChatStore((s) => s.switchToChannel);
+ const client = useStreamChatStore((s) => s.client);
+
+ useEffect(() => {
+ if (id && client) {
+ switchToChannel(id as string);
+ }
+ }, [id, client]);
return (
@@ -18,7 +28,7 @@ export default function ChatPage() {
setIsSidebarOpen(!isSidebarOpen)}
+ onClick={() => setSettingOpen(!settingOpen)}
className="absolute top-1 right-1"
variant="ghost"
size="small"
@@ -26,8 +36,7 @@ export default function ChatPage() {
-
- {isSidebarOpen &&
}
+
);
}
diff --git a/src/app/(main)/character/[id]/chat/store.ts b/src/app/(main)/chat/[id]/store.ts
similarity index 66%
rename from src/app/(main)/character/[id]/chat/store.ts
rename to src/app/(main)/chat/[id]/store.ts
index 46e8a63..240597f 100644
--- a/src/app/(main)/character/[id]/chat/store.ts
+++ b/src/app/(main)/chat/[id]/store.ts
@@ -1,4 +1,4 @@
-import { create } from 'zustand'
+import { create } from 'zustand';
type SideBar =
| 'profile'
@@ -9,11 +9,12 @@ type SideBar =
| 'max_token'
| 'background'
| 'model'
+ | 'language';
interface ChatStore {
- isSidebarOpen: boolean
- setIsSidebarOpen: (isSidebarOpen: boolean) => void
- sideBar: SideBar
- setSideBar: (sideBar: SideBar) => void
+ isSidebarOpen: boolean;
+ setIsSidebarOpen: (isSidebarOpen: boolean) => void;
+ sideBar: SideBar;
+ setSideBar: (sideBar: SideBar) => void;
}
export const useChatStore = create
((set) => ({
@@ -21,4 +22,4 @@ export const useChatStore = create((set) => ({
setIsSidebarOpen: (isSidebarOpen: boolean) => set({ isSidebarOpen }),
sideBar: 'profile',
setSideBar: (sideBar: SideBar) => set({ sideBar }),
-}))
+}));
diff --git a/src/app/(main)/crushcoin/components/CheckInGrid.tsx b/src/app/(main)/crushcoin/components/CheckInGrid.tsx
index 3beefbf..4489007 100644
--- a/src/app/(main)/crushcoin/components/CheckInGrid.tsx
+++ b/src/app/(main)/crushcoin/components/CheckInGrid.tsx
@@ -1,52 +1,52 @@
-'use client'
+'use client';
-import { useGetSevenDaysSignList, useSignIn } from '@/hooks/useHome'
-import { SignInListOutput } from '@/services/home/types'
-import { useQueryClient } from '@tanstack/react-query'
-import { homeKeys } from '@/lib/query-keys'
-import { CheckInCard } from './CheckInCard'
-import { useEffect, useRef } from 'react'
-import { toast } from 'sonner'
+// import { useGetSevenDaysSignList, useSignIn } from '@/hooks/useHome'
+// import { SignInListOutput } from '@/services/home/types'
+import { useQueryClient } from '@tanstack/react-query';
+import { homeKeys } from '@/lib/query-keys';
+import { CheckInCard } from './CheckInCard';
+import { useEffect, useRef } from 'react';
+import { toast } from 'sonner';
export function CheckInGrid() {
- const queryClient = useQueryClient()
- const { data: signListData, isLoading } = useGetSevenDaysSignList()
- const signInMutation = useSignIn()
- const hasSignRef = useRef(false)
+ const queryClient = useQueryClient();
+ // const { data: signListData, isLoading } = useGetSevenDaysSignList()
+ // const signInMutation = useSignIn()
+ const hasSignRef = useRef(false);
useEffect(() => {
const initializeCheckIn = async () => {
- if (hasSignRef.current) return
- hasSignRef.current = true
+ if (hasSignRef.current) return;
+ hasSignRef.current = true;
try {
// 先进行签到
- const resp = await signInMutation.mutateAsync()
+ const resp = await signInMutation.mutateAsync();
if (resp) {
- toast.success('Check-in Successful!')
+ toast.success('Check-in Successful!');
}
// 签到成功后再获取列表数据
await queryClient.invalidateQueries({
queryKey: homeKeys.getSevenDaysSignList(),
- })
+ });
await queryClient.invalidateQueries({
queryKey: ['wallet'],
- })
+ });
} catch (error) {
- console.error('初始化签到失败:', error)
+ console.error('初始化签到失败:', error);
// 即使签到失败,也要获取列表数据显示界面
queryClient.invalidateQueries({
queryKey: homeKeys.getSevenDaysSignList(),
- })
+ });
queryClient.invalidateQueries({
queryKey: ['wallet'],
- })
+ });
}
- }
+ };
if (signListData) {
- initializeCheckIn()
+ initializeCheckIn();
}
- }, [signListData])
+ }, [signListData]);
if (isLoading) {
return (
@@ -58,38 +58,38 @@ export function CheckInGrid() {
/>
))}
- )
+ );
}
- const signList = signListData?.list || []
- const today = new Date()
- const todayStr = today.toISOString().split('T')[0] // yyyy-MM-dd 格式
+ const signList = signListData?.list || [];
+ const today = new Date();
+ const todayStr = today.toISOString().split('T')[0]; // yyyy-MM-dd 格式
// 确保有7天的数据,如果不足则补充默认数据
const fullSignList: (SignInListOutput & { day: number })[] = Array.from(
{ length: 7 },
(_, index) => {
- const day = index + 1
- const existingData = signList.find((item, itemIndex) => itemIndex === index)
+ const day = index + 1;
+ const existingData = signList.find((item, itemIndex) => itemIndex === index);
return {
day,
coinNum: existingData?.coinNum || [5, 10, 15, 20, 30, 50, 80][index],
dayStr: existingData?.dayStr || '',
signIn: existingData?.signIn || false,
- }
+ };
}
- )
+ );
// 找到今天应该签到的是第几天
const todayIndex = fullSignList.findIndex((item) => {
- if (!item.dayStr) return false
- return item.dayStr === todayStr
- })
+ if (!item.dayStr) return false;
+ return item.dayStr === todayStr;
+ });
// 如果没有找到今天的数据,假设是按顺序签到,找到第一个未签到的
const currentDayIndex =
- todayIndex >= 0 ? todayIndex : fullSignList.findIndex((item) => !item.signIn)
+ todayIndex >= 0 ? todayIndex : fullSignList.findIndex((item) => !item.signIn);
return (
@@ -105,7 +105,7 @@ export function CheckInGrid() {
loading={signInMutation.isPending}
className="col-span-1 row-span-2"
/>
- )
+ );
}
if (index < 3) {
return (
@@ -118,7 +118,7 @@ export function CheckInGrid() {
loading={signInMutation.isPending}
className="col-span-1 row-span-1"
/>
- )
+ );
} else {
return (
- )
+ );
}
})}
- )
+ );
}
-export default CheckInGrid
+export default CheckInGrid;
diff --git a/src/app/(main)/home/components/Filter.tsx b/src/app/(main)/home/components/Filter.tsx
index 7a3f720..9d6accf 100644
--- a/src/app/(main)/home/components/Filter.tsx
+++ b/src/app/(main)/home/components/Filter.tsx
@@ -6,13 +6,35 @@ import { Chip } from '@/components/ui/chip';
import { useHomeStore } from '../store';
import { useQuery } from '@tanstack/react-query';
import { fetchCharacterTags } from '@/services/editor';
+import { useRef } from 'react';
const Filter = () => {
const tab = useHomeStore((state) => state.tab);
const setTab = useHomeStore((state) => state.setTab);
+ const ref = useRef
(null);
const selectedTags = useHomeStore((state) => state.selectedTags);
const setSelectedTags = useHomeStore((state) => state.setSelectedTags);
+ // useEffect(() => {
+ // const mainContent = document.getElementById('main-content');
+ // if (!mainContent) {
+ // return;
+ // }
+ // const handleScroll = () => {
+ // const scrollTop = mainContent.scrollTop;
+ // console.log('scrollTop', scrollTop, ref.current);
+ // const className = 'absolute bg-bg-primary-normal';
+ // if (scrollTop > 248) {
+ // ref.current?.classList.add('absolute bg-bg-primary-normal');
+ // } else {
+ // }
+ // };
+ // mainContent?.addEventListener('scroll', handleScroll);
+ // return () => {
+ // mainContent?.removeEventListener('scroll', handleScroll);
+ // };
+ // }, []);
+
const { data: tags = [] } = useQuery({
queryKey: ['tags', tab],
queryFn: async () => {
@@ -39,8 +61,16 @@ const Filter = () => {
},
] as const;
+ const handleSelect = (tagId: string) => {
+ if (selectedTags.includes(tagId)) {
+ setSelectedTags(selectedTags.filter((id) => id !== tagId));
+ } else {
+ setSelectedTags([...selectedTags, tagId]);
+ }
+ };
+
return (
-
+
{tabs.map((item) => {
const active = tab === item.value;
@@ -71,7 +101,7 @@ const Filter = () => {
size="small"
className="px-4"
state={selectedTags.includes(tag.id) ? 'active' : 'inactive'}
- onClick={() => setSelectedTags([tag.id])}
+ onClick={() => handleSelect(tag.id)}
>
# {tag.name}
diff --git a/src/app/(main)/home/components/Header.tsx b/src/app/(main)/home/components/Header.tsx
index 4e286f6..665a6fd 100644
--- a/src/app/(main)/home/components/Header.tsx
+++ b/src/app/(main)/home/components/Header.tsx
@@ -8,61 +8,62 @@ import { useMedia } from '@/hooks/tools';
const Header = React.memo(() => {
const response = useMedia();
+
return (
-
-
-
-
Spicyxx.ai
-
- A Different World
-
-
-
-
Daily Free CrushCoins
-
-
+
+
+
+
+
+
+ Check-in{' '}
+
+
+
+
+ Daily Free crush coinsh
+
+
+
-
+
+ {response?.lg && (
+
+ )}
- {response?.sm && (
-
- )}
-
+
);
});
diff --git a/src/app/(main)/home/components/Story/index.tsx b/src/app/(main)/home/components/Story/index.tsx
index cc4d885..91286bc 100644
--- a/src/app/(main)/home/components/Story/index.tsx
+++ b/src/app/(main)/home/components/Story/index.tsx
@@ -1,7 +1,7 @@
-'use client'
+'use client';
const Story = () => {
- return
Story
-}
+ return
Story
;
+};
-export default Story
+export default Story;
diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx
index 03fc8a6..530c3b3 100644
--- a/src/app/(main)/home/page.tsx
+++ b/src/app/(main)/home/page.tsx
@@ -13,16 +13,16 @@ const HomePage = () => {
const response = useMedia();
return (
-
-
-
+ <>
+
{response?.sm &&
}
-
+ >
);
};
diff --git a/src/app/globals.css b/src/app/globals.css
index 4ab72be..9594481 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -17,6 +17,25 @@ body {
background: #888;
}
+.show-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
+}
+
+.show-scrollbar::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+.show-scrollbar::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 999px;
+}
+
+.show-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+}
+
@utility text-gradient {
background: linear-gradient(
135deg,
diff --git a/src/atoms/chat.ts b/src/atoms/chat.ts
deleted file mode 100644
index 13844c7..0000000
--- a/src/atoms/chat.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { atom } from 'jotai'
-
-/**
- * 自动播放语音
- */
-export const playVoiceAtom = atom({
- voiceType: '',
- dialogueSpeechRate: 0,
- dialoguePitch: 0,
- isAutoPlayVoice: false,
-})
-
-/**
- * 抽屉状态类型
- */
-export interface DrawerState {
- open: boolean
- timestamp: number
-}
-
-/**
- * 创建抽屉打开状态的辅助函数
- */
-export const createDrawerOpenState = (open: boolean): DrawerState => ({
- open,
- timestamp: Date.now(),
-})
-
-/**
- * 是否打开发送礼物抽屉
- */
-export const isSendGiftsDrawerOpenAtom = atom
({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 crush level 抽屉
- */
-export const isCrushLevelDrawerOpenAtom = atom({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 crush level 获取抽屉
- */
-export const isCrushLevelRetrieveDrawerOpenAtom = atom({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 chat profile 抽屉
- */
-export const isChatProfileDrawerOpenAtom = atom({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 chat profile 编辑抽屉
- */
-export const isChatProfileEditDrawerOpenAtom = atom({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 chat model 抽屉
- */
-export const isChatModelDrawerOpenAtom = atom({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 chat buttle 抽屉
- */
-export const isChatButtleDrawerOpenAtom = atom({ open: false, timestamp: 0 })
-
-/**
- * 是否打开 chat background 抽屉
- */
-export const isChatBackgroundDrawerOpenAtom = atom({ open: false, timestamp: 0 })
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
index f2d0141..103f43c 100644
--- a/src/components/ui/alert-dialog.tsx
+++ b/src/components/ui/alert-dialog.tsx
@@ -1,28 +1,28 @@
-'use client'
+'use client';
-import * as React from 'react'
-import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
+import * as React from 'react';
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
-import { cn } from '@/lib/utils'
-import { Button } from '@/components/ui/button'
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
// 关闭图标组件
const CloseIcon = ({ className }: { className?: string }) => (
-)
+);
function AlertDialog({ ...props }: React.ComponentProps) {
- return
+ return ;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function AlertDialogPortal({ ...props }: React.ComponentProps) {
- return
+ return ;
}
function AlertDialogOverlay({
@@ -38,7 +38,7 @@ function AlertDialogOverlay({
)}
{...props}
/>
- )
+ );
}
function AlertDialogContent({
@@ -47,8 +47,8 @@ function AlertDialogContent({
showOverlay = true,
...props
}: React.ComponentProps & {
- showCloseButton?: boolean
- showOverlay?: boolean
+ showCloseButton?: boolean;
+ showOverlay?: boolean;
}) {
return (
@@ -71,7 +71,7 @@ function AlertDialogContent({
)}
- )
+ );
}
function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
@@ -81,7 +81,7 @@ function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>)
className={cn('mb-4 flex flex-col gap-4 text-left', className)}
{...props}
/>
- )
+ );
}
function AlertDialogFooter({
@@ -89,7 +89,7 @@ function AlertDialogFooter({
variant = 'horizontal',
...props
}: React.ComponentProps<'div'> & {
- variant?: 'horizontal' | 'vertical'
+ variant?: 'horizontal' | 'vertical';
}) {
return (
- )
+ );
}
function AlertDialogTitle({
@@ -109,14 +109,14 @@ function AlertDialogTitle({
...props
}: React.ComponentProps) {
return (
-
+
- )
+ );
}
function AlertDialogDescription({
@@ -129,7 +129,7 @@ function AlertDialogDescription({
className={cn('txt-body-l text-txt-primary-normal w-full break-words', className)}
{...props}
/>
- )
+ );
}
function AlertDialogIcon({ className, children, ...props }: React.ComponentProps<'div'>) {
@@ -141,7 +141,7 @@ function AlertDialogIcon({ className, children, ...props }: React.ComponentProps
>
{children}
- )
+ );
}
function AlertDialogAction({
@@ -150,14 +150,14 @@ function AlertDialogAction({
loading,
...props
}: React.ComponentProps & {
- variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive'
- loading?: boolean
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
+ loading?: boolean;
}) {
return (
- )
+ );
}
function AlertDialogCancel({
@@ -166,14 +166,14 @@ function AlertDialogCancel({
loading,
...props
}: React.ComponentProps & {
- variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive'
- loading?: boolean
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
+ loading?: boolean;
}) {
return (
- )
+ );
}
export {
@@ -189,4 +189,4 @@ export {
AlertDialogIcon,
AlertDialogAction,
AlertDialogCancel,
-}
+};
diff --git a/src/components/ui/infinite-scroll-list.tsx b/src/components/ui/infinite-scroll-list.tsx
index 3be938f..62de68b 100644
--- a/src/components/ui/infinite-scroll-list.tsx
+++ b/src/components/ui/infinite-scroll-list.tsx
@@ -205,7 +205,7 @@ export function InfiniteScrollList({
{/* 加载更多触发器 - 只在没有错误时显示 */}
{hasNextPage && !hasError && (
-
+
{LoadingMore ? (
) : (
diff --git a/src/components/ui/virtual-list.tsx b/src/components/ui/virtual-list.tsx
index 1f9b64f..b8cf037 100644
--- a/src/components/ui/virtual-list.tsx
+++ b/src/components/ui/virtual-list.tsx
@@ -1,12 +1,12 @@
-'use client'
-import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
-import { useRef, useState } from 'react'
+'use client';
+import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
+import { useRef, useState } from 'react';
type VirtualListProps
= {
- data?: { type: string; data?: T }[]
- itemContent?: (index: number, item: { type: string; data: T }) => React.ReactNode
- virtuosoProps?: React.ComponentProps
-} & React.HTMLAttributes
+ data?: { type: string; data?: T }[];
+ itemContent?: (index: number, item: { type: string; data: T }) => React.ReactNode;
+ virtuosoProps?: React.ComponentProps;
+} & React.HTMLAttributes;
export default function VirtualList(props: VirtualListProps) {
const {
@@ -14,17 +14,17 @@ export default function VirtualList(props: VirtualListProps) {
itemContent = (index) => {index}
,
virtuosoProps,
...restProps
- } = props
- const virtuosoRef = useRef(null)
- const [showScrollButton, setShowScrollButton] = useState(false)
+ } = props;
+ const virtuosoRef = useRef(null);
+ // const [showScrollButton, setShowScrollButton] = useState(false);
- // 滚动到最新消息
- const scrollToBottom = () => {
- virtuosoRef.current?.scrollToIndex({
- index: data.length - 1,
- behavior: 'smooth',
- })
- }
+ // // 滚动到最新消息
+ // const scrollToBottom = () => {
+ // virtuosoRef.current?.scrollToIndex({
+ // index: data.length - 1,
+ // behavior: 'smooth',
+ // });
+ // };
return (
@@ -36,22 +36,22 @@ export default function VirtualList
(props: VirtualListProps) {
data={data}
followOutput="smooth"
initialTopMostItemIndex={data.length - 1}
- atBottomStateChange={(atBottom) => {
- // 当不在底部时显示按钮,在底部时隐藏
- setShowScrollButton(!atBottom)
- }}
+ // atBottomStateChange={(atBottom) => {
+ // // 当不在底部时显示按钮,在底部时隐藏
+ // setShowScrollButton(!atBottom);
+ // }}
itemContent={(index, item) => itemContent(index, item as any)}
/>
{/* 回到底部按钮 */}
- {showScrollButton && (
+ {/* {showScrollButton && (
scroll
- )}
+ )} */}
- )
+ );
}
diff --git a/src/css/tailwindcss.css b/src/css/tailwindcss.css
index 1c6f14e..e8e6f13 100644
--- a/src/css/tailwindcss.css
+++ b/src/css/tailwindcss.css
@@ -92,16 +92,16 @@
--glo-color-purple-70: #6e0098;
--glo-color-purple-80: #520073;
--glo-color-purple-90: #36004d;
- --glo-color-magenta-0: #fbdeff;
- --glo-color-magenta-10: #fdb6d3;
- --glo-color-magenta-20: #f98dbc;
- --glo-color-magenta-30: #f264a4;
- --glo-color-magenta-40: rgb(107, 134, 255);
- --glo-color-magenta-50: #d21f77;
- --glo-color-magenta-60: #b80761;
- --glo-color-magenta-70: #980050;
- --glo-color-magenta-80: #73003e;
- --glo-color-magenta-90: #4d002a;
+ --glo-color-magenta-0: #e5eaff;
+ --glo-color-magenta-10: #cbd5ff;
+ --glo-color-magenta-20: #b1c1ff;
+ --glo-color-magenta-30: #96acff;
+ --glo-color-magenta-40: #6b86ff;
+ --glo-color-magenta-50: #4861f5;
+ --glo-color-magenta-60: #3348dd;
+ --glo-color-magenta-70: #2536bf;
+ --glo-color-magenta-80: #1a2898;
+ --glo-color-magenta-90: #121d72;
--glo-color-red-0: #ffdede;
--glo-color-red-10: #ffbcbc;
--glo-color-red-20: #ff9696;
diff --git a/src/hooks/auth.ts b/src/hooks/auth.ts
index 7f0a06f..b019152 100644
--- a/src/hooks/auth.ts
+++ b/src/hooks/auth.ts
@@ -20,7 +20,6 @@ export function useLogin() {
return useMutation({
mutationFn: (data: LoginRequest): Promise => authService.login(data),
onSuccess: (response: LoginResponse) => {
- console.log('useLogin onSuccess save token', response.token);
// 保存token到cookie
tokenManager.setToken(response.token);
// 刷新当前用户信息
diff --git a/src/hooks/services/character.ts b/src/hooks/services/character.ts
new file mode 100644
index 0000000..3e3467f
--- /dev/null
+++ b/src/hooks/services/character.ts
@@ -0,0 +1,10 @@
+import { fetchCharacter } from '@/services/editor';
+import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+export function useCharacter(id?: string) {
+ return useQuery({
+ queryKey: ['character', id],
+ queryFn: () => fetchCharacter({ id }),
+ enabled: !!id,
+ });
+}
diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts
index 6ead2ae..68d949f 100644
--- a/src/hooks/useInfiniteScroll.ts
+++ b/src/hooks/useInfiniteScroll.ts
@@ -72,6 +72,7 @@ export function useInfiniteScroll({
observerRef.current = new IntersectionObserver((entries) => {
const [entry] = entries;
+
if (entry.isIntersecting) {
loadMore();
}
@@ -87,7 +88,7 @@ export function useInfiniteScroll({
observerRef.current.unobserve(currentRef);
}
};
- }, [enabled, threshold, loadMore]);
+ }, [enabled, threshold, loadMore, !!loadMoreRef.current]);
// 清理observer
useEffect(() => {
diff --git a/src/layout/BasicLayout.tsx b/src/layout/BasicLayout.tsx
index e806ae8..b7ca733 100644
--- a/src/layout/BasicLayout.tsx
+++ b/src/layout/BasicLayout.tsx
@@ -11,20 +11,23 @@ import CreateReachedLimitDialog from '../components/features/create-reached-limi
import { useMedia } from '@/hooks/tools';
import BottomBar from './BottomBar';
import { useStreamChatStore } from '@/stores/stream-chat';
-import { useLogin } from '@/hooks/auth';
+import { useCurrentUser } from '@/hooks/auth';
interface ConditionalLayoutProps {
children: React.ReactNode;
}
const useInitChat = () => {
- const { data } = useLogin();
+ const { data } = useCurrentUser();
const connect = useStreamChatStore((state) => state.connect);
const queryChannels = useStreamChatStore((state) => state.queryChannels);
const initChat = async () => {
if (data) {
- await connect(data);
+ await connect({
+ userId: data.userId + '',
+ userName: data.nickname,
+ });
await queryChannels({});
}
};
@@ -58,7 +61,7 @@ export default function ConditionalLayout({ children }: ConditionalLayoutProps)
{response?.sm && }
-
+
{children}
{response && !response.sm && }
diff --git a/src/layout/Sidebar.tsx b/src/layout/Sidebar.tsx
index 8c5d998..ee6b479 100644
--- a/src/layout/Sidebar.tsx
+++ b/src/layout/Sidebar.tsx
@@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { MenuItem } from '@/types/global';
import Image from 'next/image';
import { cn } from '@/lib/utils';
-// import ChatSidebar from './components/ChatSidebar'
+import ChatSidebar from './components/ChatSidebar';
import { Badge } from '../components/ui/badge';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
@@ -98,8 +98,11 @@ function Sidebar() {
);
})}
-
- {/* */}
+ {/* 分割线 */}
+
+
diff --git a/src/layout/Topbar.tsx b/src/layout/Topbar.tsx
index ff93681..d11389a 100644
--- a/src/layout/Topbar.tsx
+++ b/src/layout/Topbar.tsx
@@ -10,6 +10,8 @@ import { usePathname, useSearchParams, useRouter } from 'next/navigation';
import { useMedia } from '@/hooks/tools';
import { items } from './BottomBar';
+const mobileHidenMenus = ['/profile/edit', '/profile/account'];
+
function Topbar() {
const [isBlur, setIsBlur] = useState(false);
const { data: user } = useCurrentUser();
@@ -92,14 +94,13 @@ function Topbar() {
);
};
+ if (response && !response.sm && mobileHidenMenus.some((item) => item === pathname)) return null;
+
return (
{isBlur && }
diff --git a/src/layout/components/ChatSearchResults.tsx b/src/layout/components/ChatSearchResults.tsx
index 947872a..b7e0908 100644
--- a/src/layout/components/ChatSearchResults.tsx
+++ b/src/layout/components/ChatSearchResults.tsx
@@ -1,228 +1,54 @@
-'use client'
-import { useState, useMemo, useEffect } from 'react'
-import { useAtomValue } from 'jotai'
-import { conversationListAtom } from '@/atoms/im'
-import { useInfiniteQuery } from '@tanstack/react-query'
-import { imService } from '@/services/im'
-import { imKeys } from '@/lib/query-keys'
-import ChatSidebarItem from './ChatSidebarItem'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
-import { InfiniteScrollList } from '@/components/ui/infinite-scroll-list'
-import { getAge } from '@/lib/utils'
-import { useRouter } from 'next/navigation'
-import { HeartbeatRelationListOutput } from '@/services/im/types'
-import Empty from '@/components/ui/empty'
-import AIRelationTag from '@/components/features/AIRelationTag'
-import Image from 'next/image'
-import { IconButton } from '@/components/ui/button'
-import { usePrefetchRoutes } from '@/hooks/useGlobalPrefetchRoutes'
+'use client';
+import { useState, useMemo } from 'react';
+import ChatSidebarItem from './ChatSidebarItem';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import Empty from '@/components/ui/empty';
+import { useStreamChatStore } from '@/stores/stream-chat';
interface ChatSearchResultsProps {
- searchKeyword: string
- isExpanded: boolean
- onCloseSearch: () => void
+ searchKeyword: string;
+ isExpanded: boolean;
}
-// 高亮搜索关键词的组件
-const HighlightText = ({ text, keyword }: { text: string; keyword: string }) => {
- if (!keyword) return
{text}
-
- const parts = text.split(new RegExp(`(${keyword})`, 'gi'))
- return (
-
- {parts.map((part, index) =>
- part.toLowerCase() === keyword.toLowerCase() ? (
-
- {part}
-
- ) : (
- {part}
- )
- )}
-
- )
-}
-
-// Person Tab中的关系列表项组件
-const PersonItem = ({
- person,
- keyword,
- onCloseSearch,
-}: {
- person: HeartbeatRelationListOutput
- keyword: string
- onCloseSearch: () => void
-}) => {
- const router = useRouter()
- const chatHref = useMemo(() => (person.aiId ? `/chat/${person.aiId}` : null), [person.aiId])
- usePrefetchRoutes(chatHref ? [chatHref] : undefined)
-
- const handleClick = () => {
- if (chatHref) {
- router.push(chatHref)
- onCloseSearch()
- }
- }
-
- return (
-
- {/* 头像 */}
-
-
-
-
- {(person.nickname || '').charAt(0)}
-
-
- {/* 心动等级显示 */}
-
-
- {/* 用户信息 */}
-
-
-
-
-
- {person.heartbeatLevel && person.isShow && (
-
- )}
-
-
- {/* 心动值和角色信息 */}
-
- {/* 心动值 */}
- {person.heartbeatVal !== undefined && (
- <>
-
-
- {person.heartbeatVal}℃
-
-
- {/* 分隔线 */}
- {person.characterName &&
}
- >
- )}
-
- {/* 角色信息 */}
- {person.characterName && (
-
- {[getAge(person.birthday as unknown as number), person.characterName, person.tagName]
- .filter(Boolean)
- .join(' · ')}
-
- )}
-
-
-
-
-
- )
-}
-
-// Person列表加载骨架屏组件
-const PersonSkeleton = () => (
-
-)
-
-// Person空状态组件
-const PersonEmptyState = () => (
-
-
-
-)
-
-const ChatSearchResults = ({
- searchKeyword,
- isExpanded,
- onCloseSearch,
-}: ChatSearchResultsProps) => {
- const conversationList = useAtomValue(conversationListAtom)
- const [activeTab, setActiveTab] = useState('message')
- const [debouncedSearchKeyword, setDebouncedSearchKeyword] = useState(searchKeyword)
-
- // 防抖处理搜索关键词,避免频繁调用API
- useEffect(() => {
- const timer = setTimeout(() => {
- setDebouncedSearchKeyword(searchKeyword)
- }, 500) // 500ms防抖延迟
-
- return () => clearTimeout(timer)
- }, [searchKeyword])
+const ChatSearchResults = ({ searchKeyword, isExpanded }: ChatSearchResultsProps) => {
+ const channels = useStreamChatStore((state) => state.channels);
+ const [activeTab, setActiveTab] = useState('message');
// 筛选Message搜索结果 - 对用户名或最后一条消息内容进行搜索
- const messageResults = useMemo(() => {
- if (!searchKeyword) return []
+ const { nameRes, messageRes } = useMemo(() => {
+ const nameRes: any[] = [];
+ const messageRes: any[] = [];
+ if (!searchKeyword) {
+ return {
+ nameRes,
+ messageRes,
+ };
+ }
- const conversations = Array.from(conversationList.values())
- return conversations.filter((conversation) => {
- const { name } = conversation
- const keyword = searchKeyword.toLowerCase()
+ channels.forEach((chanel) => {
+ const { name } = (chanel?.data as any) ?? {};
+ const keyword = searchKeyword.toLowerCase();
// 搜索用户名
if (name?.toLowerCase().includes(keyword)) {
- return true
+ nameRes.push(chanel);
}
+ const messages = chanel?.state?.messages || [];
+ const lastMessage = messages[messages.length - 1];
+ // 搜索最后一条消息内容
+ if (lastMessage?.text?.toLowerCase().includes(keyword)) {
+ messageRes.push(chanel);
+ }
+ });
- // // 搜索最后一条消息内容
- // if (lastMessage?.text?.toLowerCase().includes(keyword)) {
- // return true;
- // }
+ return {
+ nameRes,
+ messageRes,
+ };
+ }, [channels, searchKeyword]);
- return false
- })
- }, [conversationList, searchKeyword])
-
- // 使用无限查询获取Person搜索结果
- const {
- data: personData,
- fetchNextPage,
- hasNextPage,
- isFetchingNextPage,
- isLoading: isPersonLoading,
- } = useInfiniteQuery({
- queryKey: [...imKeys.heartbeatRelationList(debouncedSearchKeyword), 'infinite'],
- queryFn: ({ pageParam = 1 }) =>
- imService.getHeartbeatRelationList({
- nickname: debouncedSearchKeyword,
- page: { pn: pageParam, ps: 20 },
- }),
- initialPageParam: 1,
- getNextPageParam: (lastPage, allPages) => {
- const currentPage = allPages.length
- const totalPages = Math.ceil((lastPage.tc || 0) / 20)
- return currentPage < totalPages ? currentPage + 1 : undefined
- },
- enabled: !!debouncedSearchKeyword && activeTab === 'person',
- })
-
- const personResults = personData?.pages.flatMap((page) => page.datas || []) || []
-
- // 判断是否正在等待防抖
- const isWaitingForDebounce = searchKeyword !== debouncedSearchKeyword
-
- // 如果没有搜索关键词,返回空状态
if (!searchKeyword) {
- return
+ return
;
}
return (
@@ -232,25 +58,24 @@ const ChatSearchResults = ({
- Chats
+ Person
- My Crushes
+ Message
- {/* Message Tab 内容 */}
- {messageResults.length > 0 ? (
- messageResults.map((conversation) => (
+ {nameRes.length > 0 ? (
+ nameRes.map((chanel: any) => (
- {/* Person Tab 内容 */}
- {isPersonLoading || isWaitingForDebounce ? (
-
- {Array.from({ length: 6 }).map((_, index) => (
-
- ))}
-
+ {messageRes.length > 0 ? (
+ messageRes.map((chanel: any) => (
+
+ ))
) : (
-
- items={personResults}
- renderItem={(person) => (
-
- )}
- getItemKey={(person) => person.aiId?.toString() || 'unknown'}
- hasNextPage={!!hasNextPage}
- isLoading={isPersonLoading || isFetchingNextPage}
- fetchNextPage={fetchNextPage}
- columns={1}
- gap={1}
- className="!grid-cols-1 space-y-1" // 强制单列布局
- LoadingSkeleton={PersonSkeleton}
- EmptyComponent={PersonEmptyState}
- hasError={false}
- enabled={!!debouncedSearchKeyword}
- />
+
+
+
)}
- )
-}
+ );
+};
-export default ChatSearchResults
+export default ChatSearchResults;
diff --git a/src/layout/components/ChatSidebar.tsx b/src/layout/components/ChatSidebar.tsx
index 71aa469..fac8481 100644
--- a/src/layout/components/ChatSidebar.tsx
+++ b/src/layout/components/ChatSidebar.tsx
@@ -6,17 +6,15 @@ import { Input } from '@/components/ui/input';
import { useState, useEffect, useCallback } from 'react';
import { useStreamChatStore } from '@/stores/stream-chat';
import { useLayoutStore } from '@/stores';
+import { useParams } from 'next/navigation';
-const ChatSidebar = () => {
+const ChatSidebar = ({ expand }: { expand?: boolean }) => {
const isSidebarExpanded = useLayoutStore((s) => s.isSidebarExpanded);
- const currentChannel = useStreamChatStore((state) => state.currentChannel);
+ const { id } = useParams<{ id: string }>();
const channels = useStreamChatStore((state) => state.channels);
const [search, setSearch] = useState('');
const [inSearching, setIsSearching] = useState(false);
-
- const datas = Array.from(channels.values()).sort((a, b) => {
- return false;
- });
+ // console.log('channels', channels);
// 当侧边栏收缩时,取消搜索功能
useEffect(() => {
@@ -34,111 +32,80 @@ const ChatSidebar = () => {
// 如果有搜索关键词,显示搜索结果
const isShowingSearchResults = search.trim().length > 0;
- if (!datas.length && !isShowingSearchResults) {
+ if (!channels.length && !isShowingSearchResults) {
return ;
}
+ const finalExpand = expand || isSidebarExpanded;
+
return (
- <>
- {/* 分割线 */}
-
-
+
+ {/* 聊天标题 */}
+
+ Chats
+ {finalExpand && (
+ setIsSearching(true)}
+ onCancelSearch={handleCloseSearch}
+ isSearchActive={inSearching}
+ />
+ )}
-
- {/* 聊天标题 */}
-
- {isSidebarExpanded ? (
- <>
- Chats
- setIsSearching(true)}
- onCancelSearch={handleCloseSearch}
- isSearchActive={inSearching}
- />
- >
- ) : (
- Chats
- )}
-
- {/* 搜索框 - 根据设计稿实现 */}
- {inSearching && isSidebarExpanded && (
-
+
+
+
+
+ )}
+
+ {/* 根据搜索状态显示不同内容 */}
+ {inSearching ? (
+ isShowingSearchResults ? (
+
) : (
- <>
- {/* 聊天项列表 */}
-
-
-
- {datas.map((chat) => (
-
- ))}
-
- {/* 底部渐变遮罩 */}
-
-
-
- >
- )}
-
- >
+
+ )
+ ) : (
+
+
+ {channels.map((chat) => (
+
+ ))}
+
+
+ )}
+
);
};
diff --git a/src/layout/components/ChatSidebarItem.tsx b/src/layout/components/ChatSidebarItem.tsx
index 5309bc7..47a938d 100644
--- a/src/layout/components/ChatSidebarItem.tsx
+++ b/src/layout/components/ChatSidebarItem.tsx
@@ -1,12 +1,9 @@
'use client';
import { useMemo } from 'react';
-import AIRelationTag from '@/components/features/AIRelationTag';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { Badge } from '@/components/ui/badge';
-import { cn, durationText, getConversationTime } from '@/lib/utils';
-import { CustomMessageType } from '@/types/im';
-import Image from 'next/image';
+import { cn } from '@/lib/utils';
import { useRouter } from 'next/navigation';
+import { Channel } from 'stream-chat';
// 高亮搜索关键词的组件
const HighlightText = ({ text, keyword }: { text: string; keyword?: string }) => {
@@ -30,7 +27,7 @@ const HighlightText = ({ text, keyword }: { text: string; keyword?: string }) =>
// 聊天项组件
export default function ChatSidebarItem({
- conversation,
+ chanel,
isExpanded,
isSelected = false,
searchKeyword,
@@ -38,29 +35,27 @@ export default function ChatSidebarItem({
isExpanded: boolean;
isSelected?: boolean;
searchKeyword?: string;
+ chanel: Channel;
}) {
- const { avatar, name, lastMessage, unreadCount, updateTime, serverExtension } = conversation;
- const { text, attachment } = lastMessage || {};
+ const { name, id, headPortrait } = (chanel?.data as any) ?? {};
const router = useRouter();
- const { heartbeatVal, heartbeatLevel, isShow } = JSON.parse(serverExtension || '{}') || {};
+ const lastMessage = useMemo(() => {
+ const messages = chanel?.state?.messages || [];
+ if (!messages.length) return null;
+ return messages[messages.length - 1];
+ }, [chanel]);
const handleChat = () => {
- router.push('/');
+ router.push(`/chat/${id}`);
};
const renderText = () => {
- const { raw } = attachment || {};
- const customData = JSON.parse(raw || '{}');
- const { type, duration } = customData || {};
- if (type === CustomMessageType.CALL_CANCEL) {
- return 'Call Canceled';
- } else if (type === CustomMessageType.CALL) {
- return `Call duration ${durationText(duration)}`;
- } else if (type == CustomMessageType.IMAGE) {
- return '[Image]';
+ if (!lastMessage) return '';
+ if (searchKeyword && lastMessage.text?.includes(searchKeyword)) {
+ return ;
}
- return text;
+ return lastMessage.text || '';
};
return (
@@ -75,7 +70,7 @@ export default function ChatSidebarItem({
@@ -84,9 +79,9 @@ export default function ChatSidebarItem({
)}
{/* 未读消息数量 */}
- {!!unreadCount && unreadCount > 0 && (
+ {/* {!!unreadCount && unreadCount > 0 && (
- )}
+ )} */}
{isExpanded && (
@@ -97,25 +92,28 @@ export default function ChatSidebarItem({
-
+ {/*
{heartbeatLevel && isShow && (
)}
-
+
*/}
+
+
+ {renderText()}
-
{renderText()}
- {getConversationTime(lastMessage?.messageRefer.createTime || updateTime)}
+ 16:27
+ {/* {getConversationTime(lastMessage?.messageRefer.createTime || updateTime)} */}
- {!!heartbeatVal && heartbeatLevel && (
+ {/* {!!heartbeatVal && heartbeatLevel && (
- )}
+ )} */}
diff --git a/src/lib/client/index.ts b/src/lib/client/index.ts
index 3e0c247..64296ca 100644
--- a/src/lib/client/index.ts
+++ b/src/lib/client/index.ts
@@ -1,3 +1,4 @@
-import createClient from './request'
+import createClient from './request';
-export const editorRequest = createClient({ serviceName: 'editor' })
+export const editorRequest = createClient({ serviceName: 'editor' });
+export const chatRequest = createClient({ serviceName: 'chat' });
diff --git a/src/lib/client/request.ts b/src/lib/client/request.ts
index f9fff4b..a71b75d 100644
--- a/src/lib/client/request.ts
+++ b/src/lib/client/request.ts
@@ -1,26 +1,27 @@
-import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
-import axios from 'axios'
-import Cookies from 'js-cookie'
-import { getToken, saveAuthInfo } from './auth'
+import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+import axios from 'axios';
+import Cookies from 'js-cookie';
+import { getToken, saveAuthInfo } from './auth';
const endpoints = {
editor: process.env.NEXT_PUBLIC_EDITOR_API_URL,
-}
+ chat: process.env.NEXT_PUBLIC_CHAT_API_URL,
+};
export default function createClient({ serviceName }: { serviceName: keyof typeof endpoints }) {
- const baseURL = endpoints[serviceName] || '/'
+ const baseURL = endpoints[serviceName] || '/';
const instance = axios.create({
withCredentials: false,
baseURL,
validateStatus: (status) => {
- return status >= 200 && status < 500
+ return status >= 200 && status < 500;
},
- })
+ });
instance.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
- const token = await getToken()
+ const token = await getToken();
if (token) {
- config.headers.setAuthorization(`Bearer ${token}`)
+ config.headers.setAuthorization(`Bearer ${token}`);
}
// 从 cookie 中读取语言设置,并添加到请求头
@@ -33,8 +34,8 @@ export default function createClient({ serviceName }: { serviceName: keyof typeo
// }
// }
- return config
- })
+ return config;
+ });
instance.interceptors.response.use(
async (response: AxiosResponse): Promise
=> {
@@ -57,12 +58,12 @@ export default function createClient({ serviceName }: { serviceName: keyof typeo
// });
}
- return response
+ return response;
},
(error) => {
- console.log('error', error)
+ console.log('error', error);
if (axios.isCancel(error)) {
- return Promise.resolve('请求取消')
+ return Promise.resolve('请求取消');
}
// notification.error({
@@ -70,29 +71,29 @@ export default function createClient({ serviceName }: { serviceName: keyof typeo
// description: error,
// });
- return Promise.reject(error)
+ return Promise.reject(error);
}
- )
+ );
type ResponseType = {
- code: number
- message: string
- data: T
- }
+ code: number;
+ message: string;
+ data: T;
+ };
return async function request(
url: string,
config?: AxiosRequestConfig
): Promise> {
- let data: any
+ let data: any;
if (config && config?.params) {
- const { params } = config
- data = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== ''))
+ const { params } = config;
+ data = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== ''));
}
const response = await instance>(url, {
...config,
params: data,
- })
- return response.data
- }
+ });
+ return response.data;
+ };
}
diff --git a/src/services/editor/index.ts b/src/services/editor/index.ts
index 3d574d6..d6b6627 100644
--- a/src/services/editor/index.ts
+++ b/src/services/editor/index.ts
@@ -1,4 +1,4 @@
-import { editorRequest } from '@/lib/client';
+import { chatRequest, editorRequest } from '@/lib/client';
export async function fetchCharacters({ index, limit, query }: any) {
const { data } = await editorRequest('/api/character/list', {
@@ -9,9 +9,31 @@ export async function fetchCharacters({ index, limit, query }: any) {
}
export async function fetchCharacter(params: any) {
- return editorRequest('/api/character/detail', { method: 'POST', data: params });
+ const { data } = await editorRequest('/api/character/detail', { method: 'POST', data: params });
+ return data;
}
export async function fetchCharacterTags(params: any = {}) {
return editorRequest('/api/tag/list', { method: 'POST', data: params });
}
+
+export async function getUserToken(data: { userId: string; userName: string }) {
+ return await chatRequest('/chat-api/v1/im/user/createOrGet', {
+ method: 'post',
+ data: data,
+ });
+}
+
+export async function createChannel(data: any) {
+ return await chatRequest('/chat-api/v1/im/user/conversation/create', {
+ method: 'post',
+ data: data,
+ });
+}
+
+export async function deleteChannel(chanelId: string) {
+ return await chatRequest('/chat-api/v1/im/user/conversation/delete', {
+ method: 'post',
+ data: { chanelId },
+ });
+}
diff --git a/src/stores/stream-chat.ts b/src/stores/stream-chat.ts
index 34b8234..1c04ae9 100644
--- a/src/stores/stream-chat.ts
+++ b/src/stores/stream-chat.ts
@@ -1,60 +1,138 @@
'use client';
import { Channel, StreamChat } from 'stream-chat';
import { create } from 'zustand';
+import { getUserToken, createChannel } from '@/services/editor';
+import { parseSSEStream, parseData } from '@/utils/streamParser';
+
+type Message = {
+ key: string;
+ role: string;
+ content: string;
+};
interface StreamChatStore {
+ client: StreamChat | null;
+ user: {
+ userId: string;
+ userName: string;
+ };
+ // 连接 StreamChat 客户端
connect: (user: any) => Promise;
+ // 频道
channels: Channel[];
currentChannel: Channel | null;
+ // 创建某个角色的聊天频道, 返回channelId
+ createChannel: (characterId: string) => Promise;
switchToChannel: (id: string) => Promise;
queryChannels: (filter: any) => Promise;
deleteChannel: (id: string) => Promise;
clearChannels: () => Promise;
+ getCurrentCharacter: () => any | null;
+
+ // 消息列表
+ messages: Message[];
+ setMessages: (messages: Message[]) => void;
+
+ // 发送消息
+ sendMessage: (content: string) => Promise;
+
+ // 清除通知
clearNotifications: () => Promise;
}
-
-let client: StreamChat | null = null;
export const useStreamChatStore = create((set, get) => ({
+ client: null,
+ user: {
+ userId: '',
+ userName: '',
+ },
channels: [],
+ messages: [],
+ setMessages: (messages: any[]) => set({ messages }),
currentChannel: null,
+ // 获取当前聊天频道中的角色id
+ getCurrentCharacter() {
+ const { currentChannel, user } = get();
+ return (
+ Object.values(currentChannel?.state?.members || {})?.find((i) => i.user?.id !== user?.userId)
+ ?.user?.id || null
+ );
+ },
+ // 创建某个角色的聊天频道
+ async createChannel(characterId: string) {
+ const { user, client } = get();
+ const { switchToChannel, queryChannels } = get();
+ if (!client) {
+ return false;
+ }
+ const { data } = await createChannel({
+ userId: user.userId,
+ userName: user.userName,
+ characterId,
+ });
+ if (!data?.channelId) {
+ return false;
+ }
+ await queryChannels({});
+ switchToChannel(data.channelId);
+ return data.channelId;
+ },
+
async connect(user) {
+ const { client } = get();
+ set({ user });
if (client) return;
- console.log('connecting stream chat', user);
const { data } = await getUserToken(user);
- client = new StreamChat(process.env.NEXT_PUBLIC_STREAM_CHAT_API_KEY || '');
- await client.connectUser(
+ const streamClient = new StreamChat(process.env.NEXT_PUBLIC_STREAM_CHAT_API_KEY || '');
+ const res = await streamClient.connectUser(
{
id: user.userId,
name: user.userName,
},
data
);
+ set({ client: streamClient });
},
+
async switchToChannel(id: string) {
- const { channels } = get();
- const channel = channels.find((ch) => ch.id === id);
- if (channel) {
- set({ currentChannel: channel });
- // 可选:监听该频道的消息
- await channel.watch();
- } else {
- console.warn(`Channel with id ${id} not found in channels list`);
- }
+ const { client, user } = get();
+ const channel = client!.channel('messaging', id);
+ const result = await channel.query({
+ messages: { limit: 100 },
+ });
+ const messages = result.messages.map((i) => ({
+ key: i.id,
+ role: i.user?.id === user.userId ? 'user' : 'assistant',
+ content: i.text!,
+ }));
+ set({ currentChannel: channel, messages });
},
- async queryChannels(filter: any) {
+
+ async queryChannels() {
+ const { user, client } = get();
if (!client) {
console.error('StreamChat client is not connected');
return;
}
try {
- const channels = await client.queryChannels(filter, {
- last_message_at: -1,
- });
+ const channels = await client.queryChannels(
+ {
+ members: {
+ $in: [user.userId],
+ },
+ },
+ {
+ last_message_at: -1,
+ },
+ {
+ message_limit: 1, // 返回最新的1条消息
+ }
+ );
set({ channels });
} catch (error) {
console.error('Failed to query channels:', error);
}
},
+
async deleteChannel(id: string) {
const { channels, currentChannel, queryChannels } = get();
const channel = channels.find((ch) => ch.id === id);
@@ -65,11 +143,14 @@ export const useStreamChatStore = create((set, get) => ({
try {
await channel.delete();
await queryChannels({});
- set({ currentChannel: null });
+ if (currentChannel?.id === id) {
+ set({ currentChannel: null });
+ }
} catch (error) {
console.error(`Failed to delete channel ${id}:`, error);
}
},
+
async clearChannels() {
const { channels } = get();
@@ -93,4 +174,48 @@ export const useStreamChatStore = create((set, get) => ({
}
},
async clearNotifications() {},
+
+ // 发送消息
+ sendMessage: async (content: any) => {
+ const { user, currentChannel, getCurrentCharacter, setMessages, messages } = get();
+ // 过滤出用户和助手的消息
+ const filteredMessages = messages.filter((i) => i.role === 'user' || i.role === 'assistant');
+ let finalMessages = [
+ ...filteredMessages,
+ { key: user.userId, role: 'user', content: content },
+ { key: 'assistant', role: 'assistant', content: '' },
+ ];
+ setMessages(finalMessages);
+
+ // 发送消息到服务器
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_CHAT_API_URL}/chat-api/chat/testPrompt`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userId: user.userId,
+ channelId: currentChannel?.id || '',
+ message: content,
+ promptTemplateId: 'default',
+ characterId: getCurrentCharacter()?.id,
+ modelName: 'gpt-3.5-turbo',
+ }),
+ }
+ );
+
+ // 处理服务器返回的 SSE 流
+ await parseSSEStream(response, (event: string, data: string) => {
+ if (event === 'chat-message') {
+ const d = parseData(data);
+ const lastMsg = finalMessages[finalMessages.length - 1];
+ if (lastMsg.role === 'assistant') {
+ lastMsg.content = d.content || '';
+ }
+ setMessages([...finalMessages]);
+ }
+ });
+ },
}));
diff --git a/src/utils/streamParser.ts b/src/utils/streamParser.ts
new file mode 100644
index 0000000..5e19096
--- /dev/null
+++ b/src/utils/streamParser.ts
@@ -0,0 +1,76 @@
+export type SSEHandler = (event: string, data: string) => void;
+
+export const parseData = (data: string): any => {
+ try {
+ return JSON.parse(data);
+ } catch (error) {
+ return data;
+ }
+};
+
+/**
+ * 处理 SSE 流的通用函数
+ * @param response fetch 返回的 Response 对象
+ * @param onMessage 收到消息时的回调函数
+ */
+export async function parseSSEStream(response: Response, onMessage: SSEHandler) {
+ if (!response.body) {
+ throw new Error('Response body is empty');
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+
+ // 1. 解码新收到的数据并追加到 buffer
+ if (value) {
+ buffer += decoder.decode(value, { stream: !done });
+ }
+
+ // 2. 按标准 SSE 分隔符 \n\n 切分消息
+ const parts = buffer.split('\n\n');
+
+ // 3. 最后一个部分通常是不完整的,留到下一次处理
+ // 但如果流已经结束(done=true),那么剩下的所有内容都必须强制处理
+ buffer = parts.pop() || '';
+
+ if (done && buffer.trim()) {
+ parts.push(buffer);
+ buffer = '';
+ }
+
+ // 4. 解析切分出来的每一条完整消息
+ for (const part of parts) {
+ if (!part.trim()) continue;
+
+ const lines = part.split('\n');
+ let event = '';
+ let data = '';
+
+ for (const line of lines) {
+ if (line.startsWith('event:')) {
+ event = line.slice(6).trim();
+ } else if (line.startsWith('data:')) {
+ const lineData = line.slice(5);
+ data = lineData;
+ }
+ }
+
+ if (data) {
+ onMessage(event, data);
+ }
+ }
+
+ if (done) break;
+ }
+ } catch (error) {
+ console.error('Stream parsing error:', error);
+ throw error; // 继续抛出,让调用者处理
+ } finally {
+ reader.releaseLock();
+ }
+}