@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-theme="cupcake" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
|
||||
2268
package-lock.json
generated
Normal file
2268
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@types/ungap__structured-clone": "^0.3.1",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
@@ -21,6 +22,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.6.0",
|
||||
"daisyui": "^4.12.10",
|
||||
"theme-change": "^2.5.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0"
|
||||
}
|
||||
|
||||
@@ -41,15 +41,15 @@ export function AddImage({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded p-2 z-20"
|
||||
className="bg-base-200 p-2 z-20"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between p-1">
|
||||
<span>Add Images</span>
|
||||
<div className="flex justify-between items-center p-1">
|
||||
<h3>Add Images</h3>
|
||||
<button
|
||||
className="m-1 p-1 border-2 bg-red-400"
|
||||
className="btn btn-sm btn-neutral"
|
||||
onClick={() => {
|
||||
setShowAddImage(false);
|
||||
}}
|
||||
@@ -57,10 +57,9 @@ export function AddImage({
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<span>
|
||||
<span class="">
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-secondary btn-sm disabled:btn-disabled"
|
||||
onClick={() => {
|
||||
const image_url = prompt("Image URL");
|
||||
if (!image_url) {
|
||||
@@ -81,7 +80,7 @@ export function AddImage({
|
||||
Add from URL
|
||||
</button>
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-primary btn-sm disabled:btn-disabled"
|
||||
onClick={() => {
|
||||
// select file and load it to base64 image URL format
|
||||
const input = document.createElement("input");
|
||||
@@ -122,23 +121,24 @@ export function AddImage({
|
||||
<input type="checkbox" checked={enableHighResolution} />
|
||||
</span>
|
||||
</span>
|
||||
<div class="divider"></div>
|
||||
{chatStore.image_gen_api && chatStore.image_gen_key && (
|
||||
<div className="flex flex-col">
|
||||
<hr className="my-2" />
|
||||
<h3>Generate Image</h3>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-col justify-between m-1 p-1">
|
||||
<label>Prompt: </label>
|
||||
<textarea
|
||||
className="border rounded border-gray-400"
|
||||
className="textarea textarea-sm textarea-bordered"
|
||||
value={imageGenPrompt}
|
||||
onChange={(e: any) => {
|
||||
setImageGenPrompt(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Model: </label>
|
||||
<select
|
||||
class="select select-sm select-bordered"
|
||||
value={imageGenModel}
|
||||
onChange={(e: any) => {
|
||||
setImageGenModel(e.target.value);
|
||||
@@ -148,9 +148,10 @@ export function AddImage({
|
||||
<option value="dall-e-2">DALL-E 2</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>n: </label>
|
||||
<input
|
||||
class="input input-sm input-bordered"
|
||||
value={imageGenN}
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -158,9 +159,10 @@ export function AddImage({
|
||||
onChange={(e: any) => setImageGenN(parseInt(e.target.value))}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Quality: </label>
|
||||
<select
|
||||
class="select select-sm select-bordered"
|
||||
value={imageGenQuality}
|
||||
onChange={(e: any) => setImageGEnQuality(e.target.value)}
|
||||
>
|
||||
@@ -168,9 +170,10 @@ export function AddImage({
|
||||
<option value="standard">Standard</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Response Format: </label>
|
||||
<select
|
||||
class="select select-sm select-bordered"
|
||||
value={imageGenResponseFormat}
|
||||
onChange={(e: any) => setImageGenResponseFormat(e.target.value)}
|
||||
>
|
||||
@@ -178,9 +181,10 @@ export function AddImage({
|
||||
<option value="url">url</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Size: </label>
|
||||
<select
|
||||
class="select select-sm select-bordered"
|
||||
value={imageGenSize}
|
||||
onChange={(e: any) => setImageGenSize(e.target.value)}
|
||||
>
|
||||
@@ -191,9 +195,10 @@ export function AddImage({
|
||||
<option value="1024x1792">1024x1792 (dall-e-3)</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Style (only dall-e-3): </label>
|
||||
<select
|
||||
class="select select-sm select-bordered"
|
||||
value={imageGenStyle}
|
||||
onChange={(e: any) => setImageGenStyle(e.target.value)}
|
||||
>
|
||||
@@ -201,9 +206,9 @@ export function AddImage({
|
||||
<option value="natural">natural</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<button
|
||||
className="bg-sky-400 m-1 p-1 rounded disabled:bg-slate-500"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={imageGenGenerating}
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
||||
31
src/app.tsx
31
src/app.tsx
@@ -228,7 +228,9 @@ export function App() {
|
||||
|
||||
if (oldVersion < 11) {
|
||||
if (oldVersion < 11 && oldVersion >= 1) {
|
||||
alert("Start upgrading storage, just a sec... (Click OK to continue)");
|
||||
alert(
|
||||
"Start upgrading storage, just a sec... (Click OK to continue)"
|
||||
);
|
||||
}
|
||||
if (
|
||||
transaction
|
||||
@@ -401,16 +403,16 @@ export function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex text-sm h-full bg-slate-200 dark:bg-slate-800 dark:text-white">
|
||||
<div className="flex flex-col h-full p-2 border-r-indigo-500 border-2 dark:border-slate-800 dark:border-r-indigo-500 dark:text-black">
|
||||
<div className="flex text-sm h-full">
|
||||
<div className="flex flex-col h-full p-2 bg-primary">
|
||||
<div className="grow overflow-scroll">
|
||||
<button
|
||||
className="w-full bg-violet-300 p-1 rounded hover:bg-violet-400"
|
||||
className="btn btn-sm btn-info p-1 my-1 w-full"
|
||||
onClick={handleNewChatStore}
|
||||
>
|
||||
{Tr("NEW")}
|
||||
</button>
|
||||
<ul>
|
||||
<ul class="pt-2">
|
||||
{(allChatStoreIndexes as number[])
|
||||
.slice()
|
||||
.reverse()
|
||||
@@ -419,8 +421,8 @@ export function App() {
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
className={`w-full my-1 p-1 rounded hover:bg-blue-500 ${
|
||||
i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200"
|
||||
className={`w-full my-1 p-1 btn btn-sm ${
|
||||
i === selectedChatIndex ? "btn-accent" : "btn-secondary"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedChatIndex(i);
|
||||
@@ -433,12 +435,18 @@ export function App() {
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="rounded bg-rose-400 p-1 my-1 w-full"
|
||||
className="btn btn-warning btn-sm p-1 my-1 w-full"
|
||||
onClick={async () => {
|
||||
if (!confirm("Are you sure you want to delete this chat history?"))
|
||||
if (
|
||||
!confirm("Are you sure you want to delete this chat history?")
|
||||
)
|
||||
return;
|
||||
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
|
||||
console.log(
|
||||
"remove item",
|
||||
`${STORAGE_NAME}-${selectedChatIndex}`
|
||||
);
|
||||
(await db).delete(STORAGE_NAME, selectedChatIndex);
|
||||
const newAllChatStoreIndexes = await (
|
||||
await db
|
||||
@@ -461,7 +469,7 @@ export function App() {
|
||||
</button>
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="rounded bg-rose-800 p-1 my-1 w-full text-white"
|
||||
className="btn btn-sm btn-warning p-1 my-1 w-full"
|
||||
onClick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
@@ -480,6 +488,7 @@ export function App() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChatBOX
|
||||
db={db}
|
||||
chatStore={chatStore}
|
||||
|
||||
188
src/chatbox.tsx
188
src/chatbox.tsx
@@ -14,6 +14,7 @@ import {
|
||||
TemplateAPI,
|
||||
TemplateTools,
|
||||
addTotalCost,
|
||||
getTotalCost,
|
||||
} from "./app";
|
||||
import ChatGPT, {
|
||||
calculate_token_length,
|
||||
@@ -34,6 +35,16 @@ import { ListToolsTempaltes } from "./listToolsTemplates";
|
||||
import { autoHeight } from "./textarea";
|
||||
import Search from "./search";
|
||||
import { IDBPDatabase } from "idb";
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
CubeIcon,
|
||||
BanknotesIcon,
|
||||
DocumentTextIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
ScissorsIcon,
|
||||
SwatchIcon,
|
||||
SparklesIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
export interface TemplateChatStore extends ChatStore {
|
||||
name: string;
|
||||
@@ -434,7 +445,7 @@ export default function ChatBOX(props: {
|
||||
const userInputRef = createRef();
|
||||
|
||||
return (
|
||||
<div className="grow flex flex-col p-2 dark:text-black">
|
||||
<div className="grow flex flex-col p-2">
|
||||
{showSettings && (
|
||||
<Settings
|
||||
chatStore={chatStore}
|
||||
@@ -463,12 +474,66 @@ export default function ChatBOX(props: {
|
||||
setShow={setShowSearch}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="relative cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800"
|
||||
onClick={() => setShowSettings(true)}
|
||||
<div class="navbar bg-base-100">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex={0} role="button" class="btn btn-ghost btn-circle">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex={0}
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<p>
|
||||
<ChatBubbleLeftEllipsisIcon class="h-4 w-4" />
|
||||
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<ScissorsIcon class="h-4 w-4" />
|
||||
Cut:
|
||||
{chatStore.postBeginIndex}/
|
||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<BanknotesIcon class="h-4 w-4" />
|
||||
Cost: ${chatStore.cost.toFixed(4)}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-center">
|
||||
<div class="indicator">
|
||||
<span class="indicator-item badge badge-primary">
|
||||
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
|
||||
</span>
|
||||
<a class="btn btn-ghost text-xl">
|
||||
<SparklesIcon class="h-4 w-4" />
|
||||
{chatStore.model}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<button
|
||||
className="absolute right-1 bg-gray-300 rounded p-1 m-1"
|
||||
class="btn btn-ghost btn-circle"
|
||||
onClick={(event) => {
|
||||
// stop propagation to parent
|
||||
event.stopPropagation();
|
||||
@@ -476,8 +541,68 @@ export default function ChatBOX(props: {
|
||||
setShowSearch(true);
|
||||
}}
|
||||
>
|
||||
🔍
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-circle"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<div class="indicator">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="badge badge-xs badge-primary indicator-item"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div
|
||||
className="relative cursor-pointer rounded p-2"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<button
|
||||
className="absolute right-1 rounded p-1 m-1"
|
||||
onClick={(event) => {
|
||||
// stop propagation to parent
|
||||
event.stopPropagation();
|
||||
|
||||
setShowSearch(true);
|
||||
}}
|
||||
>
|
||||
<MagnifyingGlassIcon class="w-5 h-5" />
|
||||
</button>
|
||||
<div class="hidden lg:inline-grid"></div>
|
||||
<div class="lg:hidden">
|
||||
<div>
|
||||
<button className="underline">
|
||||
{chatStore.systemMessageContent.length > 16
|
||||
@@ -492,16 +617,16 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="underline">{chatStore.model}</span>{" "}
|
||||
<span class="underline">{chatStore.model}</span>{" "}
|
||||
<span>
|
||||
Tokens:{" "}
|
||||
<span className="underline">
|
||||
<span class="underline">
|
||||
{chatStore.totalTokens}/{chatStore.maxTokens}
|
||||
</span>
|
||||
</span>{" "}
|
||||
<span>
|
||||
{Tr("Cut")}:{" "}
|
||||
<span className="underline">
|
||||
<span class="underline">
|
||||
{chatStore.postBeginIndex}/
|
||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||
</span>{" "}
|
||||
@@ -512,14 +637,15 @@ export default function ChatBOX(props: {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="grow overflow-scroll">
|
||||
{!chatStore.apiKey && (
|
||||
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
|
||||
<p className="bg-base-200 p-6 rounded my-3 text-left">
|
||||
{Tr("Please click above to set")} (OpenAI) API KEY
|
||||
</p>
|
||||
)}
|
||||
{!chatStore.apiEndpoint && (
|
||||
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
|
||||
<p className="bg-base-200 p-6 rounded my-3 text-left">
|
||||
{Tr("Please click above to set")} API Endpoint
|
||||
</p>
|
||||
)}
|
||||
@@ -581,7 +707,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
|
||||
{chatStore.history.filter((msg) => !msg.example).length == 0 && (
|
||||
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black">
|
||||
<div className="bg-base-200 break-all p-3 my-3 text-left">
|
||||
<h2>
|
||||
<span>{Tr("Saved prompt templates")}</span>
|
||||
<button
|
||||
@@ -596,7 +722,7 @@ export default function ChatBOX(props: {
|
||||
{Tr("Reset Current")}
|
||||
</button>
|
||||
</h2>
|
||||
<hr className="my-2" />
|
||||
<div class="divider"></div>
|
||||
<div className="flex flex-wrap">
|
||||
{templates.map((t, index) => (
|
||||
<div
|
||||
@@ -712,6 +838,13 @@ export default function ChatBOX(props: {
|
||||
<br />
|
||||
</p>
|
||||
)}
|
||||
<div class="chat chat-start">
|
||||
<div class="chat-header">Prompt</div>
|
||||
<div class="chat-bubble chat-bubble-accent">
|
||||
{chatStore.systemMessageContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{chatStore.history.map((_, messageIndex) => (
|
||||
<Message
|
||||
chatStore={chatStore}
|
||||
@@ -721,7 +854,7 @@ export default function ChatBOX(props: {
|
||||
/>
|
||||
))}
|
||||
{showGenerating && (
|
||||
<p className="p-2 my-2 animate-pulse dark:text-white message-content">
|
||||
<p className="p-2 my-2 animate-pulse message-content">
|
||||
{generatingMessage || Tr("Generating...")}
|
||||
...
|
||||
</p>
|
||||
@@ -729,7 +862,7 @@ export default function ChatBOX(props: {
|
||||
<p className="text-center">
|
||||
{chatStore.history.length > 0 && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-teal-500 hover:bg-teal-600"
|
||||
className="btn btn-sm btn-warning disabled:line-through disabled:btn-neutral disabled:text-white m-2 p-2"
|
||||
disabled={showGenerating}
|
||||
onClick={async () => {
|
||||
const messageIndex = chatStore.history.length - 1;
|
||||
@@ -749,7 +882,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-yellow-500 hover:bg-yellow-600"
|
||||
className="btn btn-outline btn-sm btn-warning disabled:line-through disabled:bg-neural"
|
||||
disabled={showGenerating}
|
||||
onClick={async () => {
|
||||
await complete();
|
||||
@@ -852,15 +985,16 @@ export default function ChatBOX(props: {
|
||||
<input type="checkbox" checked={follow} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-primary disabled:line-through disabled:text-white disabled:bg-neutral m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
setShowAddImage(!showAddImage);
|
||||
}}
|
||||
>
|
||||
Img
|
||||
Image
|
||||
</button>
|
||||
{showAddImage && (
|
||||
<AddImage
|
||||
@@ -891,11 +1025,11 @@ export default function ChatBOX(props: {
|
||||
autoHeight(event.target);
|
||||
setInputMsg(event.target.value);
|
||||
}}
|
||||
className="rounded grow m-1 p-1 border-2 border-gray-400 w-0"
|
||||
className="textarea textarea-bordered textarea-sm grow"
|
||||
placeholder="Type here..."
|
||||
></textarea>
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-primary disabled:btn-neutral disabled:line-through m-1 p-1"
|
||||
disabled={showGenerating}
|
||||
onClick={() => {
|
||||
send(inputMsg, true);
|
||||
@@ -909,10 +1043,8 @@ export default function ChatBOX(props: {
|
||||
chatStore.whisper_key &&
|
||||
(chatStore.whisper_key || chatStore.apiKey) && (
|
||||
<button
|
||||
className={`disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 ${
|
||||
isRecording === "Recording"
|
||||
? "bg-red-400 hover:bg-red-600"
|
||||
: "bg-cyan-400 hover:bg-cyan-600"
|
||||
className={`btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1 ${
|
||||
isRecording === "Recording" ? "btn-error" : "btn-success"
|
||||
} ${isRecording !== "Mic" ? "animate-pulse" : ""}`}
|
||||
disabled={isRecording === "Transcribing"}
|
||||
ref={mediaRef}
|
||||
@@ -1027,7 +1159,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
chatStore.history.push({
|
||||
@@ -1051,7 +1183,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
send(inputMsg, false);
|
||||
@@ -1062,7 +1194,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
setShowAddToolMsg(true);
|
||||
@@ -1110,7 +1242,7 @@ export default function ChatBOX(props: {
|
||||
</span>
|
||||
<span className={`flex justify-between p-2`}>
|
||||
<button
|
||||
className="rounded m-1 p-1 border-2 bg-red-400 hover:bg-red-600"
|
||||
className="btn btn-info m-1 p-1"
|
||||
onClick={() => setShowAddToolMsg(false)}
|
||||
>
|
||||
{Tr("Cancle")}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ListAPIs({
|
||||
keyField,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black">
|
||||
<div className="break-all opacity-80 p-3 rounded base-200 my-3 text-left">
|
||||
<h2>{Tr(`Saved ${label} templates`)}</h2>
|
||||
<hr className="my-2" />
|
||||
<div className="flex flex-wrap">
|
||||
@@ -31,8 +31,8 @@ export function ListAPIs({
|
||||
chatStore[apiField] === t.endpoint &&
|
||||
// @ts-ignore
|
||||
chatStore[keyField] === t.key
|
||||
? "bg-red-600"
|
||||
: "bg-red-400"
|
||||
? "bg-info"
|
||||
: "bg-base-300"
|
||||
} w-fit p-2 m-1 flex flex-col`}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
@@ -43,9 +43,9 @@ export function ListAPIs({
|
||||
}}
|
||||
>
|
||||
<span className="w-full text-center">{t.name}</span>
|
||||
<hr className="mt-2" />
|
||||
<span className="flex justify-between">
|
||||
<span className="flex justify-between gap-x-2">
|
||||
<button
|
||||
class="link"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give **${label}** template a name`);
|
||||
if (!name) {
|
||||
@@ -55,9 +55,10 @@ export function ListAPIs({
|
||||
setTmps(structuredClone(tmps));
|
||||
}}
|
||||
>
|
||||
🖋
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="link"
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(
|
||||
@@ -70,7 +71,7 @@ export function ListAPIs({
|
||||
setTmps(structuredClone(tmps));
|
||||
}}
|
||||
>
|
||||
❌
|
||||
Delete
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -33,8 +33,8 @@ export function ListToolsTempaltes({
|
||||
<div
|
||||
className={`cursor-pointer rounded ${
|
||||
chatStore.toolsString === t.toolsString
|
||||
? "bg-red-600"
|
||||
: "bg-red-400"
|
||||
? "bg-info"
|
||||
: "bg-base-300"
|
||||
} w-fit p-2 m-1 flex flex-col`}
|
||||
onClick={() => {
|
||||
chatStore.toolsString = t.toolsString;
|
||||
@@ -42,9 +42,9 @@ export function ListToolsTempaltes({
|
||||
}}
|
||||
>
|
||||
<span className="w-full text-center">{t.name}</span>
|
||||
<hr className="mt-2" />
|
||||
<span className="flex justify-between">
|
||||
<span className="flex justify-between gap-x-2">
|
||||
<button
|
||||
class="link"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give **tools** template a name`);
|
||||
if (!name) {
|
||||
@@ -54,9 +54,10 @@ export function ListToolsTempaltes({
|
||||
setTemplateTools(structuredClone(templateTools));
|
||||
}}
|
||||
>
|
||||
🖋
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="link"
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(`Are you sure to delete this **tools** template?`)
|
||||
@@ -67,7 +68,7 @@ export function ListToolsTempaltes({
|
||||
setTemplateTools(structuredClone(templateTools));
|
||||
}}
|
||||
>
|
||||
❌
|
||||
Delete
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ const logprobToColor = (logprob: number) => {
|
||||
// 绿色的RGB值为(0, 255, 0),红色的RGB值为(255, 0, 0)
|
||||
const red = Math.round(255 * (1 - percent / 100));
|
||||
const green = Math.round(255 * (percent / 100));
|
||||
const color = `rgb(${red}, ${green}, 0)`;
|
||||
const color = `rgba(${red}, ${green}, 0, 0.5)`;
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
19
src/main.tsx
19
src/main.tsx
@@ -3,25 +3,28 @@ import { App } from "./app";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
|
||||
import { themeChange } from "theme-change";
|
||||
|
||||
function Base() {
|
||||
const [langCode, _setLangCode] = useState("en-US");
|
||||
|
||||
const setLangCode = (langCode: string) => {
|
||||
_setLangCode(langCode)
|
||||
if (!localStorage) return
|
||||
_setLangCode(langCode);
|
||||
if (!localStorage) return;
|
||||
|
||||
localStorage.setItem('chatgpt-api-web-lang', langCode)
|
||||
}
|
||||
localStorage.setItem("chatgpt-api-web-lang", langCode);
|
||||
};
|
||||
|
||||
// select language
|
||||
useEffect(() => {
|
||||
themeChange(false);
|
||||
// query localStorage
|
||||
if (localStorage) {
|
||||
const lang = localStorage.getItem('chatgpt-api-web-lang')
|
||||
const lang = localStorage.getItem("chatgpt-api-web-lang");
|
||||
if (lang) {
|
||||
console.log(`query langCode ${lang} from localStorage`)
|
||||
_setLangCode(lang)
|
||||
return
|
||||
console.log(`query langCode ${lang} from localStorage`);
|
||||
_setLangCode(lang);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MessageToolCall } from "./messageToolCall";
|
||||
import { MessageToolResp } from "./messageToolResp";
|
||||
import { EditMessage } from "./editMessage";
|
||||
import logprobToColor from "./logprob";
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const isVailedJSON = (str: string): boolean => {
|
||||
try {
|
||||
@@ -51,17 +52,26 @@ export default function Message(props: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
🗑️
|
||||
Delete
|
||||
</button>
|
||||
);
|
||||
const CopiedHint = () => (
|
||||
<span
|
||||
className={
|
||||
"bg-purple-400 p-1 rounded shadow-md absolute z-20 left-1/2 top-3/4 transform -translate-x-1/2 -translate-y-1/2"
|
||||
}
|
||||
<div role="alert" class="alert">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
{Tr("Message copied to clipboard!")}
|
||||
</span>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{Tr("Message copied to clipboard!")}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
@@ -78,7 +88,7 @@ export default function Message(props: Props) {
|
||||
copyToClipboard(textToCopy);
|
||||
}}
|
||||
>
|
||||
📋
|
||||
Copy
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
@@ -92,8 +102,8 @@ export default function Message(props: Props) {
|
||||
chatStore.history.slice(0, messageIndex).filter(({ hide }) => !hide)
|
||||
.length && (
|
||||
<div className="flex items-center relative justify-center">
|
||||
<hr className="w-full h-px my-4 border-0 bg-slate-800 dark:bg-white" />
|
||||
<span className="absolute px-3 bg-slate-800 text-white rounded p-1 dark:bg-white dark:text-black">
|
||||
<hr className="w-full h-px my-4 border-0" />
|
||||
<span className="absolute px-3 rounded p-1">
|
||||
Above messages are "forgotten"
|
||||
</span>
|
||||
</div>
|
||||
@@ -105,20 +115,33 @@ export default function Message(props: Props) {
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={`w-fit p-2 rounded my-2 ${
|
||||
chat.role === "assistant"
|
||||
? "bg-white dark:bg-gray-700 dark:text-white"
|
||||
: "bg-green-400"
|
||||
className={`chat min-w-16 w-fit p-2 my-2 ${
|
||||
chat.role === "assistant" ? "chat-start" : "chat-end"
|
||||
} ${chat.hide ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`chat-bubble ${
|
||||
chat.role === "assistant"
|
||||
? renderColor
|
||||
? "chat-bubble-neutral"
|
||||
: "chat-bubble-secondary"
|
||||
: "chat-bubble-primary"
|
||||
}`}
|
||||
>
|
||||
{chat.hide ? (
|
||||
<MessageHide chat={chat} />
|
||||
) : typeof chat.content !== "string" ? (
|
||||
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
|
||||
) : chat.tool_calls ? (
|
||||
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
|
||||
<MessageToolCall
|
||||
chat={chat}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
) : chat.role === "tool" ? (
|
||||
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
|
||||
<MessageToolResp
|
||||
chat={chat}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
) : renderMarkdown ? (
|
||||
// @ts-ignore
|
||||
<Markdown markdown={getMessageText(chat)} />
|
||||
@@ -134,7 +157,7 @@ export default function Message(props: Props) {
|
||||
.map((c) => (
|
||||
<div
|
||||
style={{
|
||||
color: logprobToColor(c.logprob),
|
||||
backgroundColor: logprobToColor(c.logprob),
|
||||
display: "inline",
|
||||
}}
|
||||
>
|
||||
@@ -145,11 +168,11 @@ export default function Message(props: Props) {
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<hr className="mt-2" />
|
||||
<TTSPlay chat={chat} />
|
||||
<div className="w-full flex justify-between">
|
||||
</div>
|
||||
<div class="chat-footer opacity-50 flex gap-x-2">
|
||||
<DeleteIcon />
|
||||
<button onClick={() => setShowEdit(true)}>🖋</button>
|
||||
<button onClick={() => setShowEdit(true)}>Edit</button>
|
||||
<CopyIcon textToCopy={getMessageText(chat)} />
|
||||
{chatStore.tts_api && chatStore.tts_key && (
|
||||
<TTSButton
|
||||
chatStore={chatStore}
|
||||
@@ -157,7 +180,7 @@ export default function Message(props: Props) {
|
||||
setChatStore={setChatStore}
|
||||
/>
|
||||
)}
|
||||
<CopyIcon textToCopy={getMessageText(chat)} />
|
||||
<TTSPlay chat={chat} />
|
||||
</div>
|
||||
</div>
|
||||
{showEdit && (
|
||||
@@ -170,11 +193,11 @@ export default function Message(props: Props) {
|
||||
)}
|
||||
{showCopiedHint && <CopiedHint />}
|
||||
{chatStore.develop_mode && (
|
||||
<div>
|
||||
<span className="dark:text-white">token</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span className="">token</span>
|
||||
<input
|
||||
value={chat.token}
|
||||
className="w-20"
|
||||
className="input input-bordered input-xs w-16"
|
||||
onChange={(event: any) => {
|
||||
chat.token = parseInt(event.target.value);
|
||||
props.update_total_tokens();
|
||||
@@ -199,7 +222,7 @@ export default function Message(props: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
❌
|
||||
<XMarkIcon class="w-4 h-4" />
|
||||
</button>
|
||||
<span
|
||||
onClick={(event: any) => {
|
||||
@@ -207,17 +230,17 @@ export default function Message(props: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
<label className="dark:text-white">{Tr("example")}</label>
|
||||
<label className="">{Tr("example")}</label>
|
||||
<input type="checkbox" checked={chat.example} />
|
||||
</span>
|
||||
<span
|
||||
onClick={(event: any) => setRenderWorkdown(!renderMarkdown)}
|
||||
>
|
||||
<label className="dark:text-white">{Tr("render")}</label>
|
||||
<label className="">{Tr("render")}</label>
|
||||
<input type="checkbox" checked={renderMarkdown} />
|
||||
</span>
|
||||
<span onClick={(event: any) => setRenderColor(!renderColor)}>
|
||||
<label className="dark:text-white">{Tr("color")}</label>
|
||||
<label className="">{Tr("color")}</label>
|
||||
<input type="checkbox" checked={renderColor} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function MessageDetail({ chat, renderMarkdown }: Props) {
|
||||
{chat.content.map((mdt) =>
|
||||
mdt.type === "text" ? (
|
||||
chat.hide ? (
|
||||
mdt.text?.split("\n")[0].slice(0, 16) + "... (deleted)"
|
||||
mdt.text?.split("\n")[0].slice(0, 16) + " ..."
|
||||
) : renderMarkdown ? (
|
||||
// @ts-ignore
|
||||
<Markdown markdown={mdt.text} />
|
||||
@@ -22,7 +22,7 @@ export function MessageDetail({ chat, renderMarkdown }: Props) {
|
||||
)
|
||||
) : (
|
||||
<img
|
||||
className="cursor-pointer max-w-xs max-h-32 p-1"
|
||||
className="my-2 rounded-md max-w-64 max-h-64"
|
||||
src={mdt.image_url?.url}
|
||||
onClick={() => {
|
||||
window.open(mdt.image_url?.url, "_blank");
|
||||
|
||||
@@ -6,7 +6,5 @@ interface Props {
|
||||
}
|
||||
|
||||
export function MessageHide({ chat }: Props) {
|
||||
return (
|
||||
<div>{getMessageText(chat).split("\n")[0].slice(0, 18)} ... (deleted)</div>
|
||||
);
|
||||
return <div>{getMessageText(chat).split("\n")[0].slice(0, 18)} ...</div>;
|
||||
}
|
||||
|
||||
@@ -30,23 +30,23 @@ export default function Search(props: {
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
className="m-2 p-2 bg-white rounded-lg h-fit w-2/3 z-20"
|
||||
className="m-2 p-2 bg-base-300 rounded-lg h-fit w-2/3 z-20"
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span className="m-1 p-1 font-bold">Search</span>
|
||||
<button
|
||||
className="m-1 p-1 bg-cyan-400 rounded"
|
||||
className="m-1 p-1 btn btn-sm btn-secondary"
|
||||
onClick={() => props.setShow(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<input
|
||||
autoFocus
|
||||
className="m-1 p-1 w-full border"
|
||||
className="input input-bordered w-full w-full border"
|
||||
type="text"
|
||||
placeholder="Type Something..."
|
||||
onInput={async (event: any) => {
|
||||
const query = event.target.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
@@ -119,44 +119,14 @@ export default function Search(props: {
|
||||
/>
|
||||
</div>
|
||||
{searching && <div>Searching {searchingNow}%...</div>}
|
||||
{searchResult.length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
className="m-1 p-1 rounded bg-green-300 disabled:bg-slate-400"
|
||||
disabled={pageIndex === 0}
|
||||
onClick={() => {
|
||||
if (pageIndex === 0) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex - 1);
|
||||
}}
|
||||
>
|
||||
Last page
|
||||
</button>
|
||||
<div className="m-1 p-1">
|
||||
Page: {pageIndex + 1} / {Math.floor(searchResult.length / 10) + 1}
|
||||
</div>
|
||||
<button
|
||||
className="m-1 p-1 rounded bg-green-300 disabled:bg-slate-400"
|
||||
disabled={pageIndex === Math.floor(searchResult.length / 10)}
|
||||
onClick={() => {
|
||||
if (pageIndex === Math.floor(searchResult.length / 10)) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex + 1);
|
||||
}}
|
||||
>
|
||||
Next page
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{searchResult
|
||||
.slice(pageIndex * 10, (pageIndex + 1) * 10)
|
||||
.map((result: ChatStoreSearchResult) => {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-start p-1 m-1 rounded border bg-slate-300 cursor-pointer"
|
||||
className="flex justify-start p-1 m-1 rounded border bg-base-200 cursor-pointer"
|
||||
key={result.key}
|
||||
onClick={() => {
|
||||
props.setSelectedChatIndex(parseInt(result.key.toString()));
|
||||
@@ -169,6 +139,40 @@ export default function Search(props: {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{searchResult.length > 0 && (
|
||||
<div className="flex justify-center my-2">
|
||||
<div class="join">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
disabled={pageIndex === 0}
|
||||
onClick={() => {
|
||||
if (pageIndex === 0) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex - 1);
|
||||
}}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button class="join-item btn btn-sm">
|
||||
Page {pageIndex + 1} /{" "}
|
||||
{Math.floor(searchResult.length / 10) + 1}
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
disabled={pageIndex === Math.floor(searchResult.length / 10)}
|
||||
onClick={() => {
|
||||
if (pageIndex === Math.floor(searchResult.length / 10)) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex + 1);
|
||||
}}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function SetAPIsTemplate({
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
className="p-1 m-1 rounded bg-blue-300"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give this **${label}** template a name:`);
|
||||
if (!name) {
|
||||
|
||||
755
src/settings.tsx
755
src/settings.tsx
@@ -16,6 +16,22 @@ import { SetAPIsTemplate } from "./setAPIsTemplate";
|
||||
import { autoHeight } from "./textarea";
|
||||
import getDefaultParams from "./getDefaultParam";
|
||||
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
CheckIcon,
|
||||
NoSymbolIcon,
|
||||
CogIcon,
|
||||
KeyIcon,
|
||||
EyeIcon,
|
||||
EllipsisHorizontalCircleIcon,
|
||||
HandRaisedIcon,
|
||||
AdjustmentsHorizontalIcon,
|
||||
Cog6ToothIcon,
|
||||
ListBulletIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { themeChange } from "theme-change";
|
||||
|
||||
const TTS_VOICES: string[] = [
|
||||
"alloy",
|
||||
"echo",
|
||||
@@ -26,18 +42,10 @@ const TTS_VOICES: string[] = [
|
||||
];
|
||||
const TTS_FORMAT: string[] = ["mp3", "opus", "aac", "flac"];
|
||||
|
||||
const Help = (props: { children: any; help: string }) => {
|
||||
const Help = (props: { children: any; help: string; field: string }) => {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
className="absolute"
|
||||
onClick={() => {
|
||||
alert(props.help);
|
||||
}}
|
||||
>
|
||||
❓
|
||||
</button>
|
||||
<p className="flex justify-between">{props.children}</p>
|
||||
<div class="b-2">
|
||||
<label class="form-control w-full">{props.children}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -55,20 +63,36 @@ const SelectModel = (props: {
|
||||
}
|
||||
const [useCustomModel, setUseCustomModel] = useState(shouldIUseCustomModel);
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<label className="m-2 p-2">Model</label>
|
||||
<span
|
||||
<Help help={props.help} field="">
|
||||
<div class="label">
|
||||
<span class="flex gap-2 items-center">
|
||||
<ListBulletIcon class="w-4 h-4" />
|
||||
Model
|
||||
</span>{" "}
|
||||
<div class="flex gap-3">
|
||||
<span class="label-text">
|
||||
<span class="label-text flex gap-2 items-center">
|
||||
<Cog6ToothIcon class="w-4 h-4" />
|
||||
{Tr("Custom")}
|
||||
</span>
|
||||
</span>
|
||||
<span class="label-text-alt">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useCustomModel}
|
||||
class="checkbox"
|
||||
onClick={() => {
|
||||
setUseCustomModel(!useCustomModel);
|
||||
}}
|
||||
className="m-2 p-2"
|
||||
>
|
||||
<label>{Tr("Custom")}</label>
|
||||
<input className="" type="checkbox" checked={useCustomModel} />
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{/* <span class="label-text-alt">Top Right label</span> */}
|
||||
</div>
|
||||
<label class="form-control w-full">
|
||||
{useCustomModel ? (
|
||||
<input
|
||||
className="m-2 p-2 border rounded focus w-32 md:w-fit"
|
||||
className="input input-bordered"
|
||||
value={props.chatStore.model}
|
||||
onChange={(event: any) => {
|
||||
const model = event.target.value as string;
|
||||
@@ -78,7 +102,7 @@ const SelectModel = (props: {
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
className="m-2 p-2"
|
||||
className="select select-bordered"
|
||||
value={props.chatStore.model}
|
||||
onChange={(event: any) => {
|
||||
const model = event.target.value as string;
|
||||
@@ -95,6 +119,8 @@ const SelectModel = (props: {
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</label>
|
||||
<div class="pb-5"></div>
|
||||
</Help>
|
||||
);
|
||||
};
|
||||
@@ -103,12 +129,25 @@ const LongInput = (props: {
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
field: "systemMessageContent" | "toolsString";
|
||||
label: string;
|
||||
help: string;
|
||||
}) => {
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">{props.label}</span>
|
||||
<span class="label-text-alt">
|
||||
<button
|
||||
onClick={() => {
|
||||
alert(props.help);
|
||||
}}
|
||||
>
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
className="m-2 p-2 border rounded focus w-full"
|
||||
className="textarea textarea-bordered h-24 w-full"
|
||||
value={props.chatStore[props.field]}
|
||||
onChange={(event: any) => {
|
||||
props.chatStore[props.field] = event.target.value;
|
||||
@@ -119,7 +158,7 @@ const LongInput = (props: {
|
||||
autoHeight(event.target);
|
||||
}}
|
||||
></textarea>
|
||||
</Help>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -139,26 +178,48 @@ const Input = (props: {
|
||||
}) => {
|
||||
const [hideInput, setHideInput] = useState(true);
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<label className="m-2 p-2">{props.field}</label>
|
||||
<Help field={props.field} help={props.help}>
|
||||
<div class="label">
|
||||
<span class="label-text">{props.field}</span>
|
||||
<span class="label-text-alt">
|
||||
<button
|
||||
className="p-2"
|
||||
onClick={() => {
|
||||
alert(props.help);
|
||||
}}
|
||||
>
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<label class="input input-bordered flex items-center gap-2 grow">
|
||||
{hideInput ? (
|
||||
<EyeIcon
|
||||
class="w-4 h-4"
|
||||
onClick={() => {
|
||||
setHideInput(!hideInput);
|
||||
console.log("clicked", hideInput);
|
||||
}}
|
||||
>
|
||||
{hideInput ? "👀" : "🙈"}
|
||||
</button>
|
||||
/>
|
||||
) : (
|
||||
<KeyIcon
|
||||
class="w-4 h-4"
|
||||
onClick={() => {
|
||||
setHideInput(!hideInput);
|
||||
console.log("clicked", hideInput);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
type={hideInput ? "password" : "text"}
|
||||
className="m-2 p-2 border rounded focus w-32 md:w-fit"
|
||||
class="grow"
|
||||
value={props.chatStore[props.field]}
|
||||
onChange={(event: any) => {
|
||||
props.chatStore[props.field] = event.target.value;
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
}}
|
||||
></input>
|
||||
/>
|
||||
</label>
|
||||
</Help>
|
||||
);
|
||||
};
|
||||
@@ -190,20 +251,37 @@ const Slicer = (props: {
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
};
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<span>
|
||||
<label className="m-2 p-2">{props.field}</label>
|
||||
<Help help={props.help} field={props.field}>
|
||||
<span class="py-3">
|
||||
<div class="form-control">
|
||||
<label class="flex gap-2">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<AdjustmentsHorizontalIcon className="w-4 h-4" />
|
||||
{props.field}{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
alert(props.help);
|
||||
}}
|
||||
>
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.chatStore[enable_filed_name]}
|
||||
class="checkbox"
|
||||
onClick={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
{enabled ? (
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
disabled={!enabled}
|
||||
className="m-2 p-2 border rounded focus w-16"
|
||||
className="range"
|
||||
type="range"
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
@@ -226,6 +304,10 @@ const Slicer = (props: {
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Help>
|
||||
);
|
||||
};
|
||||
@@ -245,10 +327,20 @@ const Number = (props: {
|
||||
help: string;
|
||||
}) => {
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<Help help={props.help} field="">
|
||||
<span>
|
||||
<label className="m-2 p-2">{props.field}</label>
|
||||
<label className="py-2 flex items-center gap-1">
|
||||
<EllipsisHorizontalCircleIcon class="h-4 w-4" /> {props.field}{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
alert(props.help);
|
||||
}}
|
||||
>
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
{props.field === "maxGenTokens" && (
|
||||
<div class="form-control">
|
||||
<label class="label grow">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.chatStore.maxGenTokens_enabled}
|
||||
@@ -258,8 +350,12 @@ const Number = (props: {
|
||||
!newChatStore.maxGenTokens_enabled;
|
||||
props.setChatStore({ ...newChatStore });
|
||||
}}
|
||||
class="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
</span>
|
||||
<input
|
||||
readOnly={props.readOnly}
|
||||
@@ -268,7 +364,7 @@ const Number = (props: {
|
||||
!props.chatStore.maxGenTokens_enabled
|
||||
}
|
||||
type="number"
|
||||
className="m-2 p-2 border rounded focus w-28"
|
||||
className="input input-bordered input-sm w-full"
|
||||
value={props.chatStore[props.field]}
|
||||
onChange={(event: any) => {
|
||||
console.log("type", typeof event.target.value);
|
||||
@@ -288,17 +384,29 @@ const Choice = (props: {
|
||||
help: string;
|
||||
}) => {
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<label className="m-2 p-2">{props.field}</label>
|
||||
<Help help={props.help} field={props.field}>
|
||||
<div class="form-control">
|
||||
<label class="py-2 flex items-center gap-1">
|
||||
<CheckIcon class="h-4 w-4" />
|
||||
<span class="label-text">{props.field}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
alert(props.help);
|
||||
}}
|
||||
>
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="m-2 p-2 border rounded focus"
|
||||
checked={props.chatStore[props.field]}
|
||||
class="checkbox"
|
||||
onChange={(event: any) => {
|
||||
props.chatStore[props.field] = event.target.checked;
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
}}
|
||||
></input>
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Help>
|
||||
);
|
||||
};
|
||||
@@ -343,6 +451,7 @@ export default (props: {
|
||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||
|
||||
useEffect(() => {
|
||||
themeChange(false);
|
||||
const handleKeyPress = (event: any) => {
|
||||
if (event.keyCode === 27) {
|
||||
// keyCode for ESC key is 27
|
||||
@@ -360,17 +469,136 @@ export default (props: {
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.setShow(false)}
|
||||
className="left-0 top-0 overflow-scroll flex justify-center absolute w-screen h-full bg-black bg-opacity-50 z-10"
|
||||
className="left-0 top-0 overflow-scroll flex justify-center absolute mt-6 w-screen h-full z-10"
|
||||
>
|
||||
<div
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
className="m-2 p-2 bg-white rounded-lg h-fit lg:w-2/3 z-20"
|
||||
className="modal-box"
|
||||
>
|
||||
<h3 className="text-xl text-center flex justify-between">
|
||||
<span>{Tr("Settings")}</span>
|
||||
<select>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 class="text-lg font-bold">{Tr("Settings")}</h3>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
onClick={() => {
|
||||
props.setShow(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Close")}
|
||||
</button>
|
||||
</div>
|
||||
<div class="join join-vertical w-full">
|
||||
<div class="join-item collapse collapse-plus bg-base-200">
|
||||
<input type="radio" name="setting-accordion" />
|
||||
<div class="collapse-title text-xl font-medium">Session</div>
|
||||
<div class="collapse-content">
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Session cost</div>
|
||||
<div class="stat-value">
|
||||
{props.chatStore.cost.toFixed(4)} $
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LongInput
|
||||
label="System Prompt"
|
||||
field="systemMessageContent"
|
||||
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<LongInput
|
||||
label="Tools String"
|
||||
field="toolsString"
|
||||
help="function call tools, should be valid json format in list"
|
||||
{...props}
|
||||
/>
|
||||
<span className="pt-1">
|
||||
JSON Check:{" "}
|
||||
{isVailedJSON(props.chatStore.toolsString) ? (
|
||||
<CheckIcon className="inline w-4 h-4" />
|
||||
) : (
|
||||
<NoSymbolIcon className="inline w-4 h-4" />
|
||||
)}
|
||||
</span>
|
||||
<div className="box">
|
||||
<div className="flex justify-evenly flex-wrap">
|
||||
{props.chatStore.toolsString.trim() && (
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
const name = prompt(
|
||||
`Give this **Tools** template a name:`
|
||||
);
|
||||
if (!name) {
|
||||
alert("No template name specified");
|
||||
return;
|
||||
}
|
||||
const newToolsTmp: TemplateTools = {
|
||||
name,
|
||||
toolsString: props.chatStore.toolsString,
|
||||
};
|
||||
props.templateTools.push(newToolsTmp);
|
||||
props.setTemplateTools([...props.templateTools]);
|
||||
}}
|
||||
>
|
||||
{Tr(`Save Tools`)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="join-item collapse collapse-plus bg-base-200">
|
||||
<input type="radio" name="setting-accordion" />
|
||||
<div class="collapse-title text-xl font-medium">System</div>
|
||||
<div class="collapse-content">
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Accumulated cost</div>
|
||||
<div class="stat-value">{totalCost.toFixed(4)} $</div>
|
||||
<div class="stat-desc">
|
||||
in all sessions -{" "}
|
||||
<a
|
||||
class="btn btn-xs btn-primary"
|
||||
onClick={() => {
|
||||
clearTotalCost();
|
||||
setTotalCost(getTotalCost());
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Choice
|
||||
field="develop_mode"
|
||||
help="开发者模式,开启后会显示更多选项及功能"
|
||||
{...props}
|
||||
/>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Theme Switch</span>
|
||||
</div>
|
||||
<select data-choose-theme class="select select-bordered">
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="cupcake">Cupcake</option>
|
||||
<option value="halloween">Halloween</option>
|
||||
<option value="wireframe">Wireframe</option>
|
||||
<option value="sunset">Sunset</option>
|
||||
<option value="lemonade">Lemonade</option>
|
||||
<option value="luxury">Luxury</option>
|
||||
<option value="cyberpunk">Cyberpunk</option>
|
||||
<option value="black">Black</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Language</span>
|
||||
</div>
|
||||
<select class="select select-bordered">
|
||||
{Object.keys(LANG_OPTIONS).map((opt) => (
|
||||
<option
|
||||
value={opt}
|
||||
@@ -384,11 +612,14 @@ export default (props: {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</h3>
|
||||
<hr />
|
||||
<div className="flex justify-between">
|
||||
</label>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Quick Actions</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-purple-600 text-white"
|
||||
class="btn btn-sm btn-outline btn-neural"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(link);
|
||||
alert(tr(`Copied link:`, langCode) + `${link}`);
|
||||
@@ -397,9 +628,13 @@ export default (props: {
|
||||
{Tr("Copy Setting Link")}
|
||||
</button>
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-rose-600 text-white"
|
||||
class="btn btn-sm btn-outline btn-neural"
|
||||
onClick={() => {
|
||||
if (!confirm(tr("Are you sure to clear all history?", langCode)))
|
||||
if (
|
||||
!confirm(
|
||||
tr("Are you sure to clear all history?", langCode)
|
||||
)
|
||||
)
|
||||
return;
|
||||
props.chatStore.history = props.chatStore.history.filter(
|
||||
(msg) => msg.example && !msg.hide
|
||||
@@ -411,45 +646,122 @@ export default (props: {
|
||||
{Tr("Clear History")}
|
||||
</button>
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-cyan-600 text-white"
|
||||
class="btn btn-sm btn-outline btn-neural"
|
||||
onClick={() => {
|
||||
props.setShow(false);
|
||||
let dataStr =
|
||||
"data:text/json;charset=utf-8," +
|
||||
encodeURIComponent(
|
||||
JSON.stringify(props.chatStore, null, "\t")
|
||||
);
|
||||
let downloadAnchorNode = document.createElement("a");
|
||||
downloadAnchorNode.setAttribute("href", dataStr);
|
||||
downloadAnchorNode.setAttribute(
|
||||
"download",
|
||||
`chatgpt-api-web-${props.selectedChatStoreIndex}.json`
|
||||
);
|
||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}}
|
||||
>
|
||||
{Tr("Close")}
|
||||
{Tr("Export")}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-neural"
|
||||
onClick={() => {
|
||||
const name = prompt(
|
||||
tr("Give this template a name:", langCode)
|
||||
);
|
||||
if (!name) {
|
||||
alert(tr("No template name specified", langCode));
|
||||
return;
|
||||
}
|
||||
const tmp: ChatStore = structuredClone(props.chatStore);
|
||||
tmp.history = tmp.history.filter((h) => h.example);
|
||||
// clear api because it is stored in the API template
|
||||
tmp.apiEndpoint = "";
|
||||
tmp.apiKey = "";
|
||||
tmp.whisper_api = "";
|
||||
tmp.whisper_key = "";
|
||||
tmp.tts_api = "";
|
||||
tmp.tts_key = "";
|
||||
tmp.image_gen_api = "";
|
||||
tmp.image_gen_key = "";
|
||||
// @ts-ignore
|
||||
tmp.name = name;
|
||||
props.templates.push(tmp as TemplateChatStore);
|
||||
props.setTemplates([...props.templates]);
|
||||
}}
|
||||
>
|
||||
{Tr("As template")}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-neural"
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(
|
||||
tr(
|
||||
"This will OVERWRITE the current chat history! Continue?",
|
||||
langCode
|
||||
)
|
||||
)
|
||||
)
|
||||
return;
|
||||
console.log("importFileRef", importFileRef);
|
||||
importFileRef.current.click();
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
<p className="m-2 p-2">
|
||||
{Tr("Total cost in this session")} ${props.chatStore.cost.toFixed(4)}
|
||||
</p>
|
||||
<hr />
|
||||
<div className="box">
|
||||
<LongInput
|
||||
field="systemMessageContent"
|
||||
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
||||
{...props}
|
||||
/>
|
||||
<span>
|
||||
Valied JSON:{" "}
|
||||
{isVailedJSON(props.chatStore.toolsString) ? "🆗" : "❌"}
|
||||
</span>
|
||||
<LongInput
|
||||
field="toolsString"
|
||||
help="function call tools, should be valied json format in list"
|
||||
{...props}
|
||||
/>
|
||||
<div className="relative border-slate-300 border rounded">
|
||||
<div className="flex justify-between">
|
||||
<strong className="p-1 m-1">Chat API</strong>
|
||||
<SetAPIsTemplate
|
||||
label="Chat API"
|
||||
endpoint={props.chatStore.apiEndpoint}
|
||||
APIkey={props.chatStore.apiKey}
|
||||
tmps={props.templateAPIs}
|
||||
setTmps={props.setTemplateAPIs}
|
||||
<input
|
||||
className="hidden"
|
||||
ref={importFileRef}
|
||||
type="file"
|
||||
onChange={() => {
|
||||
const file = importFileRef.current.files[0];
|
||||
console.log("file to import", file);
|
||||
if (!file || file.type !== "application/json") {
|
||||
alert(tr("Please select a json file", langCode));
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
console.log("import content", reader.result);
|
||||
if (!reader) {
|
||||
alert(tr("Empty file", langCode));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newChatStore: ChatStore = JSON.parse(
|
||||
reader.result as string
|
||||
);
|
||||
if (!newChatStore.chatgpt_api_web_version) {
|
||||
throw tr(
|
||||
"This is not an exported chatgpt-api-web chatstore file. The key 'chatgpt_api_web_version' is missing!",
|
||||
langCode
|
||||
);
|
||||
}
|
||||
props.setChatStore({ ...newChatStore });
|
||||
} catch (e) {
|
||||
alert(
|
||||
tr(`Import error on parsing json:`, langCode) + `${e}`
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="join-item collapse collapse-plus bg-base-200">
|
||||
<input type="radio" name="setting-accordion" />
|
||||
<div class="collapse-title text-xl font-medium">Chat</div>
|
||||
<div class="collapse-content">
|
||||
<div>
|
||||
<h2 class="card-title">Chat API</h2>
|
||||
<p>
|
||||
<Input
|
||||
field="apiKey"
|
||||
help="OPEN AI API 密钥,请勿泄漏此密钥"
|
||||
@@ -460,6 +772,15 @@ export default (props: {
|
||||
help="API 端点,方便在不支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions"
|
||||
{...props}
|
||||
/>
|
||||
</p>
|
||||
<SetAPIsTemplate
|
||||
label="Chat API"
|
||||
endpoint={props.chatStore.apiEndpoint}
|
||||
APIkey={props.chatStore.apiKey}
|
||||
tmps={props.templateAPIs}
|
||||
setTmps={props.setTemplateAPIs}
|
||||
/>
|
||||
<div class="divider" />
|
||||
</div>
|
||||
<SelectModel
|
||||
help="模型,默认 3.5。不同模型性能和定价也不同,请参考 API 文档。"
|
||||
@@ -478,11 +799,6 @@ export default (props: {
|
||||
{...props}
|
||||
/>
|
||||
<Choice field="logprobs" help="返回每个Token的概率" {...props} />
|
||||
<Choice
|
||||
field="develop_mode"
|
||||
help="开发者模式,开启后会显示更多选项及功能"
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="maxTokens"
|
||||
help="最大上下文 token 数量。此值会根据选择的模型自动设置。"
|
||||
@@ -533,19 +849,17 @@ export default (props: {
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="relative border-slate-300 border rounded">
|
||||
<div className="flex justify-between">
|
||||
<strong className="p-1 m-1">Whisper API</strong>
|
||||
<SetAPIsTemplate
|
||||
label="Whisper API"
|
||||
endpoint={props.chatStore.whisper_api}
|
||||
APIkey={props.chatStore.whisper_key}
|
||||
tmps={props.templateAPIsWhisper}
|
||||
setTmps={props.setTemplateAPIsWhisper}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="join-item collapse collapse-plus bg-base-200">
|
||||
<input type="radio" name="setting-accordion" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Speech Recognition
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div>
|
||||
<h2 class="card-title">Whisper API</h2>
|
||||
<p>
|
||||
<Input
|
||||
field="whisper_key"
|
||||
help="用于 Whisper 服务的 key,默认为 上方使用的OPENAI key,可在此单独配置专用key"
|
||||
@@ -556,11 +870,36 @@ export default (props: {
|
||||
help="Whisper 语言转文字服务,填入此api才会开启,默认为 https://api.openai.com/v1/audio/transriptions"
|
||||
{...props}
|
||||
/>
|
||||
</p>
|
||||
<SetAPIsTemplate
|
||||
label="Whisper API"
|
||||
endpoint={props.chatStore.whisper_api}
|
||||
APIkey={props.chatStore.whisper_key}
|
||||
tmps={props.templateAPIsWhisper}
|
||||
setTmps={props.setTemplateAPIsWhisper}
|
||||
/>
|
||||
<div class="divider" />
|
||||
</div>
|
||||
|
||||
<div className="relative border-slate-300 border rounded mt-1">
|
||||
<div className="flex justify-between">
|
||||
<strong className="p-1 m-1">TTS API</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="join-item collapse collapse-plus bg-base-200">
|
||||
<input type="radio" name="setting-accordion" />
|
||||
<div class="collapse-title text-xl font-medium">TTS</div>
|
||||
<div class="collapse-content">
|
||||
<div>
|
||||
<h2 class="card-title">TTS API</h2>
|
||||
<p>
|
||||
<Input
|
||||
field="tts_key"
|
||||
help="tts service api key"
|
||||
{...props}
|
||||
/>
|
||||
<Input
|
||||
field="tts_api"
|
||||
help="tts api, eg. https://api.openai.com/v1/audio/speech"
|
||||
{...props}
|
||||
/>
|
||||
</p>
|
||||
<SetAPIsTemplate
|
||||
label="TTS API"
|
||||
endpoint={props.chatStore.tts_api}
|
||||
@@ -568,19 +907,12 @@ export default (props: {
|
||||
tmps={props.templateAPIsTTS}
|
||||
setTmps={props.setTemplateAPIsTTS}
|
||||
/>
|
||||
<div class="divider" />
|
||||
</div>
|
||||
<hr />
|
||||
<Input field="tts_key" help="tts service api key" {...props} />
|
||||
<Input
|
||||
field="tts_api"
|
||||
help="tts api, eg. https://api.openai.com/v1/audio/speech"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<Help help="tts voice style">
|
||||
<label className="m-2 p-2">TTS Voice</label>
|
||||
<Help help="tts voice style" field="AAAAA">
|
||||
<label className="">TTS Voice</label>
|
||||
<select
|
||||
className="m-2 p-2"
|
||||
className="select select-bordered"
|
||||
value={props.chatStore.tts_voice}
|
||||
onChange={(event: any) => {
|
||||
const voice = event.target.value as string;
|
||||
@@ -600,10 +932,10 @@ export default (props: {
|
||||
help={"TTS Speed"}
|
||||
{...props}
|
||||
/>
|
||||
<Help help="tts response format">
|
||||
<label className="m-2 p-2">TTS Format</label>
|
||||
<Help help="tts response format" field="AAAAA">
|
||||
<label className="">TTS Format</label>
|
||||
<select
|
||||
className="m-2 p-2"
|
||||
className="select select-bordered"
|
||||
value={props.chatStore.tts_format}
|
||||
onChange={(event: any) => {
|
||||
const format = event.target.value as string;
|
||||
@@ -616,19 +948,17 @@ export default (props: {
|
||||
))}
|
||||
</select>
|
||||
</Help>
|
||||
|
||||
<div className="relative border-slate-300 border rounded">
|
||||
<div className="flex justify-between">
|
||||
<strong className="p-1 m-1">Image Gen API</strong>
|
||||
<SetAPIsTemplate
|
||||
label="Image Gen API"
|
||||
endpoint={props.chatStore.image_gen_api}
|
||||
APIkey={props.chatStore.image_gen_key}
|
||||
tmps={props.templateAPIsImageGen}
|
||||
setTmps={props.setTemplateAPIsImageGen}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="join-item collapse collapse-plus bg-base-200">
|
||||
<input type="radio" name="setting-accordion" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Image Generation
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div>
|
||||
<h2 class="card-title">Image Gen API</h2>
|
||||
<p>
|
||||
<Input
|
||||
field="image_gen_key"
|
||||
help="image generation service api key"
|
||||
@@ -639,156 +969,26 @@ export default (props: {
|
||||
help="DALL image gen key, eg. https://api.openai.com/v1/images/generations"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<p className="m-2 p-2">
|
||||
{Tr("Accumulated cost in all sessions")} ${totalCost.toFixed(4)}
|
||||
</p>
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-emerald-500"
|
||||
onClick={() => {
|
||||
clearTotalCost();
|
||||
setTotalCost(getTotalCost());
|
||||
}}
|
||||
>
|
||||
{Tr("Reset")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-evenly flex-wrap">
|
||||
{props.chatStore.toolsString.trim() && (
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-blue-300"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give this **Tools** template a name:`);
|
||||
if (!name) {
|
||||
alert("No template name specified");
|
||||
return;
|
||||
}
|
||||
const newToolsTmp: TemplateTools = {
|
||||
name,
|
||||
toolsString: props.chatStore.toolsString,
|
||||
};
|
||||
props.templateTools.push(newToolsTmp);
|
||||
props.setTemplateTools([...props.templateTools]);
|
||||
}}
|
||||
>
|
||||
{Tr(`Save Tools`)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="flex justify-evenly">
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-amber-500"
|
||||
onClick={() => {
|
||||
let dataStr =
|
||||
"data:text/json;charset=utf-8," +
|
||||
encodeURIComponent(
|
||||
JSON.stringify(props.chatStore, null, "\t")
|
||||
);
|
||||
let downloadAnchorNode = document.createElement("a");
|
||||
downloadAnchorNode.setAttribute("href", dataStr);
|
||||
downloadAnchorNode.setAttribute(
|
||||
"download",
|
||||
`chatgpt-api-web-${props.selectedChatStoreIndex}.json`
|
||||
);
|
||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}}
|
||||
>
|
||||
{Tr("Export")}
|
||||
</button>
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-amber-500"
|
||||
onClick={() => {
|
||||
const name = prompt(tr("Give this template a name:", langCode));
|
||||
if (!name) {
|
||||
alert(tr("No template name specified", langCode));
|
||||
return;
|
||||
}
|
||||
const tmp: ChatStore = structuredClone(props.chatStore);
|
||||
tmp.history = tmp.history.filter((h) => h.example);
|
||||
// clear api because it is stored in the API template
|
||||
tmp.apiEndpoint = "";
|
||||
tmp.apiKey = "";
|
||||
tmp.whisper_api = "";
|
||||
tmp.whisper_key = "";
|
||||
tmp.tts_api = "";
|
||||
tmp.tts_key = "";
|
||||
tmp.image_gen_api = "";
|
||||
tmp.image_gen_key = "";
|
||||
// @ts-ignore
|
||||
tmp.name = name;
|
||||
props.templates.push(tmp as TemplateChatStore);
|
||||
props.setTemplates([...props.templates]);
|
||||
}}
|
||||
>
|
||||
{Tr("As template")}
|
||||
</button>
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-amber-500"
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(
|
||||
tr(
|
||||
"This will OVERWRITE the current chat history! Continue?",
|
||||
langCode
|
||||
)
|
||||
)
|
||||
)
|
||||
return;
|
||||
console.log("importFileRef", importFileRef);
|
||||
importFileRef.current.click();
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<input
|
||||
className="hidden"
|
||||
ref={importFileRef}
|
||||
type="file"
|
||||
onChange={() => {
|
||||
const file = importFileRef.current.files[0];
|
||||
console.log("file to import", file);
|
||||
if (!file || file.type !== "application/json") {
|
||||
alert(tr("Please select a json file", langCode));
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
console.log("import content", reader.result);
|
||||
if (!reader) {
|
||||
alert(tr("Empty file", langCode));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newChatStore: ChatStore = JSON.parse(
|
||||
reader.result as string
|
||||
);
|
||||
if (!newChatStore.chatgpt_api_web_version) {
|
||||
throw tr(
|
||||
"This is not an exported chatgpt-api-web chatstore file. The key 'chatgpt_api_web_version' is missing!",
|
||||
langCode
|
||||
);
|
||||
}
|
||||
props.setChatStore({ ...newChatStore });
|
||||
} catch (e) {
|
||||
alert(
|
||||
tr(`Import error on parsing json:`, langCode) + `${e}`
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
<SetAPIsTemplate
|
||||
label="Image Gen API"
|
||||
endpoint={props.chatStore.image_gen_api}
|
||||
APIkey={props.chatStore.image_gen_key}
|
||||
tmps={props.templateAPIsImageGen}
|
||||
setTmps={props.setTemplateAPIsImageGen}
|
||||
/>
|
||||
</p>
|
||||
<p className="text-center m-2 p-2">
|
||||
<div class="divider" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4 pb-2">
|
||||
<p className="text-center">
|
||||
chatgpt-api-web ChatStore {Tr("Version")}{" "}
|
||||
{props.chatStore.chatgpt_api_web_version}
|
||||
</p>
|
||||
<p>
|
||||
⚠{Tr("Documents and source code are avaliable here")}:{" "}
|
||||
<p className="text-center">
|
||||
{Tr("Documents and source code are avaliable here")}:{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href="https://github.com/heimoshuiyu/chatgpt-api-web"
|
||||
@@ -798,7 +998,6 @@ export default (props: {
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { ChatStore, ChatStoreMessage, addTotalCost } from "./app";
|
||||
import { Message, getMessageText } from "./chatgpt";
|
||||
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
interface TTSProps {
|
||||
chatStore: ChatStore;
|
||||
@@ -78,7 +79,11 @@ export default function TTSButton(props: TTSProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{generating ? "🤔" : "🔈"}
|
||||
{generating ? (
|
||||
<span class="loading loading-dots loading-xs"></span>
|
||||
) : (
|
||||
<SpeakerWaveIcon class="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
daisyui: {
|
||||
themes: ["light",
|
||||
"dark",
|
||||
"cupcake",
|
||||
"bumblebee",
|
||||
"emerald",
|
||||
"corporate",
|
||||
"synthwave",
|
||||
"retro",
|
||||
"cyberpunk",
|
||||
"valentine",
|
||||
"halloween",
|
||||
"garden",
|
||||
"forest",
|
||||
"aqua",
|
||||
"lofi",
|
||||
"pastel",
|
||||
"fantasy",
|
||||
"wireframe",
|
||||
"black",
|
||||
"luxury",
|
||||
"dracula",
|
||||
"cmyk",
|
||||
"autumn",
|
||||
"business",
|
||||
"acid",
|
||||
"lemonade",
|
||||
"night",
|
||||
"coffee",
|
||||
"winter",
|
||||
"dim",
|
||||
"nord",
|
||||
"sunset",],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require('daisyui')],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user