add copy function
This commit is contained in:
parent
4c195b96f4
commit
66c0fe57df
|
@ -7,6 +7,9 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "chatgpt-web",
|
"name": "chatgpt-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-to-clipboard": "^3.3.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fullhuman/postcss-purgecss": "^5.0.0",
|
"@fullhuman/postcss-purgecss": "^5.0.0",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
|
@ -627,6 +630,14 @@
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"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/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
@ -1583,6 +1594,11 @@
|
||||||
"node": ">=8.0"
|
"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/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||||
|
|
|
@ -28,5 +28,8 @@
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^4.1.0"
|
"vite": "^4.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"copy-to-clipboard": "^3.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
src/app.scss
11
src/app.scss
|
@ -83,4 +83,15 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
|
||||||
@import "/node_modules/bulma-prefers-dark/build/bulma-prefers-dark.sass";
|
@import "/node_modules/bulma-prefers-dark/build/bulma-prefers-dark.sass";
|
||||||
.modal-card-body { // remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
|
.modal-card-body { // remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
|
||||||
background-color: $background-dark;
|
background-color: $background-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for copy button */
|
||||||
|
pre[data-language="plaintext"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
div.copy-prompt {
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
}
|
}
|
|
@ -1,10 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
//import { fetchEventSource } from "@microsoft/fetch-event-source";
|
//import { fetchEventSource } from "@microsoft/fetch-event-source";
|
||||||
|
|
||||||
import { apiKeyStorage, chatsStorage, addMessage, clearMessages } from "./Storage.svelte";
|
import {
|
||||||
|
apiKeyStorage,
|
||||||
|
chatsStorage,
|
||||||
|
addMessage,
|
||||||
|
clearMessages,
|
||||||
|
} from "./Storage.svelte";
|
||||||
import type { Request, Response, Message, Settings } from "./Types.svelte";
|
import type { Request, Response, Message, Settings } from "./Types.svelte";
|
||||||
import Code from "./Code.svelte";
|
import Code from "./Code.svelte";
|
||||||
|
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
|
||||||
import { afterUpdate, onMount } from "svelte";
|
import { afterUpdate, onMount } from "svelte";
|
||||||
import SvelteMarkdown from "svelte-markdown";
|
import SvelteMarkdown from "svelte-markdown";
|
||||||
|
|
||||||
|
@ -95,6 +102,7 @@
|
||||||
// Scroll to the bottom of the page after any updates to the messages array
|
// Scroll to the bottom of the page after any updates to the messages array
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
input.focus();
|
input.focus();
|
||||||
|
copyFunction()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Marked options
|
// Marked options
|
||||||
|
@ -141,9 +149,14 @@
|
||||||
|
|
||||||
// Provide the settings by mapping the settingsMap to key/value pairs
|
// Provide the settings by mapping the settingsMap to key/value pairs
|
||||||
...settingsMap.reduce((acc, setting) => {
|
...settingsMap.reduce((acc, setting) => {
|
||||||
const value = (settings.querySelector(`#settings-${setting.key}`) as HTMLInputElement).value;
|
const value = (
|
||||||
|
settings.querySelector(
|
||||||
|
`#settings-${setting.key}`
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
if (value) {
|
if (value) {
|
||||||
acc[setting.key] = setting.type === "number" ? parseFloat(value) : value;
|
acc[setting.key] =
|
||||||
|
setting.type === "number" ? parseFloat(value) : value;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
|
@ -195,7 +208,9 @@
|
||||||
addMessage(chatId, choice.message);
|
addMessage(chatId, choice.message);
|
||||||
// Use TTS to read the response, if query was recorded
|
// Use TTS to read the response, if query was recorded
|
||||||
if (recorded && "SpeechSynthesisUtterance" in window) {
|
if (recorded && "SpeechSynthesisUtterance" in window) {
|
||||||
const utterance = new SpeechSynthesisUtterance(choice.message.content);
|
const utterance = new SpeechSynthesisUtterance(
|
||||||
|
choice.message.content
|
||||||
|
);
|
||||||
speechSynthesis.speak(utterance);
|
speechSynthesis.speak(utterance);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -228,7 +243,9 @@
|
||||||
|
|
||||||
const deleteChat = () => {
|
const deleteChat = () => {
|
||||||
if (confirm("Are you sure you want to delete this chat?")) {
|
if (confirm("Are you sure you want to delete this chat?")) {
|
||||||
chatsStorage.update((chats) => chats.filter((chat) => chat.id !== chatId));
|
chatsStorage.update((chats) =>
|
||||||
|
chats.filter((chat) => chat.id !== chatId)
|
||||||
|
);
|
||||||
chatId = null;
|
chatId = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -243,7 +260,9 @@
|
||||||
|
|
||||||
const clearSettings = () => {
|
const clearSettings = () => {
|
||||||
settingsMap.forEach((setting) => {
|
settingsMap.forEach((setting) => {
|
||||||
const input = settings.querySelector(`#settings-${setting.key}`) as HTMLInputElement;
|
const input = settings.querySelector(
|
||||||
|
`#settings-${setting.key}`
|
||||||
|
) as HTMLInputElement;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -257,6 +276,32 @@
|
||||||
recognition?.start();
|
recognition?.start();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// copy code block
|
||||||
|
// reference: https://vyacheslavbasharov.com/blog/adding-click-to-copy-code-markdown-blog
|
||||||
|
const copyFunction = () => {
|
||||||
|
const codeBlocks = document.querySelectorAll("pre");
|
||||||
|
const showCopyMessage = "Copy";
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
|
const copyPrompt = document.createElement("div");
|
||||||
|
copyPrompt.className = "copy-prompt";
|
||||||
|
const copyPromptText = document.createElement("button");
|
||||||
|
copyPromptText.classList.add("button", "is-light", "is-outlined", "is-small", "is-responsive");
|
||||||
|
copyPromptText.innerHTML = showCopyMessage;
|
||||||
|
copyPrompt.appendChild(copyPromptText);
|
||||||
|
block.appendChild(copyPrompt);
|
||||||
|
block
|
||||||
|
.querySelector(".copy-prompt > button")
|
||||||
|
.addEventListener("click", (evt) => {
|
||||||
|
copy(block.querySelector("code").textContent);
|
||||||
|
block.querySelector(".copy-prompt > button").innerHTML = "Copied!";
|
||||||
|
setTimeout(() => {
|
||||||
|
block.querySelector(".copy-prompt > button").innerHTML =
|
||||||
|
showCopyMessage;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="level chat-header">
|
<nav class="level chat-header">
|
||||||
|
@ -269,7 +314,10 @@
|
||||||
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
|
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
|
||||||
title="Rename chat"
|
title="Rename chat"
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
let newChatName = prompt("Enter a new name for this chat", chat.name);
|
let newChatName = prompt(
|
||||||
|
"Enter a new name for this chat",
|
||||||
|
chat.name
|
||||||
|
);
|
||||||
if (newChatName) {
|
if (newChatName) {
|
||||||
chat.name = newChatName;
|
chat.name = newChatName;
|
||||||
chatsStorage.set($chatsStorage);
|
chatsStorage.set($chatsStorage);
|
||||||
|
@ -314,7 +362,9 @@
|
||||||
{#if message.role === "user"}
|
{#if message.role === "user"}
|
||||||
<article
|
<article
|
||||||
class="message is-info user-message"
|
class="message is-info user-message"
|
||||||
class:has-text-right={message.content.split("\n").filter((line) => line.trim()).length === 1}
|
class:has-text-right={message.content
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.trim()).length === 1}
|
||||||
>
|
>
|
||||||
<div class="message-body content">
|
<div class="message-body content">
|
||||||
<a
|
<a
|
||||||
|
@ -360,9 +410,13 @@
|
||||||
/>
|
/>
|
||||||
{#if message.usage}
|
{#if message.usage}
|
||||||
<p class="is-size-7">
|
<p class="is-size-7">
|
||||||
This message was generated using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
|
This message was generated using <span class="has-text-weight-bold"
|
||||||
|
>{message.usage.total_tokens}</span
|
||||||
|
>
|
||||||
tokens ~=
|
tokens ~=
|
||||||
<span class="has-text-weight-bold">${(message.usage.total_tokens * token_price).toFixed(6)}</span>
|
<span class="has-text-weight-bold"
|
||||||
|
>${(message.usage.total_tokens * token_price).toFixed(6)}</span
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -374,7 +428,10 @@
|
||||||
<progress class="progress is-small is-dark" max="100" />
|
<progress class="progress is-small is-dark" max="100" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form class="field has-addons has-addons-right" on:submit|preventDefault={() => submitForm()}>
|
<form
|
||||||
|
class="field has-addons has-addons-right"
|
||||||
|
on:submit|preventDefault={() => submitForm()}
|
||||||
|
>
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<textarea
|
<textarea
|
||||||
class="input is-info is-focused chat-input"
|
class="input is-info is-focused chat-input"
|
||||||
|
@ -396,12 +453,17 @@
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p class="control" class:is-hidden={!recognition}>
|
<p class="control" class:is-hidden={!recognition}>
|
||||||
<button class="button" class:is-pulse={recording} on:click|preventDefault={recordToggle}
|
<button
|
||||||
|
class="button"
|
||||||
|
class:is-pulse={recording}
|
||||||
|
on:click|preventDefault={recordToggle}
|
||||||
><span class="greyscale">🎤</span></button
|
><span class="greyscale">🎤</span></button
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button" on:click|preventDefault={showSettings}><span class="greyscale">⚙️</span></button>
|
<button class="button" on:click|preventDefault={showSettings}
|
||||||
|
><span class="greyscale">⚙️</span></button
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-info" type="submit">Send</button>
|
<button class="button is-info" type="submit">Send</button>
|
||||||
|
@ -427,7 +489,9 @@
|
||||||
{#each settingsMap as setting}
|
{#each settingsMap as setting}
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label class="label" for="settings-temperature">{setting.name}</label>
|
<label class="label" for="settings-temperature"
|
||||||
|
>{setting.name}</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -444,7 +508,9 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button class="button is-info" on:click={closeSettings}>Close settings</button>
|
<button class="button is-info" on:click={closeSettings}
|
||||||
|
>Close settings</button
|
||||||
|
>
|
||||||
<button class="button" on:click={clearSettings}>Clear settings</button>
|
<button class="button" on:click={clearSettings}>Clear settings</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue