├── .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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
7 |
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 |
2 |
8 |
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 |
17 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/src/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
13 |
14 |
15 |
16 |
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 |
27 |
28 |
29 |
30 |
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 |
2 |
7 |
8 |
17 |
23 |
27 |
32 | {{ title.title }}
33 |
34 |
35 |
36 |
37 |
38 | Vue ChatGPT AI
39 |
40 |
41 |
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 |
2 |
6 |
10 |
11 |
12 |
13 |
22 |
23 | mdi-robot
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ moment(message?.time).fromNow() }}
35 |
36 |
37 |
38 | {{ moment(message?.time).fromNow() }}
39 |
40 |
41 |
42 |
43 |
44 |
96 |
97 |
98 |
175 |
176 |
260 | >
261 |
--------------------------------------------------------------------------------