├── .env.example
├── docs
├── story2.png
├── user1.jpeg
├── user2.jpeg
├── scene1.jpeg
├── scene2.jpeg
├── story1.jpeg
├── story11.jpeg
├── story21.jpeg
├── character1.jpeg
├── character2.jpeg
├── japanese1.jpeg
├── japanese2.jpeg
├── screenshot3.png
├── screenshot4.png
├── screenshot5.png
├── screenshot1.jpeg
└── screenshot2.jpeg
├── public
├── favicon.ico
├── logo192.png
└── logo512.png
├── components
├── loadingtext.stories.js
├── bookdialog.stories.js
├── userdialog.stories.js
├── togglebutton.stories.js
├── avataritem.module.css
├── loadingtext.module.css
├── scenedialog.stories.js
├── deletedialog.stories.js
├── characterdialog.stories.js
├── customtheme.jsx
├── deletedialog.module.css
├── scenedialog.module.css
├── bookdialog.module.css
├── contentitem.stories.js
├── userdialog.module.css
├── characterdialog.module.css
├── loadingtext.jsx
├── deletedialog.jsx
├── avataritem.jsx
├── togglebutton.jsx
├── contentitem.module.css
├── contentitem.jsx
├── charactericon.jsx
├── scenedialog.jsx
├── userdialog.jsx
├── bookdialog.jsx
└── characterdialog.jsx
├── app
├── layout.jsx
├── page.jsx
├── api
│ └── route.js
├── sandbox.module.css
└── sandbox.jsx
├── next.config.js
├── .storybook
├── preview.js
└── main.js
├── .gitignore
├── stores
├── appStore.js
├── dataStore.js
└── bookStore.js
├── lib
└── utils.js
├── LICENSE
├── styles
├── preview.css
└── main.css
├── package.json
├── assets
├── openai-logo.svg
└── stories.json
└── README.md
/.env.example:
--------------------------------------------------------------------------------
1 | OPENAI_APIKEY=YOUR_OWN_API_KEY
--------------------------------------------------------------------------------
/docs/story2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/story2.png
--------------------------------------------------------------------------------
/docs/user1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/user1.jpeg
--------------------------------------------------------------------------------
/docs/user2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/user2.jpeg
--------------------------------------------------------------------------------
/docs/scene1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/scene1.jpeg
--------------------------------------------------------------------------------
/docs/scene2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/scene2.jpeg
--------------------------------------------------------------------------------
/docs/story1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/story1.jpeg
--------------------------------------------------------------------------------
/docs/story11.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/story11.jpeg
--------------------------------------------------------------------------------
/docs/story21.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/story21.jpeg
--------------------------------------------------------------------------------
/docs/character1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/character1.jpeg
--------------------------------------------------------------------------------
/docs/character2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/character2.jpeg
--------------------------------------------------------------------------------
/docs/japanese1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/japanese1.jpeg
--------------------------------------------------------------------------------
/docs/japanese2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/japanese2.jpeg
--------------------------------------------------------------------------------
/docs/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/screenshot3.png
--------------------------------------------------------------------------------
/docs/screenshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/screenshot4.png
--------------------------------------------------------------------------------
/docs/screenshot5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/screenshot5.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/docs/screenshot1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/screenshot1.jpeg
--------------------------------------------------------------------------------
/docs/screenshot2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supershaneski/openai-chatgpt-api/HEAD/docs/screenshot2.jpeg
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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.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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | */
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | {mode === DeleteModes.Character ? 'Delete' : 'Delete All'}
41 | Cancel
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 | }
--------------------------------------------------------------------------------
/assets/openai-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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/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 | onChange(ChatModes.Person)}
63 | color={mode === ChatModes.Person ? "primary" : "secondary"}>
64 |
67 |
68 | onChange(ChatModes.Group)}
69 | color={mode === ChatModes.Group ? "primary" : "secondary"}>
70 |
73 |
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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 | Delete
157 | }
158 | {
159 | mode === DialogModes.Save &&
160 | Save
161 | }
162 | {
163 | mode === DialogModes.Add &&
164 | Add
165 | }
166 | Cancel
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/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 | setIcon(e.target.value)}
168 | >
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | Save
180 | Cancel
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 | }
--------------------------------------------------------------------------------
/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 |
29 |
30 |
31 | Select the Story you want or Add New Story:
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Edit or write your own Story prompt:
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Edit or write your own Scene prompt:
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | Edit or write your own Character prompt:
56 |
57 |
58 |
59 |
60 |
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 |
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 | 
97 |
98 | Character Prompt:
99 |
100 | 
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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
151 | {
152 | books.map((item) => {
153 | return (
154 | { item.name }
155 | )
156 | })
157 | }
158 |
159 | Add New Story...
160 |
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 | Add Story
219 |
220 | }
221 | {
222 | bookId !== ADD_NEW_KEY &&
223 |
224 | Edit Story
227 |
228 | }
229 |
230 | {
231 | bookId !== ADD_NEW_KEY &&
232 | Delete Story
238 | }
239 | Select Story
242 | Close
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 | }
--------------------------------------------------------------------------------
/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/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 | setIcon(e.target.value)}
229 | >
230 | {
231 | iconList.map((_, index) => {
232 | return (
233 |
234 |
235 |
236 | )
237 | })
238 | }
239 |
240 |
241 |
242 |
243 | {
244 | mode === DialogModes.Save &&
245 | Delete
246 | }
247 | {
248 | mode === DialogModes.Save &&
249 | Save
250 | }
251 | {
252 | mode === DialogModes.Add &&
253 | Add
254 | }
255 | Cancel
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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------