refactor message layout for improved UI consistency
This commit is contained in:
288
src/message.tsx
288
src/message.tsx
@@ -12,6 +12,15 @@ import { MessageToolCall } from "@/messageToolCall";
|
|||||||
import { MessageToolResp } from "@/messageToolResp";
|
import { MessageToolResp } from "@/messageToolResp";
|
||||||
import { EditMessage } from "@/editMessage";
|
import { EditMessage } from "@/editMessage";
|
||||||
import logprobToColor from "@/logprob";
|
import logprobToColor from "@/logprob";
|
||||||
|
import {
|
||||||
|
ChatBubble,
|
||||||
|
ChatBubbleAvatar,
|
||||||
|
ChatBubbleMessage,
|
||||||
|
ChatBubbleAction,
|
||||||
|
ChatBubbleActionWrapper,
|
||||||
|
} from "@/components/ui/chat/chat-bubble";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { ClipboardIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||||
|
|
||||||
export const isVailedJSON = (str: string): boolean => {
|
export const isVailedJSON = (str: string): boolean => {
|
||||||
try {
|
try {
|
||||||
@@ -74,10 +83,13 @@ export default function Message(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
setShowCopiedHint(true);
|
// TODO: Remove show copied hint
|
||||||
setTimeout(() => setShowCopiedHint(false), 1000);
|
toast({
|
||||||
|
description: Tr("Message copied to clipboard!"),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const CopyIcon = ({ textToCopy }: { textToCopy: string }) => {
|
const CopyIcon = ({ textToCopy }: { textToCopy: string }) => {
|
||||||
@@ -108,154 +120,154 @@ export default function Message(props: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<ChatBubble
|
||||||
className={`flex ${
|
variant={chat.role === "assistant" ? "received" : "sent"}
|
||||||
chat.role === "assistant" ? "justify-start" : "justify-end"
|
className={chat.role !== "assistant" ? "flex-row-reverse" : ""}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className={`w-full`}>
|
<ChatBubbleAvatar fallback={chat.role === "assistant" ? "AI" : "US"} />
|
||||||
<div
|
<ChatBubbleMessage isLoading={false}>
|
||||||
className={`chat min-w-16 p-2 my-2 ${
|
{chat.hide ? (
|
||||||
chat.role === "assistant" ? "chat-start" : "chat-end"
|
<MessageHide chat={chat} />
|
||||||
} ${chat.hide ? "opacity-50" : ""}`}
|
) : typeof chat.content !== "string" ? (
|
||||||
>
|
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
|
||||||
<div
|
) : chat.tool_calls ? (
|
||||||
className={`chat-bubble max-w-full ${
|
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
|
||||||
chat.role === "assistant"
|
) : chat.role === "tool" ? (
|
||||||
? renderColor
|
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
|
||||||
? "chat-bubble-neutral"
|
) : renderMarkdown ? (
|
||||||
: "chat-bubble-secondary"
|
<Markdown markdown={getMessageText(chat)} />
|
||||||
: "chat-bubble-primary"
|
) : (
|
||||||
}`}
|
<div className="message-content">
|
||||||
>
|
{chat.content &&
|
||||||
{chat.hide ? (
|
(chat.logprobs && renderColor
|
||||||
<MessageHide chat={chat} />
|
? chat.logprobs.content
|
||||||
) : typeof chat.content !== "string" ? (
|
.filter((c) => c.token)
|
||||||
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
|
.map((c) => (
|
||||||
) : chat.tool_calls ? (
|
<div
|
||||||
<MessageToolCall
|
style={{
|
||||||
chat={chat}
|
backgroundColor: logprobToColor(c.logprob),
|
||||||
copyToClipboard={copyToClipboard}
|
display: "inline",
|
||||||
/>
|
}}
|
||||||
) : chat.role === "tool" ? (
|
>
|
||||||
<MessageToolResp
|
{c.token}
|
||||||
chat={chat}
|
</div>
|
||||||
copyToClipboard={copyToClipboard}
|
))
|
||||||
/>
|
: getMessageText(chat))}
|
||||||
) : renderMarkdown ? (
|
|
||||||
// @ts-ignore
|
|
||||||
<Markdown markdown={getMessageText(chat)} />
|
|
||||||
) : (
|
|
||||||
<div className="message-content">
|
|
||||||
{
|
|
||||||
// only show when content is string or list of message
|
|
||||||
// this check is used to avoid rendering tool call
|
|
||||||
chat.content &&
|
|
||||||
(chat.logprobs && renderColor
|
|
||||||
? chat.logprobs.content
|
|
||||||
.filter((c) => c.token)
|
|
||||||
.map((c) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: logprobToColor(c.logprob),
|
|
||||||
display: "inline",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{c.token}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: getMessageText(chat))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-footer opacity-50 flex gap-x-2">
|
)}
|
||||||
<DeleteIcon />
|
</ChatBubbleMessage>
|
||||||
<button onClick={() => setShowEdit(true)}>Edit</button>
|
<ChatBubbleActionWrapper>
|
||||||
<CopyIcon textToCopy={getMessageText(chat)} />
|
<ChatBubbleAction
|
||||||
{chatStore.tts_api && chatStore.tts_key && (
|
icon={<TrashIcon className="size-4" />}
|
||||||
<TTSButton
|
onClick={() => {
|
||||||
chatStore={chatStore}
|
chatStore.history[messageIndex].hide =
|
||||||
chat={chat}
|
!chatStore.history[messageIndex].hide;
|
||||||
setChatStore={setChatStore}
|
chatStore.totalTokens = 0;
|
||||||
/>
|
for (const i of chatStore.history
|
||||||
)}
|
.filter(({ hide }) => !hide)
|
||||||
<TTSPlay chat={chat} />
|
.slice(chatStore.postBeginIndex)
|
||||||
{chat.response_model_name && (
|
.map(({ token }) => token)) {
|
||||||
<>
|
chatStore.totalTokens += i;
|
||||||
<span className="opacity-50">{chat.response_model_name}</span>
|
}
|
||||||
<hr />
|
setChatStore({ ...chatStore });
|
||||||
</>
|
}}
|
||||||
)}
|
/>
|
||||||
</div>
|
<ChatBubbleAction
|
||||||
</div>
|
icon={<PencilIcon className="size-4" />}
|
||||||
{showEdit && (
|
onClick={() => setShowEdit(true)}
|
||||||
<EditMessage
|
/>
|
||||||
setShowEdit={setShowEdit}
|
<ChatBubbleAction
|
||||||
chat={chat}
|
icon={<ClipboardIcon className="size-4" />}
|
||||||
|
onClick={() => copyToClipboard(getMessageText(chat))}
|
||||||
|
/>
|
||||||
|
{chatStore.tts_api && chatStore.tts_key && (
|
||||||
|
<TTSButton
|
||||||
chatStore={chatStore}
|
chatStore={chatStore}
|
||||||
|
chat={chat}
|
||||||
setChatStore={setChatStore}
|
setChatStore={setChatStore}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showCopiedHint && <CopiedHint />}
|
<TTSPlay chat={chat} />
|
||||||
{chatStore.develop_mode && (
|
</ChatBubbleActionWrapper>
|
||||||
<div
|
</ChatBubble>
|
||||||
className={`gap-1 chat-end flex ${
|
{showEdit && (
|
||||||
chat.role === "assistant" ? "justify-start" : "justify-end"
|
<EditMessage
|
||||||
}`}
|
setShowEdit={setShowEdit}
|
||||||
|
chat={chat}
|
||||||
|
chatStore={chatStore}
|
||||||
|
setChatStore={setChatStore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showCopiedHint && <CopiedHint />}
|
||||||
|
{chatStore.develop_mode && (
|
||||||
|
<div
|
||||||
|
className={`flex flex-wrap items-center gap-2 mt-2 ${
|
||||||
|
chat.role !== "assistant" ? "justify-end" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">token</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={chat.token}
|
||||||
|
className="h-8 w-16 rounded-md border border-input bg-background px-2 text-sm"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
chatStore.history.splice(messageIndex, 1);
|
||||||
|
chatStore.postBeginIndex = Math.max(
|
||||||
|
chatStore.postBeginIndex - 1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
chatStore.totalTokens = 0;
|
||||||
|
for (const i of chatStore.history
|
||||||
|
.filter(({ hide }) => !hide)
|
||||||
|
.slice(chatStore.postBeginIndex)
|
||||||
|
.map(({ token }) => token)) {
|
||||||
|
chatStore.totalTokens += i;
|
||||||
|
}
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="">token</span>
|
<XMarkIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
value={chat.token}
|
type="checkbox"
|
||||||
className="input input-bordered input-xs w-16"
|
className="h-4 w-4 rounded border-primary"
|
||||||
onChange={(event: any) => {
|
checked={chat.example}
|
||||||
chat.token = parseInt(event.target.value);
|
onChange={() => {
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.history.splice(messageIndex, 1);
|
|
||||||
chatStore.postBeginIndex = Math.max(
|
|
||||||
chatStore.postBeginIndex - 1,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
//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 });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XMarkIcon className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
onClick={(event: any) => {
|
|
||||||
chat.example = !chat.example;
|
chat.example = !chat.example;
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<label className="">{Tr("example")}</label>
|
<span className="text-sm font-medium">{Tr("example")}</span>
|
||||||
<input type="checkbox" checked={chat.example} />
|
</label>
|
||||||
</span>
|
<label className="flex items-center gap-2">
|
||||||
<span
|
<input
|
||||||
onClick={(event: any) => setRenderWorkdown(!renderMarkdown)}
|
type="checkbox"
|
||||||
>
|
className="h-4 w-4 rounded border-primary"
|
||||||
<label className="">{Tr("render")}</label>
|
checked={renderMarkdown}
|
||||||
<input type="checkbox" checked={renderMarkdown} />
|
onChange={() => setRenderWorkdown(!renderMarkdown)}
|
||||||
</span>
|
/>
|
||||||
<span onClick={(event: any) => setRenderColor(!renderColor)}>
|
<span className="text-sm font-medium">{Tr("render")}</span>
|
||||||
<label className="">{Tr("color")}</label>
|
</label>
|
||||||
<input type="checkbox" checked={renderColor} />
|
<label className="flex items-center gap-2">
|
||||||
</span>
|
<input
|
||||||
</div>
|
type="checkbox"
|
||||||
)}
|
className="h-4 w-4 rounded border-primary"
|
||||||
|
checked={renderColor}
|
||||||
|
onChange={() => setRenderColor(!renderColor)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">{Tr("color")}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
ScissorsIcon,
|
ScissorsIcon,
|
||||||
WholeWordIcon,
|
WholeWordIcon,
|
||||||
EllipsisIcon,
|
EllipsisIcon,
|
||||||
|
CogIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
@@ -307,90 +308,95 @@ export function App() {
|
|||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b">
|
<header className="flex sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b z-50">
|
||||||
<div className="flex items-center gap-2 px-3">
|
<div className="flex items-center gap-2 px-3">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
<div className="dropdown lg:hidden flex items-center gap-2">
|
<div className="flex justify-between items-center gap-2">
|
||||||
<h1 className="text-lg font-bold">{chatStore.model}</h1>{" "}
|
<div>
|
||||||
<Badge variant="outline">
|
<div className="dropdown lg:hidden flex items-center gap-2">
|
||||||
{chatStore.totalTokens.toString()}
|
<h1 className="text-lg font-bold">{chatStore.model}</h1>{" "}
|
||||||
</Badge>
|
<Badge variant="outline">
|
||||||
<Popover>
|
{chatStore.totalTokens.toString()}
|
||||||
<PopoverTrigger>
|
</Badge>
|
||||||
<EllipsisIcon />
|
<Popover>
|
||||||
</PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<PopoverContent>
|
<EllipsisIcon />
|
||||||
<p>
|
</PopoverTrigger>
|
||||||
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
<PopoverContent>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
||||||
Cut(s): {chatStore.postBeginIndex}/
|
</p>
|
||||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
<p>
|
||||||
</p>
|
Cut(s): {chatStore.postBeginIndex}/
|
||||||
<p>
|
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||||
Cost: ${chatStore.cost.toFixed(4)} / $
|
</p>
|
||||||
{getTotalCost().toFixed(2)}
|
<p>
|
||||||
</p>
|
Cost: ${chatStore.cost.toFixed(4)} / $
|
||||||
</PopoverContent>
|
{getTotalCost().toFixed(2)}
|
||||||
</Popover>
|
</p>
|
||||||
</div>
|
</PopoverContent>
|
||||||
<div className="hidden lg:inline-grid">
|
</Popover>
|
||||||
<Menubar>
|
</div>
|
||||||
<MenubarMenu>
|
<div className="hidden lg:inline-grid">
|
||||||
<MenubarTrigger>
|
<Menubar>
|
||||||
<BoxesIcon className="w-4 h-4 mr-2" />
|
<MenubarMenu>
|
||||||
{chatStore.model}
|
<MenubarTrigger>
|
||||||
</MenubarTrigger>
|
<BoxesIcon className="w-4 h-4 mr-2" />
|
||||||
<MenubarContent>
|
{chatStore.model}
|
||||||
<MenubarItem>
|
</MenubarTrigger>
|
||||||
{models[chatStore.model]?.price?.prompt * 1000 * 1000}$ /
|
<MenubarContent>
|
||||||
1M input tokens
|
<MenubarItem>
|
||||||
</MenubarItem>
|
{models[chatStore.model]?.price?.prompt * 1000 * 1000}
|
||||||
</MenubarContent>
|
$ / 1M input tokens
|
||||||
</MenubarMenu>
|
</MenubarItem>
|
||||||
<MenubarMenu>
|
</MenubarContent>
|
||||||
<MenubarTrigger>
|
</MenubarMenu>
|
||||||
<ArrowUpDownIcon className="w-4 h-4 mr-2" />
|
<MenubarMenu>
|
||||||
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
|
<MenubarTrigger>
|
||||||
</MenubarTrigger>
|
<ArrowUpDownIcon className="w-4 h-4 mr-2" />
|
||||||
<MenubarContent>
|
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
|
||||||
<MenubarItem>STREAM/FETCH</MenubarItem>
|
</MenubarTrigger>
|
||||||
</MenubarContent>
|
<MenubarContent>
|
||||||
</MenubarMenu>
|
<MenubarItem>STREAM/FETCH</MenubarItem>
|
||||||
<MenubarMenu>
|
</MenubarContent>
|
||||||
<MenubarTrigger>
|
</MenubarMenu>
|
||||||
<WholeWordIcon className="w-4 h-4 mr-2" />{" "}
|
<MenubarMenu>
|
||||||
{chatStore.totalTokens}
|
<MenubarTrigger>
|
||||||
</MenubarTrigger>
|
<WholeWordIcon className="w-4 h-4 mr-2" />{" "}
|
||||||
<MenubarContent>
|
{chatStore.totalTokens}
|
||||||
<MenubarItem>Max: {chatStore.maxTokens}</MenubarItem>
|
</MenubarTrigger>
|
||||||
</MenubarContent>
|
<MenubarContent>
|
||||||
</MenubarMenu>
|
<MenubarItem>Max: {chatStore.maxTokens}</MenubarItem>
|
||||||
<MenubarMenu>
|
</MenubarContent>
|
||||||
<MenubarTrigger>
|
</MenubarMenu>
|
||||||
<ScissorsIcon className="w-4 h-4 mr-2" />
|
<MenubarMenu>
|
||||||
{chatStore.postBeginIndex}
|
<MenubarTrigger>
|
||||||
</MenubarTrigger>
|
<ScissorsIcon className="w-4 h-4 mr-2" />
|
||||||
<MenubarContent>
|
{chatStore.postBeginIndex}
|
||||||
<MenubarItem>
|
</MenubarTrigger>
|
||||||
Max:{" "}
|
<MenubarContent>
|
||||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
<MenubarItem>
|
||||||
</MenubarItem>
|
Max:{" "}
|
||||||
</MenubarContent>
|
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||||
</MenubarMenu>
|
</MenubarItem>
|
||||||
<MenubarMenu>
|
</MenubarContent>
|
||||||
<MenubarTrigger>
|
</MenubarMenu>
|
||||||
<CircleDollarSignIcon className="w-4 h-4 mr-2" />
|
<MenubarMenu>
|
||||||
{chatStore.cost.toFixed(4)}
|
<MenubarTrigger>
|
||||||
</MenubarTrigger>
|
<CircleDollarSignIcon className="w-4 h-4 mr-2" />
|
||||||
<MenubarContent>
|
{chatStore.cost.toFixed(4)}
|
||||||
<MenubarItem>
|
</MenubarTrigger>
|
||||||
Accumulated: ${getTotalCost().toFixed(2)}
|
<MenubarContent>
|
||||||
</MenubarItem>
|
<MenubarItem>
|
||||||
</MenubarContent>
|
Accumulated: ${getTotalCost().toFixed(2)}
|
||||||
</MenubarMenu>
|
</MenubarItem>
|
||||||
</Menubar>
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CogIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ import VersionHint from "@/components/VersionHint";
|
|||||||
import StatusBar from "@/components/StatusBar";
|
import StatusBar from "@/components/StatusBar";
|
||||||
import WhisperButton from "@/components/WhisperButton";
|
import WhisperButton from "@/components/WhisperButton";
|
||||||
import AddToolMsg from "./AddToolMsg";
|
import AddToolMsg from "./AddToolMsg";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
ChatBubble,
|
||||||
|
ChatBubbleAvatar,
|
||||||
|
ChatBubbleMessage,
|
||||||
|
ChatBubbleAction,
|
||||||
|
ChatBubbleActionWrapper,
|
||||||
|
} from "@/components/ui/chat/chat-bubble";
|
||||||
|
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
||||||
|
|
||||||
export default function ChatBOX(props: {
|
export default function ChatBOX(props: {
|
||||||
db: Promise<IDBPDatabase<ChatStore>>;
|
db: Promise<IDBPDatabase<ChatStore>>;
|
||||||
@@ -436,7 +446,7 @@ export default function ChatBOX(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grow flex flex-col p-2 w-full">
|
<div className="grow flex flex-col p-2 w-full">
|
||||||
{showSettings && (
|
{true && (
|
||||||
<Settings
|
<Settings
|
||||||
chatStore={chatStore}
|
chatStore={chatStore}
|
||||||
setChatStore={setChatStore}
|
setChatStore={setChatStore}
|
||||||
@@ -532,128 +542,131 @@ export default function ChatBOX(props: {
|
|||||||
setChatStore={setChatStore}
|
setChatStore={setChatStore}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<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
|
||||||
|
templates={templates}
|
||||||
|
setTemplates={setTemplates}
|
||||||
|
chatStore={chatStore}
|
||||||
|
setChatStore={setChatStore}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{chatStore.history.length === 0 && (
|
||||||
|
<p className="break-all opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
|
||||||
|
{Tr("No chat history here")}
|
||||||
|
<br />⚙{Tr("Model")}: {chatStore.model}
|
||||||
|
<br />⬆{Tr("Click above to change the settings of this chat")}
|
||||||
|
<br />↖{Tr("Click the conor to create a new chat")}
|
||||||
|
<br />⚠
|
||||||
|
{Tr(
|
||||||
|
"All chat history and settings are stored in the local browser"
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{chatStore.systemMessageContent.trim() && (
|
||||||
|
<div className="chat chat-start">
|
||||||
|
<div className="chat-header">Prompt</div>
|
||||||
|
<div
|
||||||
|
className="chat-bubble chat-bubble-accent cursor-pointer message-content"
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
>
|
||||||
|
{chatStore.systemMessageContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{chatStore.history.map((_, messageIndex) => (
|
||||||
|
<Message
|
||||||
|
chatStore={chatStore}
|
||||||
|
setChatStore={setChatStore}
|
||||||
|
messageIndex={messageIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{showGenerating && (
|
||||||
|
<p className="p-2 my-2 animate-pulse message-content">
|
||||||
|
{generatingMessage || Tr("Generating...")}
|
||||||
|
...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-center">
|
||||||
|
{chatStore.history.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="m-2"
|
||||||
|
disabled={showGenerating}
|
||||||
|
onClick={async () => {
|
||||||
|
const messageIndex = chatStore.history.length - 1;
|
||||||
|
if (chatStore.history[messageIndex].role === "assistant") {
|
||||||
|
chatStore.history[messageIndex].hide = true;
|
||||||
|
}
|
||||||
|
|
||||||
{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
|
|
||||||
className="mx-2 underline cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.systemMessageContent = "";
|
|
||||||
chatStore.toolsString = "";
|
|
||||||
chatStore.history = [];
|
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
|
await complete();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Reset Current")}
|
{Tr("Re-Generate")}
|
||||||
</button>
|
</Button>
|
||||||
</h2>
|
)}
|
||||||
<div className="divider"></div>
|
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
||||||
<div className="flex flex-wrap">
|
<Button
|
||||||
<Templates
|
variant="outline"
|
||||||
templates={templates}
|
size="sm"
|
||||||
setTemplates={setTemplates}
|
disabled={showGenerating}
|
||||||
chatStore={chatStore}
|
onClick={async () => {
|
||||||
setChatStore={setChatStore}
|
await complete();
|
||||||
/>
|
}}
|
||||||
</div>
|
>
|
||||||
</div>
|
{Tr("Completion")}
|
||||||
)}
|
</Button>
|
||||||
{chatStore.history.length === 0 && (
|
|
||||||
<p className="break-all opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
|
|
||||||
{Tr("No chat history here")}
|
|
||||||
<br />⚙{Tr("Model")}: {chatStore.model}
|
|
||||||
<br />⬆{Tr("Click above to change the settings of this chat")}
|
|
||||||
<br />↖{Tr("Click the conor to create a new chat")}
|
|
||||||
<br />⚠
|
|
||||||
{Tr(
|
|
||||||
"All chat history and settings are stored in the local browser"
|
|
||||||
)}
|
)}
|
||||||
<br />
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
<p className="p-2 my-2 text-center opacity-50 dark:text-white">
|
||||||
{chatStore.systemMessageContent.trim() && (
|
{chatStore.postBeginIndex !== 0 && (
|
||||||
<div className="chat chat-start">
|
<>
|
||||||
<div className="chat-header">Prompt</div>
|
<br />
|
||||||
<div
|
{Tr("Info: chat history is too long, forget messages")}:{" "}
|
||||||
className="chat-bubble chat-bubble-accent cursor-pointer message-content"
|
{chatStore.postBeginIndex}
|
||||||
onClick={() => setShowSettings(true)}
|
</>
|
||||||
>
|
)}
|
||||||
{chatStore.systemMessageContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{chatStore.history.map((_, messageIndex) => (
|
|
||||||
<Message
|
|
||||||
chatStore={chatStore}
|
|
||||||
setChatStore={setChatStore}
|
|
||||||
messageIndex={messageIndex}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{showGenerating && (
|
|
||||||
<p className="p-2 my-2 animate-pulse message-content">
|
|
||||||
{generatingMessage || Tr("Generating...")}
|
|
||||||
...
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
<VersionHint chatStore={chatStore} />
|
||||||
<p className="text-center">
|
{showRetry && (
|
||||||
{chatStore.history.length > 0 && (
|
<p className="text-right p-2 my-2 dark:text-white">
|
||||||
<button
|
<Button
|
||||||
className="btn btn-sm btn-warning disabled:line-through disabled:btn-neutral disabled:text-white m-2 p-2"
|
variant="destructive"
|
||||||
disabled={showGenerating}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setShowRetry(false);
|
||||||
const messageIndex = chatStore.history.length - 1;
|
await complete();
|
||||||
if (chatStore.history[messageIndex].role === "assistant") {
|
}}
|
||||||
chatStore.history[messageIndex].hide = true;
|
>
|
||||||
}
|
{Tr("Retry")}
|
||||||
|
</Button>
|
||||||
//chatStore.totalTokens =
|
</p>
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
|
|
||||||
await complete();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Re-Generate")}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
<div ref={messagesEndRef}></div>
|
||||||
<button
|
</ChatMessageList>
|
||||||
className="btn btn-outline btn-sm btn-warning disabled:line-through disabled:bg-neural"
|
|
||||||
disabled={showGenerating}
|
|
||||||
onClick={async () => {
|
|
||||||
await complete();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Completion")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="p-2 my-2 text-center opacity-50 dark:text-white">
|
|
||||||
{chatStore.postBeginIndex !== 0 && (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
{Tr("Info: chat history is too long, forget messages")}:{" "}
|
|
||||||
{chatStore.postBeginIndex}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<VersionHint chatStore={chatStore} />
|
|
||||||
{showRetry && (
|
|
||||||
<p className="text-right p-2 my-2 dark:text-white">
|
|
||||||
<button
|
|
||||||
className="p-1 rounded bg-rose-500"
|
|
||||||
onClick={async () => {
|
|
||||||
setShowRetry(false);
|
|
||||||
await complete();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Retry")}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef}></div>
|
|
||||||
</div>
|
</div>
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
@@ -683,16 +696,17 @@ export default function ChatBOX(props: {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between my-1">
|
<div className="sticky top-0 z-10 bg-background flex justify-between my-1 gap-2 p-2">
|
||||||
<button
|
<Button
|
||||||
className="btn btn-primary disabled:line-through disabled:text-white disabled:bg-neutral m-1 p-1"
|
variant="default"
|
||||||
|
size="sm"
|
||||||
disabled={showGenerating || !chatStore.apiKey}
|
disabled={showGenerating || !chatStore.apiKey}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAddImage(!showAddImage);
|
setShowAddImage(!showAddImage);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Image
|
Image
|
||||||
</button>
|
</Button>
|
||||||
{showAddImage && (
|
{showAddImage && (
|
||||||
<AddImage
|
<AddImage
|
||||||
chatStore={chatStore}
|
chatStore={chatStore}
|
||||||
@@ -702,8 +716,8 @@ export default function ChatBOX(props: {
|
|||||||
setImages={setImages}
|
setImages={setImages}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<textarea
|
<Textarea
|
||||||
autofocus
|
autoFocus
|
||||||
value={inputMsg}
|
value={inputMsg}
|
||||||
ref={userInputRef}
|
ref={userInputRef}
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
@@ -711,7 +725,6 @@ export default function ChatBOX(props: {
|
|||||||
autoHeight(event.target);
|
autoHeight(event.target);
|
||||||
}}
|
}}
|
||||||
onKeyPress={(event: any) => {
|
onKeyPress={(event: any) => {
|
||||||
console.log(event);
|
|
||||||
if (event.ctrlKey && event.code === "Enter") {
|
if (event.ctrlKey && event.code === "Enter") {
|
||||||
send(event.target.value, true);
|
send(event.target.value, true);
|
||||||
setInputMsg("");
|
setInputMsg("");
|
||||||
@@ -722,14 +735,15 @@ export default function ChatBOX(props: {
|
|||||||
autoHeight(event.target);
|
autoHeight(event.target);
|
||||||
setInputMsg(event.target.value);
|
setInputMsg(event.target.value);
|
||||||
}}
|
}}
|
||||||
className="textarea textarea-bordered textarea-sm grow w-0"
|
className="min-h-[40px] flex-grow resize-none"
|
||||||
|
placeholder="Type here..."
|
||||||
style={{
|
style={{
|
||||||
lineHeight: "1.39",
|
lineHeight: "1.39",
|
||||||
}}
|
}}
|
||||||
placeholder="Type here..."
|
/>
|
||||||
></textarea>
|
<Button
|
||||||
<button
|
variant="default"
|
||||||
className="btn btn-primary disabled:btn-neutral disabled:line-through m-1 p-1"
|
size="sm"
|
||||||
disabled={showGenerating}
|
disabled={showGenerating}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
send(inputMsg, true);
|
send(inputMsg, true);
|
||||||
@@ -738,7 +752,7 @@ export default function ChatBOX(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Send")}
|
{Tr("Send")}
|
||||||
</button>
|
</Button>
|
||||||
{chatStore.whisper_api && chatStore.whisper_key && (
|
{chatStore.whisper_api && chatStore.whisper_key && (
|
||||||
<WhisperButton
|
<WhisperButton
|
||||||
chatStore={chatStore}
|
chatStore={chatStore}
|
||||||
@@ -747,8 +761,9 @@ export default function ChatBOX(props: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{chatStore.develop_mode && (
|
{chatStore.develop_mode && (
|
||||||
<button
|
<Button
|
||||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
disabled={showGenerating || !chatStore.apiKey}
|
disabled={showGenerating || !chatStore.apiKey}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
chatStore.history.push({
|
chatStore.history.push({
|
||||||
@@ -768,29 +783,31 @@ export default function ChatBOX(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("AI")}
|
{Tr("AI")}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{chatStore.develop_mode && (
|
{chatStore.develop_mode && (
|
||||||
<button
|
<Button
|
||||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
disabled={showGenerating || !chatStore.apiKey}
|
disabled={showGenerating || !chatStore.apiKey}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
send(inputMsg, false);
|
send(inputMsg, false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("User")}
|
{Tr("User")}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{chatStore.develop_mode && (
|
{chatStore.develop_mode && (
|
||||||
<button
|
<Button
|
||||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
disabled={showGenerating || !chatStore.apiKey}
|
disabled={showGenerating || !chatStore.apiKey}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAddToolMsg(true);
|
setShowAddToolMsg(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Tool")}
|
{Tr("Tool")}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{showAddToolMsg && (
|
{showAddToolMsg && (
|
||||||
<AddToolMsg
|
<AddToolMsg
|
||||||
|
|||||||
Reference in New Issue
Block a user