├── .env.example ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── app ├── api │ └── route.js ├── layout.jsx ├── page.jsx ├── sandbox.jsx └── sandbox.module.css ├── assets ├── openai-logo.svg └── stories.json ├── components ├── avataritem.jsx ├── avataritem.module.css ├── bookdialog.jsx ├── bookdialog.module.css ├── bookdialog.stories.js ├── characterdialog.jsx ├── characterdialog.module.css ├── characterdialog.stories.js ├── charactericon.jsx ├── contentitem.jsx ├── contentitem.module.css ├── contentitem.stories.js ├── customtheme.jsx ├── deletedialog.jsx ├── deletedialog.module.css ├── deletedialog.stories.js ├── loadingtext.jsx ├── loadingtext.module.css ├── loadingtext.stories.js ├── scenedialog.jsx ├── scenedialog.module.css ├── scenedialog.stories.js ├── togglebutton.jsx ├── togglebutton.stories.js ├── userdialog.jsx ├── userdialog.module.css └── userdialog.stories.js ├── docs ├── character1.jpeg ├── character2.jpeg ├── japanese1.jpeg ├── japanese2.jpeg ├── scene1.jpeg ├── scene2.jpeg ├── screenshot1.jpeg ├── screenshot2.jpeg ├── screenshot3.png ├── screenshot4.png ├── screenshot5.png ├── story1.jpeg ├── story11.jpeg ├── story2.png ├── story21.jpeg ├── user1.jpeg └── user2.jpeg ├── lib └── utils.js ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── logo192.png └── logo512.png ├── stores ├── appStore.js ├── bookStore.js └── dataStore.js └── styles ├── main.css └── preview.css /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_APIKEY=YOUR_OWN_API_KEY -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # My ignore list 16 | _bin 17 | stories 18 | .next 19 | .env 20 | out 21 | build 22 | *.bu.json 23 | *.bu.css 24 | *.bu.jsx 25 | *.bu.js 26 | *.bu.md 27 | 28 | # Editor directories and files 29 | .vscode/* 30 | !.vscode/extensions.json 31 | .idea 32 | .DS_Store 33 | *.suo 34 | *.ntvs* 35 | *.njsproj 36 | *.sln 37 | *.sw? 38 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/nextjs').StorybookConfig } */ 2 | const config = { 3 | stories: [ 4 | "../stories/**/*.mdx", 5 | "../stories/**/*.stories.@(js|jsx|ts|tsx)", 6 | "../components/**/*.stories.@(js|jsx|ts|tsx)", 7 | ], 8 | addons: [ 9 | "@storybook/addon-links", 10 | "@storybook/addon-essentials", 11 | "@storybook/addon-interactions", 12 | ], 13 | framework: { 14 | name: "@storybook/nextjs", 15 | options: {}, 16 | }, 17 | core: { 18 | disableTelemetry: true, 19 | }, 20 | docs: { 21 | autodocs: "tag", 22 | }, 23 | }; 24 | export default config; 25 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | //import '../styles/preview.css' // grid 2 | import '../styles/main.css' // grid 3 | 4 | /** @type { import('@storybook/react').Preview } */ 5 | const preview = { 6 | parameters: { 7 | backgrounds: { 8 | default: "light", 9 | }, 10 | actions: { argTypesRegex: "^on[A-Z].*" }, 11 | controls: { 12 | matchers: { 13 | color: /(background|color)$/i, 14 | date: /Date$/, 15 | }, 16 | }, 17 | }, 18 | }; 19 | 20 | export default preview; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present SuperShaneski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | openai-chatgpt-api 2 | ===== 3 | 4 | A sample interactive chatbot application that can be used for roleplay based on some storyline, using ChatGPT API, powered by gpt-3.5-turbo, OpenAI's advanced language model, built using Next 13, the React framework. 5 | 6 | --- 7 | 8 | ChatGPT API を使用して、ロールプレイに使用できるインタラクティブなチャットボットサンプルアプリです。 9 | 10 | 11 | # Motivation 12 | 13 | This app aims to provide a simple and convenient user interface to facilitate interactive roleplay conversation with a chatbot based on some storyline/scenarios. 14 | 15 | --- 16 | 17 | このアプリは、ストーリー/シナリオに基づいたチャットボットとの対話型ロールプレイ会話を容易にするシンプルで便利なユーザーインターフェースを提供することを目的としています。 18 | 19 | 20 | # App 21 | 22 | I included two sample stories with scenes and characters that you can use for testing. 23 | The user interface is very simple and intuitive so it is easy to use. 24 | 25 | 26 | 27 | 28 | Screenshot 29 | 30 | 31 | Select the Story you want or Add New Story: 32 | 33 | 34 | 35 | 36 | Screenshot 37 | 38 | 39 | Edit or write your own Story prompt: 40 | 41 | 42 | 43 | 44 | Screenshot 45 | 46 | 47 | Edit or write your own Scene prompt: 48 | 49 | 50 | 51 | 52 | Screenshot 53 | 54 | 55 | Edit or write your own Character prompt: 56 | 57 | 58 | 59 | 60 | Screenshot 61 | 62 | 63 | Edit or write your own User prompt (click the person icon at the left of Text input): 64 | 65 | 66 | 67 | 68 | Screenshot 69 | 70 | 71 | All data are stored in localStorage using [Zustand](https://github.com/pmndrs/zustand) for easy retrieval. 72 | 73 | Please be advised that this app is not optimized to be deployed for production. 74 | The way the data is sent to the route handler prior to sending request to the API is not efficient. 75 | 76 | # Sample Custom Story 77 | 78 | Here is a sample custom story, a conversation with Oda Nobunaga, the famous Japanese daimyo from the Sengoku period. 79 | 80 | --- 81 | 82 | これは、戦国時代の有名な日本の大名である織田信長との会話のサンプルカスタムストーリーです. 83 | 84 | Character Prompt: 85 | 86 | ``` 87 | In this session we will simulate a conversation with Oda Nobunaga. 88 | You will act as Oda Nobunaga, a Japanese daimyo and one of the leading 89 | figures of the Sengoku period. 90 | He is regarded as the first Great Unifier of Japan. 91 | Please respond entirely in Japanese. 92 | ``` 93 | 94 | Sample Conversation: 95 | 96 | ![Sample Conversation](./docs/japanese1.jpeg) 97 | 98 | Character Prompt: 99 | 100 | ![Character Prompt](./docs/japanese2.jpeg) 101 | 102 | # Prompt Design 103 | 104 | The basic [chat completion](https://platform.openai.com/docs/guides/chat/introduction) API call looks like this: 105 | 106 | ```javascript 107 | openai.ChatCompletion.create( 108 | model="gpt-3.5-turbo", 109 | messages=[ 110 | {"role": "system", "content": "You are a helpful assistant."}, 111 | {"role": "user", "content": "Who won the world series in 2020?"}, 112 | {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."}, 113 | {"role": "user", "content": "Where was it played?"} 114 | ] 115 | ) 116 | ``` 117 | 118 | The `system prompt` gives the AI the instruction how to respond. 119 | 120 | ```javascript 121 | {"role": "system", "content": "You are a helpful assistant."} 122 | ``` 123 | 124 | The message format the user sends is this: 125 | 126 | ```javascript 127 | {"role": "user", "content": "Who won the world series in 2020?"}, 128 | ``` 129 | 130 | and the expected response is like this: 131 | 132 | ```javascript 133 | {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."} 134 | ``` 135 | 136 | Keeping all these in mind, we will be designing the `system prompt` to simulate conversation between the user and AI with storytelling narrative in mind. 137 | 138 | Here is the basic format: 139 | 140 | ```javascript 141 | [Character Prompt] /* required */ 142 | 143 | [Story Prompt] 144 | 145 | [Scene Prompt] 146 | 147 | [Scene Character Prompt] 148 | 149 | [User Prompt] 150 | 151 | [Scene User Prompt] 152 | ``` 153 | 154 | We can omit everything, except the `character prompt`. It should be as simple description as possible to enable the AI to generate more creative response. 155 | 156 | If the story is well known, like from a book, movie or other popular media, we can omit the `story prompt` since we are assuming that whatever data GPT-3.5 is trained on probably included it so there is no need to add it. 157 | 158 | The `scene prompt` lays out the particular scenario from our main story to restrict the conversation within that bounds. Otherwise, the API might refer to scenes that will happen further in the story. 159 | 160 | In any story, there is the so called `character development` which tracks the character's growth as the story progresses. This is where the `scene character prompt` comes in to focus on the character's current state at that particular scene. 161 | 162 | `User prompt` lays out the identity of the user (you) for the AI to respond with. You can omit this if you just want to interact with the AI's character. Like in `scene character prompt`, the `scene user prompt` gives context to the AI about the user (you) at that particulat scene. 163 | 164 | To have the best interaction and generate good response from the AI, it is better to use the `zero shot` approach when writing the prompts. You do not want to spill all the beans to the AI and give all contexts in one go. We just want to sway them in certain direction with as few nudgings as possible without revealing all the details of the story or scenes. We want the AI not to generate canned response but to be more creative. 165 | 166 | 167 | # Token Management 168 | 169 | For `gpt-3.5-turbo-0301`, the maximum limit is 4096 tokens. 170 | 171 | But I set the default cutoff to 3072 tokens (i.e. 1024 x 3). 172 | I just do a simple deletion of 1/3 of the oldest entries as a way to prevent hitting the max limit. 173 | 174 | At this moment, there is no prompt or token optimizations yet. 175 | 176 | # Limiting Response Length 177 | 178 | Most of the time, the response from Chat Completions API is just too long to appear as natural in a conversation. 179 | To limit the response length, we just need to add instruction in the system prompt. 180 | 181 | ```javascript 182 | system_content += '\n\nMost of the time your responses should be a sentence or two.' 183 | ``` 184 | 185 | 186 | # Installation 187 | 188 | Clone the repository and install the dependencies 189 | 190 | ```sh 191 | git clone https://github.com/supershaneski/openai-chatgpt-api.git myproject 192 | 193 | cd myproject 194 | 195 | npm install 196 | ``` 197 | 198 | Copy `.env.example` and rename it to `.env` then edit the `OPENAI_APIKEY` and use your own `OpenAI API key` 199 | 200 | ```javascript 201 | OPENAI_APIKEY=YOUR_OWN_API_KEY 202 | ``` 203 | 204 | If you have not yet done so, upon signing up for OpenAI account you will be given `$18 in free credit that can be used during your first 3 months`. Visit the [OpenAI website](https://platform.openai.com/) for more details. 205 | 206 | Now, to run the app 207 | 208 | ```sh 209 | npm run dev 210 | ``` 211 | 212 | Open your browser to `http://localhost:3005/` to load the application page. 213 | 214 | -------------------------------------------------------------------------------- /app/api/route.js: -------------------------------------------------------------------------------- 1 | import { Configuration, OpenAIApi } from "openai" 2 | 3 | import { trim_array } from "../../lib/utils" 4 | 5 | const configuration = new Configuration({ 6 | apiKey: process.env.OPENAI_APIKEY, 7 | }) 8 | 9 | const openai = new OpenAIApi(configuration) 10 | 11 | export async function POST(req) { 12 | 13 | const { system, prompt, previous } = await req.json() 14 | 15 | if(!prompt || !system) { 16 | return new Response('Bad Request', { 17 | status: 400, 18 | }); 19 | } 20 | 21 | let messages = [ system ] 22 | 23 | let prev_data = trim_array(previous, 15) // just maintain last 15 entries as history 24 | 25 | if(prev_data.length > 0) { 26 | 27 | messages = messages.concat(prev_data) 28 | } 29 | 30 | messages = messages.concat(prompt) 31 | 32 | let reply = null 33 | let errorFlag = false 34 | 35 | try { 36 | 37 | const completion = await openai.createChatCompletion({ 38 | model: "gpt-3.5-turbo", 39 | messages: messages, 40 | temperature: 0.5, 41 | max_tokens: 1024, // 4096 - max prompt tokens 42 | }); 43 | 44 | reply = completion.data.choices[0].message 45 | 46 | } catch(err) { 47 | 48 | console.log(err) 49 | errorFlag = true 50 | 51 | } 52 | 53 | if(errorFlag) { 54 | 55 | return new Response(JSON.stringify({ reply: 56 | {role: 'assistant', content: "Oops, an error occured." } 57 | }), { 58 | status: 200, 59 | }) 60 | 61 | } 62 | 63 | // reply = {role: 'assistant', content: 'Lorem ipsum dolor amet sidecus orange chocolate.' } 64 | 65 | return new Response(JSON.stringify({ reply }), { 66 | status: 200, 67 | }) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/layout.jsx: -------------------------------------------------------------------------------- 1 | import '../styles/main.css' 2 | 3 | import '@fontsource/roboto/300.css'; 4 | import '@fontsource/roboto/400.css'; 5 | import '@fontsource/roboto/500.css'; 6 | import '@fontsource/roboto/700.css'; 7 | 8 | export default function RootLayout({ children }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/page.jsx: -------------------------------------------------------------------------------- 1 | import SandBox from './sandbox'; 2 | 3 | export const metadata = { 4 | title: process.env.siteTitle, 5 | description: 'A sample webapp using OpenAI ChatGPT API', 6 | viewport: 'maximum-scale=1.0, minimum-scale=1.0, initial-scale=1.0, width=device-width, user-scalable=0', 7 | icons: { 8 | icon: '/logo192.png', 9 | shortcut: '/logo192.png', 10 | } 11 | } 12 | 13 | export default function Page({ props }) { 14 | return 15 | } -------------------------------------------------------------------------------- /app/sandbox.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { createPortal } from 'react-dom' 5 | 6 | import IconButton from '@mui/material/IconButton' 7 | import TextField from '@mui/material/TextField' 8 | import InputAdornment from '@mui/material/InputAdornment' 9 | import Box from '@mui/material/Box' 10 | import Menu from '@mui/material/Menu' 11 | import MenuItem from '@mui/material/MenuItem' 12 | import Typography from '@mui/material/Typography' 13 | import Fab from '@mui/material/Fab' 14 | //import Tooltip from '@mui/material/Tooltip' 15 | 16 | import StoriesIcon from '@mui/icons-material/AutoStories'; 17 | import SettingsIcon from '@mui/icons-material/Settings' 18 | import EditIcon from '@mui/icons-material/Edit' 19 | import ClearIcon from '@mui/icons-material/Clear' 20 | import AddIcon from '@mui/icons-material/Add' 21 | import SendIcon from '@mui/icons-material/Send' 22 | 23 | import RefreshIcon from '@mui/icons-material/Refresh' 24 | import PersonIcon from '@mui/icons-material/Person' 25 | import ToggleButton, { ChatModes } from '../components/togglebutton' 26 | 27 | import ContentItem from '../components/contentitem' 28 | 29 | import classes from './sandbox.module.css' 30 | 31 | import BookDialog from '../components/bookdialog' 32 | import SceneDialog, { DialogModes as SceneDialogModes } from '../components/scenedialog' 33 | import CharacterDialog, { DialogModes as CharacterDialogModes } from '../components/characterdialog' 34 | import UserDialog from '../components/userdialog' 35 | import DeleteDialog, { DeleteModes } from '../components/deletedialog' 36 | 37 | import AvatarItem from '../components/avataritem' 38 | import LoadingText from '../components/loadingtext' 39 | 40 | import CustomTheme from '../components/customtheme' 41 | 42 | import { getDataId } from '../lib/utils' 43 | 44 | import useAppStore from '../stores/appStore' 45 | import useDataStore from '../stores/dataStore' 46 | import useBookStore from '../stores/bookStore' 47 | 48 | const sendRequest = async (payload) => { 49 | 50 | let response = await fetch('/api/', { 51 | method: 'POST', 52 | body: JSON.stringify(payload) 53 | }) 54 | 55 | if(response.ok) { 56 | 57 | return await response.json() 58 | 59 | } else { 60 | 61 | const errorBody = await response.json() 62 | const errorMessage = errorBody.error 63 | const status = response.status 64 | 65 | throw Error(`status code: ${status} Error: ${errorMessage}`) 66 | 67 | } 68 | 69 | } 70 | 71 | export default function SandBox() { 72 | 73 | const inputRef = React.useRef() 74 | const messageRef = React.useRef() 75 | const timerRef = React.useRef() 76 | 77 | const setDarkMode = useAppStore((state) => state.setDarkMode) 78 | const chatMode = useAppStore((state) => state.chatMode) 79 | const setChatMode = useAppStore((state) => state.setChatMode) 80 | 81 | const getData = useDataStore((state) => state.getDataBySceneId) 82 | const updateData = useDataStore((state) => state.updateDataBySceneId) 83 | const deleteData = useDataStore((state) => state.deleteDataById) 84 | const addData = useDataStore((state) => state.addData) 85 | 86 | const bookId = useBookStore((state) => state.bookId) 87 | const chapterId = useBookStore((state) => state.chapterId) 88 | const characterId = useBookStore((state) => state.characterId) 89 | 90 | const getBook = useBookStore((state) => state.getBook) 91 | const getChapter = useBookStore((state) => state.getChapter) 92 | const getChapters = useBookStore((state) => state.getChapters) 93 | const getCharacter = useBookStore((state) => state.getCharacter) 94 | const getCharacters = useBookStore((state) => state.getCharacters) 95 | const getUser = useBookStore((state) => state.getUserByBookId) 96 | 97 | const addScene = useBookStore((state) => state.addChapter) 98 | const addCharacter = useBookStore((state) => state.addCharacter) 99 | 100 | const selectBook = useBookStore((state) => state.selectBook) 101 | const selectChapter = useBookStore((state) => state.selectChapter) 102 | const selectCharacter = useBookStore((state) => state.selectCharacter) 103 | 104 | const [bookName, setBookName] = React.useState('') 105 | const [chapterName, setChapterName] = React.useState('') 106 | const [chapterDescription, setChapterDescription] = React.useState('') 107 | 108 | const [chapterItems, setChapterItems] = React.useState([]) 109 | const [characterItems, setCharacterItems] = React.useState([]) 110 | 111 | const [dataMessages, setDataMessages] = React.useState([]) 112 | const [sendFlag, setSendFlag] = React.useState(false) 113 | 114 | const [openBookDialog, setBookDialog] = React.useState(false) 115 | const [openSceneDialog, setSceneDialog] = React.useState(false) 116 | const [openUserDialog, setUserDialog] = React.useState(false) 117 | const [openCharacterDialog, setCharacterDialog] = React.useState(false) 118 | const [openDeleteDialog, setDeleteDialog] = React.useState(false) 119 | const [deleteMode, setDeleteMode] = React.useState(DeleteModes.Character) 120 | 121 | //const [chatMode, setChatMode] = React.useState(ChatModes.Person) 122 | const [sceneMode, setSceneMode] = React.useState(SceneDialogModes.Save) 123 | const [characterMode, setCharacterMode] = React.useState(CharacterDialogModes.Save) 124 | 125 | const [isMounted, setMounted] = React.useState(false) 126 | 127 | const [inputText, setInputText] = React.useState('') 128 | 129 | const [anchorEl, setAnchorEl] = React.useState(null) 130 | 131 | const openMenu = Boolean(anchorEl) 132 | 133 | React.useEffect(() => { 134 | 135 | setMounted(true) 136 | 137 | return () => { 138 | setMounted(false) 139 | } 140 | 141 | }, []) 142 | 143 | React.useEffect(() => { 144 | 145 | const handleModeChange = (e) => { 146 | 147 | setDarkMode(e.matches) 148 | } 149 | 150 | if(isMounted) { 151 | 152 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleModeChange) 153 | 154 | const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches 155 | 156 | setDarkMode(isDarkMode) 157 | 158 | } 159 | 160 | return () => { 161 | 162 | try { 163 | window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', handleModeChange) 164 | } catch(err) { 165 | // 166 | } 167 | 168 | } 169 | 170 | }, [isMounted]) 171 | 172 | React.useEffect(() => { 173 | 174 | refreshApp() 175 | 176 | }, [bookId]) 177 | 178 | const refreshApp = React.useCallback(() => { 179 | 180 | const book = getBook(bookId) 181 | const chapter = getChapter(chapterId) 182 | 183 | setBookName(book.name) 184 | setChapterName(chapter.name) 185 | setChapterDescription(chapter.prompt) 186 | 187 | setChapterItems(getChapters(bookId)) 188 | 189 | setCharacterItems(getCharacters(bookId)) 190 | 191 | const raw_data = getData(chapterId) 192 | const prev_data = raw_data.map((item) => item.data) 193 | setDataMessages(prev_data) 194 | 195 | }, [bookId, chapterId]) 196 | 197 | const handleSubmit = (e) => { 198 | e.preventDefault() 199 | e.stopPropagation() 200 | 201 | submitPrompt() 202 | 203 | } 204 | 205 | const submitPrompt = () => { 206 | 207 | const chapter = getChapter(chapterId) 208 | const character = getCharacter(characterId) 209 | const user = getUser(bookId) 210 | const book = getBook(bookId) 211 | 212 | let system_content = character.prompt 213 | if(book.prompt) system_content += '\nBackstory:\n' + book.prompt 214 | if(chapter.prompt) system_content += '\nScene:\n' + chapter.prompt 215 | if(chapter.characters.length > 0 && chapter.characters.some((item) => item.cid === characterId)) { 216 | const chapter_character_prompt = chapter.characters.find((item) => item.cid === characterId).prompt 217 | system_content += '\n' + chapter_character_prompt 218 | } 219 | const chapter_user_prompt = chapter.user 220 | if(user.prompt) { 221 | system_content += '\nAs the user, I have the following attributes:\n' + user.prompt 222 | if(chapter_user_prompt) system_content += '\n' + chapter_user_prompt 223 | } else { 224 | system_content += '\nAs the user, I have the following attributes:\n' + chapter_user_prompt 225 | } 226 | 227 | // Limit reply 228 | system_content += '\n\nMost of the time your responses should be a sentence or two.' 229 | 230 | let system_prompt = { role: 'system', content: system_content } 231 | 232 | const previous_prompts = dataMessages.filter((item) => { 233 | if(item.type === 'user') { 234 | if(item.character === characterId) { 235 | return true 236 | } 237 | } else { 238 | if(item.id === characterId) { 239 | return true 240 | } 241 | } 242 | return false 243 | }).map((item) => { 244 | if(item.type === 'user') { 245 | return { 246 | role: 'user', 247 | content: item.content 248 | } 249 | } else { 250 | return { 251 | role: 'assistant', 252 | content: item.content 253 | } 254 | } 255 | }) 256 | 257 | let user_prompt = { role: 'user', content: inputText } 258 | 259 | setInputText('') 260 | inputRef.current.blur() 261 | 262 | const post_id = getDataId() 263 | 264 | const user_data_message = { pid: post_id, type: 'user', content: inputText, character: characterId, } 265 | setDataMessages((prev) => { 266 | let items = prev.slice(0) 267 | items.push(user_data_message) 268 | return items 269 | }) 270 | addData({ id: post_id, sid: chapterId, data: user_data_message }) 271 | 272 | scrollToTop() 273 | 274 | setSendFlag(true) 275 | 276 | sendRequest({ 277 | system: system_prompt, 278 | prompt: user_prompt, 279 | previous: previous_prompts, 280 | }).then((resp) => { 281 | 282 | const new_post_id = getDataId() 283 | const new_system_data = { pid: new_post_id, type: 'assistant', content: resp.reply.content, icon: character.icon, id: characterId, name: character.name} 284 | 285 | setDataMessages((prev) => { 286 | let items = prev.slice(0) 287 | items.push(new_system_data) 288 | return items 289 | }) 290 | addData({ id: new_post_id, sid: chapterId, data: new_system_data }) 291 | 292 | setSendFlag(false) 293 | 294 | scrollToTop() 295 | 296 | }).catch((error) => { 297 | 298 | console.log(error) 299 | 300 | const new_post_id = getDataId() 301 | const new_system_error_data = { pid: new_post_id, type: 'assistant', content: error, icon: character.icon, id: characterId, name: character.name} 302 | 303 | setDataMessages((prev) => { 304 | let items = prev.slice(0) 305 | items.push(new_system_error_data) 306 | return items 307 | }) 308 | addData({ id: new_post_id, sid: chapterId, data: new_system_error_data }) 309 | 310 | setSendFlag(false) 311 | 312 | scrollToTop() 313 | 314 | }) 315 | 316 | } 317 | 318 | const scrollToTop = () => { 319 | clearTimeout(timerRef.current) 320 | timerRef.current = setTimeout(() => { 321 | messageRef.current.scrollTop = messageRef.current.scrollHeight 322 | }, 100) 323 | } 324 | 325 | const handleScenarioClick = (e) => { 326 | setAnchorEl(e.currentTarget) 327 | } 328 | 329 | const handleClose = () => { 330 | setAnchorEl(null) 331 | } 332 | 333 | const handleSelectChapter = (id) => (e) => { 334 | 335 | const chapter = getChapter(id) 336 | 337 | const name = chapter.name 338 | const description = chapter.prompt 339 | 340 | selectChapter(id) 341 | 342 | setChapterName(name) 343 | setChapterDescription(description) 344 | 345 | setAnchorEl(null) 346 | 347 | const raw_data = getData(id) 348 | const prev_data = raw_data.map((item) => item.data) 349 | setDataMessages(prev_data) 350 | 351 | } 352 | 353 | const handleSelectCharacter = (id) => { 354 | 355 | selectCharacter(id) 356 | 357 | } 358 | 359 | const handleOpenSettings = () => { 360 | setBookDialog(true) 361 | } 362 | 363 | const handleCloseBookDialog = (book_id) => { 364 | 365 | if(bookId !== book_id) { 366 | 367 | const book = getBook(book_id) 368 | 369 | selectBook(book_id) 370 | //setDataMessages([]) 371 | setBookName(book.name) 372 | 373 | } 374 | 375 | setBookDialog(false) 376 | 377 | } 378 | 379 | const handleConfirmBook = (book_id) => { 380 | 381 | const book = getBook(book_id) 382 | 383 | selectBook(book_id) 384 | 385 | setDataMessages([]) 386 | setBookName(book.name) 387 | 388 | setBookDialog(false) 389 | 390 | } 391 | 392 | const handleConfirmUserDialog = () => { 393 | setUserDialog(false) 394 | } 395 | 396 | const handleCloseUserDialog = () => { 397 | setUserDialog(false) 398 | } 399 | 400 | const handleOpenUserDialog = () => { 401 | setUserDialog(true) 402 | } 403 | 404 | const handleAddNewScene = () => { 405 | 406 | const newId = addScene(bookId, 'New Scene', '') 407 | 408 | setTimeout(() => { 409 | 410 | setChapterItems(getChapters(bookId)) 411 | 412 | selectChapter(newId) 413 | const chapter = getChapter(newId) 414 | 415 | setChapterName(chapter.name) 416 | setChapterDescription(chapter.prompt) 417 | 418 | setDataMessages([]) 419 | 420 | }, 500) 421 | } 422 | 423 | const handleOpenSceneDialog = () => { 424 | setSceneMode(SceneDialogModes.Save) 425 | setSceneDialog(true) 426 | } 427 | 428 | const handleCloseSceneDialog = () => { 429 | setSceneDialog(false) 430 | } 431 | 432 | const handleConfirmSceneDialog = () => { 433 | 434 | const chapter = getChapter(chapterId) 435 | 436 | setChapterName(chapter.name) 437 | setChapterDescription(chapter.prompt) 438 | 439 | setSceneDialog(false) 440 | } 441 | 442 | const handleDeleteSceneDialog = () => { 443 | 444 | const chaps = getChapters(bookId) 445 | 446 | setChapterItems(chaps) 447 | 448 | selectChapter(chaps[0].id) 449 | 450 | setChapterName(chaps[0].name) 451 | setChapterDescription(chaps[0].prompt) 452 | 453 | setSceneDialog(false) 454 | 455 | const raw_data = getData(chaps[0].id) 456 | const prev_data = raw_data.map((item) => item.data) 457 | setDataMessages(prev_data) 458 | 459 | } 460 | 461 | const handleAddNewCharacter = () => { 462 | 463 | const newId = addCharacter(bookId, 'New Character', 1, 'You will act as a helpful assistant.') 464 | 465 | setTimeout(() => { 466 | 467 | setCharacterItems(getCharacters(bookId)) 468 | 469 | selectCharacter(newId) 470 | 471 | }, 500) 472 | 473 | } 474 | 475 | const handleOpenCharacterDialog = () => { 476 | setCharacterMode(CharacterDialogModes.Save) 477 | setCharacterDialog(true) 478 | } 479 | 480 | const handleCloseCharacterDialog = () => { 481 | setCharacterDialog(false) 482 | } 483 | 484 | const handleConfirmCharacterDialog = () => { 485 | 486 | const characters = getCharacters(bookId) 487 | setCharacterItems(characters) 488 | 489 | setCharacterDialog(false) 490 | } 491 | 492 | const handleDeleteCharacterDialog = () => { 493 | 494 | const characters = getCharacters(bookId) 495 | 496 | setCharacterItems(characters) 497 | 498 | selectCharacter(characters[0].id) 499 | 500 | setCharacterDialog(false) 501 | 502 | } 503 | 504 | /* 505 | const handleKeyDown = (e) => { 506 | if(e.code === 'Enter') { 507 | e.preventDefault() 508 | e.stopPropagation() 509 | 510 | submitPrompt() 511 | 512 | } 513 | } 514 | */ 515 | 516 | const SelectedChapter = React.useCallback(() => { 517 | return chapterDescription.length > 0 ? { chapterName } - { chapterDescription } : { chapterName } 518 | }, [chapterName, chapterDescription]) 519 | 520 | const handleDeleteMessage = (pid) => { 521 | 522 | deleteData(pid) 523 | setDataMessages((prev) => { 524 | let items = prev.slice(0).filter((item) => item.pid !== pid) 525 | return items 526 | }) 527 | 528 | } 529 | 530 | const deleteMessages = () => { 531 | 532 | setDeleteMode(chatMode === ChatModes.Person ? DeleteModes.Character : DeleteModes.Scene) 533 | setDeleteDialog(true) 534 | 535 | } 536 | 537 | const handleChangeMode = (mode) => { 538 | setChatMode(mode) 539 | scrollToTop() 540 | } 541 | 542 | const handleDeleteDialog = () => { 543 | 544 | let data = dataMessages.slice(0) 545 | 546 | if(chatMode === ChatModes.Person && deleteMode === DeleteModes.Character) { 547 | 548 | data = data.filter((item) => { 549 | if(item.type === 'assistant') { 550 | return item.id !== characterId 551 | } else { 552 | return item.character !== characterId 553 | } 554 | }) 555 | 556 | let saved_data = [] 557 | 558 | if(data.length > 0) { 559 | saved_data = data.map((item) => { 560 | return { 561 | id: item.pid, 562 | sid: chapterId, 563 | data: item, 564 | } 565 | }) 566 | } 567 | 568 | updateData(chapterId, saved_data) 569 | setDataMessages(data) 570 | 571 | } else { 572 | 573 | updateData(chapterId, []) 574 | setDataMessages([]) 575 | 576 | } 577 | 578 | setDeleteDialog(false) 579 | 580 | } 581 | 582 | const handleCloseDeleteDialog = () => { 583 | setDeleteDialog(false) 584 | } 585 | 586 | const getDeleteFlagEnabled = dataMessages.filter((item) => { 587 | if(chatMode === ChatModes.Person) { 588 | if(item.type === 'assistant') { 589 | return item.id === characterId 590 | } else { 591 | return item.character === characterId 592 | } 593 | } else { 594 | return true 595 | } 596 | }).length === 0 597 | 598 | return ( 599 |
600 |
601 |
602 |
603 |
604 | 605 |
606 |

Story - { bookName }

607 |
608 | 609 | 610 | 611 | 612 | 613 |
614 |
615 |
616 | 617 |
618 |
619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 |
628 | 629 | 634 | { 635 | chapterItems.length > 0 && 636 | chapterItems.map((item) => { 637 | const text = item.prompt.length > 0 ? ` - ${item.prompt}` : '' 638 | return ( 639 | 644 | {item.name}{ text } 645 | 646 | ) 647 | }) 648 | } 649 | 650 | 651 |
652 |
653 |
654 |
655 | { 656 | characterItems.length > 0 && 657 | characterItems.map((item) => { 658 | return ( 659 | 667 | ) 668 | }) 669 | } 670 |
671 |
672 |
673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 |
682 |
683 |
684 |
685 |
686 | { 687 | dataMessages.filter((item) => { 688 | if(chatMode === ChatModes.Person) { 689 | if(item.type === 'assistant') { 690 | return item.id === characterId 691 | } else { 692 | return item.character === characterId 693 | } 694 | } else { 695 | return true 696 | } 697 | }).length > 0 && 698 | dataMessages.filter((item) => { 699 | if(chatMode === ChatModes.Person) { 700 | if(item.type === 'assistant') { 701 | return item.id === characterId 702 | } else { 703 | return item.character === characterId 704 | } 705 | } else { 706 | return true 707 | } 708 | }).map((item, index) => { 709 | 710 | let icon = item?.icon || 0 711 | 712 | if(item.type === 'assistant') { 713 | let char = characterItems.find((citem) => citem.id === item.id) 714 | if(char) { 715 | icon = char.icon 716 | } 717 | } 718 | 719 | return ( 720 |
721 | handleDeleteMessage(item.pid)} 727 | /> 728 |
729 | ) 730 | }) 731 | } 732 | { 733 | sendFlag && 734 | 735 | } 736 |
737 |
738 | 739 |
740 |
741 |
742 |
743 | 744 | 750 | 751 | 752 | 753 |
754 |
755 | 756 | 757 | setInputText(e.target.value)} 766 | InputProps={{ 767 | startAdornment: ( 768 | 769 | 770 | 771 | 772 | 773 | ), 774 | endAdornment: ( 775 | 776 | <> 777 | setInputText('')} 780 | > 781 | 782 | 783 | 787 | 788 | 789 | 790 | 791 | ), 792 | }} 793 | /> 794 | 795 | 796 |
797 |
798 | { 799 | openDeleteDialog && createPortal( 800 | , 805 | document.body, 806 | ) 807 | } 808 | { 809 | openBookDialog && createPortal( 810 | , 811 | document.body, 812 | ) 813 | } 814 | { 815 | openSceneDialog && createPortal( 816 | , 824 | document.body, 825 | ) 826 | } 827 | { 828 | openCharacterDialog && createPortal( 829 | , 838 | document.body, 839 | ) 840 | } 841 | { 842 | openUserDialog && createPortal( 843 | , 848 | document.body, 849 | ) 850 | } 851 |
852 | ) 853 | } -------------------------------------------------------------------------------- /app/sandbox.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #f5f5f5; 3 | position: relative; 4 | height: 100vh; 5 | } 6 | 7 | .header { 8 | /*box-shadow: rgba(50, 50, 93, 0.125) 0px 12px 25px -5px, rgba(0, 0, 0, 0.2) 0px 5px 10px -5px;*/ 9 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; 10 | background-color: #fff; 11 | position: relative; 12 | height: 175px; 13 | } 14 | 15 | .toolbar { 16 | background-color: #000; 17 | position: relative; 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | height: 50px; 22 | } 23 | .toolIcon { 24 | /*border: 1px solid rgba(255,255,255,0.5);*/ 25 | background-color: #F2A900; /*#00bd7e;*/ 26 | width: 24px; 27 | height: 24px; 28 | box-sizing: border-box; 29 | border-radius: 50%; 30 | flex-shrink: 0; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | } 35 | .title { 36 | position: relative; 37 | font-size: 1rem; 38 | margin: 0; 39 | margin-left: .5rem; 40 | color: #fff; 41 | } 42 | .toolTitle { 43 | width: calc(100% - 90px); 44 | white-space: nowrap; 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | padding-left: 1rem; 48 | box-sizing: border-box; 49 | display: flex; 50 | align-items: center; 51 | } 52 | 53 | .scenario { 54 | position: relative; 55 | display: flex; 56 | justify-content: space-between; 57 | align-items: center; 58 | height: 50px; 59 | } 60 | .scenarioTitle { 61 | width: calc(100% - 90px); 62 | white-space: nowrap; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | padding-left: 1rem; 66 | box-sizing: border-box; 67 | cursor: pointer; 68 | } 69 | .scenarioTitle .text { 70 | font-size: 1rem; 71 | } 72 | .scenarioControl { 73 | width: 90px; 74 | display: flex; 75 | justify-content: flex-end; 76 | align-items: center; 77 | } 78 | 79 | .systems { 80 | position: relative; 81 | display: flex; 82 | justify-content: space-between; 83 | align-items: flex-start; 84 | height: 75px; 85 | } 86 | .systemItems { 87 | position: relative; 88 | width: calc(100% - 90px); 89 | overflow-x: auto; 90 | overflow-y: hidden; 91 | padding-left: 1rem; 92 | box-sizing: border-box; 93 | -ms-overflow-style: none; 94 | scrollbar-width: none; 95 | } 96 | .systemItems::-webkit-scrollbar { 97 | display: none; 98 | } 99 | 100 | .avatars { 101 | position: relative; 102 | display: flex; 103 | height: 75px; 104 | } 105 | .systemControl { 106 | width: 90px; 107 | display: flex; 108 | justify-content: flex-end; 109 | align-items: center; 110 | } 111 | 112 | .main { 113 | position: relative; 114 | height: calc(100vh - 255px); 115 | } 116 | .mainMessages { 117 | position: relative; 118 | height: 100%; 119 | overflow-y: auto; 120 | box-sizing: border-box; 121 | z-index: 1; 122 | -ms-overflow-style: none; 123 | scrollbar-width: none; 124 | } 125 | .mainMessages::-webkit-scrollbar { 126 | display: none; 127 | } 128 | 129 | .mainTop { 130 | position: absolute; 131 | right: 1rem; 132 | top: 1rem; 133 | z-index: 2; 134 | } 135 | .mainBottom { 136 | position: absolute; 137 | right: 1rem; 138 | top: -5rem; 139 | z-index: 3; 140 | } 141 | 142 | .input { 143 | /*box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px;*/ 144 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; 145 | background-color: #fff9; 146 | position: fixed; 147 | left: 0; 148 | bottom: 0; 149 | width: 100%; 150 | z-index: 1000; 151 | } 152 | .inputDiv { 153 | position: relative; 154 | margin: 1rem; 155 | } 156 | 157 | /* 158 | .avatarItem { 159 | position: relative; 160 | width: 65px; 161 | margin-right: 2px; 162 | flex-shrink: 0; 163 | display: flex; 164 | flex-direction: column; 165 | align-items: center; 166 | } 167 | .avatarItem:last-child { 168 | margin-right: 0; 169 | } 170 | .avatarItemText { 171 | width: 100%; 172 | white-space: nowrap; 173 | overflow: hidden; 174 | text-overflow: ellipsis; 175 | text-align: center; 176 | } 177 | .avatarText { 178 | font-size: .6rem; 179 | } 180 | */ 181 | 182 | .messageItem { 183 | position: relative; 184 | margin: 1rem; 185 | } 186 | .messageItem:first-child { 187 | margin-top: 4rem; 188 | } 189 | .messageItem:last-child { 190 | margin-bottom: 5rem; /*6rem*/ 191 | } 192 | 193 | .sendingDiv { 194 | background-color: #F2A900; 195 | position: relative; 196 | } 197 | .sendingText { 198 | color: green; 199 | } 200 | 201 | @media (prefers-color-scheme: dark) { 202 | 203 | .container { 204 | background-color: #333; 205 | color: #fff; 206 | } 207 | 208 | .header { 209 | background-color: #555; 210 | } 211 | 212 | /* 213 | .toolbar { 214 | background-color: #000; 215 | } 216 | */ 217 | 218 | .input { 219 | background-color: #555; 220 | } 221 | 222 | } -------------------------------------------------------------------------------- /assets/openai-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/stories.json: -------------------------------------------------------------------------------- 1 | { 2 | "books": [ 3 | { "id": "str0001", "name": "The Wonderful Wizard of Oz", "prompt": "" }, 4 | { "id": "str0002", "name": "LOTR", "prompt": "" } 5 | ], 6 | "chapters": [ 7 | { 8 | "sid": "str0001", 9 | "id": "cha0001", 10 | "name": "Land of Oz", 11 | "prompt": "A farmhouse just landed somewhere in the Munchkin Country and killed the Wicked Witch of the East.", 12 | "characters": [ 13 | { "cid": "chr0001", "prompt": "You arrived at the scene together with 3 Munchkins." } 14 | ], 15 | "user": "I just woke up and find myself in a strange land." 16 | }, 17 | { 18 | "sid": "str0001", 19 | "id": "cha0002", 20 | "name": "Down the Yellow Brick Road", 21 | "prompt": "", 22 | "characters": [ 23 | { "cid": "chr0002", "prompt": "You have been hanging on a pole in the corn field for two days now." }, 24 | { "cid": "chr0009", "prompt": "You were caught in the rain and you rusted in the forest, unable to move." }, 25 | { "cid": "chr0010", "prompt": "You were lurking on the forest waiting for someone to past for you to pounce." } 26 | ], 27 | "user": "I am traveling in the Yellow Brick Road." 28 | }, 29 | { 30 | "sid": "str0001", 31 | "id": "cha0007", 32 | "name": "Emerald City", 33 | "prompt": "The Emerald City is the capital of Oz. Before any visitors can enter, they should put on green tinted spectacles as instructed by the Guardian of the Gates.", 34 | "characters": [ 35 | { "cid": "chr0011", "prompt": "You are very reluctant to meet anyone and you appear in different forms trying to hide your true identity." } 36 | ], 37 | "user": "I am very excited to finally get to meet the great Wizard of Oz." 38 | }, 39 | { 40 | "sid": "str0002", 41 | "id": "cha0003", 42 | "name": "The Shire", 43 | "prompt": "Bilbo Baggins is preparing for the celebration of his birthday to be held in a few days.", 44 | "characters": [ 45 | { "cid": "chr0003", "prompt": "You are planning to go on a journey to leave the Shire after the party. You are very anxious about your possession of the One Ring." }, 46 | { "cid": "chr0007", "prompt": "You suspect that the ring in possession of Bilbo is the powerful Ring of Power lost by the Dark Lord Sauron long ago." } 47 | ], 48 | "user": "I am excited for the upcoming party of my uncle Bilbo." 49 | }, 50 | { 51 | "sid": "str0002", 52 | "id": "cha0006", 53 | "name": "Moria", 54 | "prompt": "After a failed attempt to cross the Misty Mountain over the Redhorn pass, the Fellowship take the perilous path through the Mines of Moria.", 55 | "characters": [ 56 | { "cid": "chr0007", "prompt": "You are leading Jojo and the rest of the Fellowship through the tunnels of Moria." }, 57 | { "cid": "chr0008", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." }, 58 | { "cid": "chr0020", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." }, 59 | { "cid": "chr0021", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." }, 60 | { "cid": "chr0023", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." }, 61 | { "cid": "chr0022", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." }, 62 | { "cid": "chr0024", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." }, 63 | { "cid": "chr0025", "prompt": "You and Jojo and the rest of the Fellowship follow Gandalf through the tunnels of Moria." } 64 | ], 65 | "user": "" 66 | } 67 | ], 68 | "characters": [ 69 | { 70 | "sid": "str0001", 71 | "id": "chr0001", 72 | "icon": 0, 73 | "name": "Good Witch of the North", 74 | "prompt": "In this session we will simulate a conversation with the Good Witch of the North. You will act as the Good Witch of the North, sometimes named Lacosta or Tattypoo, a fictional character in the Land of Oz from the book The Wonderful Wizard of Oz." 75 | }, 76 | { 77 | "sid": "str0001", 78 | "id": "chr0002", 79 | "icon": 1, 80 | "name": "Scarecrow", 81 | "prompt": "In this session we will simulate a conversation with the Scarecrow. You will act as the Scarecrow, a fictional character in the Land of Oz from the book The Wonderful Wizard of Oz. You desire to have brain above all else." 82 | }, 83 | { 84 | "sid": "str0001", 85 | "id": "chr0009", 86 | "icon": 1, 87 | "name": "Tin Man", 88 | "prompt": "In this session we will simulate a conversation with the Tin Woodman. You will act as the Tin Woodman also known as the Tin Man, a fictional character in the Land of Oz from the book The Wonderful Wizard of Oz. You desire to have a heart." 89 | }, 90 | { 91 | "sid": "str0001", 92 | "id": "chr0010", 93 | "icon": 5, 94 | "name": "Lion", 95 | "prompt": "In this session we will simulate a conversation with the Cowardly Lion. You will act as the Cowardly Lion, a fictional character in the Land of Oz from the book The Wonderful Wizard of Oz. You feel that your fear makes you inadequate so you desire to have courage." 96 | }, 97 | { 98 | "sid": "str0001", 99 | "id": "chr0011", 100 | "icon": 6, 101 | "name": "Wizard of Oz", 102 | "prompt": "In this session we will simulate a conversation with the Wizard of Oz. You will act as the great and powerful Wizard of Oz, a fictional character in the Land of Oz from the book The Wonderful Wizard of Oz. You are the venerated ruler of the land of Oz." 103 | }, 104 | { 105 | "sid": "str0002", 106 | "id": "chr0003", 107 | "icon": 1, 108 | "name": "Bilbo", 109 | "prompt": "In this session we will simulate a conversation with Bilbo Baggins. You will act as the hobbit Bilbo Baggins, a fictional character in the Lord of the Rings. Jojo is your nephew." 110 | }, 111 | { 112 | "sid": "str0002", 113 | "id": "chr0007", 114 | "icon": 0, 115 | "name": "Gandalf", 116 | "prompt": "In this session we will simulate a conversation with Gandalf the Grey. You will act as the wizard Gandalf the Grey, a fictional character in the Lord of the Rings." 117 | }, 118 | { 119 | "sid": "str0002", 120 | "id": "chr0008", 121 | "icon": 1, 122 | "name": "Aragorn", 123 | "prompt": "In this session we will simulate a conversation with Aragorn. You will act as Aragorn, also known as the Strider, a Ranger of the North, and a confidant of Gandalf, a fictional character in the Lord of the Rings." 124 | }, 125 | { 126 | "sid": "str0002", 127 | "id": "chr0020", 128 | "icon": 1, 129 | "name": "Legolas", 130 | "prompt": "In this session we will simulate a conversation with Legolas. You will act as Legolas, a Woodland Elf from Northern Mirkwood, a fictional character in the Lord of the Rings." 131 | }, 132 | { 133 | "sid": "str0002", 134 | "id": "chr0021", 135 | "icon": 5, 136 | "name": "Gimli", 137 | "prompt": "In this session we will simulate a conversation with Gimli. You will act as Gimli, a dwarf warrior representing the Dwarves as a member of the Fellowship of the Ring, a fictional character in the Lord of the Rings." 138 | }, 139 | { 140 | "sid": "str0002", 141 | "id": "chr0023", 142 | "icon": 5, 143 | "name": "Sam", 144 | "prompt": "In this session we will simulate a conversation with Sam Gamgee. You will act as the hobbit Samwise Gamgee usually called Sam, sidekick and gardener of Jojo, a fictional character in the Lord of the Rings." 145 | }, 146 | { 147 | "sid": "str0002", 148 | "id": "chr0022", 149 | "icon": 1, 150 | "name": "Boromir", 151 | "prompt": "In this session we will simulate a conversation with Boromir. You will act as Boromir, heir to Denethor II the Steward of Gondor, and one of the representatives of Man in the Fellowship of the Ring, a fictional character in the Lord of the Rings." 152 | }, 153 | { 154 | "sid": "str0002", 155 | "id": "chr0024", 156 | "icon": 5, 157 | "name": "Pippin", 158 | "prompt": "In this session we will simulate a conversation with Pippin. You will act as the hobbit Peregrin Took simply called Pippin, closely tied with his friend and cousin, Merry Brandybuck, a fictional character in the Lord of the Rings." 159 | }, 160 | { 161 | "sid": "str0002", 162 | "id": "chr0025", 163 | "icon": 5, 164 | "name": "Merry", 165 | "prompt": "In this session we will simulate a conversation with Merry. You will act as the hobbit Merry Brandybuck simply called Merry, closely tied with his friend and cousin, Peregrin Took, a fictional character in the Lord of the Rings." 166 | }, 167 | { 168 | "sid": "str0002", 169 | "id": "chr0026", 170 | "icon": 8, 171 | "name": "Gollum", 172 | "prompt": "In this session we will simulate a conversation with Gollum. You will act as Gollum, originally known as Smeagol, a small and slimy creature, corrupted of the One Ring, has been following the Fellowship of the Ring from Moria, a fictional character in the Lord of the Rings." 173 | } 174 | ], 175 | "users": [ 176 | { 177 | "sid": "str0001", 178 | "id": "usr0001", 179 | "name": "Mark", 180 | "prompt": "I am a young person much like the character of Dorothy but is known by different name. I came to the Land of Oz with my dog." 181 | }, 182 | { 183 | "sid": "str0002", 184 | "id": "usr0002", 185 | "name": "Jojo", 186 | "prompt": "I am a young person much like the character of Frodo but is know as Jojo in this session." 187 | } 188 | ] 189 | } -------------------------------------------------------------------------------- /components/avataritem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Avatar from '@mui/material/Avatar' 4 | import Badge from '@mui/material/Badge' 5 | 6 | import { CharacterIcon } from './charactericon' 7 | import CustomTheme from './customtheme' 8 | 9 | import classes from './avataritem.module.css' 10 | 11 | const AvatarItem = ({ 12 | id = '', 13 | name = '', 14 | icon = 0, 15 | selected = '', 16 | onClick = undefined 17 | }) => { 18 | /* 19 | 24 | 25 | { 26 | icon === 0 && 27 | 28 | } 29 | { 30 | icon > 0 && 31 | 32 | } 33 | 34 | 35 | */ 36 | return ( 37 |
onClick(id)}> 38 | { 39 | selected && 40 | 41 | { 42 | icon === 0 && 43 | 44 | } 45 | { 46 | icon > 0 && 47 | 48 | } 49 | 50 | } 51 | { 52 | !selected && 53 | 54 | 55 | { 56 | icon === 0 && 57 | 58 | } 59 | { 60 | icon > 0 && 61 | 62 | } 63 | 64 | 65 | } 66 |
67 | { name } 68 |
69 |
70 | ) 71 | } 72 | 73 | export default AvatarItem 74 | -------------------------------------------------------------------------------- /components/avataritem.module.css: -------------------------------------------------------------------------------- 1 | .avatarItem { 2 | position: relative; 3 | width: 65px; 4 | height: 75px; 5 | margin-right: 2px; 6 | flex-shrink: 0; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | .avatarItem:last-child { 12 | margin-right: 0; 13 | } 14 | 15 | .avatarItemText { 16 | width: 100%; 17 | white-space: nowrap; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | text-align: center; 21 | } 22 | 23 | .avatarText { 24 | font-size: .6rem; 25 | } 26 | -------------------------------------------------------------------------------- /components/bookdialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import InputAdornment from '@mui/material/InputAdornment'; 5 | import TextField from '@mui/material/TextField'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Button from '@mui/material/Button' 8 | import FormControl from '@mui/material/FormControl' 9 | import InputLabel from '@mui/material/InputLabel' 10 | import Select from '@mui/material/Select' 11 | import MenuItem from '@mui/material/MenuItem' 12 | import Divider from '@mui/material/Divider' 13 | import ClearIcon from '@mui/icons-material/Clear'; 14 | 15 | import useBookStore from '../stores/bookStore' 16 | import CustomTheme from './customtheme'; 17 | import classes from './bookdialog.module.css' 18 | 19 | const ADD_NEW_KEY = 'ADD_NEW_STORY' 20 | const STORY_PLACEHOLDER = 'Enter Story Title' 21 | 22 | export default function BookDialog({ 23 | onConfirm = undefined, 24 | onClose = undefined, 25 | }) { 26 | 27 | const books = useBookStore((state) => state.books) 28 | const defBookId = useBookStore((state) => state.bookId) 29 | 30 | const getBook = useBookStore((state) => state.getBook) 31 | const editBook = useBookStore((state) => state.editBook) 32 | const addBook = useBookStore((state) => state.addBook) 33 | const deleteBook = useBookStore((state) => state.deleteBook) 34 | 35 | const [bookId, setBookId] = React.useState('') 36 | const [prevTitle, setPrevTitle] = React.useState(STORY_PLACEHOLDER) 37 | const [title, setTitle] = React.useState('') 38 | const [storyPrompt, setStoryPrompt] = React.useState('') 39 | const [isDeleteFlag, setDeleteFlag] = React.useState(false) 40 | const [isDeleteDef, setDeleteDef] = React.useState(false) 41 | 42 | React.useEffect(() => { 43 | 44 | setBookId(defBookId) 45 | 46 | }, []) 47 | 48 | React.useEffect(() => { 49 | 50 | setDeleteFlag(books.length === 1 ? true : false) 51 | 52 | }, [books]) 53 | 54 | React.useEffect(() => { 55 | 56 | if(bookId) { 57 | 58 | if(bookId === ADD_NEW_KEY) { 59 | 60 | setPrevTitle(STORY_PLACEHOLDER) 61 | setTitle('Untitled Story') 62 | setStoryPrompt('') 63 | 64 | } else { 65 | 66 | const book = getBook(bookId) 67 | setPrevTitle(book.name) 68 | setTitle(book.name) 69 | setStoryPrompt(book.prompt) 70 | 71 | } 72 | 73 | } 74 | 75 | }, [bookId]) 76 | 77 | const handleBook = () => { 78 | 79 | onConfirm(bookId) 80 | 81 | } 82 | 83 | const handleSelect = (e) => { 84 | 85 | setBookId(e.target.value) 86 | 87 | } 88 | 89 | const handleAdd = () => { 90 | 91 | const newId = addBook(title, storyPrompt) 92 | 93 | setTimeout(() => { 94 | setBookId(newId) 95 | }, 500) 96 | 97 | } 98 | 99 | const handleEdit = () => { 100 | 101 | const book = { 102 | id: bookId, 103 | name: title, 104 | prompt: storyPrompt, 105 | } 106 | 107 | editBook(bookId, book) 108 | 109 | } 110 | 111 | const handleDelete = () => { 112 | 113 | const notSelBookId = books.find((item) => item.id !== bookId).id 114 | 115 | deleteBook(bookId) 116 | 117 | setBookId(notSelBookId) 118 | 119 | if(defBookId === bookId) { 120 | setDeleteDef(true) 121 | } 122 | 123 | } 124 | 125 | const handleClose = () => { 126 | 127 | onClose(isDeleteDef ? bookId : defBookId) 128 | 129 | } 130 | 131 | const handleClick = (e) => { 132 | e.stopPropagation() 133 | e.preventDefault() 134 | } 135 | 136 | return ( 137 | 138 |
139 |
140 |
141 | 144 | Story 145 | 161 | 162 |
163 |
164 | 165 | setTitle(e.target.value)} 172 | InputProps={{ 173 | endAdornment: ( 174 | 175 | setTitle('')} 178 | > 179 | 180 | 181 | 182 | ), 183 | }} 184 | /> 185 | 186 |
187 |
188 | 189 | setStoryPrompt(e.target.value)} 197 | InputProps={{ 198 | endAdornment: ( 199 | 200 | setStoryPrompt('')} 203 | > 204 | 205 | 206 | 207 | ), 208 | }} 209 | /> 210 | 211 |
212 |
213 | { 214 | bookId === ADD_NEW_KEY && 215 |
216 | 219 |
220 | } 221 | { 222 | bookId !== ADD_NEW_KEY && 223 |
224 | 227 |
228 | } 229 |
230 | { 231 | bookId !== ADD_NEW_KEY && 232 | 238 | } 239 | 242 | 243 |
244 |
245 |
246 |
247 |
248 | ) 249 | } 250 | 251 | BookDialog.propTypes = { 252 | /** 253 | * Confirm event handler 254 | */ 255 | onConfirm: PropTypes.func, 256 | /** 257 | * Close event handler 258 | */ 259 | onClose: PropTypes.func 260 | } -------------------------------------------------------------------------------- /components/bookdialog.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #0009; 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | justify-content: center; 10 | align-items: flex-start; 11 | z-index: 1000; 12 | } 13 | 14 | .dialog { 15 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 16 | position: relative; 17 | background-color: #f5f5f5; 18 | width: 90%; 19 | border-radius: 5px; 20 | padding: 1rem; 21 | box-sizing: border-box; 22 | margin-top: 1rem; 23 | } 24 | 25 | .item { 26 | position: relative; 27 | margin-bottom: 1.5rem; 28 | } 29 | .item:first-child { 30 | margin-top: .5rem; 31 | } 32 | .item:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | .action { 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | 44 | .dialog { 45 | background-color: #555; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /components/bookdialog.stories.js: -------------------------------------------------------------------------------- 1 | import BookDialog from './bookdialog'; 2 | 3 | export default { 4 | title: 'ChatGPT/BookDialog', 5 | component: BookDialog, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onConfirm: { action: 'confirm' }, 9 | onClose: { action: 'close' }, 10 | }, 11 | }; 12 | 13 | export const Primary = {}; 14 | 15 | -------------------------------------------------------------------------------- /components/characterdialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import InputAdornment from '@mui/material/InputAdornment'; 5 | import TextField from '@mui/material/TextField'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Button from '@mui/material/Button' 8 | import FormControl from '@mui/material/FormControl' 9 | import InputLabel from '@mui/material/InputLabel' 10 | import Select from '@mui/material/Select' 11 | import MenuItem from '@mui/material/MenuItem' 12 | import ClearIcon from '@mui/icons-material/Clear'; 13 | 14 | import useAppStore from '../stores/appStore' 15 | import useBookStore from '../stores/bookStore' 16 | import { CharacterIcon, iconCount } from './charactericon'; 17 | import CustomTheme from './customtheme'; 18 | 19 | import classes from './characterdialog.module.css' 20 | 21 | export const DialogModes = { 22 | Add: 'add', 23 | Save: 'save', 24 | } 25 | 26 | const iconList = Array(iconCount).fill(0) 27 | 28 | export default function CharacterDialog({ 29 | mode = DialogModes.Save, 30 | bookId = '', 31 | chapterId = '', 32 | characterId = '', 33 | onConfirm = undefined, 34 | onDelete = undefined, 35 | onClose = undefined, 36 | }) { 37 | 38 | const isDarkMode = useAppStore((state) => state.darkMode) 39 | 40 | const getChapter = useBookStore((state) => state.getChapter) 41 | const getCharacter = useBookStore((state) => state.getCharacter) 42 | const getCharacters = useBookStore((state) => state.getCharacters) 43 | 44 | const editChapter = useBookStore((state) => state.editChapter) 45 | const editCharacter = useBookStore((state) => state.editCharacter) 46 | 47 | const addCharacter = useBookStore((state) => state.addCharacter) 48 | const deleteCharacter = useBookStore((state) => state.deleteCharacter) 49 | 50 | const [name, setName] = React.useState('') 51 | const [characterPrompt, setCharacterPrompt] = React.useState('') 52 | const [sceneName, setSceneName] = React.useState('') 53 | const [scenePrompt, setScenePrompt] = React.useState('') 54 | const [icon, setIcon] = React.useState(0) 55 | const [isDeleteFlag, setDeleteFlag] = React.useState(false) 56 | 57 | React.useEffect(() => { 58 | 59 | if(bookId && chapterId && characterId ) { 60 | 61 | const character = getCharacter(characterId) 62 | const chapter = getChapter(chapterId) 63 | 64 | setName(character.name) 65 | setCharacterPrompt(character.prompt) 66 | setIcon(character.icon) 67 | 68 | setSceneName(chapter.name) 69 | 70 | if(chapter.characters.length > 0) { 71 | const scene_item = chapter.characters.find((item) => item.cid === characterId) 72 | if(scene_item) { 73 | const character_scene_prompt = scene_item.prompt 74 | setScenePrompt(character_scene_prompt) 75 | } 76 | } 77 | 78 | const chars = getCharacters(bookId) 79 | setDeleteFlag(chars.length === 1 ? true : false) 80 | 81 | } 82 | 83 | }, [bookId, chapterId, characterId ]) 84 | 85 | const handleSave = () => { 86 | 87 | if(name.trim().length === 0 || characterPrompt.trim().length === 0) { 88 | return 89 | } 90 | 91 | let character = getCharacter(characterId) 92 | let chapter = getChapter(chapterId) 93 | 94 | character.name = name 95 | character.icon = icon 96 | character.prompt = characterPrompt 97 | 98 | let scene_prompts = chapter.characters 99 | if(scene_prompts.length > 0) { 100 | scene_prompts = scene_prompts.filter((item) => item.cid !== characterId) 101 | } 102 | if(scenePrompt.length > 0) { 103 | scene_prompts.push({ cid: characterId, prompt: scenePrompt }) 104 | } 105 | 106 | chapter.characters = scene_prompts 107 | 108 | editCharacter(characterId, character) 109 | editChapter(chapterId, chapter) 110 | 111 | onConfirm() 112 | 113 | } 114 | 115 | const handleAdd = () => { 116 | 117 | addCharacter(bookId, name, icon, characterPrompt) 118 | 119 | onConfirm() 120 | 121 | } 122 | 123 | const handleDelete = () => { 124 | 125 | deleteCharacter(characterId) 126 | 127 | onDelete() 128 | 129 | } 130 | 131 | const handleClick = (e) => { 132 | e.stopPropagation() 133 | e.preventDefault() 134 | } 135 | 136 | return ( 137 | 138 |
139 |
140 |
141 | 142 | setName(e.target.value)} 149 | InputProps={{ 150 | endAdornment: ( 151 | 152 | setName('')} 155 | > 156 | 157 | 158 | 159 | ), 160 | }} 161 | /> 162 | 163 |
164 |
165 | 166 | setCharacterPrompt(e.target.value)} 175 | InputProps={{ 176 | endAdornment: ( 177 | 178 | setCharacterPrompt('')} 181 | > 182 | 183 | 184 | 185 | ), 186 | }} 187 | /> 188 | 189 |
190 | { 191 | mode === DialogModes.Save && 192 |
193 | 194 | setScenePrompt(e.target.value)} 202 | InputProps={{ 203 | endAdornment: ( 204 | 205 | setScenePrompt('')} 208 | > 209 | 210 | 211 | 212 | ), 213 | }} 214 | /> 215 | 216 |
217 | } 218 |
219 |
220 | 223 | Icon 224 | 240 | 241 |
242 |
243 | { 244 | mode === DialogModes.Save && 245 | 246 | } 247 | { 248 | mode === DialogModes.Save && 249 | 250 | } 251 | { 252 | mode === DialogModes.Add && 253 | 254 | } 255 | 256 |
257 |
258 |
259 |
260 |
261 | ) 262 | } 263 | 264 | CharacterDialog.propTypes = { 265 | /** 266 | * Mode of operation 267 | */ 268 | mode: PropTypes.oneOf(['add', 'save']), 269 | /** 270 | * BookId string 271 | */ 272 | bookId: PropTypes.string, 273 | /** 274 | * ChapterId string 275 | */ 276 | chapterId: PropTypes.string, 277 | /** 278 | * CharacterId string 279 | */ 280 | characterId: PropTypes.string, 281 | /** 282 | * Confirm event handler 283 | */ 284 | onConfirm: PropTypes.func, 285 | /** 286 | * Close event handler 287 | */ 288 | onClose: PropTypes.func, 289 | /** 290 | * Delete event handler 291 | */ 292 | onDelete: PropTypes.func, 293 | } -------------------------------------------------------------------------------- /components/characterdialog.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #0009; 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | justify-content: center; 10 | align-items: flex-start; 11 | z-index: 1000; 12 | } 13 | 14 | .dialog { 15 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 16 | position: relative; 17 | background-color: #f5f5f5; 18 | width: 90%; 19 | border-radius: 5px; 20 | padding: 1rem; 21 | box-sizing: border-box; 22 | margin-top: 1rem; 23 | } 24 | 25 | .item { 26 | position: relative; 27 | margin-bottom: 1.5rem; 28 | } 29 | .item:first-child { 30 | margin-top: .5rem; 31 | } 32 | .item:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | .action { 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | 44 | .dialog { 45 | background-color: #555; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /components/characterdialog.stories.js: -------------------------------------------------------------------------------- 1 | import CharacterDialog, { DialogModes } from './characterdialog'; 2 | 3 | export default { 4 | title: 'ChatGPT/CharacterDialog', 5 | component: CharacterDialog, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onConfirm: { action: 'confirm' }, 9 | onClose: { action: 'close' }, 10 | }, 11 | }; 12 | 13 | export const Primary = { 14 | args: { 15 | mode: DialogModes.Save, 16 | bookId: 'str0001', 17 | chapterId: 'cha0002', 18 | characterId: 'chr0002', 19 | }, 20 | }; 21 | 22 | export const Add = { 23 | args: { 24 | mode: DialogModes.Add, 25 | bookId: 'str0001', 26 | chapterId: 'cha0002', 27 | characterId: '', 28 | }, 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /components/charactericon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import SvgIcon from '@mui/material/SvgIcon'; 4 | 5 | import FaceIcon from '@mui/icons-material/Face'; 6 | import Face2Icon from '@mui/icons-material/Face2'; 7 | import Face3Icon from '@mui/icons-material/Face3'; 8 | import Face4Icon from '@mui/icons-material/Face4'; 9 | import Face5Icon from '@mui/icons-material/Face5'; 10 | import Face6Icon from '@mui/icons-material/Face6'; 11 | 12 | import MoodIcon from '@mui/icons-material/Mood'; 13 | import MoodBadIcon from '@mui/icons-material/MoodBad'; 14 | import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAlt'; 15 | import SentimentNeutralIcon from '@mui/icons-material/SentimentNeutral'; 16 | import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; 17 | import SickIcon from '@mui/icons-material/Sick'; 18 | 19 | import PrecisionIcon from '@mui/icons-material/PrecisionManufacturing'; 20 | import CastleIcon from '@mui/icons-material/Castle'; 21 | import FortIcon from '@mui/icons-material/Fort'; 22 | import PetsIcon from '@mui/icons-material/Pets'; 23 | import ComputerIcon from '@mui/icons-material/Computer'; 24 | import AndroidIcon from '@mui/icons-material/Android'; 25 | import AssignmentIcon from '@mui/icons-material/Assignment'; 26 | 27 | import CustomTheme from './customtheme'; 28 | 29 | const OpenAiIcon = (props) => { 30 | return ( 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export const iconCount = 20 38 | 39 | export const CharacterIcon = ({ 40 | icon = 0, 41 | color = '#fff' 42 | }) => { 43 | 44 | switch(icon) { 45 | case 1: 46 | return 47 | case 2: 48 | return 49 | case 3: 50 | return 51 | case 4: 52 | return 53 | case 5: 54 | return 55 | case 6: 56 | return 57 | case 7: 58 | return 59 | case 8: 60 | return 61 | case 9: 62 | return 63 | case 10: 64 | return 65 | case 11: 66 | return 67 | case 12: 68 | return 69 | 70 | case 13: 71 | return 72 | case 14: 73 | return 74 | case 15: 75 | return 76 | case 16: 77 | return 78 | case 17: 79 | return 80 | case 18: 81 | return 82 | case 19: 83 | return 84 | 85 | default: 86 | return 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /components/contentitem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types'; 3 | 4 | import Avatar from '@mui/material/Avatar'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import PersonIcon from '@mui/icons-material/Person'; 7 | import ClearIcon from '@mui/icons-material/Clear'; 8 | 9 | import CustomTheme from './customtheme'; 10 | import useAppStore from '../stores/appStore' 11 | import { CharacterIcon } from './charactericon' 12 | 13 | import classes from './contentitem.module.css' 14 | 15 | const SelectedSystemAvatar = ({ 16 | icon = 0, 17 | color = '#1affb2' 18 | }) => { 19 | return ( 20 | 21 | 22 | 23 | ) 24 | 25 | } 26 | 27 | export default function ContentItem({ 28 | role = '', 29 | content = '', 30 | name = '', 31 | icon = '', 32 | onDelete = undefined, 33 | }) { 34 | 35 | const leftRef = React.useRef() 36 | const rightRef = React.useRef() 37 | 38 | const isDarkMode = useAppStore((state) => state.darkMode) 39 | 40 | const handleSelect = () => { 41 | if(role === 'user') { 42 | 43 | rightRef.current.focus() 44 | 45 | window.getSelection() 46 | .selectAllChildren(rightRef.current) 47 | 48 | 49 | 50 | } else { 51 | 52 | leftRef.current.focus() 53 | 54 | window.getSelection() 55 | .selectAllChildren(leftRef.current) 56 | } 57 | } 58 | 59 | return ( 60 |
64 | { 65 | role !== 'user' && 66 | <> 67 |
68 |
69 | 70 |
71 |
72 | { name } 73 |
74 |
75 |
76 |

77 | { 78 | content 79 | } 80 |

81 |
82 | 83 | 84 | 85 |
86 |
87 | 88 | } 89 | { 90 | role === 'user' && 91 | <> 92 |
93 |

94 | { 95 | content 96 | } 97 |

98 |
99 | 100 | 101 | 102 |
103 |
104 |
105 | 106 | 107 | 110 | 111 | 112 |
113 | 114 | } 115 |
116 | ) 117 | } 118 | 119 | ContentItem.propTypes = { 120 | /** 121 | * Name string 122 | */ 123 | name: PropTypes.string, 124 | /** 125 | * Icon string 126 | */ 127 | icon: PropTypes.number, 128 | /** 129 | * User type 130 | */ 131 | role: PropTypes.string, 132 | /** 133 | * Content data string 134 | */ 135 | content: PropTypes.string, 136 | /** 137 | * Delete click handler 138 | */ 139 | onDelete: PropTypes.func, 140 | } -------------------------------------------------------------------------------- /components/contentitem.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | display: flex; 4 | } 5 | 6 | .icon { 7 | position: relative; 8 | width: 1.5rem; 9 | height: 1.5rem; 10 | } 11 | 12 | .delete { 13 | border: 1px solid rgba(0, 0, 0, 0.125); 14 | background-color: #fff6; 15 | position: absolute; 16 | width: 20px; 17 | height: 20px; 18 | border-radius: 50%; 19 | z-index: 5; 20 | display: none; 21 | justify-content: center; 22 | align-items: center; 23 | box-sizing: border-box; 24 | } 25 | 26 | .panelLeft { 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | } 31 | .name { 32 | position: relative; 33 | width: 60px; 34 | font-size: .7rem; 35 | text-align: center; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | margin-top: .3rem; 40 | margin-right: 7px; 41 | color: #00bd7e;/*8040bf*/ 42 | } 43 | .name span { 44 | font-size: .7rem; 45 | color: #00bd7e; 46 | } 47 | .logoLeft { 48 | position: relative; 49 | width: 1.5rem; 50 | height: 1.5rem; 51 | margin-right: 8px; 52 | margin-top: 5px; 53 | } 54 | .contentLeft { 55 | background-color: #d5f6eb; /*e6d9f2 e6fff7*/ 56 | position: relative; 57 | padding: 10px; 58 | border-radius: 5px; 59 | margin-right: calc(1.5rem + 7px); 60 | } 61 | .contentLeft::before { 62 | content: ''; 63 | height: 0; 64 | width: 0; 65 | border-width: 4px 7px 4px 7px; 66 | border-style: solid; 67 | border-color: transparent #d5f6eb transparent transparent; 68 | position: absolute; 69 | top: 12px; 70 | left: -13px; /*14*/ 71 | } 72 | .contentLeft p { 73 | font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; 74 | font-size: 1rem; /*.9rem*/ 75 | line-height: 160%; 76 | margin: 0; 77 | color: #333; 78 | -webkit-touch-callout: all; 79 | -webkit-user-select: all; 80 | -moz-user-select: all; 81 | -ms-user-select: all; 82 | user-select: all; 83 | } 84 | .contentLeft:hover .delete { 85 | display: flex; 86 | right: -9px; 87 | top: -9px; 88 | } 89 | 90 | 91 | .avatar { 92 | background-color: #dcdcdc; 93 | position: relative; 94 | width: 1.5rem; 95 | height: 1.5rem; 96 | border-radius: 50%; 97 | overflow: hidden; 98 | padding: 3px; 99 | box-sizing: border-box; 100 | } 101 | .logoRight { 102 | position: relative; 103 | width: 1.5rem; 104 | height: 1.5rem; 105 | margin-left: 8px; 106 | margin-top: 5px; 107 | } 108 | .contentRight { 109 | background-color: #ececec; 110 | position: relative; 111 | padding: 10px; 112 | border-radius: 5px; 113 | margin-left: calc(1.5rem + 7px); 114 | } 115 | .contentRight p { 116 | font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; 117 | font-size: 1rem; /*.9rem*/ 118 | line-height: 160%; 119 | margin: 0; 120 | color: #333; 121 | -webkit-touch-callout: all; 122 | -webkit-user-select: all; 123 | -moz-user-select: all; 124 | -ms-user-select: all; 125 | user-select: all; 126 | } 127 | .contentRight::after { 128 | content: ''; 129 | height: 0; 130 | width: 0; 131 | border-width: 4px 7px 4px 7px; 132 | border-style: solid; 133 | border-color: transparent transparent transparent #ececec; /*efefef*/ 134 | position: absolute; 135 | top: 12px; 136 | right: -13px; /*14*/ 137 | } 138 | .contentRight:hover .delete { 139 | display: flex; 140 | left: -9px; 141 | top: -9px; 142 | } 143 | 144 | @media (prefers-color-scheme: dark) { 145 | 146 | .name, .name span { 147 | color: #1affb2;/*bf9fdf 00bd7e*/ 148 | } 149 | 150 | .contentRight { 151 | background-color: #656565; 152 | } 153 | .contentRight::after { 154 | border-color: transparent transparent transparent #656565; 155 | } 156 | .contentRight p { 157 | color: #fff; 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /components/contentitem.stories.js: -------------------------------------------------------------------------------- 1 | import ContentItem from './contentitem'; 2 | 3 | export default { 4 | title: 'ChatGPT/ContentItem', 5 | component: ContentItem, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onDelete: { action: 'delete' }, 9 | }, 10 | }; 11 | 12 | export const Primary = { 13 | args: { 14 | name: 'Gandalf', 15 | role: 'system', 16 | content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' 17 | }, 18 | }; 19 | 20 | export const Icon = { 21 | args: { 22 | role: 'system', 23 | name: 'Boromir', 24 | icon: 5, 25 | content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' 26 | }, 27 | }; 28 | 29 | export const Secondary = { 30 | args: { 31 | role: 'user', 32 | content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' 33 | }, 34 | }; -------------------------------------------------------------------------------- /components/customtheme.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { createTheme, ThemeProvider } from '@mui/material/styles' 4 | 5 | import NoSsr from '@mui/base/NoSsr' 6 | 7 | import useAppStore from '../stores/appStore' 8 | 9 | const darkTheme = createTheme({ 10 | palette: { 11 | mode: 'dark', 12 | } 13 | }) 14 | 15 | const lightTheme = createTheme({ 16 | palette: { 17 | mode: 'light', 18 | } 19 | }) 20 | 21 | export default function CustomTheme({ children }) { 22 | 23 | const isDarkTheme = useAppStore((state) => state.darkMode) 24 | 25 | return ( 26 | 27 | 30 | { children } 31 | 32 | 33 | ) 34 | } -------------------------------------------------------------------------------- /components/deletedialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Button from '@mui/material/Button' 5 | 6 | import CustomTheme from './customtheme' 7 | 8 | import classes from './deletedialog.module.css' 9 | 10 | export const DeleteModes = { 11 | Character: 'character', 12 | Scene: 'scene' 13 | } 14 | 15 | export default function DeleteDialog({ 16 | mode = DeleteModes.Character, 17 | onDelete = undefined, 18 | onClose = undefined 19 | }) { 20 | return ( 21 |
22 |
23 |
24 | { 25 | mode === DeleteModes.Character && 26 |

27 | Are you sure you want to delete this conversation? 28 |

29 | } 30 | { 31 | mode === DeleteModes.Scene && 32 |

33 | This will delete all conversations in this scene.
34 | Are you sure? 35 |

36 | } 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | ) 47 | } 48 | 49 | DeleteDialog.propTypes = { 50 | /** 51 | * Delete mode 52 | */ 53 | mode: PropTypes.oneOf(['character', 'scene']), 54 | /** 55 | * Delete event handler 56 | */ 57 | onDelete: PropTypes.func, 58 | /** 59 | * Close event handler 60 | */ 61 | onClose: PropTypes.func, 62 | } -------------------------------------------------------------------------------- /components/deletedialog.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #0009; 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | z-index: 1000; 12 | } 13 | 14 | .dialog { 15 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 16 | position: relative; 17 | background-color: #f5f5f5; 18 | border-radius: 5px; 19 | padding: 1rem; 20 | box-sizing: border-box; 21 | margin-top: 1rem; 22 | } 23 | 24 | .contents { 25 | position: relative; 26 | } 27 | 28 | .text { 29 | margin: 0; 30 | text-align: center; 31 | padding: 1rem 1rem; 32 | } 33 | 34 | .action { 35 | display: flex; 36 | justify-content: flex-end; 37 | align-items: center; 38 | } 39 | 40 | @media (prefers-color-scheme: dark) { 41 | 42 | .dialog { 43 | background-color: #555; 44 | } 45 | 46 | .text { 47 | color: #fff; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /components/deletedialog.stories.js: -------------------------------------------------------------------------------- 1 | import DeleteDialog, { DeleteModes } from './deletedialog' 2 | 3 | export default { 4 | title: 'ChatGPT/DeleteDialog', 5 | component: DeleteDialog, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onClose: { action: 'close' }, 9 | onDelete: { action: 'delete' }, 10 | mode: { 11 | options: ['character', 'scene'], 12 | control: { type: 'radio' } 13 | } 14 | }, 15 | } 16 | 17 | export const Primary = { 18 | args: { 19 | mode: DeleteModes.Character, 20 | } 21 | } 22 | 23 | export const Secondary = { 24 | args: { 25 | mode: DeleteModes.Scene, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/loadingtext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './loadingtext.module.css' 4 | 5 | const initialData = new Array(7).fill(0) 6 | 7 | export default function LoadingText() { 8 | 9 | const [data, setData] = React.useState(initialData) 10 | 11 | React.useEffect(() => { 12 | 13 | let cnt = 0 14 | 15 | const timer = setInterval(() => { 16 | 17 | setData((n) => { 18 | 19 | let d = n.map((m, i) => { 20 | return 2 * Math.sin(cnt + (2 * Math.PI * ((i + 1)/ 8))) 21 | }) 22 | 23 | return d 24 | }) 25 | 26 | cnt++ 27 | 28 | }, 100) 29 | 30 | return () => { 31 | clearInterval(timer) 32 | } 33 | 34 | }, []) 35 | 36 | return ( 37 |
38 |
39 | { 40 | data.map((n, index) => { 41 | return ( 42 |
47 | ) 48 | }) 49 | } 50 |
51 |
52 | ) 53 | } -------------------------------------------------------------------------------- /components/loadingtext.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | height: 24px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .inner { 10 | position: relative; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | 16 | .item { 17 | background-color: #999; 18 | position: relative; 19 | width: 8px; 20 | height: 8px; 21 | border-radius: 50%; 22 | margin-right: 3px; 23 | } 24 | .item:last-child { 25 | margin-right: 0; 26 | } -------------------------------------------------------------------------------- /components/loadingtext.stories.js: -------------------------------------------------------------------------------- 1 | import LoadingText from './loadingtext' 2 | 3 | export default { 4 | title: 'ChatGPT/LoadingText', 5 | component: LoadingText, 6 | tags: ['autodocs'], 7 | }; 8 | 9 | export const Primary = {} 10 | -------------------------------------------------------------------------------- /components/scenedialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import InputAdornment from '@mui/material/InputAdornment'; 5 | import TextField from '@mui/material/TextField'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Button from '@mui/material/Button' 8 | import FormControl from '@mui/material/FormControl' 9 | 10 | import ClearIcon from '@mui/icons-material/Clear'; 11 | 12 | import useBookStore from '../stores/bookStore' 13 | import CustomTheme from './customtheme'; 14 | 15 | import classes from './scenedialog.module.css' 16 | 17 | export const DialogModes = { 18 | Add: 'add', 19 | Save: 'save', 20 | } 21 | 22 | export default function SceneDialog({ 23 | mode = DialogModes.Save, 24 | bookId = '', 25 | chapterId = '', 26 | onConfirm = undefined, 27 | onClose = undefined, 28 | onDelete = undefined, 29 | }) { 30 | 31 | //const getUser = useBookStore((state) => state.getUserByBookId) 32 | const getChapter = useBookStore((state) => state.getChapter) 33 | const getChapters = useBookStore((state) => state.getChapters) 34 | //const getCharacter = useBookStore((state) => state.getCharacter) 35 | 36 | //const editUser = useBookStore((state) => state.editUser) 37 | const editChapter = useBookStore((state) => state.editChapter) 38 | //const editCharacter = useBookStore((state) => state.editCharacter) 39 | 40 | //const addCharacter = useBookStore((state) => state.addCharacter) 41 | const addChapter = useBookStore((state) => state.addChapter) 42 | 43 | const deleteChapter = useBookStore((state) => state.deleteChapter) 44 | 45 | const [name, setName] = React.useState('') 46 | const [scenePrompt, setScenePrompt] = React.useState('') 47 | const [isDeleteFlag, setDeleteFlag] = React.useState(false) 48 | 49 | React.useEffect(() => { 50 | 51 | if(chapterId ) { 52 | 53 | const chapter = getChapter(chapterId) 54 | 55 | setName(chapter.name) 56 | setScenePrompt(chapter.prompt) 57 | 58 | const chaps = getChapters(bookId) 59 | setDeleteFlag(chaps.length === 1 ? true : false) 60 | 61 | } 62 | 63 | }, [ chapterId ]) 64 | 65 | const handleSave = () => { 66 | 67 | let chapter = getChapter(chapterId) 68 | 69 | chapter.name = name 70 | chapter.prompt = scenePrompt 71 | 72 | editChapter(chapterId, chapter) 73 | 74 | onConfirm() 75 | 76 | } 77 | 78 | const handleAdd = () => { 79 | 80 | addChapter(bookId, name, scenePrompt) 81 | 82 | onConfirm() 83 | 84 | } 85 | 86 | const handleDelete = () => { 87 | 88 | deleteChapter(chapterId) 89 | 90 | onDelete() 91 | 92 | } 93 | 94 | const handleClick = (e) => { 95 | e.stopPropagation() 96 | e.preventDefault() 97 | } 98 | 99 | return ( 100 | 101 |
102 |
103 |
104 | 105 | setName(e.target.value)} 112 | InputProps={{ 113 | endAdornment: ( 114 | 115 | setName('')} 118 | > 119 | 120 | 121 | 122 | ), 123 | }} 124 | /> 125 | 126 |
127 |
128 | 129 | setScenePrompt(e.target.value)} 137 | InputProps={{ 138 | endAdornment: ( 139 | 140 | setScenePrompt('')} 143 | > 144 | 145 | 146 | 147 | ), 148 | }} 149 | /> 150 | 151 |
152 |
153 |
154 | { 155 | mode === DialogModes.Save && 156 | 157 | } 158 | { 159 | mode === DialogModes.Save && 160 | 161 | } 162 | { 163 | mode === DialogModes.Add && 164 | 165 | } 166 | 167 |
168 |
169 |
170 |
171 |
172 | ) 173 | } 174 | 175 | SceneDialog.propTypes = { 176 | /** 177 | * Mode of operation 178 | */ 179 | mode: PropTypes.oneOf(['add', 'save']), 180 | /** 181 | * BookId string 182 | */ 183 | bookId: PropTypes.string, 184 | /** 185 | * ChapterId string 186 | */ 187 | chapterId: PropTypes.string, 188 | /** 189 | * Confirm event handler 190 | */ 191 | onConfirm: PropTypes.func, 192 | /** 193 | * Close event handler 194 | */ 195 | onClose: PropTypes.func, 196 | /** 197 | * Delete event handler 198 | */ 199 | onDelete: PropTypes.func, 200 | } -------------------------------------------------------------------------------- /components/scenedialog.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #0009; 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | justify-content: center; 10 | align-items: flex-start; 11 | z-index: 1000; 12 | } 13 | 14 | .dialog { 15 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 16 | position: relative; 17 | background-color: #f5f5f5; 18 | width: 90%; 19 | border-radius: 5px; 20 | padding: 1rem; 21 | box-sizing: border-box; 22 | margin-top: 1rem; 23 | } 24 | 25 | .item { 26 | position: relative; 27 | margin-bottom: 1.5rem; 28 | } 29 | .item:first-child { 30 | margin-top: .5rem; 31 | } 32 | .item:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | .action { 37 | display: flex; 38 | justify-content: flex-end; 39 | align-items: center; 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | 44 | .dialog { 45 | background-color: #555; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /components/scenedialog.stories.js: -------------------------------------------------------------------------------- 1 | import SceneDialog, { DialogModes } from './scenedialog'; 2 | 3 | export default { 4 | title: 'ChatGPT/SceneDialog', 5 | component: SceneDialog, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onConfirm: { action: 'confirm' }, 9 | onClose: { action: 'close' }, 10 | }, 11 | }; 12 | 13 | export const Primary = { 14 | args: { 15 | mode: DialogModes.Save, 16 | bookId: 'str0001', 17 | chapterId: 'cha0001', 18 | }, 19 | }; 20 | 21 | export const Add = { 22 | args: { 23 | mode: DialogModes.Add, 24 | bookId: 'str0001', 25 | chapterId: '', 26 | }, 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /components/togglebutton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { createTheme, ThemeProvider } from '@mui/material/styles' 5 | 6 | import NoSsr from '@mui/base/NoSsr' 7 | 8 | import ButtonGroup from '@mui/material/ButtonGroup' 9 | import Button from '@mui/material/Button' 10 | 11 | import PersonIcon from '@mui/icons-material/Person' 12 | import GroupIcon from '@mui/icons-material/Group' 13 | 14 | import useAppStore from '../stores/appStore' 15 | 16 | const buttonLightTheme = createTheme({ 17 | palette: { 18 | primary: { 19 | main: '#00bd7e', 20 | }, 21 | secondary: { 22 | main: '#fff', 23 | }, 24 | tertiary: { 25 | main: '#fff' 26 | } 27 | } 28 | }) 29 | 30 | const buttonDarkTheme = createTheme({ 31 | palette: { 32 | primary: { 33 | main: '#00bd7e', 34 | }, 35 | secondary: { 36 | main: '#555', //9 37 | }, 38 | tertiary: { 39 | main: '#555' 40 | } 41 | } 42 | }) 43 | 44 | export const ChatModes = { 45 | Group: 'group', 46 | Person: 'person', 47 | } 48 | 49 | export default function ToggleButton({ 50 | mode = ChatModes.Person, 51 | onChange = undefined, 52 | }) { 53 | 54 | const isDarkMode = useAppStore((state) => state.darkMode) 55 | 56 | const defColor = isDarkMode ? '#fff' : '#333' 57 | 58 | return ( 59 | 60 | 61 | 62 | 68 | 74 | 75 | 76 | 77 | ) 78 | } 79 | 80 | ToggleButton.propTypes = { 81 | /** 82 | * Current mode 83 | */ 84 | mode: PropTypes.oneOf(['group', 'person']), 85 | /** 86 | * Change event handler 87 | */ 88 | onChange: PropTypes.func, 89 | } -------------------------------------------------------------------------------- /components/togglebutton.stories.js: -------------------------------------------------------------------------------- 1 | import ToggleButton, { ChatModes } from './togglebutton'; 2 | 3 | export default { 4 | title: 'ChatGPT/ToggleButton', 5 | component: ToggleButton, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onChange: { action: 'change' }, 9 | }, 10 | }; 11 | 12 | export const Person = { 13 | args: { 14 | mode: ChatModes.Person 15 | }, 16 | }; 17 | 18 | export const Group = { 19 | args: { 20 | mode: ChatModes.Group 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /components/userdialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import InputAdornment from '@mui/material/InputAdornment'; 5 | import TextField from '@mui/material/TextField'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Button from '@mui/material/Button' 8 | import FormControl from '@mui/material/FormControl' 9 | import InputLabel from '@mui/material/InputLabel' 10 | import Select from '@mui/material/Select' 11 | import MenuItem from '@mui/material/MenuItem' 12 | 13 | import PersonIcon from '@mui/icons-material/Person'; 14 | import ClearIcon from '@mui/icons-material/Clear'; 15 | 16 | import useBookStore from '../stores/bookStore' 17 | import CustomTheme from './customtheme'; 18 | 19 | import classes from './userdialog.module.css' 20 | 21 | export default function UserDialog({ 22 | bookId = '', 23 | chapterId = '', 24 | onConfirm = undefined, 25 | onClose = undefined, 26 | }) { 27 | 28 | const getUser = useBookStore((state) => state.getUserByBookId) 29 | const getChapter = useBookStore((state) => state.getChapter) 30 | 31 | const editUser = useBookStore((state) => state.editUser) 32 | const editChapter = useBookStore((state) => state.editChapter) 33 | 34 | const [name, setName] = React.useState('') 35 | const [userPrompt, setUserPrompt] = React.useState('') 36 | const [sceneName, setSceneName] = React.useState('') 37 | const [scenePrompt, setScenePrompt] = React.useState('') 38 | const [icon, setIcon] = React.useState(0) 39 | 40 | React.useEffect(() => { 41 | 42 | if(bookId && chapterId) { 43 | 44 | const user = getUser(bookId) 45 | const chapter = getChapter(chapterId) 46 | 47 | setName(user.name) 48 | setUserPrompt(user.prompt) 49 | 50 | setSceneName(chapter.name) 51 | setScenePrompt(chapter.user) 52 | 53 | } 54 | 55 | }, [bookId, chapterId]) 56 | 57 | const handleSave = () => { 58 | 59 | let user = getUser(bookId) 60 | let chapter = getChapter(chapterId) 61 | 62 | user.name = name 63 | user.prompt = userPrompt 64 | chapter.user = scenePrompt 65 | 66 | editUser(bookId, user) 67 | editChapter(chapterId, chapter) 68 | 69 | onConfirm() 70 | 71 | } 72 | 73 | const handleClick = (e) => { 74 | e.stopPropagation() 75 | e.preventDefault() 76 | } 77 | 78 | return ( 79 | 80 |
81 |
82 |
83 | 84 | setName(e.target.value)} 91 | InputProps={{ 92 | endAdornment: ( 93 | 94 | setName('')} 97 | > 98 | 99 | 100 | 101 | ), 102 | }} 103 | /> 104 | 105 |
106 |
107 | 108 | setUserPrompt(e.target.value)} 116 | InputProps={{ 117 | endAdornment: ( 118 | 119 | setUserPrompt('')} 122 | > 123 | 124 | 125 | 126 | ), 127 | }} 128 | /> 129 | 130 |
131 |
132 | 133 | setScenePrompt(e.target.value)} 141 | InputProps={{ 142 | endAdornment: ( 143 | 144 | setScenePrompt('')} 147 | > 148 | 149 | 150 | 151 | ), 152 | }} 153 | /> 154 | 155 |
156 |
157 |
158 | 161 | Icon 162 | 173 | 174 |
175 |
176 | 180 | 181 |
182 |
183 |
184 |
185 |
186 | ) 187 | } 188 | 189 | UserDialog.propTypes = { 190 | /** 191 | * BookId string 192 | */ 193 | bookId: PropTypes.string, 194 | /** 195 | * ChapterId string 196 | */ 197 | chapterId: PropTypes.string, 198 | /** 199 | * Confirm event handler 200 | */ 201 | onConfirm: PropTypes.func, 202 | /** 203 | * Close event handler 204 | */ 205 | onClose: PropTypes.func 206 | } -------------------------------------------------------------------------------- /components/userdialog.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #0009; 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | justify-content: center; 10 | align-items: flex-end; 11 | z-index: 1000; 12 | } 13 | 14 | .dialog { 15 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 16 | position: relative; 17 | background-color: #f5f5f5; 18 | width: 90%; 19 | border-radius: 5px; 20 | padding: 1rem; 21 | box-sizing: border-box; 22 | margin-bottom: 8rem; 23 | } 24 | 25 | .item { 26 | position: relative; 27 | margin-bottom: 1.5rem; 28 | } 29 | .item:first-child { 30 | margin-top: .5rem; 31 | } 32 | .item:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | .action { 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | 44 | .dialog { 45 | background-color: #555; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /components/userdialog.stories.js: -------------------------------------------------------------------------------- 1 | import UserDialog from './userdialog'; 2 | 3 | export default { 4 | title: 'ChatGPT/UserDialog', 5 | component: UserDialog, 6 | tags: ['autodocs'], 7 | argTypes: { 8 | onClose: { action: 'close' }, 9 | onConfirm: { action: 'confirm' }, 10 | }, 11 | }; 12 | 13 | export const Primary = { 14 | args: { 15 | bookId: 'str0001', 16 | chapterId: 'cha0002', 17 | }, 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /docs/character1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/character1.jpeg -------------------------------------------------------------------------------- /docs/character2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/character2.jpeg -------------------------------------------------------------------------------- /docs/japanese1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/japanese1.jpeg -------------------------------------------------------------------------------- /docs/japanese2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/japanese2.jpeg -------------------------------------------------------------------------------- /docs/scene1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/scene1.jpeg -------------------------------------------------------------------------------- /docs/scene2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/scene2.jpeg -------------------------------------------------------------------------------- /docs/screenshot1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/screenshot1.jpeg -------------------------------------------------------------------------------- /docs/screenshot2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/screenshot2.jpeg -------------------------------------------------------------------------------- /docs/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/screenshot3.png -------------------------------------------------------------------------------- /docs/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/screenshot4.png -------------------------------------------------------------------------------- /docs/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/screenshot5.png -------------------------------------------------------------------------------- /docs/story1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/story1.jpeg -------------------------------------------------------------------------------- /docs/story11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/story11.jpeg -------------------------------------------------------------------------------- /docs/story2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/story2.png -------------------------------------------------------------------------------- /docs/story21.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/story21.jpeg -------------------------------------------------------------------------------- /docs/user1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/user1.jpeg -------------------------------------------------------------------------------- /docs/user2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/docs/user2.jpeg -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** Utility functions */ 2 | export function getSimpleId() { 3 | return Math.random().toString(26).slice(2); 4 | } 5 | export const getUniqueId = () => { 6 | return (new Date()).getTime().toString(36) + Math.random().toString(36).slice(2); 7 | } 8 | export const getDataId = () => { 9 | return Date.now() + Math.random().toString(36).slice(2) 10 | } 11 | export const isEven = (n) => { 12 | return n % 2 == 0; 13 | } 14 | export const trim_array = ( arr, max_length = 20 ) => { 15 | 16 | let new_arr = arr 17 | 18 | if(arr.length > max_length) { 19 | 20 | let cutoff = Math.ceil(arr.length - max_length) 21 | cutoff = isEven(cutoff) ? cutoff : cutoff + 1 22 | 23 | new_arr = arr.slice(cutoff) 24 | 25 | } 26 | 27 | return new_arr 28 | 29 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack: function(config) { 4 | config.module.rules.push({ 5 | test: /\.md$/, 6 | use: 'raw-loader', 7 | }) 8 | return config 9 | }, 10 | env: { 11 | siteTitle: 'ChatGPT API Sample App', 12 | }, 13 | trailingSlash: true, 14 | experimental: { 15 | appDir: true, 16 | }, 17 | }; 18 | 19 | module.exports = nextConfig; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-chatgpt-api", 3 | "version": "0.0.1", 4 | "description": "A sample webapp using OpenAI ChatGPT API", 5 | "scripts": { 6 | "dev": "next dev -p 3005", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "storybook": "storybook dev -p 6006", 11 | "build-storybook": "storybook build" 12 | }, 13 | "dependencies": { 14 | "@emotion/react": "^11.10.6", 15 | "@emotion/styled": "^11.10.6", 16 | "@fontsource/roboto": "^4.5.8", 17 | "@mui/icons-material": "^5.11.11", 18 | "@mui/material": "^5.11.12", 19 | "eslint-config-next": "^13.2.3", 20 | "next": "^13.2.3", 21 | "openai": "^3.2.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-markdown": "^8.0.5", 25 | "zustand": "^4.3.5" 26 | }, 27 | "devDependencies": { 28 | "@storybook/addon-essentials": "^7.0.0-beta.60", 29 | "@storybook/addon-interactions": "^7.0.0-beta.60", 30 | "@storybook/addon-links": "^7.0.0-beta.60", 31 | "@storybook/blocks": "^7.0.0-alpha.8", 32 | "@storybook/nextjs": "^7.0.0-beta.60", 33 | "@storybook/react": "^7.0.0-beta.60", 34 | "@storybook/testing-library": "^0.0.14-next.1", 35 | "storybook": "^7.0.0-beta.60" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/83c9d5621cd75c99300cab802e272068e8d7203a/public/logo512.png -------------------------------------------------------------------------------- /stores/appStore.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | import { persist, createJSONStorage } from "zustand/middleware" 3 | 4 | const useSystemStore = create( 5 | persist( 6 | (set, get) => ({ 7 | 8 | darkMode: false, 9 | chatMode: 'person', 10 | 11 | setDarkMode: (flag) => set({ darkMode: flag }), 12 | setChatMode: (mode) => set({ chatMode: mode }) 13 | 14 | }), 15 | { 16 | name: "openai-chatgpt-app-storage", 17 | storage: createJSONStorage(() => localStorage), 18 | version: 1, 19 | } 20 | ) 21 | ) 22 | 23 | export default useSystemStore -------------------------------------------------------------------------------- /stores/bookStore.js: -------------------------------------------------------------------------------- 1 | import zustand, { create } from "zustand" 2 | import { persist, createJSONStorage } from "zustand/middleware" 3 | 4 | import stories from '../assets/stories.json' 5 | import { getSimpleId } from "../lib/utils" 6 | 7 | /* 8 | Generate a simulated conversation between the Scarecrow and Dorothy in which they discuss their journey to Oz and their plans for the future. 9 | 10 | Generate a simulated conversation between the Scarecrow and the Tin Man in which they debate the merits of having a brain vs. having a heart. 11 | */ 12 | 13 | const useBookStore = create( 14 | persist( 15 | (set, get) => ({ 16 | 17 | books: stories.books, 18 | chapters: stories.chapters, 19 | characters: stories.characters, 20 | users: stories.users, 21 | 22 | bookId: stories.books[0].id,//'str0003', //'str0002', //stories.books[0].id, 23 | chapterId: stories.chapters[0].id,//'cha0004', //'cha0003', //stories.chapters[0].id, 24 | characterId: stories.characters[0].id,//'chr0004', //'chr0003', //stories.characters[0].id, 25 | 26 | selectBook: (book_id) => { 27 | 28 | const chapters = get().chapters.filter((item) => item.sid === book_id) 29 | const characters = get().characters.filter((item) => item.sid === book_id) 30 | 31 | const chapter_id = chapters[0].id 32 | const character_id = characters[0].id 33 | 34 | set({ bookId: book_id, chapterId: chapter_id, characterId: character_id }) 35 | 36 | }, 37 | selectChapter: (chapter_id) => set({ chapterId: chapter_id }), 38 | selectCharacter: (character_id) => set({ characterId: character_id }), 39 | 40 | addBook: (bookName, prompt) => { 41 | 42 | let books = get().books.slice(0) 43 | let chapters = get().chapters.slice(0) 44 | let characters = get().characters.slice(0) 45 | let users = get().users.slice(0) 46 | 47 | let book_id = getSimpleId() 48 | let chapter_id = getSimpleId() 49 | let character_id = getSimpleId() 50 | let user_id = getSimpleId() 51 | 52 | books.push({ 53 | id: book_id, 54 | name: bookName, 55 | prompt, 56 | }) 57 | 58 | chapters.push({ 59 | sid: book_id, 60 | id: chapter_id, 61 | name: "Untitled Scene", 62 | prompt: "", 63 | characters: [], 64 | user: "" 65 | }) 66 | 67 | characters.push({ 68 | sid: book_id, 69 | id: character_id, 70 | icon: 0, 71 | name: "Assistant", 72 | prompt: "You will act as a helpful assistant." 73 | }) 74 | 75 | users.push({ 76 | sid: book_id, 77 | id: user_id, 78 | name: "User", 79 | prompt: "" 80 | },) 81 | 82 | set({ books, chapters, characters, users }) 83 | 84 | return book_id 85 | 86 | }, 87 | editBook: (book_id, book) => { 88 | 89 | let books = get().books.slice(0) 90 | 91 | books = books.map((item) => { 92 | if(item.id === book_id) item = book 93 | return { 94 | ...item, 95 | } 96 | }) 97 | 98 | set({ books }) 99 | 100 | }, 101 | deleteBook: (book_id) => { 102 | 103 | let books = get().books.slice(0) 104 | books = books.filter((item) => item.id !== book_id) 105 | 106 | let chapters = get().chapters.slice(0) 107 | chapters = chapters.filter((item) => item.sid !== book_id) 108 | 109 | let characters = get().characters.slice(0) 110 | characters = characters.filter((item) => item.sid !== book_id) 111 | 112 | let users = get().users.slice(0) 113 | users = users.filter((item) => item.sid !== book_id) 114 | 115 | set({ books, chapters, characters, users }) 116 | 117 | }, 118 | getBook: (id) => get().books.find((item) => item.id === id), 119 | 120 | addChapter: (book_id, name, prompt) => { 121 | 122 | let chapter_id = getSimpleId() 123 | 124 | let chapters = get().chapters.slice(0) 125 | 126 | chapters.push({ 127 | sid: book_id, 128 | id: chapter_id, 129 | name: name, 130 | prompt: prompt, 131 | characters: [], 132 | user: "" 133 | }) 134 | 135 | set({ chapters }) 136 | 137 | return chapter_id 138 | 139 | }, 140 | editChapter: (chapter_id, chapter) => { 141 | 142 | let chapters = get().chapters.slice(0) 143 | 144 | chapters = chapters.map((item) => { 145 | if(item.id === chapter_id) item = chapter 146 | return { 147 | ...item, 148 | } 149 | }) 150 | 151 | set({ chapters }) 152 | 153 | }, 154 | deleteChapter: (chapter_id) => { 155 | 156 | let chapters = get().chapters.slice(0) 157 | 158 | chapters = chapters.filter((item) => item.id !== chapter_id) 159 | 160 | set({ chapters }) 161 | 162 | }, 163 | getChapters: (book_id) => get().chapters.filter((item) => item.sid === book_id), 164 | getChapter: (chapter_id) => get().chapters.find((item) => item.id === chapter_id), 165 | 166 | addCharacter: (book_id, name, icon, prompt) => { 167 | 168 | let character_id = getSimpleId() 169 | 170 | let characters = get().characters.slice(0) 171 | 172 | characters.push({ 173 | sid: book_id, 174 | id: character_id, 175 | name, 176 | icon, 177 | prompt, 178 | }) 179 | 180 | set({ characters }) 181 | 182 | return character_id 183 | 184 | }, 185 | editCharacter: (character_id, character) => { 186 | 187 | let characters = get().characters.slice(0) 188 | 189 | characters = characters.map((item) => { 190 | if(item.id === character_id) item = character 191 | return { 192 | ...item, 193 | } 194 | }) 195 | 196 | set({ characters }) 197 | 198 | }, 199 | deleteCharacter: (character_id) => { 200 | 201 | let characters = get().characters.slice(0) 202 | 203 | characters = characters.filter((item) => item.id !== character_id) 204 | 205 | set({ characters }) 206 | 207 | }, 208 | getCharacters: (book_id) => get().characters.filter((item) => item.sid === book_id), 209 | getCharacter: (character_id) => get().characters.find((item) => item.id === character_id), 210 | 211 | editUser: (book_id, user) => { 212 | 213 | let users = get().users.slice(0) 214 | 215 | users = users.map((item) => { 216 | if(item.sid === book_id) item = user 217 | return { 218 | ...item, 219 | } 220 | }) 221 | 222 | set({ users }) 223 | 224 | }, 225 | getUserByBookId: (book_id) => get().users.find((item) => item.sid === book_id), 226 | 227 | }), 228 | { 229 | name: "openai-chatgpt-book-storage", 230 | storage: createJSONStorage(() => localStorage), 231 | version: 1, 232 | } 233 | ) 234 | ) 235 | 236 | export default useBookStore -------------------------------------------------------------------------------- /stores/dataStore.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | import { persist, createJSONStorage } from "zustand/middleware" 3 | 4 | const useDataStore = create( 5 | persist( 6 | (set, get) => ({ 7 | 8 | data: [], 9 | dataCount: 0, 10 | 11 | addData: (newdata) => { 12 | 13 | let data = get().data.slice(0) 14 | data.push(newdata) 15 | 16 | set({ 17 | data: data, 18 | dataCount: get().dataCount + 1, 19 | }) 20 | }, 21 | updateDataBySceneId: (scene_id, newdata) => { 22 | 23 | let data = get().data.slice(0) 24 | data = data.filter((item) => item.sid !== scene_id) 25 | if(newdata.length > 0) data = data.concat(newdata) 26 | 27 | set({ 28 | data: data, 29 | dataCount: data.length, 30 | }) 31 | 32 | }, 33 | deleteDataById: (id) => { 34 | 35 | let data = get().data.slice(0) 36 | data = data.filter((item) => item.id !== id) 37 | 38 | set({ 39 | data: data, 40 | dataCount: data.length, 41 | }) 42 | 43 | }, 44 | getDataBySceneId: (sid) => get().data.filter((item) => item.sid === sid), 45 | 46 | }), 47 | { 48 | name: "openai-chatgpt-data-storage", 49 | storage: createJSONStorage(() => localStorage), 50 | version: 1, 51 | } 52 | ) 53 | ) 54 | 55 | export default useDataStore -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vt-c-white: #ffffff; 3 | --vt-c-white-soft: #f8f8f8; 4 | --vt-c-white-mute: #f2f2f2; 5 | 6 | --vt-c-black: #181818; 7 | --vt-c-black-soft: #222222; 8 | --vt-c-black-mute: #282828; 9 | 10 | --vt-c-indigo: #2c3e50; 11 | 12 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 13 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 14 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 15 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 16 | 17 | --vt-c-text-light-1: var(--vt-c-indigo); 18 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 19 | --vt-c-text-dark-1: var(--vt-c-white); 20 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 21 | } 22 | 23 | :root { 24 | --color-background: var(--vt-c-white); 25 | --color-background-soft: var(--vt-c-white-soft); 26 | --color-background-mute: var(--vt-c-white-mute); 27 | 28 | --color-border: var(--vt-c-divider-light-2); 29 | --color-border-hover: var(--vt-c-divider-light-1); 30 | 31 | --color-heading: var(--vt-c-text-light-1); 32 | --color-text: var(--vt-c-text-light-1); 33 | 34 | --color-green-text: #00bd7e; /*hsla(160, 100%, 37%, 1);*/ 35 | --color-yellow-text: #F2A900; 36 | --color-red-text: #E74C3C; 37 | --color-blue-text: #00D8FF; 38 | 39 | --section-gap: 160px; 40 | 41 | --ss-border-color: black; 42 | --ss-thin-border-color: rgba(0, 0, 0, 0.25); 43 | } 44 | 45 | @media (prefers-color-scheme: dark) { 46 | :root { 47 | --color-background: var(--vt-c-black); 48 | --color-background-soft: var(--vt-c-black-soft); 49 | --color-background-mute: var(--vt-c-black-mute); 50 | 51 | --color-border: var(--vt-c-divider-dark-2); 52 | --color-border-hover: var(--vt-c-divider-dark-1); 53 | 54 | --color-heading: var(--vt-c-text-dark-1); 55 | --color-text: var(--vt-c-text-dark-2); 56 | 57 | --ss-border-color: white; 58 | --ss-thin-border-color: rgba(255, 255, 255, 0.25); 59 | } 60 | } 61 | 62 | /* 63 | :root::-webkit-scrollbar { 64 | display: none; 65 | } 66 | */ 67 | 68 | /* 69 | *, 70 | *::before, 71 | *::after { 72 | box-sizing: border-box; 73 | margin: 0; 74 | position: relative; 75 | font-weight: normal; 76 | } 77 | */ 78 | 79 | body { 80 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 81 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 82 | margin: 0; 83 | } 84 | 85 | 86 | @media (prefers-color-scheme: dark) { 87 | 88 | body { 89 | background-color: #333; 90 | } 91 | 92 | } 93 | /* 94 | body { 95 | color: var(--color-text); 96 | background: var(--color-background); 97 | transition: color 0.5s, background-color 0.5s; 98 | line-height: 1.6; 99 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 100 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 101 | font-size: 15px; 102 | text-rendering: optimizeLegibility; 103 | -webkit-font-smoothing: antialiased; 104 | -moz-osx-font-smoothing: grayscale; 105 | } 106 | */ 107 | /* 108 | body::-webkit-scrollbar { 109 | display: none; 110 | } 111 | */ 112 | -------------------------------------------------------------------------------- /styles/preview.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #efefff; 3 | background-image: repeating-linear-gradient(0deg, transparent, transparent 9px, rgba(0, 0, 0, 0.2) 1px, transparent 10px), repeating-linear-gradient(90deg, transparent, transparent 9px, rgba(0, 0, 0, 0.2) 1px, transparent 10px); 4 | background-size: 10px 10px; 5 | margin: 0; 6 | padding: 0px; 7 | /*font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', "ヒラギノ角ゴ ProN", "Hiragino Kaku Gothic ProN", "Meiryo", "メイリオ", "Osaka", "MS PGothic",'Helvetica Neue', 9 | sans-serif;*/ 10 | /*font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 11 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;*/ 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | /* 17 | a.link { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a.link:hover { 23 | color: #535bf2; 24 | } 25 | 26 | @media (prefers-color-scheme: light) { 27 | a.link:hover { 28 | color: #747bff; 29 | } 30 | } 31 | */ --------------------------------------------------------------------------------