All checks were successful
Build static content / build (push) Successful in 8m10s
256 lines
8.5 KiB
TypeScript
256 lines
8.5 KiB
TypeScript
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
import Markdown from "preact-markdown";
|
|
import { useState, useEffect, StateUpdater } from "preact/hooks";
|
|
|
|
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
|
import { calculate_token_length, getMessageText } from "@/chatgpt";
|
|
import TTSButton, { TTSPlay } from "@/tts";
|
|
import { MessageHide } from "@/messageHide";
|
|
import { MessageDetail } from "@/messageDetail";
|
|
import { MessageToolCall } from "@/messageToolCall";
|
|
import { MessageToolResp } from "@/messageToolResp";
|
|
import { EditMessage } from "@/editMessage";
|
|
import logprobToColor from "@/logprob";
|
|
|
|
export const isVailedJSON = (str: string): boolean => {
|
|
try {
|
|
JSON.parse(str);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
interface Props {
|
|
messageIndex: number;
|
|
chatStore: ChatStore;
|
|
setChatStore: (cs: ChatStore) => void;
|
|
}
|
|
|
|
export default function Message(props: Props) {
|
|
const { chatStore, messageIndex, setChatStore } = props;
|
|
const chat = chatStore.history[messageIndex];
|
|
const [showEdit, setShowEdit] = useState(false);
|
|
const [showCopiedHint, setShowCopiedHint] = useState(false);
|
|
const [renderMarkdown, setRenderWorkdown] = 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 copyToClipboard = (text: string) => {
|
|
navigator.clipboard.writeText(text);
|
|
setShowCopiedHint(true);
|
|
setTimeout(() => setShowCopiedHint(false), 1000);
|
|
};
|
|
|
|
const CopyIcon = ({ textToCopy }: { textToCopy: string }) => {
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
copyToClipboard(textToCopy);
|
|
}}
|
|
>
|
|
Copy
|
|
</button>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{chatStore.postBeginIndex !== 0 &&
|
|
!chatStore.history[messageIndex].hide &&
|
|
chatStore.postBeginIndex ===
|
|
chatStore.history.slice(0, messageIndex).filter(({ hide }) => !hide)
|
|
.length && (
|
|
<div className="flex items-center relative justify-center">
|
|
<hr className="w-full h-px my-4 border-0" />
|
|
<span className="absolute px-3 rounded p-1">
|
|
Above messages are "forgotten"
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`flex ${
|
|
chat.role === "assistant" ? "justify-start" : "justify-end"
|
|
}`}
|
|
>
|
|
<div className={`w-full`}>
|
|
<div
|
|
className={`chat min-w-16 p-2 my-2 ${
|
|
chat.role === "assistant" ? "chat-start" : "chat-end"
|
|
} ${chat.hide ? "opacity-50" : ""}`}
|
|
>
|
|
<div
|
|
className={`chat-bubble max-w-full ${
|
|
chat.role === "assistant"
|
|
? renderColor
|
|
? "chat-bubble-neutral"
|
|
: "chat-bubble-secondary"
|
|
: "chat-bubble-primary"
|
|
}`}
|
|
>
|
|
{chat.hide ? (
|
|
<MessageHide chat={chat} />
|
|
) : typeof chat.content !== "string" ? (
|
|
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
|
|
) : chat.tool_calls ? (
|
|
<MessageToolCall
|
|
chat={chat}
|
|
copyToClipboard={copyToClipboard}
|
|
/>
|
|
) : chat.role === "tool" ? (
|
|
<MessageToolResp
|
|
chat={chat}
|
|
copyToClipboard={copyToClipboard}
|
|
/>
|
|
) : 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 className="chat-footer opacity-50 flex gap-x-2">
|
|
<DeleteIcon />
|
|
<button onClick={() => setShowEdit(true)}>Edit</button>
|
|
<CopyIcon textToCopy={getMessageText(chat)} />
|
|
{chatStore.tts_api && chatStore.tts_key && (
|
|
<TTSButton
|
|
chatStore={chatStore}
|
|
chat={chat}
|
|
setChatStore={setChatStore}
|
|
/>
|
|
)}
|
|
<TTSPlay chat={chat} />
|
|
</div>
|
|
</div>
|
|
{showEdit && (
|
|
<EditMessage
|
|
setShowEdit={setShowEdit}
|
|
chat={chat}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
/>
|
|
)}
|
|
{showCopiedHint && <CopiedHint />}
|
|
{chatStore.develop_mode && (
|
|
<div
|
|
className={`gap-1 chat-end flex ${
|
|
chat.role === "assistant" ? "justify-start" : "justify-end"
|
|
}`}
|
|
>
|
|
<span className="">token</span>
|
|
<input
|
|
value={chat.token}
|
|
className="input input-bordered input-xs w-16"
|
|
onChange={(event: any) => {
|
|
chat.token = parseInt(event.target.value);
|
|
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;
|
|
setChatStore({ ...chatStore });
|
|
}}
|
|
>
|
|
<label className="">{Tr("example")}</label>
|
|
<input type="checkbox" checked={chat.example} />
|
|
</span>
|
|
<span
|
|
onClick={(event: any) => setRenderWorkdown(!renderMarkdown)}
|
|
>
|
|
<label className="">{Tr("render")}</label>
|
|
<input type="checkbox" checked={renderMarkdown} />
|
|
</span>
|
|
<span onClick={(event: any) => setRenderColor(!renderColor)}>
|
|
<label className="">{Tr("color")}</label>
|
|
<input type="checkbox" checked={renderColor} />
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|