Files
cozsweet-frontend-nextjs/src/data/constants/character.ts
T

230 lines
7.0 KiB
TypeScript

export interface CharacterCapabilities {
readonly chat: boolean;
readonly privateRoom: boolean;
readonly tip: boolean;
}
export interface CharacterProfile {
readonly id: string;
readonly slug: string;
readonly displayName: string;
readonly shortName: string;
readonly sortOrder: number;
readonly tagline: string;
readonly emptyChatGreeting: string;
readonly capabilities: CharacterCapabilities;
readonly assets: {
readonly avatar: string;
readonly cover: string;
readonly chatBackground: string;
readonly privateRoomBanner: string;
};
readonly copy: {
readonly splashRelationship: string;
readonly privateRoomTitle: string;
readonly privateRoomSubtitle: string;
readonly tipHeader: string;
readonly tipTitle: string;
};
}
export interface CharacterCatalog {
readonly characters: readonly CharacterProfile[];
getById(characterId: string | null | undefined): CharacterProfile | null;
getBySlug(characterSlug: string | null | undefined): CharacterProfile | null;
}
export interface CharacterCatalogSnapshot {
readonly catalog: CharacterCatalog;
readonly defaultCharacter: CharacterProfile;
}
function defineCharacter(profile: CharacterProfile): CharacterProfile {
return Object.freeze({
...profile,
assets: Object.freeze(profile.assets),
capabilities: Object.freeze(profile.capabilities),
copy: Object.freeze(profile.copy),
});
}
const CHARACTER_PROFILES: readonly CharacterProfile[] = Object.freeze([
defineCharacter({
id: "elio",
slug: "elio",
displayName: "Elio Silvestri",
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: {
avatar: "/images/avatar/elio.png",
cover: "/images/cover/elio.png",
chatBackground: "/images/chat/bg-chatpage.png",
privateRoomBanner: "/images/private-room/banner/elio.png",
},
copy: {
splashRelationship: "Your exclusive AI boyfriend",
privateRoomTitle: "Elio Private room",
privateRoomSubtitle: "Join me, unlock my private room",
tipHeader: "Tip Elio",
tipTitle: "Buy Elio a coffee",
},
}),
defineCharacter({
id: "maya-tan",
slug: "maya",
displayName: "Maya Tan",
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: {
avatar: "/images/avatar/maya.png",
cover: "/images/cover/maya.png",
chatBackground: "/images/chat/bg-chatpage.png",
privateRoomBanner: "/images/private-room/banner/maya.png",
},
copy: {
splashRelationship: "Your exclusive AI girlfriend",
privateRoomTitle: "Maya Private room",
privateRoomSubtitle: "Join me, unlock my private room",
tipHeader: "Tip Maya",
tipTitle: "Buy Maya a coffee",
},
}),
defineCharacter({
id: "nayeli-cervantes",
slug: "nayeli",
displayName: "Nayeli Cervantes",
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: {
avatar: "/images/avatar/nayeli.png",
cover: "/images/cover/nayeli.png",
chatBackground: "/images/chat/bg-chatpage.png",
privateRoomBanner: "/images/private-room/banner/nayeli.png",
},
copy: {
splashRelationship: "Your exclusive AI girlfriend",
privateRoomTitle: "Nayeli Private room",
privateRoomSubtitle: "Join me, unlock my private room",
tipHeader: "Tip Nayeli",
tipTitle: "Buy Nayeli a coffee",
},
}),
]);
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 function mergeRemoteCharacterCatalog(
remote: import("@/data/schemas/character").CharacterListResponse,
localProfiles: readonly CharacterProfile[] = CHARACTERS,
): CharacterCatalogSnapshot {
const localById = new Map(localProfiles.map((profile) => [profile.id, profile]));
const profiles = remote.items.flatMap((item) => {
const local = localById.get(item.id);
if (!local || !item.isActive || !item.capabilities.chat) return [];
return [
defineCharacter({
...local,
displayName: item.displayName,
sortOrder: item.sortOrder,
capabilities: {
chat: item.capabilities.chat,
privateRoom:
local.capabilities.privateRoom && item.capabilities.privateContent,
tip: local.capabilities.tip,
},
}),
];
});
const catalog = createCharacterCatalog(profiles);
const defaultCharacter =
catalog.getById(remote.defaultCharacterId) ??
catalog.getById(DEFAULT_CHARACTER_ID) ??
catalog.characters[0] ??
DEFAULT_CHARACTER;
return Object.freeze({ catalog, defaultCharacter });
}
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_ID = DEFAULT_CHARACTER.id;
export const DEFAULT_CHARACTER_SLUG = DEFAULT_CHARACTER.slug;
export function getCharacterById(
characterId: string | null | undefined,
): CharacterProfile | null {
return LOCAL_CHARACTER_CATALOG.getById(characterId);
}
export function getCharacterBySlug(
characterSlug: string | null | undefined,
): CharacterProfile | null {
return LOCAL_CHARACTER_CATALOG.getBySlug(characterSlug);
}
function normalizeCharacterSlug(
characterSlug: string | null | undefined,
): string {
return characterSlug?.trim().toLowerCase() ?? "";
}