diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b58b895 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules/ +.nuxt/ +.output/ \ No newline at end of file diff --git a/.env.example b/.env.example index 7ae466a..da97e5b 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -API_BASE_URL=http://localhost:3000 +NUXT_PUBLIC_API_BASE_URL=http://localhost:3000 diff --git a/Dockerfile b/Dockerfile index b6ffcda..917d29a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,20 @@ -FROM node:16-alpine +FROM node:18-alpine WORKDIR /app +COPY ./package.json ./package-lock.json ./ + +RUN npm pkg set scripts.postinstall="echo no-postinstall" && npm install + COPY . . -COPY .env . +RUN npm run postinstall + +RUN npm run build -RUN npm install +EXPOSE 3000 -EXPOSE 3000 24678 +# You should use -e to override this default value +ENV NUXT_PUBLIC_API_BASE_URL=/api -CMD ["npm", "run", "dev"] \ No newline at end of file +CMD ["npm", "run", "preview"] diff --git a/README.md b/README.md index caa7a6b..45dd38e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,12 @@ You may also use PandoraAI with other API server implementations as long as the - Choose between different clients or custom presets. ![Client Dropdown](demos/client-dropdown.png) - Everything is stored in local storage, so you can use this client without an account, and it can be imported or exported to other devices. - +- Some extra features: + - Support for deleting messages and copying Markdown sources + - Support for rendering LaTeX formula + - Typing preview + - Dockerfile optimization + - UI icon beautify
Nuxt 3 Setup @@ -67,10 +72,20 @@ Check out the [deployment documentation](https://nuxt.com/docs/getting-started/d 1. Follow the Nuxt 3 setup instructions above. 2. Run the API server from [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api#api-server). -3. Copy `.env.example` to `.env` and fill in the `API_BASE_URL` variable with the URL of the API server. +3. Copy `.env.example` to `.env` and fill in the `NUXT_PUBLIC_API_BASE_URL` variable with the URL of the API server. 4. Run `npm run dev` to start the development server, or `npm run build` to build the application for production. 1. If you see an empty white page after pulling the latest changes, run `nuxi upgrade --force` first and then `npm run dev`. +### Docker Setup +Build the image, such as `docker build -t pandora-ai .`. + +Run: +```shell +docker run --name pandora-ai -it -d -p 3000:3000 -e NUXT_PUBLIC_API_BASE_URL=http://node-chatgpt-api/ pandora-ai +``` + +Make sure you override the `NUXT_PUBLIC_API_BASE_URL` environment variable. + ## Contributing If you'd like to contribute to this project, please create a pull request with a detailed description of your changes. diff --git a/app.vue b/app.vue index 879ccca..a981380 100644 --- a/app.vue +++ b/app.vue @@ -96,7 +96,7 @@ onMounted(() => { powered by - https://github.com/waylaidwanderer/node-chatgpt-api + node-chatgpt-api diff --git a/components/Chat.vue b/components/Chat.vue index 7fdbcad..6802d82 100644 --- a/components/Chat.vue +++ b/components/Chat.vue @@ -8,6 +8,8 @@ import BingIcon from '~/components/Icons/BingIcon.vue'; import GPTIcon from '~/components/Icons/GPTIcon.vue'; import ClientDropdown from '~/components/Chat/ClientDropdown.vue'; import ClientSettings from '~/components/Chat/ClientSettings.vue'; +import copy from 'copy-to-clipboard'; +import markedKatex from "marked-katex-extension"; marked.setOptions({ silent: true, @@ -15,6 +17,8 @@ marked.setOptions({ breaks: true, gfm: true, }); +marked.use(markedKatex({throwOnError: false})); + const renderer = { code(code, lang) { let language = 'plaintext'; @@ -442,21 +446,8 @@ if (!process.server) { return; } // copy text to clipboard - navigator.clipboard.writeText(codeBlock.innerText); - // find child element with class `copy-status` - const copyStatus = el.querySelector('.copy-status'); - if (copyStatus) { - // set text to "Copied" - copyStatus.innerText = 'Copied'; - setTimeout(() => { - if (!copyStatus) { - return; - } - // set text back to "Copy" - copyStatus.innerText = 'Copy'; - }, 3000); - } - return; + copyToClipboard(codeBlock.innerText, el); + } el = el.parentElement; } @@ -514,6 +505,41 @@ if (!process.server) { suggestedResponses.value = []; }); } + +const copyToClipboard = (message, element) => { + console.debug("copy message", message) + console.debug("copy element", element) + if (!copy(message)) { + prompt("Failed to copy. Please copy manually: ", message) + } + if (element) { + const copyStatus = element.querySelector('.copy-status') || element; + if (copyStatus) { + // set text to "Copied" + copyStatus.innerText = 'Copied!'; + setTimeout(() => { + if (!copyStatus) { + return; + } + // set text back to "Copy" + copyStatus.innerText = 'Copy'; + }, 2000); + } + return; + } +} + +const deleteMessage = (message, index) => { + if (typeof message.id !== 'undefined') { + messages.value = messages.value.filter((x, i) => !( + (x.id == message.id) || + (message.role === 'user' ? x.parentMessageId == message.id : message.parentMessageId == x.id) + )); + } + else { + messages.value = message.role === 'user' ? messages.value.filter((_, i) => !(i == index || i == index + 1)) : messages.value.filter((_, i) => !(i == index || i == index - 1)); + } +} + diff --git a/demos/client-dropdown.png b/demos/client-dropdown.png index f7a4684..32177bc 100644 Binary files a/demos/client-dropdown.png and b/demos/client-dropdown.png differ diff --git a/demos/client-settings.png b/demos/client-settings.png index 9bc6ce5..c956612 100644 Binary files a/demos/client-settings.png and b/demos/client-settings.png differ diff --git a/demos/client.png b/demos/client.png index 56f92c9..fdd04d3 100644 Binary files a/demos/client.png and b/demos/client.png differ diff --git a/nuxt.config.js b/nuxt.config.js index c4130a4..5b477e6 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -3,7 +3,7 @@ export default defineNuxtConfig({ ssr: false, runtimeConfig: { public: { - apiBaseUrl: process.env.API_BASE_URL, + apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL, }, }, imports: { diff --git a/package-lock.json b/package-lock.json index 9322065..3536ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "@headlessui/vue": "^1.7.14", "@microsoft/fetch-event-source": "^2.0.1", "@pinia/nuxt": "^0.4.11", + "copy-to-clipboard": "^3.3.3", "fork-corner": "^2.0.1", "highlight.js": "^11.8.0", "isomorphic-dompurify": "^1.6.0", "lodash": "^4.17.21", "marked": "^5.0.4", + "marked-katex-extension": "^2.1.0", "pinia": "^2.1.3", "uuid": "^9.0.0" }, @@ -1952,6 +1954,11 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/katex": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==" + }, "node_modules/@types/node": { "version": "20.2.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", @@ -3694,6 +3701,14 @@ "node": ">= 0.8" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6684,6 +6699,29 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -7205,6 +7243,18 @@ "node": ">= 18" } }, + "node_modules/marked-katex-extension": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/marked-katex-extension/-/marked-katex-extension-2.1.0.tgz", + "integrity": "sha512-JZ6/eBigXVKT78vFNYHUGcsO5zoVMyP0XclmUx2j7H3ZXXmSVMhXUGr3rrolwQQjM/1qn/NW3BVk436Fmm7X/Q==", + "dependencies": { + "@types/katex": "^0.16.0", + "katex": "^0.16.7" + }, + "peerDependencies": { + "marked": "^4 || ^5" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -10924,6 +10974,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index 9b94862..d8e58c4 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,13 @@ "@headlessui/vue": "^1.7.14", "@microsoft/fetch-event-source": "^2.0.1", "@pinia/nuxt": "^0.4.11", + "copy-to-clipboard": "^3.3.3", "fork-corner": "^2.0.1", "highlight.js": "^11.8.0", "isomorphic-dompurify": "^1.6.0", "lodash": "^4.17.21", "marked": "^5.0.4", + "marked-katex-extension": "^2.1.0", "pinia": "^2.1.3", "uuid": "^9.0.0" }