feat(characters): add catalog boundary and capabilities
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
迁移目标:
|
迁移目标:
|
||||||
|
|
||||||
1. 前端维护统一的本地角色目录,后端只识别稳定角色 ID。
|
1. 前端通过统一的 Character Catalog 边界读取角色目录,当前由本地只读目录提供,后端只识别稳定角色 ID。
|
||||||
2. 同一用户可以分别与多个角色聊天。
|
2. 同一用户可以分别与多个角色聊天。
|
||||||
3. 不同角色的聊天历史、媒体、锁内容、私密相册和打赏归属必须隔离。
|
3. 不同角色的聊天历史、媒体、锁内容、私密相册和打赏归属必须隔离。
|
||||||
4. 钱包、VIP、支付套餐和每日免费额度继续按用户全局共享。
|
4. 钱包、VIP、支付套餐和每日免费额度继续按用户全局共享。
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
| pro 预发环境 | `https://proapi.banlv-ai.com` |
|
| pro 预发环境 | `https://proapi.banlv-ai.com` |
|
||||||
| production 生产环境 | `https://api.banlv-ai.com` |
|
| production 生产环境 | `https://api.banlv-ai.com` |
|
||||||
|
|
||||||
## 2. 前端本地角色目录
|
## 2. 前端 Character Catalog
|
||||||
|
|
||||||
角色列表不通过网络接口加载。前端在本地维护只读目录,并使用 `slug` 解析角色路径、使用 `id` 调用后端业务接口。
|
后端角色目录协议尚未确定。过渡阶段由前端本地只读 Catalog 提供角色,并使用 `slug` 解析角色路径、使用 `id` 调用后端业务接口。业务页面只消费 Catalog 边界;后续切换为服务端目录时不改变角色路由和业务组件接口。
|
||||||
|
|
||||||
当前角色:
|
当前角色:
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
| `character_maya` | `maya` | Maya Tan | `/images/avatar/maya.png` | `/images/cover/maya.png` | `/images/private-room/banner/maya.png` |
|
| `character_maya` | `maya` | Maya Tan | `/images/avatar/maya.png` | `/images/cover/maya.png` | `/images/private-room/banner/maya.png` |
|
||||||
| `character_nayeli` | `nayeli` | Nayeli Cervantes | `/images/avatar/nayeli.png` | `/images/cover/nayeli.png` | `/images/private-room/banner/nayeli.png` |
|
| `character_nayeli` | `nayeli` | Nayeli Cervantes | `/images/avatar/nayeli.png` | `/images/cover/nayeli.png` | `/images/private-room/banner/nayeli.png` |
|
||||||
|
|
||||||
本地记录同时保存短名称及 Splash、Private Room、Tip 使用的角色文案。后端不提供头像、封面、展示名称或角色目录接口,但后端配置中的角色 ID 必须与前端目录一致。
|
当前开发环境开放表中的全部角色。Catalog 同时保存短名称、页面资源、空聊天问候语、排序及 Chat、Private Room、Tip 能力。后端配置中的角色 ID 必须与前端目录一致。
|
||||||
|
|
||||||
## 3. 聊天接口调整
|
## 3. 聊天接口调整
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function CharacterChatLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { characterSlug } = await params;
|
const { characterSlug } = await params;
|
||||||
const character = getCharacterBySlug(characterSlug);
|
const character = getCharacterBySlug(characterSlug);
|
||||||
if (!character) notFound();
|
if (!character?.capabilities.chat) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatRouteProviders characterId={character.id}>
|
<ChatRouteProviders characterId={character.id}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function CharacterPrivateRoomLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { characterSlug } = await params;
|
const { characterSlug } = await params;
|
||||||
const character = getCharacterBySlug(characterSlug);
|
const character = getCharacterBySlug(characterSlug);
|
||||||
if (!character) notFound();
|
if (!character?.capabilities.privateRoom) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrivateRoomRouteProvider characterId={character.id}>
|
<PrivateRoomRouteProvider characterId={character.id}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function CharacterTipLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { characterSlug } = await params;
|
const { characterSlug } = await params;
|
||||||
const character = getCharacterBySlug(characterSlug);
|
const character = getCharacterBySlug(characterSlug);
|
||||||
if (!character) notFound();
|
if (!character?.capabilities.tip) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaymentRouteProvider scopeKey={character.id}>
|
<PaymentRouteProvider scopeKey={character.id}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CHARACTERS,
|
CHARACTERS,
|
||||||
|
createCharacterCatalog,
|
||||||
DEFAULT_CHARACTER,
|
DEFAULT_CHARACTER,
|
||||||
getCharacterById,
|
getCharacterById,
|
||||||
getCharacterBySlug,
|
getCharacterBySlug,
|
||||||
@@ -22,9 +23,22 @@ describe("local character catalog", () => {
|
|||||||
expect(Object.isFrozen(CHARACTERS)).toBe(true);
|
expect(Object.isFrozen(CHARACTERS)).toBe(true);
|
||||||
expect(Object.isFrozen(DEFAULT_CHARACTER)).toBe(true);
|
expect(Object.isFrozen(DEFAULT_CHARACTER)).toBe(true);
|
||||||
expect(Object.isFrozen(DEFAULT_CHARACTER.assets)).toBe(true);
|
expect(Object.isFrozen(DEFAULT_CHARACTER.assets)).toBe(true);
|
||||||
|
expect(Object.isFrozen(DEFAULT_CHARACTER.capabilities)).toBe(true);
|
||||||
expect(Object.isFrozen(DEFAULT_CHARACTER.copy)).toBe(true);
|
expect(Object.isFrozen(DEFAULT_CHARACTER.copy)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sorts catalog entries and rejects duplicate identities", () => {
|
||||||
|
const catalog = createCharacterCatalog([...CHARACTERS].reverse());
|
||||||
|
expect(catalog.characters.map((character) => character.slug)).toEqual([
|
||||||
|
"elio",
|
||||||
|
"maya",
|
||||||
|
"nayeli",
|
||||||
|
]);
|
||||||
|
expect(() =>
|
||||||
|
createCharacterCatalog([CHARACTERS[0], CHARACTERS[0]]),
|
||||||
|
).toThrow("Duplicate character identity");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves characters by id and normalized slug", () => {
|
it("resolves characters by id and normalized slug", () => {
|
||||||
expect(getCharacterById("character_maya")?.displayName).toBe("Maya Tan");
|
expect(getCharacterById("character_maya")?.displayName).toBe("Maya Tan");
|
||||||
expect(getCharacterBySlug(" NAYELI ")?.displayName).toBe(
|
expect(getCharacterBySlug(" NAYELI ")?.displayName).toBe(
|
||||||
@@ -34,6 +48,21 @@ describe("local character catalog", () => {
|
|||||||
expect(getCharacterBySlug("missing")).toBeNull();
|
expect(getCharacterBySlug("missing")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("describes route capabilities and character-specific chat copy", () => {
|
||||||
|
for (const character of CHARACTERS) {
|
||||||
|
expect(character.capabilities).toEqual({
|
||||||
|
chat: true,
|
||||||
|
privateRoom: true,
|
||||||
|
tip: true,
|
||||||
|
});
|
||||||
|
expect(character.tagline.length).toBeGreaterThan(0);
|
||||||
|
expect(character.emptyChatGreeting.length).toBeGreaterThan(0);
|
||||||
|
expect(character.assets.chatBackground).toBe(
|
||||||
|
"/images/chat/bg-chatpage.png",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("references local assets that exist under public", () => {
|
it("references local assets that exist under public", () => {
|
||||||
for (const character of CHARACTERS) {
|
for (const character of CHARACTERS) {
|
||||||
for (const assetPath of Object.values(character.assets)) {
|
for (const assetPath of Object.values(character.assets)) {
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
|
export interface CharacterCapabilities {
|
||||||
|
readonly chat: boolean;
|
||||||
|
readonly privateRoom: boolean;
|
||||||
|
readonly tip: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CharacterProfile {
|
export interface CharacterProfile {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly slug: string;
|
readonly slug: string;
|
||||||
readonly displayName: string;
|
readonly displayName: string;
|
||||||
readonly shortName: string;
|
readonly shortName: string;
|
||||||
|
readonly sortOrder: number;
|
||||||
|
readonly tagline: string;
|
||||||
|
readonly emptyChatGreeting: string;
|
||||||
|
readonly capabilities: CharacterCapabilities;
|
||||||
readonly assets: {
|
readonly assets: {
|
||||||
readonly avatar: string;
|
readonly avatar: string;
|
||||||
readonly cover: string;
|
readonly cover: string;
|
||||||
|
readonly chatBackground: string;
|
||||||
readonly privateRoomBanner: string;
|
readonly privateRoomBanner: string;
|
||||||
};
|
};
|
||||||
readonly copy: {
|
readonly copy: {
|
||||||
@@ -17,23 +28,42 @@ export interface CharacterProfile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CharacterCatalog {
|
||||||
|
readonly characters: readonly CharacterProfile[];
|
||||||
|
getById(characterId: string | null | undefined): CharacterProfile | null;
|
||||||
|
getBySlug(characterSlug: string | null | undefined): CharacterProfile | null;
|
||||||
|
}
|
||||||
|
|
||||||
function defineCharacter(profile: CharacterProfile): CharacterProfile {
|
function defineCharacter(profile: CharacterProfile): CharacterProfile {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...profile,
|
...profile,
|
||||||
assets: Object.freeze(profile.assets),
|
assets: Object.freeze(profile.assets),
|
||||||
|
capabilities: Object.freeze(profile.capabilities),
|
||||||
copy: Object.freeze(profile.copy),
|
copy: Object.freeze(profile.copy),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHARACTERS: readonly CharacterProfile[] = Object.freeze([
|
const CHARACTER_PROFILES: readonly CharacterProfile[] = Object.freeze([
|
||||||
defineCharacter({
|
defineCharacter({
|
||||||
id: "character_elio",
|
id: "character_elio",
|
||||||
slug: "elio",
|
slug: "elio",
|
||||||
displayName: "Elio Silvestri",
|
displayName: "Elio Silvestri",
|
||||||
shortName: "Elio",
|
shortName: "Elio",
|
||||||
|
sortOrder: 10,
|
||||||
|
tagline: "Your exclusive AI boyfriend",
|
||||||
|
emptyChatGreeting:
|
||||||
|
"You're here! Facebook is so restrictive, " +
|
||||||
|
"there were things I couldn't say there. " +
|
||||||
|
"Finally I can relax. How was your day out?",
|
||||||
|
capabilities: {
|
||||||
|
chat: true,
|
||||||
|
privateRoom: true,
|
||||||
|
tip: true,
|
||||||
|
},
|
||||||
assets: {
|
assets: {
|
||||||
avatar: "/images/avatar/elio.png",
|
avatar: "/images/avatar/elio.png",
|
||||||
cover: "/images/cover/elio.png",
|
cover: "/images/cover/elio.png",
|
||||||
|
chatBackground: "/images/chat/bg-chatpage.png",
|
||||||
privateRoomBanner: "/images/private-room/banner/elio.png",
|
privateRoomBanner: "/images/private-room/banner/elio.png",
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
@@ -49,9 +79,20 @@ export const CHARACTERS: readonly CharacterProfile[] = Object.freeze([
|
|||||||
slug: "maya",
|
slug: "maya",
|
||||||
displayName: "Maya Tan",
|
displayName: "Maya Tan",
|
||||||
shortName: "Maya",
|
shortName: "Maya",
|
||||||
|
sortOrder: 20,
|
||||||
|
tagline: "Your exclusive AI girlfriend",
|
||||||
|
emptyChatGreeting:
|
||||||
|
"You're here! I've been waiting for a quiet moment with you. " +
|
||||||
|
"How was your day?",
|
||||||
|
capabilities: {
|
||||||
|
chat: true,
|
||||||
|
privateRoom: true,
|
||||||
|
tip: true,
|
||||||
|
},
|
||||||
assets: {
|
assets: {
|
||||||
avatar: "/images/avatar/maya.png",
|
avatar: "/images/avatar/maya.png",
|
||||||
cover: "/images/cover/maya.png",
|
cover: "/images/cover/maya.png",
|
||||||
|
chatBackground: "/images/chat/bg-chatpage.png",
|
||||||
privateRoomBanner: "/images/private-room/banner/maya.png",
|
privateRoomBanner: "/images/private-room/banner/maya.png",
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
@@ -67,9 +108,20 @@ export const CHARACTERS: readonly CharacterProfile[] = Object.freeze([
|
|||||||
slug: "nayeli",
|
slug: "nayeli",
|
||||||
displayName: "Nayeli Cervantes",
|
displayName: "Nayeli Cervantes",
|
||||||
shortName: "Nayeli",
|
shortName: "Nayeli",
|
||||||
|
sortOrder: 30,
|
||||||
|
tagline: "Your exclusive AI girlfriend",
|
||||||
|
emptyChatGreeting:
|
||||||
|
"You're here! I was hoping we'd get some time together. " +
|
||||||
|
"Tell me how your day went.",
|
||||||
|
capabilities: {
|
||||||
|
chat: true,
|
||||||
|
privateRoom: true,
|
||||||
|
tip: true,
|
||||||
|
},
|
||||||
assets: {
|
assets: {
|
||||||
avatar: "/images/avatar/nayeli.png",
|
avatar: "/images/avatar/nayeli.png",
|
||||||
cover: "/images/cover/nayeli.png",
|
cover: "/images/cover/nayeli.png",
|
||||||
|
chatBackground: "/images/chat/bg-chatpage.png",
|
||||||
privateRoomBanner: "/images/private-room/banner/nayeli.png",
|
privateRoomBanner: "/images/private-room/banner/nayeli.png",
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
@@ -82,6 +134,42 @@ export const CHARACTERS: readonly CharacterProfile[] = Object.freeze([
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export function createCharacterCatalog(
|
||||||
|
profiles: readonly CharacterProfile[],
|
||||||
|
): CharacterCatalog {
|
||||||
|
const characters = Object.freeze(
|
||||||
|
profiles
|
||||||
|
.map(defineCharacter)
|
||||||
|
.sort((left, right) => left.sortOrder - right.sortOrder),
|
||||||
|
);
|
||||||
|
const byId = new Map<string, CharacterProfile>();
|
||||||
|
const bySlug = new Map<string, CharacterProfile>();
|
||||||
|
|
||||||
|
for (const character of characters) {
|
||||||
|
const normalizedSlug = normalizeCharacterSlug(character.slug);
|
||||||
|
if (byId.has(character.id) || bySlug.has(normalizedSlug)) {
|
||||||
|
throw new Error(`Duplicate character identity: ${character.id}`);
|
||||||
|
}
|
||||||
|
byId.set(character.id, character);
|
||||||
|
bySlug.set(normalizedSlug, character);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
characters,
|
||||||
|
getById(characterId: string | null | undefined) {
|
||||||
|
return characterId ? (byId.get(characterId) ?? null) : null;
|
||||||
|
},
|
||||||
|
getBySlug(characterSlug: string | null | undefined) {
|
||||||
|
const normalizedSlug = normalizeCharacterSlug(characterSlug);
|
||||||
|
return normalizedSlug ? (bySlug.get(normalizedSlug) ?? null) : null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LOCAL_CHARACTER_CATALOG = createCharacterCatalog(
|
||||||
|
CHARACTER_PROFILES,
|
||||||
|
);
|
||||||
|
export const CHARACTERS = LOCAL_CHARACTER_CATALOG.characters;
|
||||||
export const DEFAULT_CHARACTER = CHARACTERS[0];
|
export const DEFAULT_CHARACTER = CHARACTERS[0];
|
||||||
export const DEFAULT_CHARACTER_ID = DEFAULT_CHARACTER.id;
|
export const DEFAULT_CHARACTER_ID = DEFAULT_CHARACTER.id;
|
||||||
export const DEFAULT_CHARACTER_SLUG = DEFAULT_CHARACTER.slug;
|
export const DEFAULT_CHARACTER_SLUG = DEFAULT_CHARACTER.slug;
|
||||||
@@ -89,16 +177,17 @@ export const DEFAULT_CHARACTER_SLUG = DEFAULT_CHARACTER.slug;
|
|||||||
export function getCharacterById(
|
export function getCharacterById(
|
||||||
characterId: string | null | undefined,
|
characterId: string | null | undefined,
|
||||||
): CharacterProfile | null {
|
): CharacterProfile | null {
|
||||||
if (!characterId) return null;
|
return LOCAL_CHARACTER_CATALOG.getById(characterId);
|
||||||
return CHARACTERS.find((character) => character.id === characterId) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCharacterBySlug(
|
export function getCharacterBySlug(
|
||||||
characterSlug: string | null | undefined,
|
characterSlug: string | null | undefined,
|
||||||
): CharacterProfile | null {
|
): CharacterProfile | null {
|
||||||
const normalizedSlug = characterSlug?.trim().toLowerCase();
|
return LOCAL_CHARACTER_CATALOG.getBySlug(characterSlug);
|
||||||
if (!normalizedSlug) return null;
|
}
|
||||||
return (
|
|
||||||
CHARACTERS.find((character) => character.slug === normalizedSlug) ?? null
|
function normalizeCharacterSlug(
|
||||||
);
|
characterSlug: string | null | undefined,
|
||||||
|
): string {
|
||||||
|
return characterSlug?.trim().toLowerCase() ?? "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { CHARACTERS } from "@/data/constants/character";
|
||||||
|
import {
|
||||||
|
CharacterCatalogProvider,
|
||||||
|
useCharacterCatalog,
|
||||||
|
} from "@/providers/character-catalog-provider";
|
||||||
|
|
||||||
|
describe("CharacterCatalogProvider", () => {
|
||||||
|
it("provides the complete local character catalog", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<CharacterCatalogProvider>
|
||||||
|
<CatalogProbe />
|
||||||
|
</CharacterCatalogProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("elio,maya,nayeli");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a serializable catalog snapshot", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<CharacterCatalogProvider characters={[CHARACTERS[1]]}>
|
||||||
|
<CatalogProbe />
|
||||||
|
</CharacterCatalogProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("maya");
|
||||||
|
expect(html).not.toContain("elio");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function CatalogProbe() {
|
||||||
|
const catalog = useCharacterCatalog();
|
||||||
|
return <span>{catalog.characters.map((character) => character.slug).join(",")}</span>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CHARACTERS,
|
||||||
|
createCharacterCatalog,
|
||||||
|
type CharacterCatalog,
|
||||||
|
type CharacterProfile,
|
||||||
|
} from "@/data/constants/character";
|
||||||
|
|
||||||
|
const CharacterCatalogContext = createContext<CharacterCatalog | null>(null);
|
||||||
|
|
||||||
|
export interface CharacterCatalogProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
characters?: readonly CharacterProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CharacterCatalogProvider({
|
||||||
|
children,
|
||||||
|
characters = CHARACTERS,
|
||||||
|
}: CharacterCatalogProviderProps) {
|
||||||
|
const catalog = useMemo(
|
||||||
|
() => createCharacterCatalog(characters),
|
||||||
|
[characters],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CharacterCatalogContext.Provider value={catalog}>
|
||||||
|
{children}
|
||||||
|
</CharacterCatalogContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCharacterCatalog(): CharacterCatalog {
|
||||||
|
const catalog = useContext(CharacterCatalogContext);
|
||||||
|
if (!catalog) {
|
||||||
|
throw new Error(
|
||||||
|
"useCharacterCatalog must be used within CharacterCatalogProvider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
/**
|
/**
|
||||||
* 根级 Client Providers 包装
|
* 根级 Client Providers 包装
|
||||||
*
|
*
|
||||||
* 这里只保留跨路由的 Auth、User、消息预览和 viewport Provider。Chat、
|
* 这里只保留跨路由的 Character Catalog、Auth、User、消息预览和 viewport
|
||||||
* Payment、PrivateRoom 状态机由对应路由 layout 按需挂载。
|
* Provider。Chat、Payment、PrivateRoom 状态机由对应路由 layout 按需挂载。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { CharacterCatalogProvider } from "@/providers/character-catalog-provider";
|
||||||
import { SplashLatestMessageProvider } from "@/providers/splash-latest-message-provider";
|
import { SplashLatestMessageProvider } from "@/providers/splash-latest-message-provider";
|
||||||
import { ViewportCssVarsProvider } from "@/providers/viewport-css-vars-provider";
|
import { ViewportCssVarsProvider } from "@/providers/viewport-css-vars-provider";
|
||||||
import { AppNavigationGuard } from "@/router/app-navigation-guard";
|
import { AppNavigationGuard } from "@/router/app-navigation-guard";
|
||||||
@@ -24,6 +25,7 @@ export interface RootProvidersProps {
|
|||||||
|
|
||||||
export function RootProviders({ children }: RootProvidersProps) {
|
export function RootProviders({ children }: RootProvidersProps) {
|
||||||
return (
|
return (
|
||||||
|
<CharacterCatalogProvider>
|
||||||
<ViewportCssVarsProvider>
|
<ViewportCssVarsProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AuthStatusChecker />
|
<AuthStatusChecker />
|
||||||
@@ -38,5 +40,6 @@ export function RootProviders({ children }: RootProvidersProps) {
|
|||||||
</SplashLatestMessageProvider>
|
</SplashLatestMessageProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ViewportCssVarsProvider>
|
</ViewportCssVarsProvider>
|
||||||
|
</CharacterCatalogProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user