diff --git a/src/components/character/CharacterDetailsPage.tsx b/src/components/character/CharacterDetailsPage.tsx
new file mode 100644
index 00000000..fd153ff2
--- /dev/null
+++ b/src/components/character/CharacterDetailsPage.tsx
@@ -0,0 +1,45 @@
+import { useTranslation } from "react-i18next";
+import { BasicPage, Link } from "../settings/common";
+import { TextButton } from "../textButton";
+import { useCharacterStoreContext } from "@/features/characters/characterStoreContext";
+import { NameComponent } from "./NameComponent";
+import { TagComponent } from "./TagComponent";
+import { SystemPromptComponent } from "./SystemPromptComponent";
+
+export function CharacterDetailsPage({
+ setSettingsUpdated
+ }: {
+ setSettingsUpdated: (updated: boolean) => void;
+ }) {
+ const { t } = useTranslation();
+ const characterContext = useCharacterStoreContext();
+
+ return (
+
+ {/* */}
+ { characterContext.saveCharacter(); }}
+ >
+ {t("Save Character")}
+
+
+
+ );
+}
+
diff --git a/src/components/character/CharacterListPage.tsx b/src/components/character/CharacterListPage.tsx
new file mode 100644
index 00000000..82eced51
--- /dev/null
+++ b/src/components/character/CharacterListPage.tsx
@@ -0,0 +1,25 @@
+import { useCharacterStoreContext } from "@/features/characters/characterStoreContext";
+import { VrmListPage } from "../settings/VrmListPage";
+import Character from "@/features/characters/character";
+
+export const CharacterListPage = ({
+ viewer,
+ setSettingsUpdated,
+ handleClickOpenVrmFile,
+ }: {
+ viewer: any; // TODO
+ setSettingsUpdated: (updated: boolean) => void;
+ handleClickOpenVrmFile: () => void;
+ }) => {
+ const characterStoreContext = useCharacterStoreContext();
+
+ return
+ {characterStoreContext.characterList.map((character: Character) =>
+ -
+
+ {character.tag}
+
+
+ )}
+
+}
\ No newline at end of file
diff --git a/src/components/character/CharacterListProvider.ts b/src/components/character/CharacterListProvider.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/character/CharactersPage.tsx b/src/components/character/CharactersPage.tsx
new file mode 100644
index 00000000..3dcb482b
--- /dev/null
+++ b/src/components/character/CharactersPage.tsx
@@ -0,0 +1,114 @@
+import { useState } from "react";
+import { Link } from "../settings/common";
+import { ArrowUturnLeftIcon, ChevronRightIcon, HomeIcon } from "@heroicons/react/20/solid";
+import { TextButton } from "../textButton";
+import { CharacterDetailsPage } from "./CharacterDetailsPage";
+
+export const CharactersPage = ({
+ setSettingsUpdated,
+ handleClickOpenVrmFile,
+ onClickClose
+}: {
+ setSettingsUpdated: (updated: boolean) => void;
+ handleClickOpenVrmFile: () => void;
+ onClickClose: () => void;
+}) => {
+ const [breadcrumbs, setBreadcrumbs] = useState([]);
+ const [page, setPage] = useState('current_character');
+
+ function renderPage() {
+ switch(page) {
+ case 'current_character':
+ return ;
+
+ default:
+ throw new Error('page not found');
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
{
+ if (breadcrumbs.length === 0) {
+ onClickClose();
+ return;
+ }
+ if (breadcrumbs.length === 1) {
+ setPage('main_menu');
+ setBreadcrumbs([]);
+ return;
+ }
+
+ const prevPage = breadcrumbs[breadcrumbs.length - 2];
+ setPage(prevPage.key);
+ setBreadcrumbs(breadcrumbs.slice(0, -1));
+ }}
+ >
+
+
+
+ { renderPage() }
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/character/NameComponent.tsx b/src/components/character/NameComponent.tsx
new file mode 100644
index 00000000..71d9055f
--- /dev/null
+++ b/src/components/character/NameComponent.tsx
@@ -0,0 +1,36 @@
+import { t } from "@/i18n";
+import { FormRow, ResetToDefaultButton } from "../settings/common";
+import { TextInput } from "../textInput";
+import { defaultConfig, updateConfig } from "@/utils/config";
+
+interface NameComponentProps {
+ name: string
+ setName: (name: string) => void
+ setSettingsUpdated: (updated: boolean) => void
+};
+
+export const NameComponent: React.FC = ({name, setName, setSettingsUpdated}) => {
+ return <>
+
+ ) => {
+ setName(event.target.value);
+ updateConfig("name", event.target.value);
+ setSettingsUpdated(true);
+ }}
+ />
+
+ { name !== defaultConfig("name") && (
+
+ {
+ setName(defaultConfig("name"));
+ updateConfig("name", defaultConfig("name"));
+ setSettingsUpdated(true);
+ }}
+ />
+
+ )}
+
+ >;
+}
\ No newline at end of file
diff --git a/src/components/character/SystemPromptComponent.tsx b/src/components/character/SystemPromptComponent.tsx
new file mode 100644
index 00000000..19c8ff05
--- /dev/null
+++ b/src/components/character/SystemPromptComponent.tsx
@@ -0,0 +1,41 @@
+import { useTranslation } from 'react-i18next';
+
+import { updateConfig, defaultConfig } from "@/utils/config";
+import { FormRow, ResetToDefaultButton } from '../settings/common';
+
+export function SystemPromptComponent({
+ systemPrompt,
+ setSystemPrompt,
+ setSettingsUpdated,
+}: {
+ systemPrompt: string;
+ setSystemPrompt: (prompt: string) => void;
+ setSettingsUpdated: (updated: boolean) => void;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ );
+}
diff --git a/src/components/character/TagComponent.tsx b/src/components/character/TagComponent.tsx
new file mode 100644
index 00000000..70d3dc7c
--- /dev/null
+++ b/src/components/character/TagComponent.tsx
@@ -0,0 +1,36 @@
+import { t } from "@/i18n";
+import { FormRow, ResetToDefaultButton } from "../settings/common";
+import { TextInput } from "../textInput";
+import { defaultConfig, updateConfig } from "@/utils/config";
+
+interface TagComponentProps {
+ tag: string
+ setTag: (tag: string) => void
+ setSettingsUpdated: (updated: boolean) => void
+};
+
+export const TagComponent: React.FC = ({tag, setTag, setSettingsUpdated}) => {
+ return <>
+
+ ) => {
+ setTag(event.target.value);
+ updateConfig("character_tag", event.target.value);
+ setSettingsUpdated(true);
+ }}
+ />
+
+ { tag !== defaultConfig("character_tag") && (
+
+ {
+ setTag(defaultConfig("character_tag"));
+ updateConfig("character_tag", defaultConfig("character_tag"));
+ setSettingsUpdated(true);
+ }}
+ />
+
+ )}
+
+ >;
+}
\ No newline at end of file
diff --git a/src/components/settings.tsx b/src/components/settings.tsx
index 08f42ed2..224d2768 100644
--- a/src/components/settings.tsx
+++ b/src/components/settings.tsx
@@ -28,12 +28,6 @@ import { MenuPage } from './settings/MenuPage';
import { ResetSettingsPage } from './settings/ResetSettingsPage';
import { CommunityPage } from './settings/CommunityPage';
-import { BackgroundImgPage } from './settings/BackgroundImgPage';
-import { BackgroundColorPage } from './settings/BackgroundColorPage';
-import { BackgroundVideoPage } from './settings/BackgroundVideoPage';
-import { CharacterModelPage } from './settings/CharacterModelPage';
-import { CharacterAnimationPage } from './settings/CharacterAnimationPage';
-
import { ChatbotBackendPage } from './settings/ChatbotBackendPage';
import { ChatGPTSettingsPage } from './settings/ChatGPTSettingsPage';
import { LlamaCppSettingsPage } from './settings/LlamaCppSettingsPage';
@@ -59,13 +53,21 @@ import { VisionLlamaCppSettingsPage } from './settings/VisionLlamaCppSettingsPag
import { VisionOllamaSettingsPage } from './settings/VisionOllamaSettingsPage';
import { VisionSystemPromptPage } from './settings/VisionSystemPromptPage';
-import { NamePage } from './settings/NamePage';
-import { SystemPromptPage } from './settings/SystemPromptPage';
import { useVrmStoreContext } from "@/features/vrmStore/vrmStoreContext";
export const Settings = ({
+ showNotification,
+ setShowNotification,
+ setSettingsUpdated,
+ showSettingsUpdatedNotification,
+ handleClickOpenVrmFile,
onClickClose,
}: {
+ showNotification: boolean;
+ setShowNotification: (updated: boolean) => void;
+ setSettingsUpdated: (updated: boolean) => void;
+ showSettingsUpdatedNotification: () => () => void;
+ handleClickOpenVrmFile: () => void;
onClickClose: () => void;
}) => {
const { viewer } = useContext(ViewerContext);
@@ -74,8 +76,6 @@ export const Settings = ({
const [page, setPage] = useState('main_menu');
const [breadcrumbs, setBreadcrumbs] = useState([]);
- const [showNotification, setShowNotification] = useState(false);
- const [settingsUpdated, setSettingsUpdated] = useState(false);
const [chatbotBackend, setChatbotBackend] = useState(config("chatbot_backend"));
const [openAIApiKey, setOpenAIApiKey] = useState(config("openai_apikey"));
@@ -109,14 +109,6 @@ export const Settings = ({
const [visionOllamaModel, setVisionOllamaModel] = useState(config("vision_ollama_model"));
const [visionSystemPrompt, setVisionSystemPrompt] = useState(config("vision_system_prompt"));
- const [bgUrl, setBgUrl] = useState(config("bg_url"));
- const [bgColor, setBgColor] = useState(config("bg_color"));
- const [vrmUrl, setVrmUrl] = useState(config("vrm_url"));
- const [vrmHash, setVrmHash] = useState(config("vrm_hash"));
- const [vrmSaveType, setVrmSaveType] = useState(config('vrm_save_type'));
- const [youtubeVideoID, setYoutubeVideoID] = useState(config("youtube_videoid"));
- const [animationUrl, setAnimationUrl] = useState(config("animation_url"));
-
const [sttBackend, setSTTBackend] = useState(config("stt_backend"));
const [sttWakeWordEnabled, setSTTWakeWordEnabled] = useState(config("wake_word_enabled") === 'true' ? true : false);
const [sttWakeWord, setSTTWakeWord] = useState(config("wake_word"));
@@ -127,15 +119,8 @@ export const Settings = ({
const [whisperOpenAIModel, setWhisperOpenAIModel] = useState(config("openai_whisper_model"));
const [whisperCppUrl, setWhisperCppUrl] = useState(config("whispercpp_url"));
- const [name, setName] = useState(config("name"));
const [systemPrompt, setSystemPrompt] = useState(config("system_prompt"));
-
- const vrmFileInputRef = useRef(null);
- const handleClickOpenVrmFile = useCallback(() => {
- vrmFileInputRef.current?.click();
- }, []);
-
const bgImgFileInputRef = useRef(null);
const handleClickOpenBgImgFile = useCallback(() => {
bgImgFileInputRef.current?.click();
@@ -195,15 +180,7 @@ export const Settings = ({
}
useEffect(() => {
- const timeOutId = setTimeout(() => {
- if (settingsUpdated) {
- setShowNotification(true);
- setTimeout(() => {
- setShowNotification(false);
- }, 5000);
- }
- }, 1000);
- return () => clearTimeout(timeOutId);
+ showSettingsUpdatedNotification();
}, [
chatbotBackend,
openAIApiKey, openAIUrl, openAIModel,
@@ -220,8 +197,6 @@ export const Settings = ({
visionLlamaCppUrl,
visionOllamaUrl, visionOllamaModel,
visionSystemPrompt,
- bgColor,
- bgUrl, vrmHash, vrmUrl, youtubeVideoID, animationUrl,
sttBackend,
whisperOpenAIApiKey, whisperOpenAIModel, whisperOpenAIUrl,
whisperCppUrl,
@@ -240,17 +215,17 @@ export const Settings = ({
switch(page) {
case 'main_menu':
return ;
- case 'appearance':
- return ;
+ // case 'appearance':
+ // return ;
case 'chatbot':
return ;
case 'tts':
@@ -274,49 +249,28 @@ export const Settings = ({
case 'community':
return
- case 'background_img':
- return
-
- case 'background_color':
- return
-
- case 'background_video':
- return ;
-
- case 'character_model':
- return
-
- case 'character_animation':
- return
+ // case 'background_img':
+ // return
+
+ // case 'background_video':
+ // return ;
+
+ // case 'character_animation':
+ // return
case 'chatbot_backend':
return
-
- case 'system_prompt':
- return
-
- case 'name':
- return
-
+
default:
throw new Error('page not found');
}
@@ -639,13 +579,6 @@ export const Settings = ({
-
void;
- setSettingsUpdated: (updated: boolean) => void;
-}) {
- const { t } = useTranslation();
-
- return (
-
-
- -
-
- ) => {
- setName(event.target.value);
- updateConfig("name", event.target.value);
- setSettingsUpdated(true);
- }}
- />
-
- { name !== defaultConfig("name") && (
-
- {
- setName(defaultConfig("name"));
- updateConfig("name", defaultConfig("name"));
- setSettingsUpdated(true);
- }}
- />
-
- )}
-
-
-
-
- );
-}
diff --git a/src/components/settings/SystemPromptPage.tsx b/src/components/settings/SystemPromptPage.tsx
deleted file mode 100644
index b347842e..00000000
--- a/src/components/settings/SystemPromptPage.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useTranslation } from 'react-i18next';
-
-import { BasicPage, FormRow, ResetToDefaultButton } from './common';
-import { updateConfig, defaultConfig } from "@/utils/config";
-
-export function SystemPromptPage({
- systemPrompt,
- setSystemPrompt,
- setSettingsUpdated,
-}: {
- systemPrompt: string;
- setSystemPrompt: (prompt: string) => void;
- setSettingsUpdated: (updated: boolean) => void;
-}) {
- const { t } = useTranslation();
-
- return (
-
-
-
- );
-}
diff --git a/src/components/settings/CharacterModelPage.tsx b/src/components/settings/VrmListPage.tsx
similarity index 74%
rename from src/components/settings/CharacterModelPage.tsx
rename to src/components/settings/VrmListPage.tsx
index 3d48a19f..ea097c99 100644
--- a/src/components/settings/CharacterModelPage.tsx
+++ b/src/components/settings/VrmListPage.tsx
@@ -3,32 +3,22 @@ import { clsx } from "clsx";
import { BasicPage } from "./common";
import { updateConfig } from "@/utils/config";
import { TextButton } from "@/components/textButton";
-import { VrmData } from '@/features/vrmStore/vrmData';
+import { useVrmStoreContext } from '@/features/vrmStore/vrmStoreContext';
+import { useCharacterStoreContext } from '@/features/characters/characterStoreContext';
-export function CharacterModelPage({
+export function VrmListPage({
viewer,
- vrmHash,
- vrmUrl,
- vrmSaveType,
- vrmList,
- setVrmHash,
- setVrmUrl,
- setVrmSaveType,
setSettingsUpdated,
handleClickOpenVrmFile,
}: {
viewer: any; // TODO
- vrmHash: string;
- vrmUrl: string;
- vrmSaveType: string;
- vrmList: VrmData[],
- setVrmHash: (hash: string) => void;
- setVrmUrl: (url: string) => void;
- setVrmSaveType: (saveType: string) => void;
setSettingsUpdated: (updated: boolean) => void;
handleClickOpenVrmFile: () => void;
}) {
const { t } = useTranslation();
+ const vrmStoreContext = useVrmStoreContext();
+ const vrmList = vrmStoreContext.vrmList;
+ const characterContext = useCharacterStoreContext();
return (
{
viewer.loadVrm(vrm.url);
- setVrmSaveType(vrm.saveType);
- updateConfig('vrm_save_type', vrm.saveType);
+ const hash = vrm.getHash();
+ characterContext.setVrmSaveType(hash, vrm.saveType);
if (vrm.saveType == 'local') {
- updateConfig('vrm_hash', vrm.getHash());
+ updateConfig('vrm_hash', hash);
updateConfig('vrm_url', vrm.url);
- setVrmUrl(vrm.url);
- setVrmHash(vrm.getHash());
+ characterContext.setVrmUrl(hash, vrm.url);
+ characterContext.setVrmHash(hash);
} else {
- updateConfig('vrm_hash', '');
+ updateConfig('vrm_hash', vrm.url);
updateConfig('vrm_url', vrm.url);
- setVrmUrl(vrm.url);
+ characterContext.setVrmUrl(hash, vrm.url);
}
setSettingsUpdated(true);
}}
diff --git a/src/features/characters/character.ts b/src/features/characters/character.ts
new file mode 100644
index 00000000..349fa3ac
--- /dev/null
+++ b/src/features/characters/character.ts
@@ -0,0 +1,30 @@
+export default class Character {
+ constructor(
+ id: number = -1,
+ tag: string,
+ name: string = "",
+ vrmHash: string = "",
+ bgUrl: string = "",
+ bgColor: string = "",
+ youtubeVideoId: string = "",
+ animationUrl: string = "",
+ ) {
+ this.id = id;
+ this.tag = tag;
+ this.name = name;
+ this.vrmHash = vrmHash;
+ this.bgUrl = bgUrl;
+ this.bgColor = bgColor;
+ this.youtubeVideoId = youtubeVideoId;
+ this.animationUrl = animationUrl;
+ }
+
+ public id: number;
+ public tag: string;
+ public name: string;
+ public vrmHash: string;
+ public bgUrl: string;
+ public bgColor: string;
+ public youtubeVideoId: string;
+ public animationUrl: string;
+}
diff --git a/src/features/characters/characterStore.ts b/src/features/characters/characterStore.ts
new file mode 100644
index 00000000..ffa91efa
--- /dev/null
+++ b/src/features/characters/characterStore.ts
@@ -0,0 +1,125 @@
+import { config, updateConfig } from '@/utils/config';
+import Character from './character';
+import { useState } from 'react';
+
+interface CharacterStore {
+ characterId: number;
+ setCharacterId: (characterId: number) => void;
+ characterTag: string;
+ setCharacterTag: (characterTag: string) => void;
+ name: string;
+ setName: (name: string) => void;
+ systemPrompt: string;
+ setSystemPrompt: (name: string) => void;
+ vrmHash: string;
+ setVrmHash: (vrmHash: string) => void;
+ bgUrl: string;
+ setBgUrl: (bgUrl: string) => void;
+ bgColor: string;
+ setBgColor: (bgColor: string) => void;
+ youtubeVideoId: string;
+ setYoutubeVideoId: (youtubeVideoId: string) => void;
+ animationUrl: string;
+ setAnimationUrl: (animationUrl: string) => void;
+ characters: Character[];
+ setCharacterList: (characters: Character[]) => void;
+
+ initFromConfig: () => void;
+ getCharacter: () => Character;
+ hasUnsavedChanges: () => boolean;
+}
+
+export const GetCharacterStore = (): CharacterStore => {
+ const [characters, setCharacters] = useState(new Array());
+ const [characterId, setCharacterId] = useState(-1);
+ const [characterTag, setCharacterTag] = useState('');
+ const [name, setName] = useState('');
+ const [systemPrompt, setSystemPrompt] = useState('');
+ const [vrmHash, setVrmHash] = useState('');
+ const [bgUrl, setBgUrl] = useState('');
+ const [bgColor, setBgColor] = useState('');
+ const [youtubeVideoId, setYoutubeVideoId] = useState('');
+ const [animationUrl, setAnimationUrl] = useState('');
+
+ const initFromConfig = (): void => {
+ setCharacterId(parseInt(config('character_id')));
+ setCharacterTag(config('character_tag'));
+ setName(config('name'));
+ setSystemPrompt(config('system_prompt'));
+ setVrmHash(config('vrm_hash'));
+ setBgUrl(config('bg_url'));
+ setBgColor(config('bg_color'));
+ setYoutubeVideoId(config('youtube_videoid'));
+ setAnimationUrl(config('animation_url'));
+ }
+
+ return {
+ characterId: characterId,
+ setCharacterId: (characterId: number): void => {
+ updateConfig('character_id', characterId.toString());
+ },
+ characterTag: characterTag,
+ setCharacterTag: (tag: string): void => {
+ setCharacterTag(tag);
+ updateConfig('character_tag', tag);
+ },
+ name: name,
+ setName: (name: string): void => {
+ setName(name);
+ updateConfig('name', name);
+ },
+ systemPrompt: systemPrompt,
+ setSystemPrompt: (systemPrompt: string): void => {
+ setSystemPrompt(systemPrompt);
+ updateConfig('system_prompt', systemPrompt);
+ },
+ vrmHash: vrmHash,
+ setVrmHash: (vrmHash: string): void => {
+ setVrmHash(vrmHash);
+ updateConfig('vrm_hash', vrmHash);
+ },
+ bgUrl: bgUrl,
+ setBgUrl: (bgUrl: string): void => {
+ setBgUrl(bgUrl);
+ updateConfig('bg_url', bgUrl);
+ },
+ bgColor: bgColor,
+ setBgColor: (bgColor: string): void => {
+ setBgColor(bgColor);
+ updateConfig('bg_color', bgColor);
+ },
+ youtubeVideoId: youtubeVideoId,
+ setYoutubeVideoId: (youtubeVideoId: string): void => {
+ setYoutubeVideoId(youtubeVideoId);
+ updateConfig('youtube_videoid', youtubeVideoId);
+ },
+ animationUrl: animationUrl,
+ setAnimationUrl: (animationUrl: string): void => {
+ setAnimationUrl(animationUrl);
+ updateConfig('animation_url', animationUrl);
+ },
+ characters: characters,
+ setCharacterList: (data: Character[]) => {
+ setCharacters(data);
+ },
+
+ initFromConfig: initFromConfig,
+
+ getCharacter: (): Character => {
+ return new Character(characterId, characterTag, name, vrmHash, bgUrl, bgColor, youtubeVideoId, animationUrl);
+ },
+
+ hasUnsavedChanges: (): boolean => {
+ if (characterId < 0)
+ return true;
+ const charIndex = characters.findIndex((character: Character) => { return character.id == characterId; });
+ return characterTag !== characters[charIndex].tag ||
+ name !== characters[charIndex].name ||
+ vrmHash !== characters[charIndex].vrmHash ||
+ bgUrl !== characters[charIndex].bgUrl ||
+ bgColor !== characters[charIndex].bgColor ||
+ youtubeVideoId !== characters[charIndex].youtubeVideoId ||
+ animationUrl !== characters[charIndex].animationUrl;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/features/characters/characterStoreContext.tsx b/src/features/characters/characterStoreContext.tsx
new file mode 100644
index 00000000..6017df2c
--- /dev/null
+++ b/src/features/characters/characterStoreContext.tsx
@@ -0,0 +1,157 @@
+import { PropsWithChildren, createContext, useContext, useEffect, useReducer, useState } from "react";
+import { GetCharacterStore } from "./characterStore";
+import { CharacterStoreActionType, characterStoreReducer } from "./characterStoreReducer";
+import Character from "./character";
+
+interface CharacterStoreContextType {
+ saveCharacter: () => Promise;
+
+ characterId: number;
+ characterTag: string;
+ name: string;
+ systemPrompt: string;
+ vrmHash: string;
+ bgUrl: string;
+ bgColor: string;
+ youtubeVideoId: string;
+ animationUrl: string;
+
+ setCharacterTag: (characterTag: string) => void;
+ setName: (name: string) => void;
+ setSystemPrompt: (name: string) => void;
+ setVrmHash: (vrmHash: string) => void;
+ setBgUrl: (bgUrl: string) => void;
+ setBgColor: (bgColor: string) => void;
+ setYoutubeVideoId: (youtubeVideoId: string) => void;
+ setAnimationUrl: (animationUrl: string) => void;
+
+ characterList: Character[];
+ hasUnsavedChanges: () => boolean;
+
+ isLoadingCharactersList: boolean
+}
+
+const CharacterStoreContext = createContext({
+ saveCharacter: (): Promise => { return new Promise(() => {}) },
+
+ characterId: -1,
+ characterTag: '',
+ name: '',
+ systemPrompt: '',
+ vrmHash: '',
+ bgUrl: '',
+ bgColor: '',
+ youtubeVideoId: '',
+ animationUrl: '',
+
+ setCharacterTag: () => {},
+ setName: () => {},
+ setSystemPrompt: () => {},
+ setVrmHash: () => {},
+ setBgUrl: () => {},
+ setBgColor: () => {},
+ setYoutubeVideoId: () => {},
+ setAnimationUrl: () => {},
+
+ characterList: new Array(),
+ hasUnsavedChanges: (): boolean => { return false; },
+
+ isLoadingCharactersList: false
+});
+
+export const CharacterStoreContextProvider = ({ children }: PropsWithChildren<{}>): JSX.Element => {
+ const [loadedCharactersList, charactersListDispatch] = useReducer(characterStoreReducer, new Array());
+ const [isLoadingCharactersList, setIsLoadingCharactersList] = useState(true);
+
+ const updateCharacterListWithCharacter = (character: Character) => {
+ const characterList = new Array(...loadedCharactersList);
+ const index = characterList.findIndex((characterInList: Character) => { characterInList.id == character.id });
+ const isInList = index !== -1;
+ if (isInList) {
+ characterList[index] = character;
+ } else {
+ characterList.push(character);
+ }
+ charactersListDispatch({ type: CharacterStoreActionType.setState, characters: characterList });
+ };
+
+ const characterStore = GetCharacterStore();
+
+ const characterStoreContext: CharacterStoreContextType = {
+ characterId: characterStore.characterId,
+ characterTag: characterStore.characterTag,
+ name: characterStore.name,
+ systemPrompt: characterStore.systemPrompt,
+ vrmHash: characterStore.vrmHash,
+ bgUrl: characterStore.bgUrl,
+ bgColor: characterStore.bgColor,
+ youtubeVideoId: characterStore.youtubeVideoId,
+ animationUrl: characterStore.animationUrl,
+
+ setCharacterTag: characterStore.setCharacterTag,
+ setName: characterStore.setName,
+ setSystemPrompt: characterStore.setSystemPrompt,
+ setVrmHash: characterStore.setVrmHash,
+ setBgUrl: characterStore.setBgUrl,
+ setBgColor: characterStore.setBgColor,
+ setYoutubeVideoId: characterStore.setYoutubeVideoId,
+ setAnimationUrl: characterStore.setAnimationUrl,
+
+ saveCharacter: async (): Promise => {
+ return new Promise( (resolve, reject) => {
+ if (loadedCharactersList.findIndex((loaded: Character) => { loaded.id == characterStore.characterId }) > 0)
+ charactersListDispatch({ type: CharacterStoreActionType.update,
+ character: characterStore.getCharacter(),
+ callback: (character: Character | undefined) => {
+ if (character) {
+ updateCharacterListWithCharacter(character);
+ resolve(character);
+ } else {
+ reject("Update error.");
+ }
+ }
+ });
+ else
+ charactersListDispatch({ type: CharacterStoreActionType.create,
+ character: characterStore.getCharacter(),
+ callback: (character: Character) => { updateCharacterListWithCharacter(character); resolve(character); }
+ });
+ });
+ },
+
+ characterList: loadedCharactersList,
+ hasUnsavedChanges: characterStore.hasUnsavedChanges,
+
+ isLoadingCharactersList: isLoadingCharactersList
+ };
+
+ useEffect(() => {
+ console.log("Init characters store");
+ charactersListDispatch({
+ type: CharacterStoreActionType.loadCharacters,
+ callback: (data: Character[]) => { charactersListDispatch({ type: CharacterStoreActionType.setState, characters: data}) }
+ });
+ characterStore.initFromConfig();
+ }, []);
+
+ useEffect(() => {
+ characterStore.setCharacterList(loadedCharactersList);
+ setIsLoadingCharactersList(false);
+ }, [loadedCharactersList]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useCharacterStoreContext = () => {
+ const context = useContext(CharacterStoreContext);
+
+ if (!context) {
+ throw new Error("useCharacterContext must be used inside the CharacterProvider");
+ }
+
+ return context;
+};
\ No newline at end of file
diff --git a/src/features/characters/characterStoreReducer.ts b/src/features/characters/characterStoreReducer.ts
new file mode 100644
index 00000000..8442b3b7
--- /dev/null
+++ b/src/features/characters/characterStoreReducer.ts
@@ -0,0 +1,93 @@
+import Character from "./character";
+import { characterDataProvider } from "./db/characterDataProvider";
+import CharacterDbModel from "./db/characterDbModel";
+
+export type CharacterDispatchAction = {
+ type: CharacterStoreActionType;
+ characters?: Character[];
+ character?: Character;
+ callback?: (props: any) => any;
+}
+
+export enum CharacterStoreActionType {
+ create,
+ update,
+ delete,
+ loadCharacters,
+ setState
+}
+
+export const characterStoreReducer = (state: Character[], action: CharacterDispatchAction): Character[] => {
+ let newState = state;
+
+ switch (action.type) {
+ case CharacterStoreActionType.create:
+ CreateCharacter(action);
+ break;
+
+ case CharacterStoreActionType.update:
+ UpdateCharacter(action);
+ break;
+
+ case CharacterStoreActionType.delete:
+ if (action.character)
+ characterDataProvider.delete(action.character.id);
+ break;
+
+ case CharacterStoreActionType.loadCharacters:
+ LoadFromLocalStorage(action);
+ break;
+
+ case CharacterStoreActionType.setState:
+ if (action.characters)
+ newState = action.characters;
+ break;
+
+ default: break;
+ }
+
+ return newState;
+}
+
+const CreateCharacter = (action: CharacterDispatchAction) => {
+ if (action.character){
+ characterDataProvider.create(action.character.tag, action.character.name, action.character.vrmHash,
+ action.character.bgUrl, action.character.bgColor, action.character.youtubeVideoId, action.character.animationUrl)
+ .then(CharacterDbModelToCharacter).then((character: Character) => { if (action.callback) action.callback(character); })
+ }
+}
+
+const UpdateCharacter = (action: CharacterDispatchAction) => {
+ if (action.character){
+ characterDataProvider.update(action.character.id, action.character.tag, action.character.name, action.character.vrmHash,
+ action.character.bgUrl, action.character.bgColor, action.character.youtubeVideoId, action.character.animationUrl)
+ .then((updatedCharacter: Character | undefined) => {
+ if (updatedCharacter)
+ return CharacterDbModelToCharacter(updatedCharacter);
+ else
+ return undefined;
+ }).then((character: Character | undefined) => { if (action.callback) action.callback(character); })
+ }
+}
+
+const LoadFromLocalStorage = (action: CharacterDispatchAction): void => {
+ characterDataProvider
+ .getItems()
+ .then(CharacterDbModelArrayToCharacterArray)
+ .then((characters: Character[]) => { if (action.callback) action.callback(characters); });
+};
+
+const CharacterDbModelArrayToCharacterArray = async (characters: CharacterDbModel[]): Promise => {
+ const promiseArray = characters.map((characterDbModel: CharacterDbModel): Promise => {
+ return CharacterDbModelToCharacter(characterDbModel);
+ });
+ return Promise.all(promiseArray).then((characterArray: Character[]) => { return characterArray; });
+};
+
+const CharacterDbModelToCharacter = async (characterDbModel: CharacterDbModel): Promise => {
+ return new Promise((resolve, reject) => {
+ resolve(new Character(characterDbModel.id, characterDbModel.tag, characterDbModel.name,
+ characterDbModel.vrmHash, characterDbModel.bgUrl, characterDbModel.bgColor,
+ characterDbModel.youtubeVideoId, characterDbModel.animationUrl));
+ });
+};
\ No newline at end of file
diff --git a/src/features/characters/db/characterDataProvider.ts b/src/features/characters/db/characterDataProvider.ts
new file mode 100644
index 00000000..8aeb3af3
--- /dev/null
+++ b/src/features/characters/db/characterDataProvider.ts
@@ -0,0 +1,38 @@
+import { AmicaDexie } from "../../indexedDb/amicaDb";
+import CharacterDbModel from "./characterDbModel";
+import { db } from "../../indexedDb/amicaDb";
+
+export class CharacterDataProvider {
+ private db: AmicaDexie;
+
+ constructor() {
+ this.db = db;
+ }
+
+ componentWillUnmount() {
+ this.db.close();
+ }
+
+ public create(tag: string, name: string, vrmHash: string, bgUrl: string, bgColor: string, youtubeVideoID: string, animationUrl: string): Promise {
+ return this.db.characters.add(new CharacterDbModel(1, tag, name, vrmHash, bgUrl, bgColor, youtubeVideoID, animationUrl));
+ }
+
+ public async getItems(): Promise {
+ return this.db.characters.toArray();
+ }
+
+ public async getItem(id: number): Promise {
+ return this.db.characters.get({ "id": id });
+ }
+
+ public async update(id: number, tag: string, name?: string, vrmHash?: string, bgUrl?: string, bgColor?: string, youtubeVideoId?: string, animationUrl?: string): Promise {
+ this.db.characters.where("id").equals(id).modify({tag, name, vrmHash, bgUrl, bgColor, youtubeVideoId, animationUrl});
+ return this.db.characters.get(id);
+ }
+
+ public async delete(id: number): Promise {
+ return this.db.characters.delete({ "id": id });
+ }
+}
+
+export const characterDataProvider = new CharacterDataProvider();
\ No newline at end of file
diff --git a/src/features/characters/db/characterDbModel.ts b/src/features/characters/db/characterDbModel.ts
new file mode 100644
index 00000000..d45c9443
--- /dev/null
+++ b/src/features/characters/db/characterDbModel.ts
@@ -0,0 +1,21 @@
+export default class CharacterDbModel {
+ public id: number;
+ public tag: string;
+ public name: string;
+ public vrmHash: string;
+ public bgUrl: string;
+ public bgColor: string;
+ public youtubeVideoId: string;
+ public animationUrl: string;
+
+ constructor(id: number, tag: string, name: string, vrmHash: string, bgUrl: string, bgColor: string, youtubeVideoId: string, animationUrl: string) {
+ this.id = id;
+ this.tag = tag;
+ this.name = name;
+ this.vrmHash = vrmHash;
+ this.bgUrl = bgUrl;
+ this.bgColor = bgColor;
+ this.youtubeVideoId = youtubeVideoId;
+ this.animationUrl = animationUrl;
+ }
+}
\ No newline at end of file
diff --git a/src/features/indexedDb/amicaDb.ts b/src/features/indexedDb/amicaDb.ts
new file mode 100644
index 00000000..d4838547
--- /dev/null
+++ b/src/features/indexedDb/amicaDb.ts
@@ -0,0 +1,18 @@
+import Dexie, { Table } from 'dexie';
+import VrmDbModel from '../vrmStore/db/vrmDbModel';
+import CharacterDbModel from '../characters/db/characterDbModel';
+
+export class AmicaDexie extends Dexie {
+ vrms!: Table;
+ characters!: Table;
+
+ constructor() {
+ super('AmicaDatabase');
+ this.version(1).stores({
+ vrms: 'hash',
+ characters: '++id'
+ });
+ }
+}
+
+export const db = new AmicaDexie();
\ No newline at end of file
diff --git a/src/features/vrmStore/vrmDataProvider.ts b/src/features/vrmStore/db/vrmDataProvider.ts
similarity index 83%
rename from src/features/vrmStore/vrmDataProvider.ts
rename to src/features/vrmStore/db/vrmDataProvider.ts
index eab74342..4e56fad8 100644
--- a/src/features/vrmStore/vrmDataProvider.ts
+++ b/src/features/vrmStore/db/vrmDataProvider.ts
@@ -1,10 +1,10 @@
-import { VrmDexie } from "./vrmDb";
+import { AmicaDexie } from "../../indexedDb/amicaDb";
import VrmDbModel from "./vrmDbModel";
-import { db } from "./vrmDb";
+import { db } from "../../indexedDb/amicaDb";
import { Base64ToBlob } from "@/utils/blobDataUtils";
export class VrmDataProvider {
- private db: VrmDexie;
+ private db: AmicaDexie;
constructor() {
this.db = db;
@@ -31,8 +31,8 @@ export class VrmDataProvider {
.then(vrmDbModel => { console.log(`hash: ${hash}`); console.log(`vrmDbModel: ${vrmDbModel}`); return vrmDbModel ? Base64ToBlob(vrmDbModel?.vrmData) : undefined; });
}
- public addItemUrl(hash: string, url: string) {
- this.db.vrms.where("hash").equals(hash).modify({ vrmUrl: url, saveType: 'web' });
+ public updateLoadedLocalVrm(hash: string, url: string) {
+ this.db.vrms.where("hash").equals(hash).modify({ vrmUrl: url, hash: url, saveType: 'web' });
}
}
diff --git a/src/features/vrmStore/vrmDbModel.ts b/src/features/vrmStore/db/vrmDbModel.ts
similarity index 100%
rename from src/features/vrmStore/vrmDbModel.ts
rename to src/features/vrmStore/db/vrmDbModel.ts
diff --git a/src/features/vrmStore/vrmDb.ts b/src/features/vrmStore/vrmDb.ts
deleted file mode 100644
index 7d2a296b..00000000
--- a/src/features/vrmStore/vrmDb.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import Dexie, { Table } from 'dexie';
-import VrmDbModel from './vrmDbModel';
-
-export class VrmDexie extends Dexie {
- vrms!: Table;
-
- constructor() {
- super('AmicaVrmDatabase');
- this.version(1).stores({
- vrms: 'hash' // Primary key and indexed props
- });
- }
-}
-
-export const db = new VrmDexie();
\ No newline at end of file
diff --git a/src/features/vrmStore/vrmStoreContext.tsx b/src/features/vrmStore/vrmStoreContext.tsx
index 93ad3ef8..5d989381 100644
--- a/src/features/vrmStore/vrmStoreContext.tsx
+++ b/src/features/vrmStore/vrmStoreContext.tsx
@@ -10,6 +10,7 @@ interface VrmStoreContextType {
getCurrentVrm: () => VrmData | undefined;
vrmList: VrmData[];
vrmListAddFile: (file: File, viewer: Viewer) => void;
+ updateLoadedLocalVrm: (hash: string, url: string) => void;
isLoadingVrmList: boolean;
setIsLoadingVrmList: Dispatch>;
};
@@ -22,6 +23,7 @@ export const VrmStoreContext = createContext({
getCurrentVrm: () => {return undefined;},
vrmList: vrmInitList,
vrmListAddFile: () => {},
+ updateLoadedLocalVrm: () => {},
isLoadingVrmList: false, setIsLoadingVrmList: () => {}
});
@@ -54,11 +56,16 @@ export const VrmStoreProvider = ({ children }: PropsWithChildren<{}>): JSX.Eleme
}, []);
const getCurrentVrm = () => {
- return config('vrm_save_type') == 'local' ? loadedVrmList.find(vrm => vrm.getHash() == config('vrm_hash') ) : loadedVrmList.find(vrm => vrm.url == config('vrm_url') );
+ return loadedVrmList.find(vrm => vrm.getHash() == config('vrm_hash'));
+ }
+
+ const updateLoadedLocalVrm = (hash: string, url: string) => {
+ updateConfig('vrm_save_type', 'web');
+ vrmListDispatch({ type: VrmStoreActionType.updateLoadedLocalVrm, hash, url });
}
return (
-
+
{children}
);
diff --git a/src/features/vrmStore/vrmStoreReducer.ts b/src/features/vrmStore/vrmStoreReducer.ts
index 48e4267e..b2be75fd 100644
--- a/src/features/vrmStore/vrmStoreReducer.ts
+++ b/src/features/vrmStore/vrmStoreReducer.ts
@@ -1,13 +1,14 @@
import { hashCode } from "@/components/settings/common";
import { VrmData } from "./vrmData";
-import { vrmDataProvider } from "./vrmDataProvider";
-import VrmDbModel from "./vrmDbModel";
+import { vrmDataProvider } from "./db/vrmDataProvider";
+import VrmDbModel from "./db/vrmDbModel";
import "@/utils/blobDataUtils";
import { Base64ToBlob, BlobToBase64 } from "@/utils/blobDataUtils";
export type VrmDispatchAction = {
type: VrmStoreActionType;
itemFile?: File;
+ hash?: string;
url?: string;
thumbBlob?: Blob;
vrmList?: VrmData[];
@@ -18,7 +19,8 @@ export enum VrmStoreActionType {
addItem,
updateVrmThumb,
setVrmList,
- loadFromLocalStorage
+ loadFromLocalStorage,
+ updateLoadedLocalVrm
};
export const vrmStoreReducer = (state: VrmData[], action: VrmDispatchAction): VrmData[] => {
@@ -30,12 +32,19 @@ export const vrmStoreReducer = (state: VrmData[], action: VrmDispatchAction): Vr
break;
case VrmStoreActionType.updateVrmThumb:
newState = updateVrmThumb(state, action);
+ break;
case VrmStoreActionType.setVrmList:
if (action.vrmList && action.vrmList.length)
newState = action.vrmList;
+ break;
case VrmStoreActionType.loadFromLocalStorage:
if (action.vrmList && action.callback)
newState = LoadFromLocalStorage(action)
+ break;
+ case VrmStoreActionType.updateLoadedLocalVrm:
+ if (action.hash && action.url)
+ UpdateLoadedLocalVrm(action.hash, action.url);
+ break;
default:
break;
}
@@ -99,6 +108,10 @@ const LoadFromLocalStorage = (action: VrmDispatchAction): VrmData[] => {
return action.vrmList || new Array;
};
+const UpdateLoadedLocalVrm = (hash: string, url: string): void => {
+ vrmDataProvider.updateLoadedLocalVrm(hash, url);
+}
+
const VrmDataArrayFromVrmDbModelArray = async (vrms: VrmDbModel[]): Promise => {
const promiseArray = vrms.map((vrmDbModel: VrmDbModel): Promise => {
return VrmDbModelToVrmData(vrmDbModel);
diff --git a/src/pages/import/[sqid]/index.tsx b/src/pages/import/[sqid]/index.tsx
index 9d6ac801..9d85ad34 100644
--- a/src/pages/import/[sqid]/index.tsx
+++ b/src/pages/import/[sqid]/index.tsx
@@ -6,7 +6,7 @@ import { updateConfig, defaultConfig } from '@/utils/config';
import { isTauri } from '@/utils/isTauri';
import VrmDemo from "@/components/vrmDemo";
import { supabase } from '@/utils/supabase';
-import { vrmDataProvider } from '@/features/vrmStore/vrmDataProvider';
+import { vrmDataProvider } from '@/features/vrmStore/db/vrmDataProvider';
import { BlobToBase64 } from '@/utils/blobDataUtils';
export default function Import() {
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index aaa24b0c..96ec6d65 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,14 +1,16 @@
import {
Fragment,
+ useCallback,
useContext,
useEffect,
+ useRef,
useState,
} from "react";
import Link from "next/link";
import { Menu, Transition } from '@headlessui/react'
import { clsx } from "clsx";
import { M_PLUS_2, Montserrat } from "next/font/google";
-import { useTranslation, Trans } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import {
ChatBubbleLeftIcon,
ChatBubbleLeftRightIcon,
@@ -44,7 +46,9 @@ import { AlertContext } from "@/features/alert/alertContext";
import { config, updateConfig } from '@/utils/config';
import { isTauri } from '@/utils/isTauri';
import { langs } from '@/i18n/langs';
-import { VrmStoreProvider } from "@/features/vrmStore/vrmStoreContext";
+import { UsersIcon } from "@heroicons/react/20/solid";
+import { CharacterStoreContextProvider } from "@/features/characters/characterStoreContext";
+import { CharactersPage } from "@/components/character/CharactersPage";
const m_plus_2 = M_PLUS_2({
variable: "--font-m-plus-2",
@@ -79,13 +83,15 @@ export default function Home() {
const [showSettings, setShowSettings] = useState(false);
const [showChatLog, setShowChatLog] = useState(false);
+ const [showCharacters, setShowCharacters] = useState(false);
const [showDebug, setShowDebug] = useState(false);
// null indicates havent loaded config yet
const [muted, setMuted] = useState(null);
const [webcamEnabled, setWebcamEnabled] = useState(false);
const [showLanguageSelector, setShowLanguageSelector] = useState(false);
-
+ const [settingsUpdated, setSettingsUpdated] = useState(false);
+ const [showNotification, setShowNotification] = useState(false);
useEffect(() => {
if (muted === null) {
@@ -124,6 +130,49 @@ export default function Home() {
// this exists to prevent build errors with ssr
useEffect(() => setShowContent(true), []);
+
+ // useEffect(() => {
+ // showSettingsUpdatedNotification();
+ // }, [bgColor, bgUrl, vrmUrl, youtubeVideoID, animationUrl]);
+
+ const showSettingsUpdatedNotification = (): () => void => {
+ const timeOutId = setTimeout(() => {
+ if (settingsUpdated) {
+ setShowNotification(true);
+ setTimeout(() => {
+ setShowNotification(false);
+ }, 5000);
+ }
+ }, 1000);
+ return () => clearTimeout(timeOutId);
+ }
+
+ const vrmFileInputRef = useRef(null);
+ const handleClickOpenVrmFile = useCallback(() => {
+ vrmFileInputRef.current?.click();
+ }, []);
+
+ const handleChangeVrmFile = useCallback(
+ (event: React.ChangeEvent) => {
+ const files = event.target.files;
+ if (!files) return;
+
+ const file = files[0];
+ if (!file) return;
+
+ const file_type = file.name.split(".").pop();
+
+ if (file_type === "vrm") {
+ const blob = new Blob([file], { type: "application/octet-stream" });
+ const url = window.URL.createObjectURL(blob);
+ viewer.loadVrm(url);
+ }
+
+ event.target.value = "";
+ },
+ [viewer]
+ );
+
if (!showContent) return <>>;
return (
@@ -147,14 +196,7 @@ export default function Home() {
{ webcamEnabled && }
{ showDebug && setShowDebug(false) }/> }
-
-
- {showSettings && (
- setShowSettings(false)}
- />
- )}
-
+
@@ -262,6 +304,15 @@ export default function Home() {
+
+ setShowCharacters(!showCharacters)}
+ />
+
+
+
);
}
diff --git a/src/pages/share.tsx b/src/pages/share.tsx
index ca1e8877..8da3d4f3 100644
--- a/src/pages/share.tsx
+++ b/src/pages/share.tsx
@@ -16,7 +16,7 @@ import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
-import { vrmDataProvider } from "@/features/vrmStore/vrmDataProvider";
+import { vrmDataProvider } from "@/features/vrmStore/db/vrmDataProvider";
registerPlugin(
FilePondPluginImagePreview,
@@ -114,7 +114,7 @@ export default function Share() {
useEffect(() => {
if (vrmLoadedFromIndexedDb) {
- vrmDataProvider.addItemUrl(vrmHash, vrmUrl);
+ vrmDataProvider.updateLoadedLocalVrm(vrmHash, vrmUrl);
updateConfig('vrm_url', vrmUrl);
updateConfig('vrm_save_type', 'web');
setVrmSaveType('web');
diff --git a/src/utils/config.ts b/src/utils/config.ts
index b287e2eb..25a2dc86 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -10,6 +10,8 @@ const defaults = {
bg_color: process.env.NEXT_PUBLIC_BG_COLOR ?? '',
bg_url: process.env.NEXT_PUBLIC_BG_URL ?? '/bg/bg-landscape1.jpg',
vrm_url: process.env.NEXT_PUBLIC_VRM_HASH ?? '/vrm/AvatarSample_A.vrm',
+ character_id: '-1',
+ character_tag: '',
vrm_hash: '',
vrm_save_type: 'web',
youtube_videoid: '',