feat: 优化了一些代码
This commit is contained in:
parent
b9cf8bc55f
commit
859ef2d320
|
|
@ -1,10 +1,17 @@
|
|||
import type { NextConfig } from 'next';
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
// 指定 i18n 配置文件路径,让 next-intl 知道支持的语言
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: false,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
images: {
|
||||
// 这些域不走next的image代理
|
||||
domains: ['example.com'],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -15,4 +22,4 @@ const nextConfig: NextConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
export default withNextIntl(nextConfig);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_229_4076)">
|
||||
<rect width="20" height="20" rx="10" fill="white"/>
|
||||
<path d="M-1.66666 -0.416016H38.9583V26.6673H-1.66666V-0.416016Z" fill="#D8231D"/>
|
||||
<path d="M14.8497 6.66423C14.9848 6.77821 15.1127 6.90012 15.2327 7.02928L15.613 7.41959L16.5875 6.95999C16.5183 7.12456 16.4386 7.28473 16.349 7.43957C16.2672 7.59678 16.1813 7.75131 16.0955 7.9125L16.8642 8.69316C16.8369 8.69476 16.8096 8.69476 16.7824 8.69316L15.8528 8.52265C15.7629 8.50665 15.7629 8.50933 15.7165 8.59059L15.2859 9.3899L15.2341 9.47384L15.2013 9.25801C15.1605 8.99157 15.1237 8.72513 15.0869 8.4587C15.076 8.39477 15.076 8.39341 15.0092 8.38012L14.0551 8.20959C14.0377 8.20655 14.021 8.20021 14.0061 8.19093C14.1655 8.10299 14.3291 8.03107 14.4899 7.95379L14.9833 7.72201L14.847 6.67091L14.8497 6.66423ZM11.5296 4.2983L11.5459 4.28497L12.5395 4.68463L13.251 3.86133V3.97321L13.1897 4.85646C13.1897 4.94441 13.1897 4.94441 13.2674 4.97636L14.1383 5.32007C14.1587 5.32007 14.1751 5.33605 14.2105 5.35072L14.1287 5.37869L13.2087 5.5985C13.1311 5.61448 13.1311 5.61448 13.1242 5.69441L13.0588 6.65759L13.0506 6.70554L12.9948 6.62161C12.8312 6.36584 12.6677 6.10871 12.5 5.85295C12.455 5.781 12.4523 5.781 12.3637 5.80367L11.4533 6.0208L11.3756 6.03544C11.6168 5.7517 11.8499 5.48394 12.087 5.20816L11.5269 4.30761L11.5296 4.2983ZM9.83955 8.04705L7.68474 9.6017L7.63158 9.64036C7.57025 9.68699 7.57025 9.68699 7.5948 9.76158L8.18358 11.5201L8.45616 12.338L8.47661 12.4033H8.46026L8.41256 12.3713L6.12282 10.7328L6.10237 10.7208H6.08329L3.73085 12.4113L3.71448 12.3993L4.60857 9.667L2.25342 7.99243V7.97245H5.05698C5.17965 7.97245 5.1592 7.9871 5.19327 7.87253L6.05193 5.36935C6.06107 5.33833 6.07635 5.30937 6.09692 5.28411L6.98964 7.97112C7.15045 7.97911 7.31265 7.97112 7.47213 7.97112H9.90634V7.98845L9.83955 8.04705ZM14.1315 14.6534L13.2196 14.8692L13.1338 14.8932L13.0588 15.9776L13.0057 15.9043L12.5068 15.129C12.4591 15.0557 12.4578 15.0557 12.3705 15.0757L11.4615 15.2955L11.3797 15.3088L12.0925 14.4802L11.5323 13.5809L11.5432 13.5676C11.7122 13.6236 11.8744 13.7008 12.0407 13.7608L12.5436 13.9606L13.2415 13.1506H13.2551V13.2412L13.1937 14.1218V14.1538C13.1913 14.1627 13.1908 14.1719 13.1922 14.181C13.1936 14.19 13.1969 14.1987 13.2019 14.2064C13.2069 14.2142 13.2135 14.2208 13.2212 14.226C13.229 14.2312 13.2377 14.2347 13.2469 14.2364L13.6231 14.3843L14.1465 14.5894L14.2187 14.6187L14.1328 14.6467L14.1315 14.6534ZM16.7715 11.4668L16.0519 12.0143C15.981 12.0676 15.981 12.0676 16.011 12.1475L16.3217 12.9961C16.3322 13.0228 16.3354 13.0517 16.3313 13.0801L15.4317 12.4713L14.5676 13.1267L14.554 13.116L14.8579 12.0889L13.9666 11.4894V11.4694C14.3236 11.4521 14.6821 11.4561 15.0501 11.4361L15.3649 10.4263H15.3786L15.4113 10.5063L15.7179 11.3482C15.7452 11.4241 15.7452 11.4241 15.827 11.4215L16.7388 11.3948H16.8246V11.4108L16.7715 11.4668Z" fill="#FFFA30"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_229_4076">
|
||||
<rect width="20" height="20" rx="10" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_229_4071)">
|
||||
<path d="M0 10C0 12.6522 1.05357 15.1957 2.92893 17.0711C4.8043 18.9464 7.34784 20 10 20C12.6522 20 15.1957 18.9464 17.0711 17.0711C18.9464 15.1957 20 12.6522 20 10C20 7.34784 18.9464 4.8043 17.0711 2.92893C15.1957 1.05357 12.6522 0 10 0C7.34784 0 4.8043 1.05357 2.92893 2.92893C1.05357 4.8043 0 7.34784 0 10Z" fill="#F0F0F0"/>
|
||||
<path d="M2.06733 3.91301C1.28182 4.935 0.689518 6.11293 0.344635 7.39258H5.5469L2.06733 3.91301ZM19.6556 7.39258C19.3108 6.11297 18.7184 4.93504 17.933 3.91305L14.4535 7.39258H19.6556ZM0.344635 12.61C0.689557 13.8896 1.28186 15.0675 2.06733 16.0895L5.54678 12.61H0.344635ZM16.0883 2.06844C15.0664 1.28293 13.8885 0.690625 12.6088 0.345703V5.54793L16.0883 2.06844ZM3.91194 17.934C4.93393 18.7195 6.11186 19.3118 7.39147 19.6567V14.4545L3.91194 17.934ZM7.39143 0.345703C6.11182 0.690625 4.93389 1.28293 3.91194 2.0684L7.39143 5.54789V0.345703ZM12.6089 19.6567C13.8885 19.3118 15.0664 18.7195 16.0883 17.934L12.6089 14.4545V19.6567ZM14.4535 12.61L17.933 16.0895C18.7184 15.0675 19.3108 13.8896 19.6556 12.61H14.4535Z" fill="#0052B4"/>
|
||||
<path d="M19.9154 8.69566H11.3044V0.0846484C10.8719 0.0282971 10.4362 2.17268e-05 10 0C9.56385 1.78888e-05 9.12816 0.0282933 8.69566 0.0846484V8.69563H0.0846484C0.0282971 9.12813 2.17268e-05 9.56384 0 10C0 10.4421 0.0290625 10.8774 0.0846484 11.3043H8.69563V19.9154C9.56153 20.0282 10.4384 20.0282 11.3043 19.9154V11.3044H19.9154C19.9717 10.8719 20 10.4362 20 10C20 9.55793 19.9709 9.12262 19.9154 8.69566Z" fill="#D80027"/>
|
||||
<path d="M12.6085 12.6074L17.0708 17.0698C17.276 16.8644 17.4721 16.6502 17.6586 16.4278L13.8383 12.6074H12.6085V12.6074ZM7.39107 12.6074H7.39099L2.92869 17.0697C3.13407 17.2749 3.34827 17.471 3.57068 17.6575L7.39107 13.8371V12.6074ZM7.39107 7.39004V7.38996L2.92872 2.92758C2.72358 3.13296 2.52746 3.34716 2.34091 3.56957L6.16134 7.39H7.39107V7.39004ZM12.6085 7.39004L17.0709 2.92762C16.8655 2.72248 16.6513 2.52637 16.4289 2.33984L12.6085 6.16027V7.39004Z" fill="#D80027"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_229_4071">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -4,6 +4,7 @@ import { RightArrowIcon } from '@/assets/chatacter';
|
|||
import IconFont from '@/components/ui/iconFont';
|
||||
import ChatButton from './ChatButton';
|
||||
import Tags from '@/components/ui/Tags';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type CharacterBasicInfoProps = {
|
||||
characterId: string;
|
||||
|
|
@ -14,6 +15,8 @@ export default function CharacterBasicInfo({
|
|||
characterId,
|
||||
characterDetail,
|
||||
}: CharacterBasicInfoProps) {
|
||||
const msg = useTranslations();
|
||||
|
||||
if (!characterDetail) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -41,6 +44,7 @@ export default function CharacterBasicInfo({
|
|||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-text-color text-4xl font-bold">
|
||||
{characterName}
|
||||
{msg('common_desc')}
|
||||
</h1>
|
||||
<div>
|
||||
<IconFont type="icon-jubao" size={20} />
|
||||
|
|
@ -114,7 +118,7 @@ export default function CharacterBasicInfo({
|
|||
width={18}
|
||||
height={20}
|
||||
/>
|
||||
Description:
|
||||
{'Description'}:
|
||||
</h2>
|
||||
<p className="text-text-color/60 mt-5 text-sm">
|
||||
{characterDetail.description}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { publicServerRequest } from '@/lib/server-request';
|
|||
*/
|
||||
|
||||
export const fetchCharacterDetail = cache(async (id: string) => {
|
||||
return {};
|
||||
const { data } = await publicServerRequest(
|
||||
`/character/select/roleInfo/${id}`,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { historyListOpenAtom } from '../atoms';
|
||||
import { historyListOpenAtom } from '../../atoms';
|
||||
import Image from 'next/image';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { historyListOpenAtom, leftTabActiveKeyAtom } from '../atoms';
|
||||
import { historyListOpenAtom, leftTabActiveKeyAtom } from '../../atoms';
|
||||
import { cn } from '@/lib';
|
||||
import Image from 'next/image';
|
||||
import { CharaterHistoryIcon } from '@/assets/chatacter';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon } from '@/components';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
|
|
@ -19,22 +18,27 @@ export default function Side() {
|
|||
|
||||
const tabs = [
|
||||
{
|
||||
element: (
|
||||
<div className="border-0.25 h-10 w-10 hover:border-amber-500">
|
||||
<Image
|
||||
src="/component/star_full.svg"
|
||||
alt="info"
|
||||
width={38}
|
||||
height={38}
|
||||
/>
|
||||
render: (isActive: boolean) => (
|
||||
<div
|
||||
className={cn(
|
||||
'h-10 w-10 overflow-hidden rounded-full border-2',
|
||||
isActive ? 'border-blue-500' : 'border-white/80'
|
||||
)}
|
||||
>
|
||||
<Image src="/avator.png" alt="info" width={40} height={40} />
|
||||
</div>
|
||||
),
|
||||
key: 'info',
|
||||
},
|
||||
{
|
||||
element: (
|
||||
<div className="">
|
||||
<CharaterHistoryIcon />
|
||||
render: (isActive: boolean) => (
|
||||
<div
|
||||
className={cn(
|
||||
'h-10 w-10',
|
||||
isActive ? 'text-white' : 'text-white/60 hover:text-white/80'
|
||||
)}
|
||||
>
|
||||
<IconFont type="icon-jiaoselishi" size={40} />
|
||||
</div>
|
||||
),
|
||||
key: 'history',
|
||||
|
|
@ -78,13 +82,13 @@ export default function Side() {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-10 justify-end border-r-[2px] pr-4.5 hover:cursor-pointer',
|
||||
'flex h-10 cursor-pointer justify-end border-r-[2px] pr-4.5',
|
||||
isActive ? 'border-blue-500' : 'border-transparent'
|
||||
)}
|
||||
onClick={() => setActiveKey(item.key as any)}
|
||||
key={item.key}
|
||||
>
|
||||
{item.element}
|
||||
{item.render(isActive)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Side from './Side';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { historyListOpenAtom } from '../atoms';
|
||||
import { historyListOpenAtom } from '../../atoms';
|
||||
import { memo } from 'react';
|
||||
import { Drawer } from '@/components';
|
||||
import ChatHistory from './ChatHisory';
|
||||
|
|
@ -57,13 +57,13 @@ const SettingForm = React.memo(() => {
|
|||
}}
|
||||
>
|
||||
<Title text="Switch Model">
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="model"
|
||||
render={({ value, onChange }) => (
|
||||
<ModelSelectDialog value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="short_text"
|
||||
render={({ value, onChange }) => (
|
||||
<Switch
|
||||
|
|
@ -77,13 +77,13 @@ const SettingForm = React.memo(() => {
|
|||
</Title>
|
||||
|
||||
<Title text="Sound">
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="voiceActor"
|
||||
render={({ value, onChange }) => (
|
||||
<VoiceActorSelectDialog value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="dialogueOnly"
|
||||
render={({ value, onChange }) => (
|
||||
<Switch
|
||||
|
|
@ -97,7 +97,7 @@ const SettingForm = React.memo(() => {
|
|||
</Title>
|
||||
|
||||
<Title text="Maximum number of response tokens">
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="max_tokens"
|
||||
render={({ value, onChange }) => (
|
||||
<Number value={value} onChange={onChange} />
|
||||
|
|
@ -106,13 +106,13 @@ const SettingForm = React.memo(() => {
|
|||
</Title>
|
||||
|
||||
<Title text="Appearance">
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="fontSize"
|
||||
render={({ value, onChange }) => {
|
||||
return <FontSize value={value} onChange={onChange} />;
|
||||
}}
|
||||
/>
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="chat_mode"
|
||||
render={({ value, onChange }) => (
|
||||
<Select
|
||||
|
|
@ -134,7 +134,7 @@ const SettingForm = React.memo(() => {
|
|||
</Title>
|
||||
|
||||
<Title text="Background">
|
||||
<FormItem<SettingFormValues>
|
||||
<FormItem
|
||||
name="background"
|
||||
render={({ value, onChange }) => (
|
||||
<Background value={value} onChange={onChange} />
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
'use client';
|
||||
import {
|
||||
GenerateInputIcon,
|
||||
PhoneCallIcon,
|
||||
PortraitModeIcon,
|
||||
} from '@/assets/chatacter';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { isPhoneCallModeAtom, isPortraitModeAtom } from '../../atoms';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
import { cn } from '@/lib';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Actions() {
|
||||
const [isPortraitMode, setIsPortraitMode] = useAtom(isPortraitModeAtom);
|
||||
const setIsPhoneCallMode = useSetAtom(isPhoneCallModeAtom);
|
||||
const className = 'text-[#0066FF] cursor-pointer hover:text-[#4269D6]';
|
||||
|
||||
const suggestMessages = [
|
||||
'The threads of fate intertwine once more...The threads of fate intert',
|
||||
'The threads of fate intertwine once more.',
|
||||
'The threads of fate intertwine once more.',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="relative mb-5 flex flex-col gap-2">
|
||||
{suggestMessages.map((message, index) => (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgba(36, 44, 80, 0.9)',
|
||||
width: '70%',
|
||||
}}
|
||||
className="flex h-10 cursor-pointer items-center rounded-full px-4"
|
||||
key={`message-${index}`}
|
||||
>
|
||||
<span className="line-clamp-2 text-xs">{message}</span>
|
||||
</div>
|
||||
))}
|
||||
<span
|
||||
style={{ left: '71%' }}
|
||||
className="flex-center absolute bottom-0 h-7 w-7 cursor-pointer rounded-full bg-black/40"
|
||||
>
|
||||
<IconFont type="icon-zhongxie" size={18} />
|
||||
</span>
|
||||
</div>
|
||||
{/* action */}
|
||||
<div className="flex justify-between">
|
||||
<div onClick={() => null} className="flex items-center gap-5">
|
||||
<div className={className}>
|
||||
<GenerateInputIcon />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsPhoneCallMode(true)}
|
||||
className={cn(className, 'relative')}
|
||||
>
|
||||
<PhoneCallIcon />
|
||||
<Image
|
||||
className="absolute right-0 bottom-0"
|
||||
src="/component/vip.svg"
|
||||
alt="phone call"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsPortraitMode(!isPortraitMode)}
|
||||
className="hover:cursor-pointer"
|
||||
>
|
||||
<PortraitModeIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
'use client';
|
||||
|
||||
import Input from './input';
|
||||
import Actions from './actions';
|
||||
import ChatList from './ChatList';
|
||||
import { useAtom } from 'jotai';
|
||||
import { settingOpenAtom } from '../atoms';
|
||||
import SettingForm from './Right';
|
||||
import { Drawer } from '@/components';
|
||||
import Left from './Left';
|
||||
import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common';
|
||||
import { cn } from '@/lib';
|
||||
|
||||
export default function Main() {
|
||||
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* main */}
|
||||
<div className="flex h-full w-[calc(100vw-900px)] flex-col px-10">
|
||||
{/* chat list */}
|
||||
<ChatList />
|
||||
|
||||
{/* actions */}
|
||||
<Actions />
|
||||
|
||||
{/* inputs */}
|
||||
<Input />
|
||||
</div>
|
||||
|
||||
{/* 左侧 */}
|
||||
<Drawer open={settingOpen} position="left" width={448} destroyOnClose>
|
||||
<Left />
|
||||
</Drawer>
|
||||
|
||||
{/* 右侧设置 */}
|
||||
<Drawer open={settingOpen} position="right" width={448} destroyOnClose>
|
||||
<SettingForm />
|
||||
</Drawer>
|
||||
|
||||
{/* 设置按钮 */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-8 right-10 h-10 w-10 select-none hover:cursor-pointer',
|
||||
'text-text-color/10 hover:text-text-color/20'
|
||||
)}
|
||||
onClick={() => setSettingOpen(!settingOpen)}
|
||||
>
|
||||
{settingOpen ? <FullScreenIcon /> : <ExitFullScreenIcon />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,8 +27,8 @@ export default function Input() {
|
|||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="bg-text-color/15 mb-10 flex min-h-13 w-full items-end justify-between gap-2 rounded-[25px] px-1 py-1">
|
||||
<div className={cn(className, 'hover:bg-text-color/20')}>
|
||||
<div className="mb-10 flex min-h-13 w-full items-end justify-between gap-2 rounded-[25px] bg-white/15 px-1 py-1">
|
||||
<div className={cn(className, 'hover:bg-white/20')}>
|
||||
<VoiceIcon />
|
||||
</div>
|
||||
<textarea
|
||||
|
|
@ -39,7 +39,13 @@ export default function Input() {
|
|||
className="hide-scrollbar h-10 max-h-25 w-full resize-none bg-transparent px-1 py-2.5 leading-normal outline-none"
|
||||
rows={1}
|
||||
/>
|
||||
<div className={cn(className, 'bg-text-color/20')}>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(92.99deg, rgba(166, 83, 255, 1) 0%, rgba(0, 101, 255, 1) 112.3%, rgba(0, 157, 255, 1) 140.37%)',
|
||||
}}
|
||||
className={cn(className)}
|
||||
>
|
||||
<Image src="/component/send.svg" width={20} height={20} alt="send" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface ChatContextType {}
|
||||
|
||||
export const ChatContext = createContext<ChatContextType>({});
|
||||
|
||||
export default function ChatContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <ChatContext.Provider value={{}}>{children}</ChatContext.Provider>;
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
'use client';
|
||||
import {
|
||||
GenerateInputIcon,
|
||||
PhoneCallIcon,
|
||||
PortraitModeIcon,
|
||||
} from '@/assets/chatacter';
|
||||
import { useAtom } from 'jotai';
|
||||
import { isPortraitModeAtom } from '../atoms';
|
||||
|
||||
export default function Actions() {
|
||||
const [isPortraitMode, setIsPortraitMode] = useAtom(isPortraitModeAtom);
|
||||
const className = 'text-[#4269D6] hover:cursor-pointer hover:text-[#0066FF]';
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className={className}>
|
||||
<GenerateInputIcon />
|
||||
</div>
|
||||
<div className={className}>
|
||||
<PhoneCallIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsPortraitMode(!isPortraitMode)}
|
||||
className="hover:cursor-pointer"
|
||||
>
|
||||
<PortraitModeIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import Input from './input';
|
||||
import Actions from './actions';
|
||||
import ChatList from './ChatList';
|
||||
|
||||
export default function Main() {
|
||||
return (
|
||||
<div className="flex h-full w-[calc(100vw-900px)] flex-col px-10">
|
||||
{/* chat list */}
|
||||
<ChatList />
|
||||
|
||||
{/* actions */}
|
||||
<Actions />
|
||||
|
||||
{/* inputs */}
|
||||
<Input />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
'use client';
|
||||
|
||||
export default function PhoneCallMode() {
|
||||
return <div>PhoneCallMode</div>;
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@ export const settingOpenAtom = atom(true);
|
|||
// 是否是立绘模式
|
||||
export const isPortraitModeAtom = atom(false);
|
||||
|
||||
// 是否是通话模式
|
||||
export const isPhoneCallModeAtom = atom(false);
|
||||
|
||||
// 左侧 tab active key
|
||||
export const leftTabActiveKeyAtom = atom<'info' | 'history'>('info');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { cn } from '@/lib';
|
||||
import SettingForm from './Right';
|
||||
import { useAtom } from 'jotai';
|
||||
import { settingOpenAtom } from './atoms';
|
||||
import Main from './Main';
|
||||
import Left from './Left';
|
||||
import { Drawer } from '@/components';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { isPhoneCallModeAtom } from './atoms';
|
||||
import ChatMode from './ChatMode';
|
||||
import './index.css';
|
||||
import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common';
|
||||
import PhoneCallMode from './PhoneCallMode';
|
||||
import ChatContextProvider from './Context';
|
||||
|
||||
export default function CharacterChat() {
|
||||
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
|
||||
const isPhoneCallMode = useAtomValue(isPhoneCallModeAtom);
|
||||
|
||||
return (
|
||||
<ChatContextProvider>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
|
|
@ -21,27 +19,8 @@ export default function CharacterChat() {
|
|||
}}
|
||||
className="relative flex h-full w-full justify-center overflow-hidden"
|
||||
>
|
||||
<Main />
|
||||
|
||||
{/* 左侧 */}
|
||||
<Drawer open={settingOpen} position="left" width={448} destroyOnClose>
|
||||
<Left />
|
||||
</Drawer>
|
||||
{/* 右侧设置 */}
|
||||
<Drawer open={settingOpen} position="right" width={448} destroyOnClose>
|
||||
<SettingForm />
|
||||
</Drawer>
|
||||
|
||||
{/* 设置按钮 */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-8 right-10 h-10 w-10 select-none hover:cursor-pointer',
|
||||
'text-text-color/10 hover:text-text-color/20'
|
||||
)}
|
||||
onClick={() => setSettingOpen(!settingOpen)}
|
||||
>
|
||||
{settingOpen ? <FullScreenIcon /> : <ExitFullScreenIcon />}
|
||||
</div>
|
||||
{isPhoneCallMode ? <PhoneCallMode /> : <ChatMode />}
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,14 +63,12 @@ export default function Novel() {
|
|||
queryKey: ['tags'],
|
||||
queryFn: async () => {
|
||||
const res = await fetchTags();
|
||||
return res.rows;
|
||||
},
|
||||
});
|
||||
|
||||
const options = tags?.map((tag: any) => ({
|
||||
return res.rows?.map((tag: any) => ({
|
||||
label: tag.name,
|
||||
value: tag.id,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<VirtualGrid
|
||||
|
|
@ -83,7 +81,7 @@ export default function Novel() {
|
|||
loadMore={onLoadMore}
|
||||
header={
|
||||
<TagSelect
|
||||
options={options}
|
||||
options={tags}
|
||||
render={(item) => `# ${item.label}`}
|
||||
onChange={(v) => {
|
||||
onSearch({ tagId: v });
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function RootLayout({
|
|||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<Script
|
||||
src="//at.alicdn.com/t/c/font_5054282_5o7sf0csg4w.js"
|
||||
src="//at.alicdn.com/t/c/font_5054282_ij9uesv751.js"
|
||||
strategy="afterInteractive"
|
||||
async
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -235,27 +235,3 @@ export const ScrollTobottom = () => {
|
|||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CharaterHistoryIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="20" cy="10" r="6" fill="white" fillOpacity="0.6" />
|
||||
<path
|
||||
d="M20 18C22.2563 18 24.3869 18.4472 26.2744 19.2373C21.5445 20.2582 18 24.4647 18 29.5C18 31.7314 18.6979 33.799 19.8848 35.5C12.206 35.5058 6.00021 36.0779 6 29.667C6 23.2237 12.268 18 20 18Z"
|
||||
// fill="white"
|
||||
fillOpacity="0.6"
|
||||
/>
|
||||
<path
|
||||
d="M29 22C32.866 22 36 25.134 36 29C36 32.866 32.866 36 29 36C25.134 36 22 32.866 22 29C22 25.134 25.134 22 29 22ZM29 23.665C28.2627 23.665 27.665 24.2627 27.665 25V29.4453L29.4316 31.8008C29.8739 32.3905 30.711 32.5105 31.3008 32.0684C31.8905 31.6261 32.0105 30.789 31.5684 30.1992L30.335 28.5547V25C30.335 24.2627 29.7373 23.665 29 23.665Z"
|
||||
// fill="white"
|
||||
fillOpacity="0.6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export default function ModelSelectDialog(props: ModelSelectDialogProps) {
|
|||
defaultValuePropName: 'defaultValue',
|
||||
trigger: 'onChange',
|
||||
});
|
||||
console.log('ModelSelectDialog', value);
|
||||
|
||||
const options = [
|
||||
{ label: 'Model 1', value: 'model1' },
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export default function VoiceActorSelectDialog(
|
|||
props: VoiceActorSelectDialogProps
|
||||
) {
|
||||
const { value, onChange } = props;
|
||||
console.log('VoiceActorSelectDialog', value);
|
||||
const options = [
|
||||
{ label: 'Voice Actor 1', value: 'voiceActor1', gender: 'male' },
|
||||
{ label: 'Voice Actor 2', value: 'voiceActor2', gender: 'female' },
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
'use client';
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
'use client';
|
||||
interface IconFontProps {
|
||||
/** 图标名称,对应 iconfont 中的图标 ID */
|
||||
type: string;
|
||||
|
|
|
|||
|
|
@ -26,3 +26,9 @@
|
|||
);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
/* 移除关闭按钮的焦点轮廓 */
|
||||
.dialog-close-btn:focus,
|
||||
.dialog-close-btn:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ type ModalProps = {
|
|||
trigger?: React.ReactNode;
|
||||
title?: string;
|
||||
classNames?: Partial<Record<'content' | 'overlay', string>>;
|
||||
destroyOnClose?: boolean;
|
||||
};
|
||||
|
||||
export default function Modal(props: ModalProps) {
|
||||
const { children, trigger, title, classNames } = props;
|
||||
const { children, trigger, title, classNames, destroyOnClose = true } = props;
|
||||
const [open, setOpen] = useControllableValue(props, {
|
||||
defaultValue: false,
|
||||
defaultValuePropName: 'defaultOpen',
|
||||
|
|
@ -24,6 +25,8 @@ export default function Modal(props: ModalProps) {
|
|||
trigger: 'onOpenChange',
|
||||
});
|
||||
|
||||
const hiden = destroyOnClose && !open;
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={setOpen} modal>
|
||||
{trigger && (
|
||||
|
|
@ -35,6 +38,7 @@ export default function Modal(props: ModalProps) {
|
|||
{trigger}
|
||||
</DialogPrimitive.Trigger>
|
||||
)}
|
||||
{!hiden && (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="dialog-overlay" />
|
||||
<DialogPrimitive.Content
|
||||
|
|
@ -46,7 +50,7 @@ export default function Modal(props: ModalProps) {
|
|||
</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Close
|
||||
onClick={() => setOpen(false)}
|
||||
className="translate-x-2.5 -translate-y-2.5 cursor-pointer hover:opacity-80"
|
||||
className="dialog-close-btn translate-x-2.5 -translate-y-2.5 cursor-pointer hover:opacity-80"
|
||||
>
|
||||
<IconFont type="icon-guanbi" size={30} />
|
||||
</DialogPrimitive.Close>
|
||||
|
|
@ -54,6 +58,7 @@ export default function Modal(props: ModalProps) {
|
|||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
)}
|
||||
</DialogPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,19 @@ export const useMsg = (prefix: string) => {
|
|||
[msg, prefix]
|
||||
);
|
||||
|
||||
const cmnMsg = useCallback(
|
||||
(
|
||||
key: string,
|
||||
values?: TranslationValues
|
||||
): ReturnType<TranslatorFunction> => {
|
||||
return msg(`common_${key}`, values);
|
||||
},
|
||||
[msg]
|
||||
);
|
||||
|
||||
return {
|
||||
pageMsg,
|
||||
cmnMsg,
|
||||
msg,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
const store = await cookies();
|
||||
const cookieLocale = store.get('locale')?.value || 'en';
|
||||
|
||||
return {
|
||||
locale: cookieLocale,
|
||||
messages: (await import(`@/locales/${cookieLocale}.ts`)).default,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import zhMessages from '@/locales/zh';
|
||||
import enMessages from '@/locales/en';
|
||||
|
||||
|
|
@ -31,13 +39,39 @@ interface IntlProviderProps {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
function setLocaleToCookie(locale: Locale) {
|
||||
if (typeof window === 'undefined') return;
|
||||
Cookies.set('locale', locale, { expires: 365, path: '/' });
|
||||
}
|
||||
|
||||
function getLocaleFromCookie(): Locale {
|
||||
if (typeof window === 'undefined') return 'en';
|
||||
const cookieLocale = Cookies.get('locale') as Locale | undefined;
|
||||
if (cookieLocale && (cookieLocale === 'zh' || cookieLocale === 'en')) {
|
||||
return cookieLocale;
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
export function IntlProvider({ children }: IntlProviderProps) {
|
||||
const [locale, setLocale] = useState<Locale>('zh');
|
||||
const [locale, setLocaleState] = useState<Locale>('en');
|
||||
|
||||
useEffect(() => {
|
||||
const cookieLocale = getLocaleFromCookie();
|
||||
if (cookieLocale) {
|
||||
setLocaleState(cookieLocale);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
setLocaleState(newLocale);
|
||||
setLocaleToCookie(newLocale);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={{ locale, setLocale }}>
|
||||
<NextIntlClientProvider
|
||||
locale={locale}
|
||||
locale={locale as any}
|
||||
messages={messages[locale]}
|
||||
timeZone="Asia/Shanghai"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
'use client';
|
||||
|
||||
import { useLocale } from '@/layouts/GlobalContainer/IntlProvider';
|
||||
import { Select } from 'radix-ui';
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
|
||||
const options = [
|
||||
{
|
||||
icon: '/locale/cn.svg',
|
||||
value: 'zh',
|
||||
},
|
||||
{
|
||||
icon: '/locale/en.svg',
|
||||
value: 'en',
|
||||
},
|
||||
];
|
||||
|
||||
const LocaleSelect = React.memo(() => {
|
||||
const { locale, setLocale } = useLocale();
|
||||
const currentOption = options.find((option) => option.value === locale)!;
|
||||
|
||||
return (
|
||||
<Select.Root value={locale} onValueChange={setLocale}>
|
||||
<Select.Trigger className="outline-none focus:outline-none">
|
||||
<div className="flex h-9 w-14 cursor-pointer items-center justify-between rounded-full bg-white/10 px-2.5">
|
||||
<Image
|
||||
src={currentOption?.icon}
|
||||
alt={currentOption?.value}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="text-white/80">
|
||||
<IconFont className="rotate-90" type="icon-youjiantou" size={10} />
|
||||
</span>
|
||||
</div>
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content
|
||||
position="popper"
|
||||
side="bottom"
|
||||
className="rounded-[10px] bg-neutral-900 p-1 outline-none"
|
||||
align="start"
|
||||
sideOffset={3}
|
||||
style={{
|
||||
width: 'var(--radix-select-trigger-width)',
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Select.Item
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className="outline-none focus:outline-none"
|
||||
>
|
||||
<div className="flex h-7 justify-center rounded-[10px] hover:bg-white/10">
|
||||
<Image
|
||||
src={option.icon}
|
||||
alt={option.value}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
);
|
||||
});
|
||||
|
||||
export default LocaleSelect;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useMsg } from '@/hooks';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
|
||||
const NavRoutes = React.memo(() => {
|
||||
const { pageMsg } = useMsg('menu');
|
||||
const pathname = usePathname();
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/novel',
|
||||
icon: 'icon-novel',
|
||||
label: pageMsg('novel'),
|
||||
},
|
||||
{
|
||||
path: '/video',
|
||||
icon: 'icon-video',
|
||||
label: pageMsg('video'),
|
||||
},
|
||||
{
|
||||
path: '/character',
|
||||
icon: 'icon-character',
|
||||
label: pageMsg('character'),
|
||||
},
|
||||
{
|
||||
path: '/record',
|
||||
icon: 'icon-record',
|
||||
label: pageMsg('record'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-7">
|
||||
{routes.map((route) => {
|
||||
const isActive = pathname.startsWith(route.path);
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
isActive
|
||||
? 'text-text-color'
|
||||
: 'text-[#2C223F] hover:text-[#4F3F6D]'
|
||||
)}
|
||||
key={route.path}
|
||||
href={route.path}
|
||||
>
|
||||
<IconFont size={34} type={route.icon} />
|
||||
{route.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
export default NavRoutes;
|
||||
|
|
@ -1,66 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useMsg } from '@/hooks';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
import { Select } from '@/components/ui/inputs';
|
||||
|
||||
const NavRoutes = React.memo(() => {
|
||||
const { pageMsg } = useMsg('menu');
|
||||
const pathname = usePathname();
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/novel',
|
||||
icon: 'icon-novel',
|
||||
label: pageMsg('novel'),
|
||||
},
|
||||
{
|
||||
path: '/video',
|
||||
icon: 'icon-video',
|
||||
label: pageMsg('video'),
|
||||
},
|
||||
{
|
||||
path: '/character',
|
||||
icon: 'icon-character',
|
||||
label: pageMsg('character'),
|
||||
},
|
||||
{
|
||||
path: '/record',
|
||||
icon: 'icon-record',
|
||||
label: pageMsg('record'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-7">
|
||||
{routes.map((route) => {
|
||||
const isActive = pathname.startsWith(route.path);
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
isActive
|
||||
? 'text-text-color'
|
||||
: 'text-[#2C223F] hover:text-[#4F3F6D]'
|
||||
)}
|
||||
key={route.path}
|
||||
href={route.path}
|
||||
>
|
||||
<IconFont size={34} type={route.icon} />
|
||||
{route.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
import NavRoutes from './components/NavRoutes';
|
||||
import LocaleSelect from './components/LocaleSelect';
|
||||
|
||||
const RightActions = () => {
|
||||
return <div>Avator</div>;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LocaleSelect />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Header() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
import { getToken, saveAuthInfo } from './auth';
|
||||
|
||||
const instance = axios.create({
|
||||
|
|
@ -20,6 +21,17 @@ instance.interceptors.request.use(
|
|||
if (token) {
|
||||
config.headers.setAuthorization(`Bearer ${token}`);
|
||||
}
|
||||
|
||||
// 从 cookie 中读取语言设置,并添加到请求头
|
||||
// 这样后端 API 可以从请求头中获取语言信息
|
||||
// 使用 X-Locale 避免与浏览器自动发送的 Accept-Language 冲突
|
||||
if (typeof window !== 'undefined') {
|
||||
const locale = Cookies.get('locale');
|
||||
if (locale) {
|
||||
config.headers.set('X-Locale', locale);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@ export default {
|
|||
menu_video: 'Video Comics',
|
||||
menu_character: 'Characters',
|
||||
menu_record: 'Record',
|
||||
|
||||
common_desc: 'Description',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export default {};
|
||||
|
|
@ -3,4 +3,6 @@ export default {
|
|||
menu_video: '视频',
|
||||
menu_character: '角色',
|
||||
menu_record: '记录',
|
||||
|
||||
common_desc: '描述',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export default {};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const localeCookie = request.cookies.get('locale');
|
||||
|
||||
if (localeCookie?.value) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
const response = NextResponse.next();
|
||||
response.cookies.set('locale', 'en', {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
||||
};
|
||||
Loading…
Reference in New Issue