├── .prettierignore ├── src ├── locales │ ├── messages.json │ └── index.js ├── pages │ ├── audio.vue │ ├── image.vue │ ├── text.vue │ ├── README.md │ └── index.vue ├── utils │ ├── index.js │ ├── time.test.js │ └── time.js ├── styles │ ├── README.md │ └── settings.scss ├── plugins │ ├── router.js │ ├── README.md │ ├── i18.js │ ├── vuetify.js │ ├── pinia.js │ └── index.js ├── stores │ ├── index.js │ ├── README.md │ ├── main.js │ ├── message.js │ └── chat.js ├── services │ ├── index.js │ └── firebase │ │ ├── index.js │ │ ├── initialize.js │ │ ├── database.js │ │ └── storage.js ├── App.vue ├── layouts │ ├── default.vue │ └── README.md ├── router │ └── index.js ├── components │ ├── messages │ │ ├── ImageMessage.vue │ │ └── AudioMessage.vue │ ├── utils │ │ └── GetErrorSuccess.vue │ ├── README.md │ ├── layout │ │ └── Sidebar.vue │ └── PageContainer.vue ├── main.js └── CONSTANTS.js ├── .browserslistrc ├── .gitattributes ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── site.webmanifest ├── firebase.json ├── .editorconfig ├── .eslintrc.js ├── database.rules.json ├── .gitignore ├── storage.rules ├── jsconfig.json ├── .prettierrc.json ├── index.html ├── package.json ├── vite.config.mjs ├── .eslintrc-auto-import.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /src/locales/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": {} 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/audio.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/image.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/text.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { dateDisplay, minutesDisplaying, timeDisplay } from './time' 2 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | import messages from './messages.json'; 2 | 3 | export default messages; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue3-chatgpt-ai/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/styles/README.md: -------------------------------------------------------------------------------- 1 | # Styles 2 | 3 | This directory is for configuring the styles of the application. 4 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue3-chatgpt-ai/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue3-chatgpt-ai/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue3-chatgpt-ai/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/plugins/router.js: -------------------------------------------------------------------------------- 1 | import router from '@/router' 2 | 3 | export const initRouter = () => { 4 | return router 5 | } 6 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue3-chatgpt-ai/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue3-chatgpt-ai/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "storage": { 6 | "rules": "storage.rules" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | export { useChatStore } from './chat' 2 | export { useMessageStore } from './message' 3 | export { useMainStore } from './main' 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | import { initializeFirebase } from './firebase' 2 | 3 | export const registerServices = () => { 4 | initializeFirebase() 5 | } 6 | -------------------------------------------------------------------------------- /src/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally. 4 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ 3 | "rules": { 4 | ".read": false, 5 | ".write": false 6 | } 7 | } -------------------------------------------------------------------------------- /src/services/firebase/index.js: -------------------------------------------------------------------------------- 1 | export { default as initializeFirebase } from './initialize' 2 | export { addMessage, createTitle, fetchChats, setMessageDetails } from './database' 3 | export { fetchFile, upload } from './storage' 4 | -------------------------------------------------------------------------------- /src/stores/README.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | Pinia stores are used to store reactive state and expose actions to mutate it. 4 | 5 | Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository. 6 | -------------------------------------------------------------------------------- /src/stores/main.js: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { defineStore } from 'pinia' 3 | 4 | export const useMainStore = defineStore('main', { 5 | state: () => ({ 6 | isLoading: false, 7 | }), 8 | getters: {}, 9 | actions: {}, 10 | }) 11 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/pages/README.md: -------------------------------------------------------------------------------- 1 | # Pages 2 | 3 | Vue components created in this folder will automatically be converted to navigatable routes. 4 | 5 | Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository. 6 | -------------------------------------------------------------------------------- /src/plugins/i18.js: -------------------------------------------------------------------------------- 1 | import messages from '@/locales' 2 | import { createI18n } from 'vue-i18n' 3 | 4 | const DEFAULT_LOCALE = 'en' 5 | 6 | export const initI18n = () => { 7 | return createI18n({ 8 | legacy: false, 9 | locale: DEFAULT_LOCALE, 10 | messages, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/settings.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * src/styles/settings.scss 3 | * 4 | * Configures SASS variables and Vuetify overwrites 5 | */ 6 | 7 | // https://vuetifyjs.com/features/sass-variables/` 8 | // @use 'vuetify/settings' with ( 9 | // $color-pack: false 10 | // ); 11 | html { 12 | overflow-y: auto; 13 | } 14 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | # Layouts 2 | 3 | Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages. 4 | 5 | Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | .env 24 | .firebaserc 25 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | // Craft rules based on data in your Firestore database 4 | // allow write: if firestore.get( 5 | // /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; 6 | service firebase.storage { 7 | match /b/{bucket}/o { 8 | match /{allPaths=**} { 9 | allow read, write: if false; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "es5", 5 | "module": "esnext", 6 | "baseUrl": "./", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/*": [ 10 | "src/*" 11 | ] 12 | }, 13 | "lib": [ 14 | "esnext", 15 | "dom", 16 | "dom.iterable", 17 | "scripthost" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * router/index.ts 3 | * 4 | * Automatic routes for `./src/pages/*.vue` 5 | */ 6 | 7 | // Composables 8 | import { createRouter, createWebHistory } from 'vue-router/auto' 9 | import { setupLayouts } from 'virtual:generated-layouts' 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(import.meta.env.BASE_URL), 13 | extendRoutes: setupLayouts, 14 | }) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /src/utils/time.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { dateDisplay, minutesDisplaying, timeDisplay } from './index.js' 3 | 4 | test('100 secs in minutes', () => { 5 | expect(minutesDisplaying(100)).toBe('01:40') 6 | }) 7 | 8 | test('1000 secs in minutes', () => { 9 | expect(minutesDisplaying(1000)).toBe('16:40') 10 | }) 11 | 12 | test('use timeDisplay', () => { 13 | expect(timeDisplay(1000)).toBe('02:00, 1/1/1970') 14 | }) 15 | -------------------------------------------------------------------------------- /src/components/messages/ImageMessage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/vuetify.js 3 | * 4 | * Framework documentation: https://vuetifyjs.com` 5 | */ 6 | 7 | // Styles 8 | import '@mdi/font/css/materialdesignicons.css' 9 | import 'vuetify/styles' 10 | 11 | // Composables 12 | import { createVuetify } from 'vuetify' 13 | 14 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 15 | export default createVuetify({ 16 | theme: { 17 | defaultTheme: 'dark', 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/messages/AudioMessage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /src/plugins/pinia.js: -------------------------------------------------------------------------------- 1 | import router from '@/router' 2 | import { createPinia } from 'pinia' 3 | import { markRaw } from 'vue' 4 | 5 | const pinia = createPinia() 6 | 7 | pinia.use(({ store }) => { 8 | store.router = markRaw(router) 9 | store.i18n = markRaw(i18n) 10 | }) 11 | 12 | export const initPinia = (app, router, i18n) => { 13 | const pinia = createPinia() 14 | 15 | pinia.use(({ store }) => { 16 | store.router = markRaw(router) 17 | store.i18n = markRaw(i18n) 18 | }) 19 | 20 | return pinia 21 | } 22 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": true, 8 | "printWidth": 120, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": false, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false, 17 | "vueIndentScriptAndStyle": false, 18 | "endOfLine": "lf", 19 | "singleAttributePerLine": true 20 | } -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/index.js 3 | * 4 | * Automatically included in `./src/main.js` 5 | */ 6 | 7 | // Plugins 8 | import vuetify from './vuetify' 9 | import { initI18n } from './i18' 10 | import { initRouter } from './router' 11 | import { initPinia } from './pinia' 12 | 13 | export const registerPlugins = app => { 14 | const i18n = initI18n(app) 15 | const router = initRouter(app) 16 | const pinia = initPinia(app, router, i18n) 17 | 18 | app.use(vuetify) 19 | app.use(i18n) 20 | app.use(pinia) 21 | app.use(router) 22 | } 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * main.js 3 | * 4 | * Bootstraps Vuetify and other plugins then mounts the App` 5 | */ 6 | 7 | // Plugins 8 | import { registerPlugins } from '@/plugins' 9 | import { registerServices } from '@/services' 10 | 11 | import './styles/settings.scss' 12 | 13 | // Components 14 | import App from './App.vue' 15 | 16 | // Composables 17 | import { createApp } from 'vue' 18 | 19 | const app = createApp(App) 20 | 21 | registerPlugins(app) 22 | registerServices() 23 | 24 | import { useChatStore } from '@/stores' 25 | await useChatStore().init() 26 | 27 | app.mount('#app') 28 | -------------------------------------------------------------------------------- /src/services/firebase/initialize.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from '@firebase/app' 2 | // See: https://firebase.google.com/docs/web/learn-more#config-object 3 | 4 | const firebaseConfig = { 5 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY, 6 | authDomain: `${import.meta.env.VITE_FIREBASE_PROJECT_ID}.firebaseapp.com`, 7 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, 8 | storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, 9 | messagingSenderId: import.meta.env.VITE_FIREBASE_SENDER_ID, 10 | appId: import.meta.env.VITE_FIREBASE_APP_ID, 11 | databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL, 12 | } 13 | 14 | export default () => initializeApp(firebaseConfig) 15 | -------------------------------------------------------------------------------- /src/utils/time.js: -------------------------------------------------------------------------------- 1 | const timeDisplay = value => { 2 | let date = new Date(value) 3 | let hours = date.getHours() 4 | let minutes = date.getMinutes() 5 | 6 | hours = hours < 10 ? '0' + hours : hours 7 | minutes = minutes < 10 ? '0' + minutes : minutes 8 | 9 | return `${hours}:${minutes}, ${date.toLocaleDateString()}` || '-' 10 | } 11 | 12 | const minutesDisplaying = secs => { 13 | const mins = Math.floor(secs / 60) 14 | secs %= 60 15 | 16 | return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs 17 | } 18 | 19 | const dateDisplay = value => { 20 | const now = new Date() 21 | const date = new Date(value ? value : now) 22 | 23 | return `${date.toLocaleDateString()}` 24 | } 25 | 26 | export { dateDisplay, minutesDisplaying, timeDisplay } 27 | -------------------------------------------------------------------------------- /src/components/utils/GetErrorSuccess.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 33 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Vue - ChatGPT AI 10 | 15 | 21 | 27 | 31 | 32 | 33 | 34 |
35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | Vue template files in this folder are automatically imported. 4 | 5 | ## 🚀 Usage 6 | 7 | Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it. 8 | 9 | The following example assumes a component located at `src/components/MyComponent.vue`: 10 | 11 | ```vue 12 | 17 | 18 | 21 | ``` 22 | 23 | When your template is rendered, the component's import will automatically be inlined, which renders to this: 24 | 25 | ```vue 26 | 31 | 32 | 35 | ``` 36 | -------------------------------------------------------------------------------- /src/services/firebase/database.js: -------------------------------------------------------------------------------- 1 | import { get, getDatabase, ref, set } from '@firebase/database' 2 | import { initializeFirebase } from './index' 3 | 4 | const db = getDatabase(initializeFirebase()) 5 | 6 | export async function fetchChats() { 7 | const chatsRef = ref(db, 'chats') 8 | const snapshot = await get(chatsRef) 9 | 10 | if (snapshot.exists()) { 11 | return snapshot.val() 12 | } else { 13 | return {} 14 | } 15 | } 16 | 17 | export async function createTitle(payload) { 18 | const { createdAt, id, messages, page, title } = payload 19 | 20 | set(ref(db, `chats/${page}/${id}`), { 21 | createdAt, 22 | id, 23 | messages, 24 | title, 25 | updatedAt: createdAt, 26 | }) 27 | } 28 | 29 | export async function addMessage(payload) { 30 | const { messages, id, page, updatedAt } = payload 31 | 32 | set(ref(db, `chats/${page}/${id}/messages`), [...messages]) 33 | if (updatedAt) set(ref(db, `chats/${page}/${id}/updatedAt`), updatedAt) 34 | } 35 | 36 | export async function setMessageDetails(payload) { 37 | const { createdAt, id, page, title, updatedAt } = payload 38 | 39 | if (title) set(ref(db, `chats/${page}/${id}/title`), title) 40 | if (createdAt) set(ref(db, `chats/${page}/${id}/createdAt`), createdAt) 41 | if (updatedAt) set(ref(db, `chats/${page}/${id}/updatedAt`), updatedAt) 42 | } 43 | -------------------------------------------------------------------------------- /src/stores/message.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useMessageStore = defineStore('message', { 4 | state: () => ({ error: null, isSuccess: null, errorTime: 5000, successTime: 5000 }), 5 | getters: { 6 | getError() { 7 | return this.error 8 | }, 9 | getIsSuccess() { 10 | return this.isSuccess 11 | }, 12 | }, 13 | actions: { 14 | setErrorClear(payload) { 15 | this.error = payload?.error 16 | let time = payload?.time && payload?.time != undefined ? payload?.time : this.errorTime 17 | setTimeout(() => { 18 | this.error = null 19 | }, time) 20 | }, 21 | 22 | setIsSuccessClear(payload) { 23 | try { 24 | this.error = null 25 | this.isSuccess = payload?.message || null 26 | 27 | const time = payload?.time ? payload.time : this.successTime 28 | 29 | setTimeout(() => { 30 | this.isSuccess = null 31 | }, time) 32 | } catch (error) { 33 | this.setErrorClear({ error }) 34 | } 35 | }, 36 | setIsSuccess(payload) { 37 | try { 38 | this.error = null 39 | this.setIsSuccessClear(payload) 40 | } catch (error) { 41 | this.setErrorClear({ error }) 42 | } 43 | }, 44 | 45 | setError(payload) { 46 | try { 47 | this.isSuccess = null 48 | this.setErrorClear(payload) 49 | } catch (error) { 50 | this.setErrorClear({ error }) 51 | } 52 | }, 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /src/CONSTANTS.js: -------------------------------------------------------------------------------- 1 | export default { 2 | defaultPage: 'text', 3 | pages: { 4 | text: 'text', 5 | image: 'image', 6 | audio: 'audio', 7 | }, 8 | defaultModels: { 9 | text: 'gpt-3.5-turbo', 10 | image: 'dall-e-2', 11 | audio: 'tts-1', 12 | }, 13 | image: { 14 | sizes: { 'dall-e-2': ['256x256', '512x512', '1024x1024'], 'dall-e-3': ['1024x1024', '1792x1024', '1024x1792 '] }, 15 | defaultSizes: { 'dall-e-2': '256x256', 'dall-e-3': '1024x1024' }, 16 | }, 17 | audio: { 18 | voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'], 19 | defaultVoice: 'alloy', 20 | formats: ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'], 21 | defaultFormat: 'mp3', 22 | mimeMaps: { 23 | mp3: 'audio/mp3', 24 | opus: 'audio/ogg', 25 | aac: 'audio/aac', 26 | flac: 'audio/flac', 27 | wav: 'audio/wav', 28 | pcm: 'audio/L16', 29 | }, 30 | speeds: [0.25, 1, 4], 31 | defaultSpeed: 1, 32 | }, 33 | models: { 34 | text: [ 35 | 'gpt-3.5-turbo', 36 | 'gpt-3.5-turbo-0125', 37 | 'gpt-3.5-turbo-1106', 38 | 'gpt-3.5-turbo-instruct ', 39 | 'gpt-4-turbo', 40 | 'gpt-4-turbo-2024-04-09', 41 | 'gpt-4-turbo-preview', 42 | 'gpt-4-0125-preview', 43 | 'gpt-4-1106-preview', 44 | 'gpt-4-0613', 45 | 'gpt-4-vision-preview', 46 | 'gpt-4-1106-vision-preview', 47 | 'gpt-4', 48 | 'gpt-4-32k', 49 | 'gpt-4-32k-0613', 50 | ], 51 | image: ['dall-e-2', 'dall-e-3'], 52 | audio: ['tts-1', 'tts-1-hd'], 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-chatgpt-ai", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "lint": "eslint . --fix --ignore-path .gitignore", 9 | "test": "vitest" 10 | }, 11 | "dependencies": { 12 | "@firebase/app": "^0.10.3", 13 | "@firebase/database": "^1.0.5", 14 | "@firebase/storage": "^0.12.5", 15 | "@mdi/font": "7.4.47", 16 | "@vueuse/core": "^10.9.0", 17 | "core-js": "^3.37.0", 18 | "lodash": "^4.17.21", 19 | "moment": "^2.30.1", 20 | "openai": "^4.45.0", 21 | "roboto-fontface": "*", 22 | "vite-plugin-pages": "^0.32.1", 23 | "vue": "^3.4.27", 24 | "vue-i18n": "^9.13.1", 25 | "vuetify": "^3.6.5" 26 | }, 27 | "devDependencies": { 28 | "@vitejs/plugin-vue": "^5.0.4", 29 | "eslint": "^9.2.0", 30 | "eslint-config-standard": "^17.1.0", 31 | "eslint-plugin-import": "^2.29.1", 32 | "eslint-plugin-n": "^17.6.0", 33 | "eslint-plugin-node": "^11.1.0", 34 | "eslint-plugin-promise": "^6.1.1", 35 | "eslint-plugin-vue": "^9.26.0", 36 | "jsdom": "^24.1.0", 37 | "pinia": "^2.1.7", 38 | "resize-observer-polyfill": "^1.5.1", 39 | "sass": "^1.77.1", 40 | "unplugin-auto-import": "^0.17.6", 41 | "unplugin-fonts": "^1.1.1", 42 | "unplugin-vue-components": "^0.27.0", 43 | "unplugin-vue-router": "^0.8.6", 44 | "vite": "^5.2.11", 45 | "vite-plugin-vue-layouts": "^0.11.0", 46 | "vite-plugin-vuetify": "^2.0.3", 47 | "vitest": "^1.6.0", 48 | "vue-router": "^4.3.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import AutoImport from 'unplugin-auto-import/vite' 3 | import Components from 'unplugin-vue-components/vite' 4 | import Fonts from 'unplugin-fonts/vite' 5 | import Layouts from 'vite-plugin-vue-layouts' 6 | import Vue from '@vitejs/plugin-vue' 7 | import VueRouter from 'unplugin-vue-router/vite' 8 | import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 9 | 10 | // Utilities 11 | import { defineConfig } from 'vite' 12 | import { fileURLToPath, URL } from 'node:url' 13 | 14 | // https://vitejs.dev/config/ 15 | export default defineConfig({ 16 | plugins: [ 17 | VueRouter(), 18 | Layouts(), 19 | Vue({ 20 | template: { transformAssetUrls }, 21 | }), 22 | // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme 23 | Vuetify({ 24 | autoImport: true, 25 | styles: { 26 | configFile: 'src/styles/settings.scss', 27 | }, 28 | }), 29 | Components(), 30 | Fonts({ 31 | google: { 32 | families: [ 33 | { 34 | name: 'Roboto', 35 | styles: 'wght@100;300;400;500;700;900', 36 | }, 37 | ], 38 | }, 39 | }), 40 | AutoImport({ 41 | imports: ['vue', 'vue-router'], 42 | eslintrc: { 43 | enabled: true, 44 | }, 45 | vueTemplate: true, 46 | }), 47 | ], 48 | test: { 49 | globals: true, 50 | environment: 'jsdom', 51 | server: { 52 | deps: { 53 | inline: ['vuetify'], 54 | }, 55 | }, 56 | }, 57 | define: { 'process.env': {} }, 58 | resolve: { 59 | alias: { 60 | vue$: 'vue/dist/vue.esm-bundler.js', 61 | '@': fileURLToPath(new URL('./src', import.meta.url)), 62 | }, 63 | extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'], 64 | }, 65 | server: { 66 | port: 5017, 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "EffectScope": true, 7 | "ExtractDefaultPropTypes": true, 8 | "ExtractPropTypes": true, 9 | "ExtractPublicPropTypes": true, 10 | "InjectionKey": true, 11 | "PropType": true, 12 | "Ref": true, 13 | "VNode": true, 14 | "WritableComputedRef": true, 15 | "computed": true, 16 | "createApp": true, 17 | "customRef": true, 18 | "defineAsyncComponent": true, 19 | "defineComponent": true, 20 | "effectScope": true, 21 | "getCurrentInstance": true, 22 | "getCurrentScope": true, 23 | "h": true, 24 | "inject": true, 25 | "isProxy": true, 26 | "isReactive": true, 27 | "isReadonly": true, 28 | "isRef": true, 29 | "markRaw": true, 30 | "nextTick": true, 31 | "onActivated": true, 32 | "onBeforeMount": true, 33 | "onBeforeRouteLeave": true, 34 | "onBeforeRouteUpdate": true, 35 | "onBeforeUnmount": true, 36 | "onBeforeUpdate": true, 37 | "onDeactivated": true, 38 | "onErrorCaptured": true, 39 | "onMounted": true, 40 | "onRenderTracked": true, 41 | "onRenderTriggered": true, 42 | "onScopeDispose": true, 43 | "onServerPrefetch": true, 44 | "onUnmounted": true, 45 | "onUpdated": true, 46 | "provide": true, 47 | "reactive": true, 48 | "readonly": true, 49 | "ref": true, 50 | "resolveComponent": true, 51 | "shallowReactive": true, 52 | "shallowReadonly": true, 53 | "shallowRef": true, 54 | "toRaw": true, 55 | "toRef": true, 56 | "toRefs": true, 57 | "toValue": true, 58 | "triggerRef": true, 59 | "unref": true, 60 | "useAttrs": true, 61 | "useCssModule": true, 62 | "useCssVars": true, 63 | "useLink": true, 64 | "useRoute": true, 65 | "useRouter": true, 66 | "useSlots": true, 67 | "watch": true, 68 | "watchEffect": true, 69 | "watchPostEffect": true, 70 | "watchSyncEffect": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/services/firebase/storage.js: -------------------------------------------------------------------------------- 1 | import CONSTANTS from '@/CONSTANTS' 2 | import { getDownloadURL, getStorage, uploadBytes, uploadString, ref } from '@firebase/storage' 3 | import { useMessageStore } from '@/stores' 4 | 5 | export async function upload(payload = {}) { 6 | const { extension, file, mimeType, page } = payload 7 | const storage = getStorage() 8 | const storageRef = ref(storage, crypto.randomUUID() + '.' + extension) 9 | 10 | let fileName 11 | const metadata = { 12 | contentType: mimeType, 13 | } 14 | 15 | if (page === CONSTANTS?.pages?.image) { 16 | await uploadBytes(storageRef, file, metadata).then(snapshot => { 17 | fileName = snapshot.ref.name 18 | }) 19 | } else if (page === CONSTANTS?.pages?.audio) { 20 | await uploadString(storageRef, file, 'data_url').then(snapshot => { 21 | fileName = snapshot.ref.name 22 | }) 23 | } 24 | 25 | return fileName 26 | } 27 | 28 | export function fetchFile(payload) { 29 | const { fileName } = payload 30 | 31 | const storage = getStorage() 32 | const storageRef = ref(storage, fileName) 33 | 34 | // Get the download URL 35 | return getDownloadURL(storageRef) 36 | .then(url => url) 37 | .catch(error => { 38 | // A full list of error codes is available at 39 | // https://firebase.google.com/docs/storage/web/handle-errors 40 | switch (error.code) { 41 | case 'storage/object-not-found': 42 | // 43 | useMessageStore().setError({ error: "File doesn't exist" }) 44 | break 45 | case 'storage/unauthorized': 46 | useMessageStore().setError({ error: "User doesn't have permission to access the object" }) 47 | break 48 | case 'storage/canceled': 49 | useMessageStore().setError({ error: 'User canceled the upload' }) 50 | break 51 | 52 | // ... 53 | 54 | case 'storage/unknown': 55 | useMessageStore().setError({ error: 'Unknown error occurred, inspect the server response' }) 56 | break 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/layout/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 77 | 78 | 99 | -------------------------------------------------------------------------------- /src/stores/chat.js: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { addMessage, createTitle, fetchChats, setMessageDetails } from '@/services/firebase' 3 | import { defineStore } from 'pinia' 4 | import { getChatCompletion } from '@/services/openaiService' 5 | import { isEmpty } from 'lodash' 6 | import { useMainStore } from './main' 7 | import { useStorage } from '@vueuse/core' 8 | 9 | import moment from 'moment' 10 | import CONTANTS from '@/CONSTANTS' 11 | 12 | export const useChatStore = defineStore('chat', { 13 | state: () => ({ 14 | chats: useStorage('chats', { 15 | text: {}, 16 | image: {}, 17 | audio: {}, 18 | }), 19 | }), 20 | getters: { 21 | getTitles: state => state.chats.map(chat => chat.title), 22 | getChatsById: 23 | state => 24 | (id, page = CONTANTS.defaultPage) => 25 | state.chats?.[page]?.[id]?.messages || [], 26 | getWholeTitle: 27 | state => 28 | (id, page = CONTANTS.defaultPage) => 29 | state.chats[page]?.[id] || {}, 30 | getChatsByPage: state => payload => { 31 | const { limit, page } = payload 32 | 33 | const entries = Object.entries(state.chats?.[page] || {}) 34 | const result = entries.map(([id, chat]) => ({ 35 | id, 36 | ...chat, 37 | })) 38 | 39 | // Sort chats by updatedAt in descending order 40 | result.sort((a, b) => { 41 | const updatedAtA = a.updatedAt ? new Date(a.updatedAt) : new Date(0) 42 | const updatedAtB = b.updatedAt ? new Date(b.updatedAt) : new Date(0) 43 | return updatedAtB - updatedAtA 44 | }) 45 | 46 | return limit ? result.slice(0, limit) : result 47 | }, 48 | getTitlesByPage: getters => payload => 49 | getters.getChatsByPage(payload).map(chat => ({ id: chat.id, title: chat.title })) || [], 50 | }, 51 | actions: { 52 | async init() { 53 | const chats = await fetchChats() 54 | this.chats = { ...chats } 55 | }, 56 | async addMessage(payload) { 57 | const { content, format, model, speed, type, voice } = payload 58 | let { id, page } = payload 59 | 60 | const time = moment().format() 61 | page ??= CONTANTS.defaultPage 62 | 63 | if (!id) { 64 | id = crypto.randomUUID() 65 | this.createTitle({ id, page }) 66 | } 67 | 68 | if (!content) return 69 | 70 | let found = this.chats[page]?.[id] 71 | 72 | if (!found) { 73 | this.createTitle({ id, page }) 74 | found = this.chats[page]?.[id] 75 | } 76 | 77 | if (!Array.isArray(found?.messages) || found.messages.length === 0) { 78 | const temp = document.createElement('div') 79 | temp.innerHTML = content 80 | const text = temp.textContent || temp.innerText || '' 81 | 82 | found.title = text.substring(0, 100) 83 | found.messages = [] 84 | setMessageDetails({ id, page, title: found.title, updatedAt: time }) 85 | } 86 | 87 | found.messages.push({ content, time, type }) 88 | addMessage({ id, page, messages: found.messages, updatedAt: time }) 89 | 90 | useMainStore().isLoading = true 91 | 92 | try { 93 | const botContent = await getChatCompletion({ content, format, model, page, speed, voice }) 94 | found.messages.push({ content: botContent, time, type: 'bot' }) 95 | addMessage({ id, page, messages: found.messages, updatedAt: time }) 96 | } finally { 97 | useMainStore().isLoading = false 98 | } 99 | }, 100 | createTitle(payload) { 101 | const time = moment().format() 102 | 103 | let { id, page } = payload 104 | page ??= CONTANTS.defaultPage 105 | id ??= crypto.randomUUID() 106 | 107 | if (isEmpty(this.chats[page])) { 108 | this.chats[page] = {} 109 | } 110 | 111 | createTitle({ createdAt: time, id, messages: [], page, title: 'New Chat' }) 112 | this.chats[page][id] = { createdAt: time, messages: [], title: 'New Chat', updatedAt: time } 113 | 114 | return id 115 | }, 116 | getLastTitleId(page = CONSTANTS.defaultPage) { 117 | // Ensure page is provided and exists in this.chats, fallback to an empty object if not 118 | const chatsForPage = this.chats?.[page] ?? {} 119 | 120 | // Get the keys of the chats for the given page 121 | const chatIds = Object.keys(chatsForPage) 122 | 123 | // If there are chat ids, return the last one 124 | if (chatIds.length > 0) { 125 | return chatIds[chatIds.length - 1] 126 | } 127 | 128 | // If no valid id found, create a new title 129 | return this.createTitle({ page }) 130 | }, 131 | isTheTitleExist: 132 | state => 133 | (id, page = CONTANTS.defaultPage) => { 134 | if (!state.chats?.[page]?.[id]) { 135 | this.router.push(`/${page}`) 136 | } 137 | }, 138 | }, 139 | }) 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌟 Vue ChatGPT AI 2 | 3 | ✨ Powered by Modern Technologies 4 | 5 | - **Vue 3**: The versatile powerhouse that makes building dynamic user interfaces a breeze. 6 | - **Vuetify 3**: Elevate your design with the elegance of Material Design, right at your fingertips. 7 | - **Pinia**: Your go-to state management solution, as easy as pie, and just as satisfying. 8 | - **Vite**: Lightning-fast build tool that ensures your development experience is as smooth as silk. 9 | - **Firebase**: Real-time magic for your database and storage needs, ensuring your data is always in sync. 10 | - **OpenAI**: The brains behind the operation, bringing sophisticated AI chat capabilities to your app. 11 | 12 | This project harnesses these cutting-edge technologies to deliver a seamless and responsive chat interface, enriched with AI-driven interactions. Enjoy the power of real-time data, sleek design, and intelligent conversation in one cohesive package. 13 | 14 | 15 | 16 | https://github.com/mustafacagri/vue3-chatgpt-ai/assets/7488394/8d46056f-8746-4fb4-adb7-9ae181830f5c 17 | 18 | 19 | 20 | ## 🚀 Features 21 | 22 | 1. **Real-time AI Chat** 23 | 24 | - **OpenAI Integration**: Leverage the power of OpenAI's ChatGPT for intelligent and dynamic conversations. 25 | - **Seamless Interaction**: Chat with the AI in real-time, with instant responses and fluid interaction. 26 | 27 | 2. **Multi-format Support** 28 | 29 | - **Text Messages**: Exchange plain text messages effortlessly. 30 | - **Image Support**: Send and receive images directly in the chat. 31 | - **Audio Messages**: Record and share audio clips with ease. 32 | 33 | 3. **Persistent Chat History** 34 | 35 | - **Local Storage**: Messages are saved locally, ensuring chat history is retained even after the page refreshes. 36 | - **Firebase Realtime Database**: Store messages in the cloud for access across multiple devices. 37 | 38 | 4. **User-friendly Interface** 39 | 40 | - **Vuetify 3**: Aesthetic and intuitive design with Material Design components. 41 | - **Responsive Layout**: Optimized for both desktop and mobile devices. 42 | 43 | 5. **State Management** 44 | 45 | - **Pinia**: Simplified and intuitive state management for maintaining the chat application state. 46 | 47 | 6. **Efficient Development Workflow** 48 | 49 | - **Vite**: Fast build tool for an efficient development experience. 50 | - **Modular Code Structure**: Clean and maintainable codebase with Vue 3's Composition API. 51 | 52 | 7. **Security and Scalability** 53 | 54 | - **Firebase Authentication**: Secure user authentication to protect chat data. 55 | - **Scalable Infrastructure**: Firebase backend ensures scalability and reliability as the user base grows. 56 | 57 | 8. **File Management** 58 | - **Firebase Storage**: Store and manage images and audio files securely in the cloud. 59 | 60 | ## 🛠️ Setup Instructions 61 | 62 | ### Cloning and Running Locally 63 | 64 | To get started, clone the repository and run the development server: 65 | 66 | ```bash 67 | git clone https://github.com/your/repository.git 68 | cd repository-name 69 | yarn install 70 | yarn dev 71 | ``` 72 | 73 | The application will be running at http://localhost:5017/. 74 | 75 | ### 🔥 Firebase Integration 76 | 77 | To fully utilize Firebase features: 78 | 79 | 1. **Create a Firebase Project:** 80 | 81 | - Navigate to the [Firebase Console](https://console.firebase.google.com/) and create a new project. 82 | 83 | 2. **Enable Firebase Services:** 84 | 85 | - In your Firebase project settings, enable Firebase Storage to store files like images and audio. 86 | 87 | 3. **Obtain Firebase Configuration:** 88 | - Go to your Firebase project settings and find the Firebase SDK snippet. You'll need to copy the configuration details including: 89 | - `apiKey` 90 | - `authDomain` 91 | - `databaseURL` 92 | - `projectId` 93 | - `storageBucket` 94 | - `messagingSenderId` 95 | - `appId` 96 | 4. **Add Firebase Configuration to `.env` File:** 97 | - Create a `.env` file in the root directory of your project if it doesn't exist. 98 | - Add your Firebase configuration details to the `.env` file using the following format: 99 | ```env 100 | VITE_FIREBASE_API_KEY=your-firebase-api-key 101 | VITE_FIREBASE_AUTH_DOMAIN=your-auth-domain 102 | VITE_FIREBASE_DATABASE_URL=your-database-url 103 | VITE_FIREBASE_PROJECT_ID=your-firebase-project-id 104 | VITE_FIREBASE_STORAGE_BUCKET=your-firebase-storage-bucket 105 | VITE_FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-id 106 | VITE_FIREBASE_APP_ID=your-firebase-app-id 107 | ``` 108 | 109 | ### 🗄️ Local Storage 110 | 111 | Messages are stored in local storage using the `useStorage` hook from `@vueuse/core`. This ensures that chat history persists even when the page is refreshed. 112 | 113 | ### 🤖 How to Obtain OpenAI API Key 114 | 115 | 1. **Sign up for OpenAI API:** 116 | 117 | - Go to the [OpenAI website](https://www.openai.com/) and sign up for an account. 118 | 119 | 2. **Generate API Key:** 120 | 121 | - After signing in, navigate to your account settings or API section to generate a new API key specifically for ChatGPT. 122 | - Copy the generated API key. 123 | 124 | 3. **Add OpenAI API Key to `.env` File:** 125 | - Open or create the `.env` file in your project directory. 126 | - Add your OpenAI API key to the `.env` file using the following format: 127 | ```env 128 | VITE_OPENAI_API_KEY=your-openai-api-key 129 | ``` 130 | 131 | ## How can I support? 🌟 132 | 133 | - ⭐ Star my GitHub repo 134 | - 🛠 Create pull requests, submit bugs, suggest new features or updates 135 | -------------------------------------------------------------------------------- /src/components/PageContainer.vue: -------------------------------------------------------------------------------- 1 |