Merge pull request #10 from heimoshuiyu/dev
Major Structure Refactor and Feature Enhancements
This commit is contained in:
369
src/addImage.tsx
369
src/addImage.tsx
@@ -1,369 +0,0 @@
|
|||||||
import { useContext, useState } from "react";
|
|
||||||
import { ChatStore } from "@/types/chatstore";
|
|
||||||
import { MessageDetail } from "@/chatgpt";
|
|
||||||
import { Tr } from "@/translate";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from "@/components/ui/drawer";
|
|
||||||
|
|
||||||
import { Button } from "./components/ui/button";
|
|
||||||
import { PenIcon, XIcon } from "lucide-react";
|
|
||||||
import { Checkbox } from "./components/ui/checkbox";
|
|
||||||
import { Label } from "./components/ui/label";
|
|
||||||
import { Textarea } from "./components/ui/textarea";
|
|
||||||
import { Separator } from "./components/ui/separator";
|
|
||||||
import { AppContext } from "./pages/App";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
images: MessageDetail[];
|
|
||||||
showAddImage: boolean;
|
|
||||||
setShowAddImage: (se: boolean) => void;
|
|
||||||
setImages: (images: MessageDetail[]) => void;
|
|
||||||
}
|
|
||||||
interface ImageResponse {
|
|
||||||
url?: string;
|
|
||||||
b64_json?: string;
|
|
||||||
revised_prompt: string;
|
|
||||||
}
|
|
||||||
export function AddImage({
|
|
||||||
showAddImage,
|
|
||||||
setShowAddImage,
|
|
||||||
setImages,
|
|
||||||
images,
|
|
||||||
}: Props) {
|
|
||||||
const ctx = useContext(AppContext);
|
|
||||||
if (ctx === null) return <></>;
|
|
||||||
|
|
||||||
const [enableHighResolution, setEnableHighResolution] = useState(true);
|
|
||||||
const [imageGenPrompt, setImageGenPrompt] = useState("");
|
|
||||||
const [imageGenModel, setImageGenModel] = useState("dall-e-3");
|
|
||||||
const [imageGenN, setImageGenN] = useState(1);
|
|
||||||
const [imageGenQuality, setImageGEnQuality] = useState("standard");
|
|
||||||
const [imageGenResponseFormat, setImageGenResponseFormat] =
|
|
||||||
useState("b64_json");
|
|
||||||
const [imageGenSize, setImageGenSize] = useState("1024x1024");
|
|
||||||
const [imageGenStyle, setImageGenStyle] = useState("vivid");
|
|
||||||
const [imageGenGenerating, setImageGenGenerating] = useState(false);
|
|
||||||
useState("b64_json");
|
|
||||||
return (
|
|
||||||
<Drawer open={showAddImage} onOpenChange={setShowAddImage}>
|
|
||||||
<DrawerContent>
|
|
||||||
<div className="mx-auto w-full max-w-lg">
|
|
||||||
<DrawerHeader>
|
|
||||||
<DrawerTitle>Add Images</DrawerTitle>
|
|
||||||
</DrawerHeader>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={false}
|
|
||||||
onClick={() => {
|
|
||||||
const image_url = prompt("Image URL");
|
|
||||||
if (!image_url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImages([
|
|
||||||
...images,
|
|
||||||
{
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url: image_url,
|
|
||||||
detail: enableHighResolution ? "high" : "low",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add from URL
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
disabled={false}
|
|
||||||
onClick={() => {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "file";
|
|
||||||
input.accept = "image/*";
|
|
||||||
input.onchange = (event) => {
|
|
||||||
const file = (event.target as HTMLInputElement).files?.[0];
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const base64data = reader.result;
|
|
||||||
setImages([
|
|
||||||
...images,
|
|
||||||
{
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url: String(base64data),
|
|
||||||
detail: enableHighResolution ? "high" : "low",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add from local file
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={enableHighResolution}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setEnableHighResolution(checked === true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="terms"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
High resolution
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
{ctx.chatStore.image_gen_api && ctx.chatStore.image_gen_key && (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<h3>Generate Image</h3>
|
|
||||||
<span className="flex flex-col justify-between m-1 p-1">
|
|
||||||
<Label>Prompt: </Label>
|
|
||||||
<Textarea
|
|
||||||
className="textarea textarea-sm textarea-bordered"
|
|
||||||
value={imageGenPrompt}
|
|
||||||
onChange={(e: any) => {
|
|
||||||
setImageGenPrompt(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<label>Model: </label>
|
|
||||||
<select
|
|
||||||
className="select select-sm select-bordered"
|
|
||||||
value={imageGenModel}
|
|
||||||
onChange={(e: any) => {
|
|
||||||
setImageGenModel(e.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="dall-e-3">DALL-E 3</option>
|
|
||||||
<option value="dall-e-2">DALL-E 2</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<label>n: </label>
|
|
||||||
<input
|
|
||||||
className="input input-sm input-bordered"
|
|
||||||
value={imageGenN}
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
onChange={(e: any) => setImageGenN(parseInt(e.target.value))}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<label>Quality: </label>
|
|
||||||
<select
|
|
||||||
className="select select-sm select-bordered"
|
|
||||||
value={imageGenQuality}
|
|
||||||
onChange={(e: any) => setImageGEnQuality(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="hd">HD</option>
|
|
||||||
<option value="standard">Standard</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<label>Response Format: </label>
|
|
||||||
<select
|
|
||||||
className="select select-sm select-bordered"
|
|
||||||
value={imageGenResponseFormat}
|
|
||||||
onChange={(e: any) =>
|
|
||||||
setImageGenResponseFormat(e.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="b64_json">b64_json</option>
|
|
||||||
<option value="url">url</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<label>Size: </label>
|
|
||||||
<select
|
|
||||||
className="select select-sm select-bordered"
|
|
||||||
value={imageGenSize}
|
|
||||||
onChange={(e: any) => setImageGenSize(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="256x256">256x256 (dall-e-2)</option>
|
|
||||||
<option value="512x512">512x512 (dall-e-2)</option>
|
|
||||||
<option value="1024x1024">1024x1024 (dall-e-2/3)</option>
|
|
||||||
<option value="1792x1024">1792x1024 (dall-e-3)</option>
|
|
||||||
<option value="1024x1792">1024x1792 (dall-e-3)</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<label>Style (only dall-e-3): </label>
|
|
||||||
<select
|
|
||||||
className="select select-sm select-bordered"
|
|
||||||
value={imageGenStyle}
|
|
||||||
onChange={(e: any) => setImageGenStyle(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="vivid">vivid</option>
|
|
||||||
<option value="natural">natural</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
disabled={imageGenGenerating}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setImageGenGenerating(true);
|
|
||||||
const body: any = {
|
|
||||||
prompt: imageGenPrompt,
|
|
||||||
model: imageGenModel,
|
|
||||||
n: imageGenN,
|
|
||||||
quality: imageGenQuality,
|
|
||||||
response_format: imageGenResponseFormat,
|
|
||||||
size: imageGenSize,
|
|
||||||
};
|
|
||||||
if (imageGenModel === "dall-e-3") {
|
|
||||||
body.style = imageGenStyle;
|
|
||||||
}
|
|
||||||
const resp: ImageResponse[] = (
|
|
||||||
await fetch(ctx.chatStore.image_gen_api, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${ctx.chatStore.image_gen_key}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
}).then((resp) => resp.json())
|
|
||||||
).data;
|
|
||||||
console.log("image gen resp", resp);
|
|
||||||
|
|
||||||
for (const image of resp) {
|
|
||||||
let url = "";
|
|
||||||
if (image.url) url = image.url;
|
|
||||||
if (image.b64_json)
|
|
||||||
url = "data:image/png;base64," + image.b64_json;
|
|
||||||
if (!url) continue;
|
|
||||||
|
|
||||||
ctx.chatStore.history.push({
|
|
||||||
role: "assistant",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url,
|
|
||||||
detail: "low",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: image.revised_prompt,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hide: false,
|
|
||||||
token: 65,
|
|
||||||
example: false,
|
|
||||||
audio: null,
|
|
||||||
logprobs: null,
|
|
||||||
response_model_name: imageGenModel,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.setChatStore({ ...ctx.chatStore });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
alert("Failed to generate image: " + e);
|
|
||||||
} finally {
|
|
||||||
setImageGenGenerating(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Generate")}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-wrap">
|
|
||||||
{images.map((image, index) => (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{image.type === "image_url" && (
|
|
||||||
<img
|
|
||||||
className="rounded m-1 p-1 border-2 border-gray-400 w-32"
|
|
||||||
src={image.image_url?.url}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const image_url = prompt("Image URL");
|
|
||||||
if (!image_url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
images[index].image_url = {
|
|
||||||
url: image_url,
|
|
||||||
detail: enableHighResolution ? "high" : "low",
|
|
||||||
};
|
|
||||||
setImages([...images]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PenIcon />
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id={`hires-${index}`}
|
|
||||||
checked={image.image_url?.detail === "high"}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
if (image.image_url === undefined) return;
|
|
||||||
image.image_url.detail =
|
|
||||||
image.image_url?.detail === "low" ? "high" : "low";
|
|
||||||
setImages([...images]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor={`hires-${index}`}
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
HiRes
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (!confirm("Are you sure to delete this image?")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
images.splice(index, 1);
|
|
||||||
setImages([...images]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DrawerFooter>
|
|
||||||
<Button onClick={() => setShowAddImage(false)}>Done</Button>
|
|
||||||
</DrawerFooter>
|
|
||||||
</div>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
243
src/components/ImageGenDrawer.tsx
Normal file
243
src/components/ImageGenDrawer.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { MessageDetail } from "@/chatgpt";
|
||||||
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { AppContext } from "@/pages/App";
|
||||||
|
import { PaintBucketIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
disableFactor: boolean[];
|
||||||
|
}
|
||||||
|
interface ImageResponse {
|
||||||
|
url?: string;
|
||||||
|
b64_json?: string;
|
||||||
|
revised_prompt: string;
|
||||||
|
}
|
||||||
|
export function ImageGenDrawer({ disableFactor }: Props) {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (ctx === null) return <></>;
|
||||||
|
|
||||||
|
const [showGenImage, setShowGenImage] = useState(false);
|
||||||
|
const [imageGenPrompt, setImageGenPrompt] = useState("");
|
||||||
|
const [imageGenModel, setImageGenModel] = useState("dall-e-3");
|
||||||
|
const [imageGenN, setImageGenN] = useState(1);
|
||||||
|
const [imageGenQuality, setImageGEnQuality] = useState("standard");
|
||||||
|
const [imageGenResponseFormat, setImageGenResponseFormat] =
|
||||||
|
useState("b64_json");
|
||||||
|
const [imageGenSize, setImageGenSize] = useState("1024x1024");
|
||||||
|
const [imageGenStyle, setImageGenStyle] = useState("vivid");
|
||||||
|
const [imageGenGenerating, setImageGenGenerating] = useState(false);
|
||||||
|
useState("b64_json");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ctx.chatStore.image_gen_api && ctx.chatStore.image_gen_key ? (
|
||||||
|
<Drawer open={showGenImage} onOpenChange={setShowGenImage}>
|
||||||
|
<DrawerTrigger>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
disabled={disableFactor.some((factor) => factor)}
|
||||||
|
>
|
||||||
|
<PaintBucketIcon className="size-4" />
|
||||||
|
<span className="sr-only">Generate Image</span>
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerDescription className="sr-only">
|
||||||
|
Generate images using the DALL-E model.
|
||||||
|
</DrawerDescription>
|
||||||
|
<DrawerContent>
|
||||||
|
<div className="mx-auto w-full max-w-lg">
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>Generate Image</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="flex flex-col justify-between m-1 p-1">
|
||||||
|
<Label>Prompt: </Label>
|
||||||
|
<Textarea
|
||||||
|
className="textarea textarea-sm textarea-bordered"
|
||||||
|
value={imageGenPrompt}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setImageGenPrompt(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<label>Model: </label>
|
||||||
|
<select
|
||||||
|
className="select select-sm select-bordered"
|
||||||
|
value={imageGenModel}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setImageGenModel(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="dall-e-3">DALL-E 3</option>
|
||||||
|
<option value="dall-e-2">DALL-E 2</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<label>n: </label>
|
||||||
|
<input
|
||||||
|
className="input input-sm input-bordered"
|
||||||
|
value={imageGenN}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
onChange={(e: any) =>
|
||||||
|
setImageGenN(parseInt(e.target.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<label>Quality: </label>
|
||||||
|
<select
|
||||||
|
className="select select-sm select-bordered"
|
||||||
|
value={imageGenQuality}
|
||||||
|
onChange={(e: any) => setImageGEnQuality(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="hd">HD</option>
|
||||||
|
<option value="standard">Standard</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<label>Response Format: </label>
|
||||||
|
<select
|
||||||
|
className="select select-sm select-bordered"
|
||||||
|
value={imageGenResponseFormat}
|
||||||
|
onChange={(e: any) =>
|
||||||
|
setImageGenResponseFormat(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="b64_json">b64_json</option>
|
||||||
|
<option value="url">url</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<label>Size: </label>
|
||||||
|
<select
|
||||||
|
className="select select-sm select-bordered"
|
||||||
|
value={imageGenSize}
|
||||||
|
onChange={(e: any) => setImageGenSize(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="256x256">256x256 (dall-e-2)</option>
|
||||||
|
<option value="512x512">512x512 (dall-e-2)</option>
|
||||||
|
<option value="1024x1024">1024x1024 (dall-e-2/3)</option>
|
||||||
|
<option value="1792x1024">1792x1024 (dall-e-3)</option>
|
||||||
|
<option value="1024x1792">1024x1792 (dall-e-3)</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<label>Style (only dall-e-3): </label>
|
||||||
|
<select
|
||||||
|
className="select select-sm select-bordered"
|
||||||
|
value={imageGenStyle}
|
||||||
|
onChange={(e: any) => setImageGenStyle(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="vivid">vivid</option>
|
||||||
|
<option value="natural">natural</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
disabled={imageGenGenerating}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
setImageGenGenerating(true);
|
||||||
|
const body: any = {
|
||||||
|
prompt: imageGenPrompt,
|
||||||
|
model: imageGenModel,
|
||||||
|
n: imageGenN,
|
||||||
|
quality: imageGenQuality,
|
||||||
|
response_format: imageGenResponseFormat,
|
||||||
|
size: imageGenSize,
|
||||||
|
};
|
||||||
|
if (imageGenModel === "dall-e-3") {
|
||||||
|
body.style = imageGenStyle;
|
||||||
|
}
|
||||||
|
const resp: ImageResponse[] = (
|
||||||
|
await fetch(ctx.chatStore.image_gen_api, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${ctx.chatStore.image_gen_key}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then((resp) => resp.json())
|
||||||
|
).data;
|
||||||
|
console.log("image gen resp", resp);
|
||||||
|
|
||||||
|
for (const image of resp) {
|
||||||
|
let url = "";
|
||||||
|
if (image.url) url = image.url;
|
||||||
|
if (image.b64_json)
|
||||||
|
url = "data:image/png;base64," + image.b64_json;
|
||||||
|
if (!url) continue;
|
||||||
|
|
||||||
|
ctx.chatStore.history.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url,
|
||||||
|
detail: "low",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: image.revised_prompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hide: false,
|
||||||
|
token: 65,
|
||||||
|
example: false,
|
||||||
|
audio: null,
|
||||||
|
logprobs: null,
|
||||||
|
response_model_name: imageGenModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.setChatStore({ ...ctx.chatStore });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Failed to generate image: " + e);
|
||||||
|
} finally {
|
||||||
|
setImageGenGenerating(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Generate")}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DrawerFooter>
|
||||||
|
<Button onClick={() => setShowGenImage(false)}>Done</Button>
|
||||||
|
</DrawerFooter>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="icon" type="button" disabled={true}>
|
||||||
|
<PaintBucketIcon className="size-4" />
|
||||||
|
<span className="sr-only">Generate Image</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
196
src/components/ImageUploadDrawer.tsx
Normal file
196
src/components/ImageUploadDrawer.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { MessageDetail } from "@/chatgpt";
|
||||||
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PenIcon, XIcon, ImageIcon } from "lucide-react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { AppContext } from "@/pages/App";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
images: MessageDetail[];
|
||||||
|
setImages: (images: MessageDetail[]) => void;
|
||||||
|
disableFactor: boolean[];
|
||||||
|
}
|
||||||
|
export function ImageUploadDrawer({ setImages, images, disableFactor }: Props) {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (ctx === null) return <></>;
|
||||||
|
const [showAddImage, setShowAddImage] = useState(false);
|
||||||
|
const [enableHighResolution, setEnableHighResolution] = useState(true);
|
||||||
|
useState("b64_json");
|
||||||
|
return (
|
||||||
|
<Drawer open={showAddImage} onOpenChange={setShowAddImage}>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
disabled={disableFactor.some((factor) => factor)}
|
||||||
|
>
|
||||||
|
<ImageIcon className="size-4" />
|
||||||
|
<span className="sr-only">Add Image</span>
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerDescription className="sr-only">
|
||||||
|
Add images to the chat.
|
||||||
|
</DrawerDescription>
|
||||||
|
<DrawerContent>
|
||||||
|
<div className="mx-auto w-full max-w-lg">
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>Add Images</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={false}
|
||||||
|
onClick={() => {
|
||||||
|
const image_url = prompt("Image URL");
|
||||||
|
if (!image_url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setImages([
|
||||||
|
...images,
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: image_url,
|
||||||
|
detail: enableHighResolution ? "high" : "low",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add from URL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
disabled={false}
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = "image/*";
|
||||||
|
input.onchange = (event) => {
|
||||||
|
const file = (event.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const base64data = reader.result;
|
||||||
|
setImages([
|
||||||
|
...images,
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: String(base64data),
|
||||||
|
detail: enableHighResolution ? "high" : "low",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add from local file
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={enableHighResolution}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setEnableHighResolution(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="terms"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
High resolution
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{image.type === "image_url" && (
|
||||||
|
<img
|
||||||
|
className="rounded m-1 p-1 border-2 border-gray-400 w-32"
|
||||||
|
src={image.image_url?.url}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const image_url = prompt("Image URL");
|
||||||
|
if (!image_url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
images[index].image_url = {
|
||||||
|
url: image_url,
|
||||||
|
detail: enableHighResolution ? "high" : "low",
|
||||||
|
};
|
||||||
|
setImages([...images]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PenIcon />
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`hires-${index}`}
|
||||||
|
checked={image.image_url?.detail === "high"}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
if (image.image_url === undefined) return;
|
||||||
|
image.image_url.detail =
|
||||||
|
image.image_url?.detail === "low" ? "high" : "low";
|
||||||
|
setImages([...images]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`hires-${index}`}
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
HiRes
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (!confirm("Are you sure to delete this image?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
images.splice(index, 1);
|
||||||
|
setImages([...images]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DrawerFooter>
|
||||||
|
<Button onClick={() => setShowAddImage(false)}>Done</Button>
|
||||||
|
</DrawerFooter>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
239
src/components/ListAPI.tsx
Normal file
239
src/components/ListAPI.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ChatStore, TemplateAPI } from "@/types/chatstore";
|
||||||
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
import {
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
} from "@/components/ui/navigation-menu";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { AppContext } from "@/pages/App";
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
} from "@/components/ui/navigation-menu";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { BrushIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface APITemplateDropdownProps {
|
||||||
|
label: string;
|
||||||
|
shortLabel: string;
|
||||||
|
ctx: any;
|
||||||
|
apiField: string;
|
||||||
|
keyField: string;
|
||||||
|
}
|
||||||
|
function APIsDropdownList({
|
||||||
|
label,
|
||||||
|
shortLabel,
|
||||||
|
ctx,
|
||||||
|
apiField,
|
||||||
|
keyField,
|
||||||
|
}: APITemplateDropdownProps) {
|
||||||
|
let API = ctx.templateAPIs;
|
||||||
|
if (label === "Chat API") {
|
||||||
|
API = ctx.templateAPIs;
|
||||||
|
} else if (label === "Whisper API") {
|
||||||
|
API = ctx.templateAPIsWhisper;
|
||||||
|
} else if (label === "TTS API") {
|
||||||
|
API = ctx.templateAPIsTTS;
|
||||||
|
} else if (label === "Image Gen API") {
|
||||||
|
API = ctx.templateAPIsImageGen;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>
|
||||||
|
<span className="lg:hidden">{shortLabel}</span>
|
||||||
|
<span className="hidden lg:inline">
|
||||||
|
{label}{" "}
|
||||||
|
{API.find(
|
||||||
|
(t: TemplateAPI) =>
|
||||||
|
ctx.chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||||
|
ctx.chatStore[keyField as keyof ChatStore] === t.key
|
||||||
|
)?.name &&
|
||||||
|
`: ${
|
||||||
|
API.find(
|
||||||
|
(t: TemplateAPI) =>
|
||||||
|
ctx.chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||||
|
ctx.chatStore[keyField as keyof ChatStore] === t.key
|
||||||
|
)?.name
|
||||||
|
}`}
|
||||||
|
</span>
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
||||||
|
{API.map((t: TemplateAPI, index: number) => (
|
||||||
|
<li key={index}>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
// @ts-ignore
|
||||||
|
ctx.chatStore[apiField as keyof ChatStore] = t.endpoint;
|
||||||
|
// @ts-ignore
|
||||||
|
ctx.chatStore[keyField] = t.key;
|
||||||
|
ctx.setChatStore({ ...ctx.chatStore });
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
|
ctx.chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||||
|
ctx.chatStore[keyField as keyof ChatStore] === t.key
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium leading-none">
|
||||||
|
{t.name}
|
||||||
|
</div>
|
||||||
|
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||||
|
{new URL(t.endpoint).host}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolsDropdownList() {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (!ctx) return <div>error</div>;
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const { chatStore, setChatStore } = ctx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-4 mx-3">
|
||||||
|
<p className="text-sm text-muted-foreground">{Tr(`Tools`)}</p>
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="w-[150px] justify-start">
|
||||||
|
{chatStore.toolsString ? (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
ctx.templateTools.find(
|
||||||
|
(t) => t.toolsString === chatStore.toolsString
|
||||||
|
)?.name
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>+ {Tr(`Set tools`)}</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" side="bottom" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="You can search..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>{Tr(`No results found.`)}</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{chatStore.toolsString && (
|
||||||
|
<CommandItem
|
||||||
|
key={-1}
|
||||||
|
value=""
|
||||||
|
onSelect={() => {
|
||||||
|
chatStore.toolsString = "";
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
toast({
|
||||||
|
title: "Tools Cleaned",
|
||||||
|
description: "Tools cleaned successfully",
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BrushIcon /> {Tr(`Clear tools`)}
|
||||||
|
</CommandItem>
|
||||||
|
)}
|
||||||
|
{ctx.templateTools.map((t, index) => (
|
||||||
|
<CommandItem
|
||||||
|
key={index}
|
||||||
|
value={t.toolsString}
|
||||||
|
onSelect={(value) => {
|
||||||
|
chatStore.toolsString = value;
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const APIListMenu: React.FC = () => {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (!ctx) return <div>error</div>;
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col m-2 gap-2 w-full">
|
||||||
|
{ctx.templateTools.length > 0 && <ToolsDropdownList />}
|
||||||
|
<NavigationMenu>
|
||||||
|
<NavigationMenuList>
|
||||||
|
{ctx.templateAPIs.length > 0 && (
|
||||||
|
<APIsDropdownList
|
||||||
|
label="Chat API"
|
||||||
|
shortLabel="Chat"
|
||||||
|
ctx={ctx}
|
||||||
|
apiField="apiEndpoint"
|
||||||
|
keyField="apiKey"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ctx.templateAPIsWhisper.length > 0 && (
|
||||||
|
<APIsDropdownList
|
||||||
|
label="Whisper API"
|
||||||
|
shortLabel="Whisper"
|
||||||
|
ctx={ctx}
|
||||||
|
apiField="whisper_api"
|
||||||
|
keyField="whisper_key"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ctx.templateAPIsTTS.length > 0 && (
|
||||||
|
<APIsDropdownList
|
||||||
|
label="TTS API"
|
||||||
|
shortLabel="TTS"
|
||||||
|
ctx={ctx}
|
||||||
|
apiField="tts_api"
|
||||||
|
keyField="tts_key"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ctx.templateAPIsImageGen.length > 0 && (
|
||||||
|
<APIsDropdownList
|
||||||
|
label="Image Gen API"
|
||||||
|
shortLabel="ImgGen"
|
||||||
|
ctx={ctx}
|
||||||
|
apiField="image_gen_api"
|
||||||
|
keyField="image_gen_key"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default APIListMenu;
|
||||||
@@ -1,41 +1,239 @@
|
|||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState, useMemo } from "react";
|
||||||
|
import { ChatStoreMessage } from "@/types/chatstore";
|
||||||
|
import { addTotalCost } from "@/utils/totalCost";
|
||||||
|
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
import { getMessageText } from "@/chatgpt";
|
||||||
import { calculate_token_length, getMessageText } from "@/chatgpt";
|
import { EditMessage } from "@/components/editMessage";
|
||||||
import TTSButton, { TTSPlay } from "@/tts";
|
import logprobToColor from "@/utils/logprob";
|
||||||
import { MessageHide } from "@/messageHide";
|
|
||||||
import { MessageDetail } from "@/messageDetail";
|
|
||||||
import { MessageToolCall } from "@/messageToolCall";
|
|
||||||
import { MessageToolResp } from "@/messageToolResp";
|
|
||||||
import { EditMessage } from "@/editMessage";
|
|
||||||
import logprobToColor from "@/logprob";
|
|
||||||
import {
|
import {
|
||||||
ChatBubble,
|
ChatBubble,
|
||||||
ChatBubbleAvatar,
|
|
||||||
ChatBubbleMessage,
|
ChatBubbleMessage,
|
||||||
ChatBubbleAction,
|
ChatBubbleAction,
|
||||||
ChatBubbleActionWrapper,
|
ChatBubbleActionWrapper,
|
||||||
} from "@/components/ui/chat/chat-bubble";
|
} from "@/components/ui/chat/chat-bubble";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
MessageSquareOffIcon,
|
MessageSquareOffIcon,
|
||||||
MessageSquarePlusIcon,
|
MessageSquarePlusIcon,
|
||||||
|
AudioLinesIcon,
|
||||||
|
LoaderCircleIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AppContext } from "./pages/App";
|
import { AppContext } from "@/pages/App";
|
||||||
|
|
||||||
export const isVailedJSON = (str: string): boolean => {
|
interface HideMessageProps {
|
||||||
try {
|
chat: ChatStoreMessage;
|
||||||
JSON.parse(str);
|
}
|
||||||
} catch (e) {
|
|
||||||
return false;
|
function MessageHide({ chat }: HideMessageProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>{getMessageText(chat).split("\n")[0].slice(0, 28)} ...</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex mt-2 justify-center">
|
||||||
|
<Badge variant="destructive">Removed from context</Badge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageDetailProps {
|
||||||
|
chat: ChatStoreMessage;
|
||||||
|
renderMarkdown: boolean;
|
||||||
|
}
|
||||||
|
function MessageDetail({ chat, renderMarkdown }: MessageDetailProps) {
|
||||||
|
if (typeof chat.content === "string") {
|
||||||
|
return <div></div>;
|
||||||
}
|
}
|
||||||
return true;
|
return (
|
||||||
};
|
<div>
|
||||||
|
{chat.content.map((mdt) =>
|
||||||
|
mdt.type === "text" ? (
|
||||||
|
chat.hide ? (
|
||||||
|
mdt.text?.split("\n")[0].slice(0, 16) + " ..."
|
||||||
|
) : renderMarkdown ? (
|
||||||
|
<Markdown>{mdt.text}</Markdown>
|
||||||
|
) : (
|
||||||
|
mdt.text
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="my-2 rounded-md max-w-64 max-h-64"
|
||||||
|
src={mdt.image_url?.url}
|
||||||
|
key={mdt.image_url?.url}
|
||||||
|
onClick={() => {
|
||||||
|
window.open(mdt.image_url?.url, "_blank");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCallMessageProps {
|
||||||
|
chat: ChatStoreMessage;
|
||||||
|
copyToClipboard: (text: string) => void;
|
||||||
|
}
|
||||||
|
function MessageToolCall({ chat, copyToClipboard }: ToolCallMessageProps) {
|
||||||
|
return (
|
||||||
|
<div className="message-content">
|
||||||
|
{chat.tool_calls?.map((tool_call) => (
|
||||||
|
<div className="bg-blue-300 dark:bg-blue-800 p-1 rounded my-1">
|
||||||
|
<strong>
|
||||||
|
Tool Call ID:{" "}
|
||||||
|
<span
|
||||||
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
|
onClick={() => copyToClipboard(String(tool_call.id))}
|
||||||
|
>
|
||||||
|
{tool_call?.id}
|
||||||
|
</span>
|
||||||
|
</strong>
|
||||||
|
<p>Type: {tool_call?.type}</p>
|
||||||
|
<p>
|
||||||
|
Function:
|
||||||
|
<span
|
||||||
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
|
onClick={() => copyToClipboard(tool_call.function.name)}
|
||||||
|
>
|
||||||
|
{tool_call.function.name}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Arguments:
|
||||||
|
<span
|
||||||
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
|
onClick={() => copyToClipboard(tool_call.function.arguments)}
|
||||||
|
>
|
||||||
|
{tool_call.function.arguments}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* [TODO] */}
|
||||||
|
{chat.content as string}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolRespondMessageProps {
|
||||||
|
chat: ChatStoreMessage;
|
||||||
|
copyToClipboard: (text: string) => void;
|
||||||
|
}
|
||||||
|
function MessageToolResp({ chat, copyToClipboard }: ToolRespondMessageProps) {
|
||||||
|
return (
|
||||||
|
<div className="message-content">
|
||||||
|
<div className="bg-blue-300 dark:bg-blue-800 p-1 rounded my-1">
|
||||||
|
<strong>
|
||||||
|
Tool Response ID:{" "}
|
||||||
|
<span
|
||||||
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
|
onClick={() => copyToClipboard(String(chat.tool_call_id))}
|
||||||
|
>
|
||||||
|
{chat.tool_call_id}
|
||||||
|
</span>
|
||||||
|
</strong>
|
||||||
|
{/* [TODO] */}
|
||||||
|
<p>{chat.content as string}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TTSProps {
|
||||||
|
chat: ChatStoreMessage;
|
||||||
|
}
|
||||||
|
interface TTSPlayProps {
|
||||||
|
chat: ChatStoreMessage;
|
||||||
|
}
|
||||||
|
export function TTSPlay(props: TTSPlayProps) {
|
||||||
|
const src = useMemo(() => {
|
||||||
|
if (props.chat.audio instanceof Blob) {
|
||||||
|
return URL.createObjectURL(props.chat.audio);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, [props.chat.audio]);
|
||||||
|
|
||||||
|
if (props.chat.hide) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
if (props.chat.audio instanceof Blob) {
|
||||||
|
return <audio className="w-64" src={src} controls />;
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
function TTSButton(props: TTSProps) {
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (!ctx) return <div>error</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const api = ctx.chatStore.tts_api;
|
||||||
|
const api_key = ctx.chatStore.tts_key;
|
||||||
|
const model = "tts-1";
|
||||||
|
const input = getMessageText(props.chat);
|
||||||
|
const voice = ctx.chatStore.tts_voice;
|
||||||
|
|
||||||
|
const body: Record<string, any> = {
|
||||||
|
model,
|
||||||
|
input,
|
||||||
|
voice,
|
||||||
|
response_format: ctx.chatStore.tts_format || "mp3",
|
||||||
|
};
|
||||||
|
if (ctx.chatStore.tts_speed_enabled) {
|
||||||
|
body["speed"] = ctx.chatStore.tts_speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGenerating(true);
|
||||||
|
|
||||||
|
fetch(api, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${api_key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
// update price
|
||||||
|
const cost = (input.length * 0.015) / 1000;
|
||||||
|
ctx.chatStore.cost += cost;
|
||||||
|
addTotalCost(cost);
|
||||||
|
ctx.setChatStore({ ...ctx.chatStore });
|
||||||
|
|
||||||
|
// save blob
|
||||||
|
props.chat.audio = blob;
|
||||||
|
ctx.setChatStore({ ...ctx.chatStore });
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const audio = new Audio(url);
|
||||||
|
audio.play();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setGenerating(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{generating ? (
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<AudioLinesIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Message(props: { messageIndex: number }) {
|
export default function Message(props: { messageIndex: number }) {
|
||||||
const ctx = useContext(AppContext);
|
const ctx = useContext(AppContext);
|
||||||
@@ -45,47 +243,8 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
|
|
||||||
const chat = chatStore.history[messageIndex];
|
const chat = chatStore.history[messageIndex];
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
const [showCopiedHint, setShowCopiedHint] = useState(false);
|
|
||||||
const [renderMarkdown, setRenderWorkdown] = useState(false);
|
const [renderMarkdown, setRenderWorkdown] = useState(false);
|
||||||
const [renderColor, setRenderColor] = useState(false);
|
const [renderColor, setRenderColor] = useState(false);
|
||||||
const DeleteIcon = () => (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.history[messageIndex].hide =
|
|
||||||
!chatStore.history[messageIndex].hide;
|
|
||||||
|
|
||||||
//chatStore.totalTokens =
|
|
||||||
chatStore.totalTokens = 0;
|
|
||||||
for (const i of chatStore.history
|
|
||||||
.filter(({ hide }) => !hide)
|
|
||||||
.slice(chatStore.postBeginIndex)
|
|
||||||
.map(({ token }) => token)) {
|
|
||||||
chatStore.totalTokens += i;
|
|
||||||
}
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
const CopiedHint = () => (
|
|
||||||
<div role="alert" className="alert">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="stroke-info h-6 w-6 shrink-0"
|
|
||||||
>
|
|
||||||
<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 { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
@@ -113,20 +272,6 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const CopyIcon = ({ textToCopy }: { textToCopy: string }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
copyToClipboard(textToCopy);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{chatStore.postBeginIndex !== 0 &&
|
{chatStore.postBeginIndex !== 0 &&
|
||||||
39
src/components/ModeToggle.tsx
Normal file
39
src/components/ModeToggle.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useTheme, Theme } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select onValueChange={(value) => setTheme(value as Theme)}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Theme</SelectLabel>
|
||||||
|
<SelectItem value="light">Light</SelectItem>
|
||||||
|
<SelectItem value="dark">Dark</SelectItem>
|
||||||
|
<SelectItem value="system">System</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { IDBPDatabase } from "idb";
|
|
||||||
import { useRef, useState, Dispatch, useContext } from "react";
|
import { useRef, useState, Dispatch, useContext } from "react";
|
||||||
|
|
||||||
import { ChatStore } from "@/types/chatstore";
|
import { ChatStore } from "@/types/chatstore";
|
||||||
import { MessageDetail } from "./chatgpt";
|
import { MessageDetail } from "../chatgpt";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -16,15 +15,15 @@ import {
|
|||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
|
|
||||||
import { Input } from "./components/ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { AppContext } from "./pages/App";
|
import { AppContext } from "../pages/App";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { SearchIcon } from "lucide-react";
|
||||||
|
|
||||||
interface ChatStoreSearchResult {
|
interface ChatStoreSearchResult {
|
||||||
key: IDBValidKey;
|
key: IDBValidKey;
|
||||||
@@ -33,10 +32,7 @@ interface ChatStoreSearchResult {
|
|||||||
preview: string;
|
preview: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Search(props: {
|
export default function Search() {
|
||||||
show: boolean;
|
|
||||||
setShow: (show: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const ctx = useContext(AppContext);
|
const ctx = useContext(AppContext);
|
||||||
if (ctx === null) return <></>;
|
if (ctx === null) return <></>;
|
||||||
const { setSelectedChatIndex, db } = ctx;
|
const { setSelectedChatIndex, db } = ctx;
|
||||||
@@ -46,9 +42,15 @@ export default function Search(props: {
|
|||||||
const [searchingNow, setSearchingNow] = useState<number>(0);
|
const [searchingNow, setSearchingNow] = useState<number>(0);
|
||||||
const [pageIndex, setPageIndex] = useState<number>(0);
|
const [pageIndex, setPageIndex] = useState<number>(0);
|
||||||
const searchAbortRef = useRef<AbortController | null>(null);
|
const searchAbortRef = useRef<AbortController | null>(null);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.show} onOpenChange={props.setShow}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<SearchIcon />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[80%]">
|
<DialogContent className="sm:max-w-[80%]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Search</DialogTitle>
|
<DialogTitle>Search</DialogTitle>
|
||||||
@@ -160,7 +162,7 @@ export default function Search(props: {
|
|||||||
key={result.key as number}
|
key={result.key as number}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedChatIndex(parseInt(result.key.toString()));
|
setSelectedChatIndex(parseInt(result.key.toString()));
|
||||||
props.setShow(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="m-1 p-1 font-bold">
|
<div className="m-1 p-1 font-bold">
|
||||||
@@ -3,17 +3,12 @@ import { themeChange } from "theme-change";
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useContext, useEffect, useState, Dispatch } from "react";
|
import { useContext, useEffect, useState, Dispatch } from "react";
|
||||||
import { clearTotalCost, getTotalCost } from "@/utils/totalCost";
|
import { clearTotalCost, getTotalCost } from "@/utils/totalCost";
|
||||||
import {
|
import { ChatStore, TemplateChatStore, TemplateTools } from "@/types/chatstore";
|
||||||
ChatStore,
|
|
||||||
TemplateChatStore,
|
|
||||||
TemplateAPI,
|
|
||||||
TemplateTools,
|
|
||||||
} from "@/types/chatstore";
|
|
||||||
import { models } from "@/types/models";
|
import { models } from "@/types/models";
|
||||||
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||||
import { isVailedJSON } from "@/message";
|
import { isVailedJSON } from "@/utils/isVailedJSON";
|
||||||
import { SetAPIsTemplate } from "@/setAPIsTemplate";
|
import { SetAPIsTemplate } from "@/components/setAPIsTemplate";
|
||||||
import { autoHeight } from "@/textarea";
|
import { autoHeight } from "@/utils/textAreaHelp";
|
||||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -53,6 +48,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
@@ -61,18 +57,20 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
BanIcon,
|
BanIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CircleEllipsisIcon,
|
CircleEllipsisIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
Ellipsis,
|
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
ListIcon,
|
ListIcon,
|
||||||
MoveHorizontalIcon,
|
MoveHorizontalIcon,
|
||||||
|
SaveIcon,
|
||||||
|
TriangleAlertIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
@@ -475,7 +473,152 @@ const Choice = (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (props: { setShow: Dispatch<boolean> }) => {
|
const APIShowBlock = (props: {
|
||||||
|
ctx: any;
|
||||||
|
index: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
apiField: string;
|
||||||
|
keyField: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="border-b border-gray-200 pb-4 pt-4">
|
||||||
|
<Badge variant="outline">{props.type}</Badge> <Label>{props.label}</Label>
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="grid w-full max-w-sm items-center gap-1.5 mt-2">
|
||||||
|
<Label>Endpoint</Label> {props.apiField}
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full max-w-sm items-center gap-1.5 mt-2">
|
||||||
|
<Label>Key</Label>
|
||||||
|
{props.keyField ? (
|
||||||
|
props.keyField
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500 italic">empty</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2 mr-2"
|
||||||
|
onClick={() => {
|
||||||
|
const name = prompt(`Give template ${props.label} a new name`);
|
||||||
|
if (!name) return;
|
||||||
|
if (props.type === "Chat") {
|
||||||
|
props.ctx.templateAPIs[props.index].name = name;
|
||||||
|
props.ctx.setTemplateAPIs(structuredClone(props.ctx.templateAPIs));
|
||||||
|
} else if (props.type === "Whisper") {
|
||||||
|
props.ctx.templateAPIsWhisper[props.index].name = name;
|
||||||
|
props.ctx.setTemplateAPIsWhisper(
|
||||||
|
structuredClone(props.ctx.templateAPIsWhisper)
|
||||||
|
);
|
||||||
|
} else if (props.type === "TTS") {
|
||||||
|
props.ctx.templateAPIsTTS[props.index].name = name;
|
||||||
|
props.ctx.setTemplateAPIsTTS(
|
||||||
|
structuredClone(props.ctx.templateAPIsTTS)
|
||||||
|
);
|
||||||
|
} else if (props.type === "ImgGen") {
|
||||||
|
props.ctx.templateAPIsImageGen[props.index].name = name;
|
||||||
|
props.ctx.setTemplateAPIsImageGen(
|
||||||
|
structuredClone(props.ctx.templateAPIsImageGen)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change Name
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (!props.ctx) return;
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure to delete ${props.label}(${props.type}) API?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.type === "Chat") {
|
||||||
|
props.ctx.templateAPIs.splice(props.index, 1);
|
||||||
|
props.ctx.setTemplateAPIs(structuredClone(props.ctx.templateAPIs));
|
||||||
|
} else if (props.type === "Whisper") {
|
||||||
|
props.ctx.templateAPIsWhisper.splice(props.index, 1);
|
||||||
|
props.ctx.setTemplateAPIsWhisper(
|
||||||
|
structuredClone(props.ctx.templateAPIsWhisper)
|
||||||
|
);
|
||||||
|
} else if (props.type === "TTS") {
|
||||||
|
props.ctx.templateAPIsTTS.splice(props.index, 1);
|
||||||
|
props.ctx.setTemplateAPIsTTS(
|
||||||
|
structuredClone(props.ctx.templateAPIsTTS)
|
||||||
|
);
|
||||||
|
} else if (props.type === "ImgGen") {
|
||||||
|
props.ctx.templateAPIsImageGen.splice(props.index, 1);
|
||||||
|
props.ctx.setTemplateAPIsImageGen(
|
||||||
|
structuredClone(props.ctx.templateAPIsImageGen)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolsShowBlock = (props: {
|
||||||
|
ctx: any;
|
||||||
|
index: number;
|
||||||
|
label: string;
|
||||||
|
content: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="border-b border-gray-200 pb-4 pt-4">
|
||||||
|
<Badge variant="outline">Tool</Badge> <Label>{props.label}</Label>
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="grid w-full max-w-sm items-center gap-1.5 mt-2">
|
||||||
|
<Label>Content</Label>
|
||||||
|
<ScrollArea className="w-72 whitespace-nowrap rounded-md border">
|
||||||
|
<pre className="text-xs">
|
||||||
|
{JSON.stringify(JSON.parse(props.content), null, 2)}
|
||||||
|
</pre>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2 mr-2"
|
||||||
|
onClick={() => {
|
||||||
|
const name = prompt(`Give the tool ${props.label} a new name`);
|
||||||
|
if (!name) return;
|
||||||
|
props.ctx.templateTools[props.index].name = name;
|
||||||
|
props.ctx.setTemplateTools(structuredClone(props.ctx.templateTools));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (!props.ctx) return;
|
||||||
|
if (!confirm(`Are you sure to delete ${props.label} Tool?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.ctx.templateTools.splice(props.index, 1);
|
||||||
|
props.ctx.setTemplateTools(structuredClone(props.ctx.templateTools));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (props: {}) => {
|
||||||
const ctx = useContext(AppContext);
|
const ctx = useContext(AppContext);
|
||||||
if (ctx === null) return <></>;
|
if (ctx === null) return <></>;
|
||||||
|
|
||||||
@@ -497,13 +640,14 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
const [totalCost, setTotalCost] = useState(getTotalCost());
|
const [totalCost, setTotalCost] = useState(getTotalCost());
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
themeChange(false);
|
themeChange(false);
|
||||||
const handleKeyPress = (event: any) => {
|
const handleKeyPress = (event: any) => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
// keyCode for ESC key is 27
|
// keyCode for ESC key is 27
|
||||||
props.setShow(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -514,10 +658,13 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
};
|
};
|
||||||
}, []); // The empty dependency array ensures that the effect runs only once
|
}, []); // The empty dependency array ensures that the effect runs only once
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="outline" className="flex-grow">
|
<Button variant="outline" className="flex-grow">
|
||||||
{Tr("Settings")}
|
{Tr("Settings")}
|
||||||
|
{(!ctx.chatStore.apiKey || !ctx.chatStore.apiEndpoint) && (
|
||||||
|
<TriangleAlertIcon className="w-4 h-4 ml-1 text-yellow-500" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="flex flex-col overflow-scroll">
|
<SheetContent className="flex flex-col overflow-scroll">
|
||||||
@@ -576,25 +723,68 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
<div className="box">
|
<div className="box">
|
||||||
<div className="flex justify-evenly flex-wrap">
|
<div className="flex justify-evenly flex-wrap">
|
||||||
{ctx.chatStore.toolsString.trim() && (
|
{ctx.chatStore.toolsString.trim() && (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">{Tr(`Save Tools`)}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Save the tool as Template</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Once saved, you can easily access your tools from
|
||||||
|
the dropdown menu.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="grid flex-1 gap-2">
|
||||||
|
<Label htmlFor="toolsName" className="sr-only">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="toolsName"
|
||||||
|
placeholder="Type Something..."
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
id="toolsNameError"
|
||||||
|
className="text-red-600"
|
||||||
|
></Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="sm:justify-start">
|
||||||
|
<DialogClose asChild>
|
||||||
<Button
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
className="px-3"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const name = prompt(
|
const name = document.getElementById(
|
||||||
`Give this **Tools** template a name:`
|
"toolsName" as string
|
||||||
);
|
) as HTMLInputElement;
|
||||||
if (!name) {
|
if (!name.value) {
|
||||||
alert("No template name specified");
|
const errorLabel = document.getElementById(
|
||||||
|
"toolsNameError" as string
|
||||||
|
) as HTMLLabelElement;
|
||||||
|
if (errorLabel) {
|
||||||
|
errorLabel.textContent =
|
||||||
|
"Tool name is required.";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newToolsTmp: TemplateTools = {
|
const newToolsTmp: TemplateTools = {
|
||||||
name,
|
name: name.value,
|
||||||
toolsString: ctx.chatStore.toolsString,
|
toolsString: ctx.chatStore.toolsString,
|
||||||
};
|
};
|
||||||
ctx.templateTools.push(newToolsTmp);
|
ctx.templateTools.push(newToolsTmp);
|
||||||
ctx.setTemplateTools([...ctx.templateTools]);
|
ctx.setTemplateTools([...ctx.templateTools]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr(`Save Tools`)}
|
<SaveIcon className="w-4 h-4" /> Save
|
||||||
|
<span className="sr-only">Save</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -854,8 +1044,8 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
label="Chat API"
|
label="Chat API"
|
||||||
endpoint={ctx.chatStore.apiEndpoint}
|
endpoint={ctx.chatStore.apiEndpoint}
|
||||||
APIkey={ctx.chatStore.apiKey}
|
APIkey={ctx.chatStore.apiKey}
|
||||||
tmps={ctx.templateAPIs}
|
temps={ctx.templateAPIs}
|
||||||
setTmps={ctx.setTemplateAPIs}
|
setTemps={ctx.setTemplateAPIs}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -959,8 +1149,8 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
label="Whisper API"
|
label="Whisper API"
|
||||||
endpoint={ctx.chatStore.whisper_api}
|
endpoint={ctx.chatStore.whisper_api}
|
||||||
APIkey={ctx.chatStore.whisper_key}
|
APIkey={ctx.chatStore.whisper_key}
|
||||||
tmps={ctx.templateAPIsWhisper}
|
temps={ctx.templateAPIsWhisper}
|
||||||
setTmps={ctx.setTemplateAPIsWhisper}
|
setTemps={ctx.setTemplateAPIsWhisper}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -992,8 +1182,8 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
label="TTS API"
|
label="TTS API"
|
||||||
endpoint={ctx.chatStore.tts_api}
|
endpoint={ctx.chatStore.tts_api}
|
||||||
APIkey={ctx.chatStore.tts_key}
|
APIkey={ctx.chatStore.tts_key}
|
||||||
tmps={ctx.templateAPIsTTS}
|
temps={ctx.templateAPIsTTS}
|
||||||
setTmps={ctx.setTemplateAPIsTTS}
|
setTemps={ctx.setTemplateAPIsTTS}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1117,13 +1307,76 @@ export default (props: { setShow: Dispatch<boolean> }) => {
|
|||||||
label="Image Gen API"
|
label="Image Gen API"
|
||||||
endpoint={ctx.chatStore.image_gen_api}
|
endpoint={ctx.chatStore.image_gen_api}
|
||||||
APIkey={ctx.chatStore.image_gen_key}
|
APIkey={ctx.chatStore.image_gen_key}
|
||||||
tmps={ctx.templateAPIsImageGen}
|
temps={ctx.templateAPIsImageGen}
|
||||||
setTmps={ctx.setTemplateAPIsImageGen}
|
setTemps={ctx.setTemplateAPIsImageGen}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
<AccordionItem value="templates">
|
||||||
|
<AccordionTrigger>Saved Template</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
{ctx.templateAPIs.map((template, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<APIShowBlock
|
||||||
|
ctx={ctx}
|
||||||
|
index={index}
|
||||||
|
label={template.name}
|
||||||
|
type="Chat"
|
||||||
|
apiField={template.endpoint}
|
||||||
|
keyField={template.key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{ctx.templateAPIsWhisper.map((template, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<APIShowBlock
|
||||||
|
ctx={ctx}
|
||||||
|
index={index}
|
||||||
|
label={template.name}
|
||||||
|
type="Whisper"
|
||||||
|
apiField={template.endpoint}
|
||||||
|
keyField={template.key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{ctx.templateAPIsTTS.map((template, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<APIShowBlock
|
||||||
|
ctx={ctx}
|
||||||
|
index={index}
|
||||||
|
label={template.name}
|
||||||
|
type="TTS"
|
||||||
|
apiField={template.endpoint}
|
||||||
|
keyField={template.key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{ctx.templateAPIsImageGen.map((template, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<APIShowBlock
|
||||||
|
ctx={ctx}
|
||||||
|
index={index}
|
||||||
|
label={template.name}
|
||||||
|
type="ImgGen"
|
||||||
|
apiField={template.endpoint}
|
||||||
|
keyField={template.key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{ctx.templateTools.map((template, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<ToolsShowBlock
|
||||||
|
ctx={ctx}
|
||||||
|
index={index}
|
||||||
|
label={template.name}
|
||||||
|
content={template.toolsString}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<div className="pt-4 space-y-2">
|
<div className="pt-4 space-y-2">
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
import {
|
|
||||||
CubeIcon,
|
|
||||||
BanknotesIcon,
|
|
||||||
ChatBubbleLeftEllipsisIcon,
|
|
||||||
ScissorsIcon,
|
|
||||||
SwatchIcon,
|
|
||||||
SparklesIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
import { ChatStore } from "@/types/chatstore";
|
|
||||||
import { models } from "@/types/models";
|
|
||||||
import { Tr } from "@/translate";
|
|
||||||
import { getTotalCost } from "@/utils/totalCost";
|
|
||||||
|
|
||||||
const StatusBar = (props: {
|
|
||||||
chatStore: ChatStore;
|
|
||||||
setShowSettings: (show: boolean) => void;
|
|
||||||
setShowSearch: (show: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
const { chatStore, setShowSettings, setShowSearch } = props;
|
|
||||||
return (
|
|
||||||
<div className="navbar bg-base-100 p-0">
|
|
||||||
<div className="navbar-start">
|
|
||||||
<div className="dropdown lg:hidden">
|
|
||||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="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}
|
|
||||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<ChatBubbleLeftEllipsisIcon className="h-4 w-4" />
|
|
||||||
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<ScissorsIcon className="h-4 w-4" />
|
|
||||||
Cut:
|
|
||||||
{chatStore.postBeginIndex}/
|
|
||||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<BanknotesIcon className="h-4 w-4" />
|
|
||||||
Cost: ${chatStore.cost?.toFixed(4)}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="navbar-center cursor-pointer py-1"
|
|
||||||
onClick={() => {
|
|
||||||
setShowSettings(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* the long staus bar */}
|
|
||||||
<div className="stats shadow hidden lg:inline-grid">
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-figure text-secondary">
|
|
||||||
<CubeIcon className="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
<div className="stat-title">Model</div>
|
|
||||||
<div className="stat-value text-base">{chatStore.model}</div>
|
|
||||||
<div className="stat-desc">
|
|
||||||
{models[chatStore.model]?.price?.prompt * 1000 * 1000} $/M tokens
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-figure text-secondary">
|
|
||||||
<SwatchIcon className="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
<div className="stat-title">Mode</div>
|
|
||||||
<div className="stat-value text-base">
|
|
||||||
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
|
|
||||||
</div>
|
|
||||||
<div className="stat-desc">STREAM/FETCH</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-figure text-secondary">
|
|
||||||
<ChatBubbleLeftEllipsisIcon className="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
<div className="stat-title">Tokens</div>
|
|
||||||
<div className="stat-value text-base">{chatStore.totalTokens}</div>
|
|
||||||
<div className="stat-desc">Max: {chatStore.maxTokens}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-figure text-secondary">
|
|
||||||
<ScissorsIcon className="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
<div className="stat-title">Cut</div>
|
|
||||||
<div className="stat-value text-base">
|
|
||||||
{chatStore.postBeginIndex}
|
|
||||||
</div>
|
|
||||||
<div className="stat-desc">
|
|
||||||
Max: {chatStore.history.filter(({ hide }) => !hide).length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-figure text-secondary">
|
|
||||||
<BanknotesIcon className="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
<div className="stat-title">Cost</div>
|
|
||||||
<div className="stat-value text-base">
|
|
||||||
${chatStore.cost?.toFixed(4)}
|
|
||||||
</div>
|
|
||||||
<div className="stat-desc">
|
|
||||||
Accumulated: ${getTotalCost().toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* the short status bar */}
|
|
||||||
<div className="indicator lg:hidden">
|
|
||||||
{chatStore.totalTokens !== 0 && (
|
|
||||||
<span className="indicator-item badge badge-primary">
|
|
||||||
Tokens: {chatStore.totalTokens}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<a className="btn btn-ghost text-base sm:text-xl p-0">
|
|
||||||
<SparklesIcon className="h-4 w-4 hidden sm:block" />
|
|
||||||
{chatStore.model}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="navbar-end">
|
|
||||||
<button
|
|
||||||
className="btn btn-ghost btn-circle"
|
|
||||||
onClick={(event) => {
|
|
||||||
// stop propagation to parent
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
setShowSearch(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="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
|
|
||||||
className="btn btn-ghost btn-circle hidden sm:block"
|
|
||||||
onClick={() => setShowSettings(true)}
|
|
||||||
>
|
|
||||||
<div className="indicator">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
className="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 className="badge badge-xs badge-primary indicator-item"></span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatusBar;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
type Theme = "dark" | "light" | "system";
|
export type Theme = "dark" | "light" | "system";
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
import { createRef, useContext } from "react";
|
import { createRef, useContext } from "react";
|
||||||
|
|
||||||
import { ChatStore } from "@/types/chatstore";
|
import { useState, Dispatch } from "react";
|
||||||
import { useEffect, useState, Dispatch } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { AudioWaveformIcon, CircleStopIcon, MicIcon } from "lucide-react";
|
||||||
AudioWaveform,
|
|
||||||
AudioWaveformIcon,
|
|
||||||
CircleStopIcon,
|
|
||||||
MicIcon,
|
|
||||||
VoicemailIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { AppContext } from "@/pages/App";
|
import { AppContext } from "@/pages/App";
|
||||||
|
|
||||||
const WhisperButton = (props: {
|
const WhisperButton = (props: {
|
||||||
@@ -24,10 +17,12 @@ const WhisperButton = (props: {
|
|||||||
const mediaRef = createRef();
|
const mediaRef = createRef();
|
||||||
const [isRecording, setIsRecording] = useState("Mic");
|
const [isRecording, setIsRecording] = useState("Mic");
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{chatStore.whisper_api && chatStore.whisper_key ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`m-1 p-1 ${isRecording !== "Mic" ? "animate-pulse" : ""}`}
|
className={`${isRecording !== "Mic" ? "animate-pulse" : ""}`}
|
||||||
disabled={isRecording === "Transcribing"}
|
disabled={isRecording === "Transcribing"}
|
||||||
ref={mediaRef as any}
|
ref={mediaRef as any}
|
||||||
onClick={async (event) => {
|
onClick={async (event) => {
|
||||||
@@ -146,6 +141,13 @@ const WhisperButton = (props: {
|
|||||||
<AudioWaveformIcon />
|
<AudioWaveformIcon />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
|
<MicIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<span className="sr-only">Use Microphone</span>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect, Dispatch, useContext } from "react";
|
import { useState, useEffect, Dispatch, useContext } from "react";
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
|
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
|
||||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { EditMessageString } from "@/editMessageString";
|
import { EditMessageString } from "@/components/editMessageString";
|
||||||
import { EditMessageDetail } from "@/editMessageDetail";
|
import { EditMessageDetail } from "@/components/editMessageDetail";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { AppContext } from "./pages/App";
|
import { AppContext } from "../pages/App";
|
||||||
|
|
||||||
interface EditMessageProps {
|
interface EditMessageProps {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { AppContext } from "./pages/App";
|
import { AppContext } from "../pages/App";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { isVailedJSON } from "@/message";
|
import { isVailedJSON } from "@/utils/isVailedJSON";
|
||||||
import { calculate_token_length } from "@/chatgpt";
|
import { calculate_token_length } from "@/chatgpt";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { AppContext } from "./pages/App";
|
import { AppContext } from "../pages/App";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Moon, Sun } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { useTheme } from "@/components/theme-provider";
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
|
||||||
const { setTheme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
||||||
Light
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
||||||
Dark
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
||||||
System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
143
src/components/navbar.tsx
Normal file
143
src/components/navbar.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import {
|
||||||
|
Menubar,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarTrigger,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
} from "@/components/ui/menubar";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
EllipsisIcon,
|
||||||
|
WholeWordIcon,
|
||||||
|
CircleDollarSignIcon,
|
||||||
|
RulerIcon,
|
||||||
|
ReceiptIcon,
|
||||||
|
WalletIcon,
|
||||||
|
ArrowUpDownIcon,
|
||||||
|
ScissorsIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { AppContext } from "@/pages/App";
|
||||||
|
import { models } from "@/types/models";
|
||||||
|
import { getTotalCost } from "@/utils/totalCost";
|
||||||
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
import Search from "@/components/Search";
|
||||||
|
import Settings from "@/components/Settings";
|
||||||
|
|
||||||
|
const Navbar: React.FC = () => {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (!ctx) return <div>error</div>;
|
||||||
|
const { chatStore, setChatStore } = ctx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="flex sticky top-0 bg-background h-14 shrink-0 items-center border-b z-50">
|
||||||
|
<div className="flex flex-1 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2 px-3">
|
||||||
|
<SidebarTrigger />
|
||||||
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
|
<h1 className="text-lg font-bold">{chatStore.model}</h1>
|
||||||
|
<div className="flex justify-between items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="dropdown lg:hidden flex items-center gap-2">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{chatStore.totalTokens.toString()}
|
||||||
|
</Badge>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<EllipsisIcon />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<p>
|
||||||
|
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Cut(s): {chatStore.postBeginIndex}/
|
||||||
|
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Cost: ${chatStore.cost?.toFixed(4)} / $
|
||||||
|
{getTotalCost().toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:inline-grid">
|
||||||
|
<Menubar>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>
|
||||||
|
<WholeWordIcon className="w-4 h-4 mr-2" />{" "}
|
||||||
|
{chatStore.totalTokens}
|
||||||
|
<CircleDollarSignIcon className="w-4 h-4 mx-2" />
|
||||||
|
{chatStore.cost?.toFixed(4)}
|
||||||
|
</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem>
|
||||||
|
<RulerIcon className="w-4 h-4 mr-2" />
|
||||||
|
Max Length: {chatStore.maxTokens}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem>
|
||||||
|
<ReceiptIcon className="w-4 h-4 mr-2" />
|
||||||
|
Price:{" "}
|
||||||
|
{models[chatStore.model]?.price?.prompt * 1000 * 1000}$
|
||||||
|
/ 1M input tokens
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem>
|
||||||
|
<WalletIcon className="w-4 h-4 mr-2" />
|
||||||
|
Total: {getTotalCost().toFixed(2)}$
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem>
|
||||||
|
<ArrowUpDownIcon className="w-4 h-4 mr-2" />
|
||||||
|
{chatStore.streamMode ? (
|
||||||
|
<>
|
||||||
|
<span>{Tr("STREAM")}</span>·
|
||||||
|
<span style={{ color: "gray" }}>{Tr("FETCH")}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span style={{ color: "gray" }}>
|
||||||
|
{Tr("STREAM")}
|
||||||
|
</span>
|
||||||
|
·<span>{Tr("FETCH")}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem>
|
||||||
|
<ScissorsIcon className="w-4 h-4 mr-2" />
|
||||||
|
{chatStore.postBeginIndex} / {chatStore.history.length}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem disabled>
|
||||||
|
Switch to Model (TODO):
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarCheckboxItem checked>gpt-4o</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem>gpt-o1</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem>gpt-o1-mini</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem>gpt-o3</MenubarCheckboxItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex ml-auto gap-2 px-3">
|
||||||
|
<Settings />
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
91
src/components/setAPIsTemplate.tsx
Normal file
91
src/components/setAPIsTemplate.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { TemplateAPI } from "@/types/chatstore";
|
||||||
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { SaveIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
temps: TemplateAPI[];
|
||||||
|
setTemps: (temps: TemplateAPI[]) => void;
|
||||||
|
label: string;
|
||||||
|
endpoint: string;
|
||||||
|
APIkey: string;
|
||||||
|
}
|
||||||
|
export function SetAPIsTemplate({
|
||||||
|
endpoint,
|
||||||
|
APIkey,
|
||||||
|
temps: temps,
|
||||||
|
setTemps: setTemps,
|
||||||
|
label,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">{Tr(`Save ${label}`)}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Save {label} as Template</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Once saved, you can easily access your templates from the dropdown
|
||||||
|
menu.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="grid flex-1 gap-2">
|
||||||
|
<Label htmlFor="templateName" className="sr-only">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input id="templateName" placeholder="Type Something..." />
|
||||||
|
<Label id="templateNameError" className="text-red-600"></Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="sm:justify-start">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
className="px-3"
|
||||||
|
onClick={() => {
|
||||||
|
const name = document.getElementById(
|
||||||
|
"templateName"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (!name.value) {
|
||||||
|
const errorLabel = document.getElementById(
|
||||||
|
"templateNameError"
|
||||||
|
) as HTMLLabelElement;
|
||||||
|
if (errorLabel) {
|
||||||
|
errorLabel.textContent = "Template name is required.";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const temp: TemplateAPI = {
|
||||||
|
name: name.value,
|
||||||
|
endpoint,
|
||||||
|
key: APIkey,
|
||||||
|
};
|
||||||
|
temps.push(temp);
|
||||||
|
setTemps([...temps]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SaveIcon className="w-4 h-4" /> Save
|
||||||
|
<span className="sr-only">Save</span>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/listAPIs.tsx
113
src/listAPIs.tsx
@@ -1,113 +0,0 @@
|
|||||||
import { ChatStore, TemplateAPI } from "@/types/chatstore";
|
|
||||||
import { Tr } from "@/translate";
|
|
||||||
|
|
||||||
import {
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuIndicator,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
NavigationMenuViewport,
|
|
||||||
} from "@/components/ui/navigation-menu";
|
|
||||||
import { Button } from "./components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { AppContext } from "./pages/App";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
label: string;
|
|
||||||
apiField: string;
|
|
||||||
keyField: string;
|
|
||||||
}
|
|
||||||
export function ListAPIs({ label, apiField, keyField }: Props) {
|
|
||||||
const ctx = useContext(AppContext);
|
|
||||||
if (ctx === null) return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>
|
|
||||||
{label}{" "}
|
|
||||||
<span className="hidden lg:inline">
|
|
||||||
{ctx.templateAPIs.find(
|
|
||||||
(t) =>
|
|
||||||
ctx.chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
|
||||||
ctx.chatStore[keyField as keyof ChatStore] === t.key
|
|
||||||
)?.name &&
|
|
||||||
`: ${
|
|
||||||
ctx.templateAPIs.find(
|
|
||||||
(t) =>
|
|
||||||
ctx.chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
|
||||||
ctx.chatStore[keyField as keyof ChatStore] === t.key
|
|
||||||
)?.name
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
|
||||||
{ctx.templateAPIs.map((t, index) => (
|
|
||||||
<li>
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
// @ts-ignore
|
|
||||||
ctx.chatStore[apiField as keyof ChatStore] = t.endpoint;
|
|
||||||
// @ts-ignore
|
|
||||||
ctx.chatStore[keyField] = t.key;
|
|
||||||
ctx.setChatStore({ ...ctx.chatStore });
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
||||||
ctx.chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
|
||||||
ctx.chatStore[keyField as keyof ChatStore] === t.key
|
|
||||||
? "bg-accent text-accent-foreground"
|
|
||||||
: ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">
|
|
||||||
{t.name}
|
|
||||||
</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
{new URL(t.endpoint).host}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
<div className="mt-2 flex justify-between">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const name = prompt(`Give **${label}** template a name`);
|
|
||||||
if (!name) return;
|
|
||||||
t.name = name;
|
|
||||||
ctx.setTemplateAPIs(structuredClone(ctx.templateAPIs));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
!confirm(
|
|
||||||
`Are you sure to delete this **${label}** template?`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.templateAPIs.splice(index, 1);
|
|
||||||
ctx.setTemplateAPIs(structuredClone(ctx.templateAPIs));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { ChatStore, TemplateTools } from "@/types/chatstore";
|
|
||||||
import { Tr } from "@/translate";
|
|
||||||
import {
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuIndicator,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
NavigationMenuViewport,
|
|
||||||
} from "@/components/ui/navigation-menu";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "./components/ui/button";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { AppContext } from "./pages/App";
|
|
||||||
|
|
||||||
export function ListToolsTempaltes() {
|
|
||||||
const ctx = useContext(AppContext);
|
|
||||||
if (!ctx) return <div>error</div>;
|
|
||||||
|
|
||||||
const { chatStore, setChatStore } = ctx;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavigationMenuItem className="p-3">
|
|
||||||
<NavigationMenuTrigger>
|
|
||||||
<span>{Tr(`Saved tools templates`)}</span>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="ml-2 text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.toolsString = "";
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr(`Clear`)}
|
|
||||||
</Button>
|
|
||||||
</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
|
|
||||||
{ctx.templateTools.map((t, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.toolsString = t.toolsString;
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
||||||
chatStore.toolsString === t.toolsString
|
|
||||||
? "bg-accent text-accent-foreground"
|
|
||||||
: ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">
|
|
||||||
{t.name}
|
|
||||||
</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
{t.toolsString}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
<div className="mt-2 flex justify-between">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const name = prompt(`Give **tools** template a name`);
|
|
||||||
if (!name) return;
|
|
||||||
t.name = name;
|
|
||||||
ctx.setTemplateTools(structuredClone(ctx.templateTools));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
!confirm(
|
|
||||||
`Are you sure to delete this **tools** template?`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.templateTools.splice(index, 1);
|
|
||||||
ctx.setTemplateTools(structuredClone(ctx.templateTools));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import { App } from "@/pages/App";
|
|||||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
function Base() {
|
function Base() {
|
||||||
const [langCode, _setLangCode] = useState("en-US");
|
const [langCode, _setLangCode] = useState("en-US");
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { ChatStoreMessage } from "@/types/chatstore";
|
|
||||||
import Markdown from "react-markdown";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
chat: ChatStoreMessage;
|
|
||||||
renderMarkdown: boolean;
|
|
||||||
}
|
|
||||||
export function MessageDetail({ chat, renderMarkdown }: Props) {
|
|
||||||
if (typeof chat.content === "string") {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{chat.content.map((mdt) =>
|
|
||||||
mdt.type === "text" ? (
|
|
||||||
chat.hide ? (
|
|
||||||
mdt.text?.split("\n")[0].slice(0, 16) + " ..."
|
|
||||||
) : renderMarkdown ? (
|
|
||||||
<Markdown>{mdt.text}</Markdown>
|
|
||||||
) : (
|
|
||||||
mdt.text
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
className="my-2 rounded-md max-w-64 max-h-64"
|
|
||||||
src={mdt.image_url?.url}
|
|
||||||
onClick={() => {
|
|
||||||
window.open(mdt.image_url?.url, "_blank");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { ChatStoreMessage } from "@/types/chatstore";
|
|
||||||
import { getMessageText } from "@/chatgpt";
|
|
||||||
import { Badge } from "./components/ui/badge";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
chat: ChatStoreMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MessageHide({ chat }: Props) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<span>{getMessageText(chat).split("\n")[0].slice(0, 28)} ...</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex mt-2 justify-center">
|
|
||||||
<Badge variant="destructive">Removed from context</Badge>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { ChatStoreMessage } from "@/types/chatstore";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
chat: ChatStoreMessage;
|
|
||||||
copyToClipboard: (text: string) => void;
|
|
||||||
}
|
|
||||||
export function MessageToolCall({ chat, copyToClipboard }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="message-content">
|
|
||||||
{chat.tool_calls?.map((tool_call) => (
|
|
||||||
<div className="bg-blue-300 dark:bg-blue-800 p-1 rounded my-1">
|
|
||||||
<strong>
|
|
||||||
Tool Call ID:{" "}
|
|
||||||
<span
|
|
||||||
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
|
||||||
onClick={() => copyToClipboard(String(tool_call.id))}
|
|
||||||
>
|
|
||||||
{tool_call?.id}
|
|
||||||
</span>
|
|
||||||
</strong>
|
|
||||||
<p>Type: {tool_call?.type}</p>
|
|
||||||
<p>
|
|
||||||
Function:
|
|
||||||
<span
|
|
||||||
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
|
||||||
onClick={() => copyToClipboard(tool_call.function.name)}
|
|
||||||
>
|
|
||||||
{tool_call.function.name}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Arguments:
|
|
||||||
<span
|
|
||||||
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
|
||||||
onClick={() => copyToClipboard(tool_call.function.arguments)}
|
|
||||||
>
|
|
||||||
{tool_call.function.arguments}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{/* [TODO] */}
|
|
||||||
{chat.content as string}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { ChatStoreMessage } from "@/types/chatstore";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
chat: ChatStoreMessage;
|
|
||||||
copyToClipboard: (text: string) => void;
|
|
||||||
}
|
|
||||||
export function MessageToolResp({ chat, copyToClipboard }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="message-content">
|
|
||||||
<div className="bg-blue-300 dark:bg-blue-800 p-1 rounded my-1">
|
|
||||||
<strong>
|
|
||||||
Tool Response ID:{" "}
|
|
||||||
<span
|
|
||||||
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
|
||||||
onClick={() => copyToClipboard(String(chat.tool_call_id))}
|
|
||||||
>
|
|
||||||
{chat.tool_call_id}
|
|
||||||
</span>
|
|
||||||
</strong>
|
|
||||||
{/* [TODO] */}
|
|
||||||
<p>{chat.content as string}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -113,7 +113,9 @@ import {
|
|||||||
RulerIcon,
|
RulerIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ModeToggle } from "@/components/mode-toggle";
|
import { ModeToggle } from "@/components/ModeToggle";
|
||||||
|
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
// init selected index
|
// init selected index
|
||||||
@@ -213,6 +215,10 @@ export function App() {
|
|||||||
const newKey = await (await db).add(STORAGE_NAME, newChatStore(chatStore));
|
const newKey = await (await db).add(STORAGE_NAME, newChatStore(chatStore));
|
||||||
setSelectedChatIndex(newKey as number);
|
setSelectedChatIndex(newKey as number);
|
||||||
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
||||||
|
toast({
|
||||||
|
title: "New chat session created",
|
||||||
|
description: `A new chat session (ID. ${newKey}) has been created.`,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleNewChatStore = async () => {
|
const handleNewChatStore = async () => {
|
||||||
return handleNewChatStoreWithOldOne(chatStore);
|
return handleNewChatStoreWithOldOne(chatStore);
|
||||||
@@ -356,7 +362,27 @@ export function App() {
|
|||||||
console.log("[PERFORMANCE!] reading localStorage");
|
console.log("[PERFORMANCE!] reading localStorage");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
db,
|
||||||
|
chatStore,
|
||||||
|
setChatStore,
|
||||||
|
selectedChatIndex,
|
||||||
|
setSelectedChatIndex,
|
||||||
|
templates,
|
||||||
|
setTemplates,
|
||||||
|
templateAPIs,
|
||||||
|
setTemplateAPIs,
|
||||||
|
templateAPIsWhisper,
|
||||||
|
setTemplateAPIsWhisper,
|
||||||
|
templateAPIsTTS,
|
||||||
|
setTemplateAPIsTTS,
|
||||||
|
templateAPIsImageGen,
|
||||||
|
setTemplateAPIsImageGen,
|
||||||
|
templateTools,
|
||||||
|
setTemplateTools,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<Button onClick={handleNewChatStore}>
|
<Button onClick={handleNewChatStore}>
|
||||||
@@ -392,6 +418,7 @@ export function App() {
|
|||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
<ModeToggle />
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive">{Tr("DEL")}</Button>
|
<Button variant="destructive">{Tr("DEL")}</Button>
|
||||||
@@ -422,126 +449,9 @@ export function App() {
|
|||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<header className="flex sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b z-50">
|
<Navbar />
|
||||||
<div className="flex items-center gap-2 px-3">
|
|
||||||
<SidebarTrigger />
|
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
||||||
<h1 className="text-lg font-bold">{chatStore.model}</h1>
|
|
||||||
<div className="flex justify-between items-center gap-2">
|
|
||||||
<div>
|
|
||||||
<div className="dropdown lg:hidden flex items-center gap-2">
|
|
||||||
<Badge variant="outline">
|
|
||||||
{chatStore.totalTokens.toString()}
|
|
||||||
</Badge>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<EllipsisIcon />
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent>
|
|
||||||
<p>
|
|
||||||
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Cut(s): {chatStore.postBeginIndex}/
|
|
||||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Cost: ${chatStore.cost?.toFixed(4)} / $
|
|
||||||
{getTotalCost().toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:inline-grid">
|
|
||||||
<Menubar>
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>
|
|
||||||
<WholeWordIcon className="w-4 h-4 mr-2" />{" "}
|
|
||||||
{chatStore.totalTokens}
|
|
||||||
<CircleDollarSignIcon className="w-4 h-4 mx-2" />
|
|
||||||
{chatStore.cost?.toFixed(4)}
|
|
||||||
</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem>
|
|
||||||
<RulerIcon className="w-4 h-4 mr-2" />
|
|
||||||
Max Length: {chatStore.maxTokens}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<ReceiptIcon className="w-4 h-4 mr-2" />
|
|
||||||
Price:{" "}
|
|
||||||
{models[chatStore.model]?.price?.prompt * 1000 * 1000}
|
|
||||||
$ / 1M input tokens
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<WalletIcon className="w-4 h-4 mr-2" />
|
|
||||||
Total: {getTotalCost().toFixed(2)}$
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<ArrowUpDownIcon className="w-4 h-4 mr-2" />
|
|
||||||
{chatStore.streamMode ? (
|
|
||||||
<>
|
|
||||||
<span>{Tr("STREAM")}</span>·
|
|
||||||
<span style={{ color: "gray" }}>
|
|
||||||
{Tr("FETCH")}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span style={{ color: "gray" }}>
|
|
||||||
{Tr("STREAM")}
|
|
||||||
</span>
|
|
||||||
·<span>{Tr("FETCH")}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<ScissorsIcon className="w-4 h-4 mr-2" />
|
|
||||||
{chatStore.postBeginIndex} /{" "}
|
|
||||||
{chatStore.history.length}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem disabled>
|
|
||||||
Switch to Model (TODO):
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarCheckboxItem checked>
|
|
||||||
gpt-4o
|
|
||||||
</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem>gpt-o1</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem>gpt-o1-mini</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem>gpt-o3</MenubarCheckboxItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
</Menubar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<AppContext.Provider
|
|
||||||
value={{
|
|
||||||
db,
|
|
||||||
chatStore,
|
|
||||||
setChatStore,
|
|
||||||
selectedChatIndex,
|
|
||||||
setSelectedChatIndex,
|
|
||||||
templates,
|
|
||||||
setTemplates,
|
|
||||||
templateAPIs,
|
|
||||||
setTemplateAPIs,
|
|
||||||
templateAPIsWhisper,
|
|
||||||
setTemplateAPIsWhisper,
|
|
||||||
templateAPIsTTS,
|
|
||||||
setTemplateAPIsTTS,
|
|
||||||
templateAPIsImageGen,
|
|
||||||
setTemplateAPIsImageGen,
|
|
||||||
templateTools,
|
|
||||||
setTemplateTools,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChatBOX />
|
<ChatBOX />
|
||||||
</AppContext.Provider>
|
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { IDBPDatabase } from "idb";
|
|
||||||
import { useContext, useRef } from "react";
|
import { useContext, useRef } from "react";
|
||||||
import { useEffect, useState, Dispatch } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
import { addTotalCost, getTotalCost } from "@/utils/totalCost";
|
import { addTotalCost } from "@/utils/totalCost";
|
||||||
import ChatGPT, {
|
import ChatGPT, {
|
||||||
calculate_token_length,
|
calculate_token_length,
|
||||||
FetchResponse,
|
FetchResponse,
|
||||||
@@ -12,33 +11,17 @@ import ChatGPT, {
|
|||||||
Logprobs,
|
Logprobs,
|
||||||
Usage,
|
Usage,
|
||||||
} from "@/chatgpt";
|
} from "@/chatgpt";
|
||||||
import {
|
import { ChatStoreMessage } from "../types/chatstore";
|
||||||
ChatStore,
|
import Message from "@/components/MessageBubble";
|
||||||
ChatStoreMessage,
|
|
||||||
TemplateChatStore,
|
|
||||||
TemplateAPI,
|
|
||||||
TemplateTools,
|
|
||||||
} from "../types/chatstore";
|
|
||||||
import Message from "@/message";
|
|
||||||
import { models } from "@/types/models";
|
import { models } from "@/types/models";
|
||||||
import Settings from "@/components/Settings";
|
import { ImageUploadDrawer } from "@/components/ImageUploadDrawer";
|
||||||
import { AddImage } from "@/addImage";
|
import { autoHeight } from "@/utils/textAreaHelp";
|
||||||
import { ListAPIs } from "@/listAPIs";
|
|
||||||
import { ListToolsTempaltes } from "@/listToolsTemplates";
|
|
||||||
import { autoHeight } from "@/textarea";
|
|
||||||
import Search from "@/search";
|
|
||||||
import Templates from "@/components/Templates";
|
|
||||||
import VersionHint from "@/components/VersionHint";
|
import VersionHint from "@/components/VersionHint";
|
||||||
import StatusBar from "@/components/StatusBar";
|
|
||||||
import WhisperButton from "@/components/WhisperButton";
|
import WhisperButton from "@/components/WhisperButton";
|
||||||
import AddToolMsg from "./AddToolMsg";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { ChatInput } from "@/components/ui/chat/chat-input";
|
import { ChatInput } from "@/components/ui/chat/chat-input";
|
||||||
import {
|
import {
|
||||||
ChatBubble,
|
ChatBubble,
|
||||||
ChatBubbleAvatar,
|
|
||||||
ChatBubbleMessage,
|
ChatBubbleMessage,
|
||||||
ChatBubbleAction,
|
ChatBubbleAction,
|
||||||
ChatBubbleActionWrapper,
|
ChatBubbleActionWrapper,
|
||||||
@@ -46,33 +29,19 @@ import {
|
|||||||
|
|
||||||
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
||||||
import {
|
import {
|
||||||
AlertTriangleIcon,
|
ArrowDownToDotIcon,
|
||||||
ArrowUpIcon,
|
|
||||||
CornerDownLeftIcon,
|
CornerDownLeftIcon,
|
||||||
CornerLeftUpIcon,
|
CornerLeftUpIcon,
|
||||||
CornerUpLeftIcon,
|
CornerRightUpIcon,
|
||||||
GlobeIcon,
|
|
||||||
ImageIcon,
|
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
KeyIcon,
|
ScissorsIcon,
|
||||||
SearchIcon,
|
|
||||||
Settings2,
|
|
||||||
Settings2Icon,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import {
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
} from "@/components/ui/navigation-menu";
|
|
||||||
|
|
||||||
import { AppContext } from "./App";
|
import { AppContext } from "./App";
|
||||||
import { addToRange } from "react-day-picker";
|
import APIListMenu from "@/components/ListAPI";
|
||||||
|
import { ImageGenDrawer } from "@/components/ImageGenDrawer";
|
||||||
|
|
||||||
export default function ChatBOX() {
|
export default function ChatBOX() {
|
||||||
const ctx = useContext(AppContext);
|
const ctx = useContext(AppContext);
|
||||||
@@ -89,11 +58,10 @@ export default function ChatBOX() {
|
|||||||
const [inputMsg, setInputMsg] = useState("");
|
const [inputMsg, setInputMsg] = useState("");
|
||||||
const [images, setImages] = useState<MessageDetail[]>([]);
|
const [images, setImages] = useState<MessageDetail[]>([]);
|
||||||
const [showAddImage, setShowAddImage] = useState(false);
|
const [showAddImage, setShowAddImage] = useState(false);
|
||||||
|
const [showGenImage, setShowGenImage] = useState(false);
|
||||||
const [showGenerating, setShowGenerating] = useState(false);
|
const [showGenerating, setShowGenerating] = useState(false);
|
||||||
const [generatingMessage, setGeneratingMessage] = useState("");
|
const [generatingMessage, setGeneratingMessage] = useState("");
|
||||||
const [showRetry, setShowRetry] = useState(false);
|
const [showRetry, setShowRetry] = useState(false);
|
||||||
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
|
||||||
let default_follow = localStorage.getItem("follow");
|
let default_follow = localStorage.getItem("follow");
|
||||||
if (default_follow === null) {
|
if (default_follow === null) {
|
||||||
default_follow = "true";
|
default_follow = "true";
|
||||||
@@ -423,116 +391,38 @@ export default function ChatBOX() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
|
||||||
const userInputRef = useRef<HTMLInputElement>(null);
|
const userInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col p-2 gap-2 w-full">
|
<APIListMenu />
|
||||||
<div className="flex items-center gap-2 justify-between">
|
|
||||||
{true && <Settings setShow={setShowSettings} />}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setShowSearch(true)}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{showSearch && <Search show={showSearch} setShow={setShowSearch} />}
|
|
||||||
|
|
||||||
{!chatStore.apiKey && (
|
|
||||||
<Alert>
|
|
||||||
<KeyIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle>Heads up!</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{Tr("Please click above to set")} (OpenAI) API KEY
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{!chatStore.apiEndpoint && (
|
|
||||||
<Alert>
|
|
||||||
<GlobeIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle>Heads up!</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{Tr("Please click above to set")} API Endpoint
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<NavigationMenu>
|
|
||||||
<NavigationMenuList>
|
|
||||||
{ctx.templateAPIs.length > 0 && (
|
|
||||||
<ListAPIs label="API" apiField="apiEndpoint" keyField="apiKey" />
|
|
||||||
)}
|
|
||||||
{ctx.templateAPIsWhisper.length > 0 && (
|
|
||||||
<ListAPIs
|
|
||||||
label="Whisper API"
|
|
||||||
apiField="whisper_api"
|
|
||||||
keyField="whisper_key"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ctx.templateAPIsTTS.length > 0 && (
|
|
||||||
<ListAPIs label="TTS API" apiField="tts_api" keyField="tts_key" />
|
|
||||||
)}
|
|
||||||
{ctx.templateAPIsImageGen.length > 0 && (
|
|
||||||
<ListAPIs
|
|
||||||
label="Image Gen API"
|
|
||||||
apiField="image_gen_api"
|
|
||||||
keyField="image_gen_key"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ctx.templateTools.length > 0 && <ListToolsTempaltes />}
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
</div>
|
|
||||||
<div className="grow flex flex-col p-2 w-full">
|
<div className="grow flex flex-col p-2 w-full">
|
||||||
<ChatMessageList>
|
<ChatMessageList>
|
||||||
{chatStore.history.filter((msg) => !msg.example).length == 0 && (
|
|
||||||
<div className="bg-base-200 break-all p-3 my-3 text-left">
|
|
||||||
<h2>
|
|
||||||
<span>{Tr("Saved prompt templates")}</span>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="mx-2"
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.systemMessageContent = "";
|
|
||||||
chatStore.toolsString = "";
|
|
||||||
chatStore.history = [];
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Reset Current")}
|
|
||||||
</Button>
|
|
||||||
</h2>
|
|
||||||
<div className="divider"></div>
|
|
||||||
<div className="flex flex-wrap">
|
|
||||||
<Templates />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{chatStore.history.length === 0 && (
|
{chatStore.history.length === 0 && (
|
||||||
<Alert variant="default" className="my-3">
|
<Alert variant="default" className="my-3">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle>{Tr("No chat history here")}</AlertTitle>
|
<AlertTitle>
|
||||||
|
{Tr("This is a new chat session, start by typing a message")}
|
||||||
|
</AlertTitle>
|
||||||
<AlertDescription className="flex flex-col gap-1 mt-5">
|
<AlertDescription className="flex flex-col gap-1 mt-5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings2Icon className="h-4 w-4" />
|
<CornerRightUpIcon className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
{Tr("Model")}: {chatStore.model}
|
{Tr(
|
||||||
</span>
|
"Settings button located at the top right corner can be used to change the settings of this chat"
|
||||||
</div>
|
)}
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ArrowUpIcon className="h-4 w-4" />
|
|
||||||
<span>
|
|
||||||
{Tr("Click above to change the settings of this chat")}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CornerLeftUpIcon className="h-4 w-4" />
|
<CornerLeftUpIcon className="h-4 w-4" />
|
||||||
<span>{Tr("Click the corner to create a new chat")}</span>
|
<span>
|
||||||
|
{Tr(
|
||||||
|
"'New' button located at the top left corner can be used to create a new chat"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangleIcon className="h-4 w-4" />
|
<ArrowDownToDotIcon className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
{Tr(
|
{Tr(
|
||||||
"All chat history and settings are stored in the local browser"
|
"All chat history and settings are stored in the local browser"
|
||||||
@@ -549,7 +439,8 @@ export default function ChatBOX() {
|
|||||||
<div className="text-sm font-bold">System Prompt</div>
|
<div className="text-sm font-bold">System Prompt</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => setShowSettings(true)}
|
// onClick={() => setShowSettings(true)}
|
||||||
|
// TODO: add a button to show settings
|
||||||
>
|
>
|
||||||
{chatStore.systemMessageContent}
|
{chatStore.systemMessageContent}
|
||||||
</div>
|
</div>
|
||||||
@@ -558,14 +449,19 @@ export default function ChatBOX() {
|
|||||||
<ChatBubbleActionWrapper>
|
<ChatBubbleActionWrapper>
|
||||||
<ChatBubbleAction
|
<ChatBubbleAction
|
||||||
className="size-7"
|
className="size-7"
|
||||||
icon={<Settings2Icon className="size-4" />}
|
icon={<ScissorsIcon className="size-4" />}
|
||||||
onClick={() => setShowSettings(true)}
|
onClick={() => {
|
||||||
|
chatStore.systemMessageContent = "";
|
||||||
|
chatStore.toolsString = "";
|
||||||
|
chatStore.history = [];
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ChatBubbleActionWrapper>
|
</ChatBubbleActionWrapper>
|
||||||
</ChatBubble>
|
</ChatBubble>
|
||||||
)}
|
)}
|
||||||
{chatStore.history.map((_, messageIndex) => (
|
{chatStore.history.map((_, messageIndex) => (
|
||||||
<Message messageIndex={messageIndex} />
|
<Message messageIndex={messageIndex} key={messageIndex} />
|
||||||
))}
|
))}
|
||||||
{showGenerating && (
|
{showGenerating && (
|
||||||
<ChatBubble variant="received">
|
<ChatBubble variant="received">
|
||||||
@@ -686,23 +582,14 @@ export default function ChatBOX() {
|
|||||||
className="min-h-12 resize-none rounded-lg bg-background border-0 p-3 shadow-none focus-visible:ring-0"
|
className="min-h-12 resize-none rounded-lg bg-background border-0 p-3 shadow-none focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center p-3 pt-0">
|
<div className="flex items-center p-3 pt-0">
|
||||||
<Button
|
<ImageUploadDrawer
|
||||||
variant="ghost"
|
images={images}
|
||||||
size="icon"
|
setImages={setImages}
|
||||||
type="button"
|
disableFactor={[showGenerating]}
|
||||||
onClick={() => setShowAddImage(true)}
|
/>
|
||||||
disabled={showGenerating}
|
<ImageGenDrawer disableFactor={[showGenerating]} />
|
||||||
>
|
|
||||||
<ImageIcon className="size-4" />
|
|
||||||
<span className="sr-only">Add Image</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{chatStore.whisper_api && chatStore.whisper_key && (
|
|
||||||
<>
|
|
||||||
<WhisperButton inputMsg={inputMsg} setInputMsg={setInputMsg} />
|
<WhisperButton inputMsg={inputMsg} setInputMsg={setInputMsg} />
|
||||||
<span className="sr-only">Use Microphone</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -720,13 +607,6 @@ export default function ChatBOX() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<AddImage
|
|
||||||
setShowAddImage={setShowAddImage}
|
|
||||||
images={images}
|
|
||||||
showAddImage={showAddImage}
|
|
||||||
setImages={setImages}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { TemplateAPI } from "@/types/chatstore";
|
|
||||||
import { Tr } from "@/translate";
|
|
||||||
import { Button } from "./components/ui/button";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
tmps: TemplateAPI[];
|
|
||||||
setTmps: (tmps: TemplateAPI[]) => void;
|
|
||||||
label: string;
|
|
||||||
endpoint: string;
|
|
||||||
APIkey: string;
|
|
||||||
}
|
|
||||||
export function SetAPIsTemplate({
|
|
||||||
endpoint,
|
|
||||||
APIkey,
|
|
||||||
tmps,
|
|
||||||
setTmps,
|
|
||||||
label,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="mt-3"
|
|
||||||
onClick={() => {
|
|
||||||
const name = prompt(`Give this **${label}** template a name:`);
|
|
||||||
if (!name) {
|
|
||||||
alert("No template name specified");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tmp: TemplateAPI = {
|
|
||||||
name,
|
|
||||||
endpoint,
|
|
||||||
key: APIkey,
|
|
||||||
};
|
|
||||||
tmps.push(tmp);
|
|
||||||
setTmps([...tmps]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr(`Save ${label}`)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,9 +11,17 @@ const LANG_MAP: Record<string, string> = {
|
|||||||
cost: "消费",
|
cost: "消费",
|
||||||
stream: "流式返回",
|
stream: "流式返回",
|
||||||
fetch: "一次获取",
|
fetch: "一次获取",
|
||||||
|
Tools: "工具",
|
||||||
|
Clear: "清空",
|
||||||
"saved api templates": "已保存的 API 模板",
|
"saved api templates": "已保存的 API 模板",
|
||||||
"saved prompt templates": "已保存的提示模板",
|
"saved prompt templates": "已保存的提示模板",
|
||||||
"no chat history here": "暂无历史对话记录",
|
"no chat history here": "暂无历史对话记录",
|
||||||
|
"This is a new chat session, start by typing a message":
|
||||||
|
"这是一个新对话,开始输入消息",
|
||||||
|
"Settings button located at the top right corner can be used to change the settings of this chat":
|
||||||
|
"右上角的设置按钮可用于更改此对话的设置",
|
||||||
|
"'New' button located at the top left corner can be used to create a new chat":
|
||||||
|
"左上角的 '新' 按钮可用于创建新对话",
|
||||||
"click above to change the settings of this chat":
|
"click above to change the settings of this chat":
|
||||||
"点击上方更改此对话的参数(请勿泄漏)",
|
"点击上方更改此对话的参数(请勿泄漏)",
|
||||||
"click the NEW to create a new chat": "点击左上角 NEW 新建对话",
|
"click the NEW to create a new chat": "点击左上角 NEW 新建对话",
|
||||||
|
|||||||
97
src/tts.tsx
97
src/tts.tsx
@@ -1,97 +0,0 @@
|
|||||||
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { useContext, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { addTotalCost } from "@/utils/totalCost";
|
|
||||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
|
||||||
import { Message, getMessageText } from "@/chatgpt";
|
|
||||||
import { AudioLinesIcon, LoaderCircleIcon } from "lucide-react";
|
|
||||||
import { Button } from "./components/ui/button";
|
|
||||||
import { AppContext } from "./pages/App";
|
|
||||||
|
|
||||||
interface TTSProps {
|
|
||||||
chat: ChatStoreMessage;
|
|
||||||
}
|
|
||||||
interface TTSPlayProps {
|
|
||||||
chat: ChatStoreMessage;
|
|
||||||
}
|
|
||||||
export function TTSPlay(props: TTSPlayProps) {
|
|
||||||
const src = useMemo(() => {
|
|
||||||
if (props.chat.audio instanceof Blob) {
|
|
||||||
return URL.createObjectURL(props.chat.audio);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}, [props.chat.audio]);
|
|
||||||
|
|
||||||
if (props.chat.hide) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
if (props.chat.audio instanceof Blob) {
|
|
||||||
return <audio className="w-64" src={src} controls />;
|
|
||||||
}
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
export default function TTSButton(props: TTSProps) {
|
|
||||||
const [generating, setGenerating] = useState(false);
|
|
||||||
const ctx = useContext(AppContext);
|
|
||||||
if (!ctx) return <div>error</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => {
|
|
||||||
const api = ctx.chatStore.tts_api;
|
|
||||||
const api_key = ctx.chatStore.tts_key;
|
|
||||||
const model = "tts-1";
|
|
||||||
const input = getMessageText(props.chat);
|
|
||||||
const voice = ctx.chatStore.tts_voice;
|
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
|
||||||
model,
|
|
||||||
input,
|
|
||||||
voice,
|
|
||||||
response_format: ctx.chatStore.tts_format || "mp3",
|
|
||||||
};
|
|
||||||
if (ctx.chatStore.tts_speed_enabled) {
|
|
||||||
body["speed"] = ctx.chatStore.tts_speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
setGenerating(true);
|
|
||||||
|
|
||||||
fetch(api, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${api_key}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
.then((response) => response.blob())
|
|
||||||
.then((blob) => {
|
|
||||||
// update price
|
|
||||||
const cost = (input.length * 0.015) / 1000;
|
|
||||||
ctx.chatStore.cost += cost;
|
|
||||||
addTotalCost(cost);
|
|
||||||
ctx.setChatStore({ ...ctx.chatStore });
|
|
||||||
|
|
||||||
// save blob
|
|
||||||
props.chat.audio = blob;
|
|
||||||
ctx.setChatStore({ ...ctx.chatStore });
|
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const audio = new Audio(url);
|
|
||||||
audio.play();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setGenerating(false);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{generating ? (
|
|
||||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<AudioLinesIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
8
src/utils/isVailedJSON.ts
Normal file
8
src/utils/isVailedJSON.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const isVailedJSON = (str: string): boolean => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user