refactor message layout for improved UI consistency

This commit is contained in:
ecwu
2024-12-20 18:21:28 +08:00
parent 17a4bf6d6b
commit 368d9810c2
3 changed files with 391 additions and 356 deletions

View File

@@ -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> )}
</> </>
); );
} }

View File

@@ -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>

View File

@@ -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