Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UX features and bugfixes #133

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.nuxt/
.output/
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1 @@
API_BASE_URL=http://localhost:3000
NUXT_PUBLIC_API_BASE_URL=http://localhost:3000
17 changes: 12 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
CMD ["npm", "run", "preview"]
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<details>
<summary><strong>Nuxt 3 Setup</strong></summary>

Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ onMounted(() => {
<span class="text-xs font-light text-slate-400">
powered by
<a href="https://github.com/waylaidwanderer/node-chatgpt-api" target="_blank">
https://github.com/waylaidwanderer/node-chatgpt-api
node-chatgpt-api
</a>
</span>
</footer>
Expand Down
154 changes: 114 additions & 40 deletions components/Chat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ 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,
xhtml: true,
breaks: true,
gfm: true,
});
marked.use(markedKatex({throwOnError: false}));

const renderer = {
code(code, lang) {
let language = 'plaintext';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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));
}
}
</script>

<template>
Expand All @@ -535,35 +561,72 @@ if (!process.server) {
<TransitionGroup name="messages">
<div
class="max-w-4xl w-full mx-auto message"
v-for="(message, index) in messages"
:key="message.id || index"
>
<div
class="p-3 rounded-sm"
:class="{
'bg-white/10 shadow': message.role === 'bot',
}"
>
<!-- role name -->
<template v-for="(message, index) in messages" :key="message.id || index">
<div
class="text-xs text-white/50 mb-1"
class="p-3 rounded-sm"
:class="{
'bg-white/10 shadow': message.role === 'bot',
}"
>
<template v-if="message.role === 'bot'">
{{ activePresetToUse?.options?.clientOptions?.chatGptLabel || 'AI' }}
</template>
<template v-else-if="message.role === 'user'">
{{ activePresetToUse?.options?.clientOptions?.userLabel || 'User' }}
</template>
<template v-else>
{{ message.role }}
</template>
<!-- role name -->
<div
class="flex flex-column text-xs text-white/50 mb-1"
>
<span class="message-role-name flex-1">
<template v-if="message.role === 'bot'">
<Icon name="bx:bx-bot"/>
{{ activePresetToUse?.options?.clientOptions?.chatGptLabel || 'AI' }}
</template>
<template v-else-if="message.role === 'user'">
<Icon name="bx:bx-user"/>
{{ activePresetToUse?.options?.clientOptions?.userLabel || 'User' }}
</template>
<template v-else>
<Icon name="bx:question-mark"/>
{{ message.role }}
</template>
</span>

<span class="message-functions flex-1">
<a href="javascript:;" class="function-buttons transition duration-300 ease-in-out
hover:bg-white/10" @click="deleteMessage(message, index)">
<Icon name="bx:bx-trash"/> Delete
</a>
<a href="javascript:;" class="function-buttons transition duration-300 ease-in-out
hover:bg-white/10" @click="copyToClipboard(message.text, $event.target)">
<Icon name="bx:bx-copy"/>&nbsp;<span class="copy-status">Copy</span>
</a>
</span>
</div>
<!-- message text -->
<div
class="prose prose-sm prose-chatgpt break-words max-w-6xl"
v-html="(message.role === 'user' || message.raw) ? parseMarkdown(message.text) : parseMarkdown(message.text, true)"
/>
</div>
<!-- message text -->
<div
class="prose prose-sm prose-chatgpt break-words max-w-6xl"
v-html="(message.role === 'user' || message.raw) ? parseMarkdown(message.text) : parseMarkdown(message.text, true)"
/>
</div>
</template>
<template v-if="message !== ''">
<div class="p-3 rounded-sm">
<!-- role name -->
<div
class="flex flex-column text-xs text-white/50 mb-1"
>
<span class="message-role-name flex-1">
<Icon name="bx:user-voice"/>
Typing...
</span>

<span class="message-functions flex-1">
</span>
</div>
<!-- message text -->
<div
class="prose prose-sm prose-chatgpt break-words max-w-6xl"
v-html="parseMarkdown(message)"
/>
</div>
</template>
</div>
</TransitionGroup>
</div>
Expand Down Expand Up @@ -692,6 +755,7 @@ if (!process.server) {
</div>
</template>

<style src="@/node_modules/katex/dist/katex.min.css"></style>
<style>
.messages-move, /* apply transition to moving elements */
.messages-enter-active {
Expand Down Expand Up @@ -770,4 +834,14 @@ input[type="range"]::-moz-range-thumb {
iframe {
@apply bg-slate-100;
}

.message-functions {
text-align: right;
}

.function-buttons {
margin-left: 2pt;
padding: 1pt;
}

</style>
Binary file modified demos/client-dropdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/client-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/client.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
55 changes: 55 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading