|
|
@@ -1,17 +1,18 @@
|
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
|
import React, {
|
|
|
- useState,
|
|
|
- useRef,
|
|
|
- useEffect,
|
|
|
- useMemo,
|
|
|
- useCallback,
|
|
|
Fragment,
|
|
|
RefObject,
|
|
|
+ useCallback,
|
|
|
+ useEffect,
|
|
|
+ useMemo,
|
|
|
+ useRef,
|
|
|
+ useState,
|
|
|
} from "react";
|
|
|
|
|
|
import SendWhiteIcon from "../icons/send-white.svg";
|
|
|
import BrainIcon from "../icons/brain.svg";
|
|
|
import RenameIcon from "../icons/rename.svg";
|
|
|
+import EditIcon from "../icons/rename.svg";
|
|
|
import ExportIcon from "../icons/share.svg";
|
|
|
import ReturnIcon from "../icons/return.svg";
|
|
|
import CopyIcon from "../icons/copy.svg";
|
|
|
@@ -24,11 +25,11 @@ import MaskIcon from "../icons/mask.svg";
|
|
|
import MaxIcon from "../icons/max.svg";
|
|
|
import MinIcon from "../icons/min.svg";
|
|
|
import ResetIcon from "../icons/reload.svg";
|
|
|
+import ReloadIcon from "../icons/reload.svg";
|
|
|
import BreakIcon from "../icons/break.svg";
|
|
|
import SettingsIcon from "../icons/chat-settings.svg";
|
|
|
import DeleteIcon from "../icons/clear.svg";
|
|
|
import PinIcon from "../icons/pin.svg";
|
|
|
-import EditIcon from "../icons/rename.svg";
|
|
|
import ConfirmIcon from "../icons/confirm.svg";
|
|
|
import CloseIcon from "../icons/close.svg";
|
|
|
import CancelIcon from "../icons/cancel.svg";
|
|
|
@@ -45,33 +46,32 @@ import QualityIcon from "../icons/hd.svg";
|
|
|
import StyleIcon from "../icons/palette.svg";
|
|
|
import PluginIcon from "../icons/plugin.svg";
|
|
|
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
|
|
-import ReloadIcon from "../icons/reload.svg";
|
|
|
import HeadphoneIcon from "../icons/headphone.svg";
|
|
|
import {
|
|
|
- ChatMessage,
|
|
|
- SubmitKey,
|
|
|
- useChatStore,
|
|
|
BOT_HELLO,
|
|
|
+ ChatMessage,
|
|
|
createMessage,
|
|
|
- useAccessStore,
|
|
|
- Theme,
|
|
|
- useAppConfig,
|
|
|
DEFAULT_TOPIC,
|
|
|
ModelType,
|
|
|
+ SubmitKey,
|
|
|
+ Theme,
|
|
|
+ useAccessStore,
|
|
|
+ useAppConfig,
|
|
|
+ useChatStore,
|
|
|
usePluginStore,
|
|
|
} from "../store";
|
|
|
|
|
|
import {
|
|
|
- copyToClipboard,
|
|
|
- selectOrCopy,
|
|
|
autoGrowTextArea,
|
|
|
- useMobileScreen,
|
|
|
- getMessageTextContent,
|
|
|
+ copyToClipboard,
|
|
|
getMessageImages,
|
|
|
- isVisionModel,
|
|
|
+ getMessageTextContent,
|
|
|
isDalle3,
|
|
|
- showPlugins,
|
|
|
+ isVisionModel,
|
|
|
safeLocalStorage,
|
|
|
+ selectOrCopy,
|
|
|
+ showPlugins,
|
|
|
+ useMobileScreen,
|
|
|
} from "../utils";
|
|
|
|
|
|
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
|
|
@@ -79,7 +79,7 @@ import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
|
|
import dynamic from "next/dynamic";
|
|
|
|
|
|
import { ChatControllerPool } from "../client/controller";
|
|
|
-import { DalleSize, DalleQuality, DalleStyle } from "../typing";
|
|
|
+import { DalleQuality, DalleSize, DalleStyle } from "../typing";
|
|
|
import { Prompt, usePromptStore } from "../store/prompt";
|
|
|
import Locale from "../locales";
|
|
|
|
|
|
@@ -102,8 +102,8 @@ import {
|
|
|
ModelProvider,
|
|
|
Path,
|
|
|
REQUEST_TIMEOUT_MS,
|
|
|
- UNFINISHED_INPUT,
|
|
|
ServiceProvider,
|
|
|
+ UNFINISHED_INPUT,
|
|
|
} from "../constant";
|
|
|
import { Avatar } from "./emoji";
|
|
|
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
|
|
@@ -113,9 +113,7 @@ import { prettyObject } from "../utils/format";
|
|
|
import { ExportMessageModal } from "./exporter";
|
|
|
import { getClientConfig } from "../config/client";
|
|
|
import { useAllModels } from "../utils/hooks";
|
|
|
-import { MultimodalContent } from "../client/api";
|
|
|
-
|
|
|
-import { ClientApi } from "../client/api";
|
|
|
+import { ClientApi, MultimodalContent } from "../client/api";
|
|
|
import { createTTSPlayer } from "../utils/audio";
|
|
|
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
|
|
|
|
|
@@ -427,6 +425,7 @@ function useScrollToBottom(
|
|
|
// for auto-scroll
|
|
|
|
|
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
|
+
|
|
|
function scrollDomToBottom() {
|
|
|
const dom = scrollRef.current;
|
|
|
if (dom) {
|
|
|
@@ -473,6 +472,7 @@ export function ChatActions(props: {
|
|
|
|
|
|
// switch themes
|
|
|
const theme = config.theme;
|
|
|
+
|
|
|
function nextTheme() {
|
|
|
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
|
|
const themeIndex = themes.indexOf(theme);
|
|
|
@@ -1237,6 +1237,7 @@ function _Chat() {
|
|
|
const accessStore = useAccessStore();
|
|
|
const [speechStatus, setSpeechStatus] = useState(false);
|
|
|
const [speechLoading, setSpeechLoading] = useState(false);
|
|
|
+
|
|
|
async function openaiSpeech(text: string) {
|
|
|
if (speechStatus) {
|
|
|
ttsPlayer.stop();
|
|
|
@@ -1336,6 +1337,7 @@ function _Chat() {
|
|
|
const [msgRenderIndex, _setMsgRenderIndex] = useState(
|
|
|
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
|
|
|
);
|
|
|
+
|
|
|
function setMsgRenderIndex(newIndex: number) {
|
|
|
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
|
|
|
newIndex = Math.max(0, newIndex);
|
|
|
@@ -1371,6 +1373,7 @@ function _Chat() {
|
|
|
setHitBottom(isHitBottom);
|
|
|
setAutoScroll(isHitBottom);
|
|
|
};
|
|
|
+
|
|
|
function scrollToBottom() {
|
|
|
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
|
|
|
scrollDomToBottom();
|
|
|
@@ -1712,252 +1715,264 @@ function _Chat() {
|
|
|
setAutoScroll(false);
|
|
|
}}
|
|
|
>
|
|
|
- {messages.map((message, i) => {
|
|
|
- const isUser = message.role === "user";
|
|
|
- const isContext = i < context.length;
|
|
|
- const showActions =
|
|
|
- i > 0 &&
|
|
|
- !(message.preview || message.content.length === 0) &&
|
|
|
- !isContext;
|
|
|
- const showTyping = message.preview || message.streaming;
|
|
|
-
|
|
|
- const shouldShowClearContextDivider =
|
|
|
- i === clearContextIndex - 1;
|
|
|
-
|
|
|
- return (
|
|
|
- <Fragment key={message.id}>
|
|
|
- <div
|
|
|
- className={
|
|
|
- isUser
|
|
|
- ? styles["chat-message-user"]
|
|
|
- : styles["chat-message"]
|
|
|
- }
|
|
|
- >
|
|
|
- <div className={styles["chat-message-container"]}>
|
|
|
- <div className={styles["chat-message-header"]}>
|
|
|
- <div className={styles["chat-message-avatar"]}>
|
|
|
- <div className={styles["chat-message-edit"]}>
|
|
|
- <IconButton
|
|
|
- icon={<EditIcon />}
|
|
|
- aria={Locale.Chat.Actions.Edit}
|
|
|
- onClick={async () => {
|
|
|
- const newMessage = await showPrompt(
|
|
|
- Locale.Chat.Actions.Edit,
|
|
|
- getMessageTextContent(message),
|
|
|
- 10,
|
|
|
- );
|
|
|
- let newContent: string | MultimodalContent[] =
|
|
|
- newMessage;
|
|
|
- const images = getMessageImages(message);
|
|
|
- if (images.length > 0) {
|
|
|
- newContent = [
|
|
|
- { type: "text", text: newMessage },
|
|
|
- ];
|
|
|
- for (let i = 0; i < images.length; i++) {
|
|
|
- newContent.push({
|
|
|
- type: "image_url",
|
|
|
- image_url: {
|
|
|
- url: images[i],
|
|
|
- },
|
|
|
- });
|
|
|
+ {messages
|
|
|
+ // TODO
|
|
|
+ // .filter((m) => !m.isMcpResponse)
|
|
|
+ .map((message, i) => {
|
|
|
+ const isUser = message.role === "user";
|
|
|
+ const isContext = i < context.length;
|
|
|
+ const showActions =
|
|
|
+ i > 0 &&
|
|
|
+ !(message.preview || message.content.length === 0) &&
|
|
|
+ !isContext;
|
|
|
+ const showTyping = message.preview || message.streaming;
|
|
|
+
|
|
|
+ const shouldShowClearContextDivider =
|
|
|
+ i === clearContextIndex - 1;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Fragment key={message.id}>
|
|
|
+ <div
|
|
|
+ className={
|
|
|
+ isUser
|
|
|
+ ? styles["chat-message-user"]
|
|
|
+ : styles["chat-message"]
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div className={styles["chat-message-container"]}>
|
|
|
+ <div className={styles["chat-message-header"]}>
|
|
|
+ <div className={styles["chat-message-avatar"]}>
|
|
|
+ <div className={styles["chat-message-edit"]}>
|
|
|
+ <IconButton
|
|
|
+ icon={<EditIcon />}
|
|
|
+ aria={Locale.Chat.Actions.Edit}
|
|
|
+ onClick={async () => {
|
|
|
+ const newMessage = await showPrompt(
|
|
|
+ Locale.Chat.Actions.Edit,
|
|
|
+ getMessageTextContent(message),
|
|
|
+ 10,
|
|
|
+ );
|
|
|
+ let newContent:
|
|
|
+ | string
|
|
|
+ | MultimodalContent[] = newMessage;
|
|
|
+ const images = getMessageImages(message);
|
|
|
+ if (images.length > 0) {
|
|
|
+ newContent = [
|
|
|
+ { type: "text", text: newMessage },
|
|
|
+ ];
|
|
|
+ for (let i = 0; i < images.length; i++) {
|
|
|
+ newContent.push({
|
|
|
+ type: "image_url",
|
|
|
+ image_url: {
|
|
|
+ url: images[i],
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- chatStore.updateTargetSession(
|
|
|
- session,
|
|
|
- (session) => {
|
|
|
- const m = session.mask.context
|
|
|
- .concat(session.messages)
|
|
|
- .find((m) => m.id === message.id);
|
|
|
- if (m) {
|
|
|
- m.content = newContent;
|
|
|
+ chatStore.updateTargetSession(
|
|
|
+ session,
|
|
|
+ (session) => {
|
|
|
+ const m = session.mask.context
|
|
|
+ .concat(session.messages)
|
|
|
+ .find((m) => m.id === message.id);
|
|
|
+ if (m) {
|
|
|
+ m.content = newContent;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ ></IconButton>
|
|
|
+ </div>
|
|
|
+ {isUser ? (
|
|
|
+ <Avatar avatar={config.avatar} />
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ {["system"].includes(message.role) ? (
|
|
|
+ <Avatar avatar="2699-fe0f" />
|
|
|
+ ) : (
|
|
|
+ <MaskAvatar
|
|
|
+ avatar={session.mask.avatar}
|
|
|
+ model={
|
|
|
+ message.model ||
|
|
|
+ session.mask.modelConfig.model
|
|
|
}
|
|
|
- },
|
|
|
- );
|
|
|
- }}
|
|
|
- ></IconButton>
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- {isUser ? (
|
|
|
- <Avatar avatar={config.avatar} />
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- {["system"].includes(message.role) ? (
|
|
|
- <Avatar avatar="2699-fe0f" />
|
|
|
- ) : (
|
|
|
- <MaskAvatar
|
|
|
- avatar={session.mask.avatar}
|
|
|
- model={
|
|
|
- message.model ||
|
|
|
- session.mask.modelConfig.model
|
|
|
- }
|
|
|
- />
|
|
|
- )}
|
|
|
- </>
|
|
|
+ {!isUser && (
|
|
|
+ <div className={styles["chat-model-name"]}>
|
|
|
+ {message.model}
|
|
|
+ </div>
|
|
|
)}
|
|
|
- </div>
|
|
|
- {!isUser && (
|
|
|
- <div className={styles["chat-model-name"]}>
|
|
|
- {message.model}
|
|
|
- </div>
|
|
|
- )}
|
|
|
|
|
|
- {showActions && (
|
|
|
- <div className={styles["chat-message-actions"]}>
|
|
|
- <div className={styles["chat-input-actions"]}>
|
|
|
- {message.streaming ? (
|
|
|
- <ChatAction
|
|
|
- text={Locale.Chat.Actions.Stop}
|
|
|
- icon={<StopIcon />}
|
|
|
- onClick={() => onUserStop(message.id ?? i)}
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <>
|
|
|
+ {showActions && (
|
|
|
+ <div className={styles["chat-message-actions"]}>
|
|
|
+ <div className={styles["chat-input-actions"]}>
|
|
|
+ {message.streaming ? (
|
|
|
<ChatAction
|
|
|
- text={Locale.Chat.Actions.Retry}
|
|
|
- icon={<ResetIcon />}
|
|
|
- onClick={() => onResend(message)}
|
|
|
- />
|
|
|
-
|
|
|
- <ChatAction
|
|
|
- text={Locale.Chat.Actions.Delete}
|
|
|
- icon={<DeleteIcon />}
|
|
|
- onClick={() => onDelete(message.id ?? i)}
|
|
|
- />
|
|
|
-
|
|
|
- <ChatAction
|
|
|
- text={Locale.Chat.Actions.Pin}
|
|
|
- icon={<PinIcon />}
|
|
|
- onClick={() => onPinMessage(message)}
|
|
|
- />
|
|
|
- <ChatAction
|
|
|
- text={Locale.Chat.Actions.Copy}
|
|
|
- icon={<CopyIcon />}
|
|
|
+ text={Locale.Chat.Actions.Stop}
|
|
|
+ icon={<StopIcon />}
|
|
|
onClick={() =>
|
|
|
- copyToClipboard(
|
|
|
- getMessageTextContent(message),
|
|
|
- )
|
|
|
+ onUserStop(message.id ?? i)
|
|
|
}
|
|
|
/>
|
|
|
- {config.ttsConfig.enable && (
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
<ChatAction
|
|
|
- text={
|
|
|
- speechStatus
|
|
|
- ? Locale.Chat.Actions.StopSpeech
|
|
|
- : Locale.Chat.Actions.Speech
|
|
|
- }
|
|
|
- icon={
|
|
|
- speechStatus ? (
|
|
|
- <SpeakStopIcon />
|
|
|
- ) : (
|
|
|
- <SpeakIcon />
|
|
|
- )
|
|
|
+ text={Locale.Chat.Actions.Retry}
|
|
|
+ icon={<ResetIcon />}
|
|
|
+ onClick={() => onResend(message)}
|
|
|
+ />
|
|
|
+
|
|
|
+ <ChatAction
|
|
|
+ text={Locale.Chat.Actions.Delete}
|
|
|
+ icon={<DeleteIcon />}
|
|
|
+ onClick={() =>
|
|
|
+ onDelete(message.id ?? i)
|
|
|
}
|
|
|
+ />
|
|
|
+
|
|
|
+ <ChatAction
|
|
|
+ text={Locale.Chat.Actions.Pin}
|
|
|
+ icon={<PinIcon />}
|
|
|
+ onClick={() => onPinMessage(message)}
|
|
|
+ />
|
|
|
+ <ChatAction
|
|
|
+ text={Locale.Chat.Actions.Copy}
|
|
|
+ icon={<CopyIcon />}
|
|
|
onClick={() =>
|
|
|
- openaiSpeech(
|
|
|
+ copyToClipboard(
|
|
|
getMessageTextContent(message),
|
|
|
)
|
|
|
}
|
|
|
/>
|
|
|
- )}
|
|
|
- </>
|
|
|
- )}
|
|
|
+ {config.ttsConfig.enable && (
|
|
|
+ <ChatAction
|
|
|
+ text={
|
|
|
+ speechStatus
|
|
|
+ ? Locale.Chat.Actions.StopSpeech
|
|
|
+ : Locale.Chat.Actions.Speech
|
|
|
+ }
|
|
|
+ icon={
|
|
|
+ speechStatus ? (
|
|
|
+ <SpeakStopIcon />
|
|
|
+ ) : (
|
|
|
+ <SpeakIcon />
|
|
|
+ )
|
|
|
+ }
|
|
|
+ onClick={() =>
|
|
|
+ openaiSpeech(
|
|
|
+ getMessageTextContent(message),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ {message?.tools?.length == 0 && showTyping && (
|
|
|
+ <div className={styles["chat-message-status"]}>
|
|
|
+ {Locale.Chat.Typing}
|
|
|
</div>
|
|
|
)}
|
|
|
- </div>
|
|
|
- {message?.tools?.length == 0 && showTyping && (
|
|
|
- <div className={styles["chat-message-status"]}>
|
|
|
- {Locale.Chat.Typing}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {/*@ts-ignore*/}
|
|
|
- {message?.tools?.length > 0 && (
|
|
|
- <div className={styles["chat-message-tools"]}>
|
|
|
- {message?.tools?.map((tool) => (
|
|
|
+ {/*@ts-ignore*/}
|
|
|
+ {message?.tools?.length > 0 && (
|
|
|
+ <div className={styles["chat-message-tools"]}>
|
|
|
+ {message?.tools?.map((tool) => (
|
|
|
+ <div
|
|
|
+ key={tool.id}
|
|
|
+ title={tool?.errorMsg}
|
|
|
+ className={styles["chat-message-tool"]}
|
|
|
+ >
|
|
|
+ {tool.isError === false ? (
|
|
|
+ <ConfirmIcon />
|
|
|
+ ) : tool.isError === true ? (
|
|
|
+ <CloseIcon />
|
|
|
+ ) : (
|
|
|
+ <LoadingButtonIcon />
|
|
|
+ )}
|
|
|
+ <span>{tool?.function?.name}</span>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className={styles["chat-message-item"]}>
|
|
|
+ <Markdown
|
|
|
+ key={message.streaming ? "loading" : "done"}
|
|
|
+ content={getMessageTextContent(message)}
|
|
|
+ loading={
|
|
|
+ (message.preview || message.streaming) &&
|
|
|
+ message.content.length === 0 &&
|
|
|
+ !isUser
|
|
|
+ }
|
|
|
+ // onContextMenu={(e) => onRightClick(e, message)} // hard to use
|
|
|
+ onDoubleClickCapture={() => {
|
|
|
+ if (!isMobileScreen) return;
|
|
|
+ setUserInput(getMessageTextContent(message));
|
|
|
+ }}
|
|
|
+ fontSize={fontSize}
|
|
|
+ fontFamily={fontFamily}
|
|
|
+ parentRef={scrollRef}
|
|
|
+ defaultShow={i >= messages.length - 6}
|
|
|
+ />
|
|
|
+ {getMessageImages(message).length == 1 && (
|
|
|
+ <img
|
|
|
+ className={styles["chat-message-item-image"]}
|
|
|
+ src={getMessageImages(message)[0]}
|
|
|
+ alt=""
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {getMessageImages(message).length > 1 && (
|
|
|
<div
|
|
|
- key={tool.id}
|
|
|
- title={tool?.errorMsg}
|
|
|
- className={styles["chat-message-tool"]}
|
|
|
+ className={styles["chat-message-item-images"]}
|
|
|
+ style={
|
|
|
+ {
|
|
|
+ "--image-count":
|
|
|
+ getMessageImages(message).length,
|
|
|
+ } as React.CSSProperties
|
|
|
+ }
|
|
|
>
|
|
|
- {tool.isError === false ? (
|
|
|
- <ConfirmIcon />
|
|
|
- ) : tool.isError === true ? (
|
|
|
- <CloseIcon />
|
|
|
- ) : (
|
|
|
- <LoadingButtonIcon />
|
|
|
+ {getMessageImages(message).map(
|
|
|
+ (image, index) => {
|
|
|
+ return (
|
|
|
+ <img
|
|
|
+ className={
|
|
|
+ styles[
|
|
|
+ "chat-message-item-image-multi"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ key={index}
|
|
|
+ src={image}
|
|
|
+ alt=""
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
)}
|
|
|
- <span>{tool?.function?.name}</span>
|
|
|
</div>
|
|
|
- ))}
|
|
|
+ )}
|
|
|
</div>
|
|
|
- )}
|
|
|
- <div className={styles["chat-message-item"]}>
|
|
|
- <Markdown
|
|
|
- key={message.streaming ? "loading" : "done"}
|
|
|
- content={getMessageTextContent(message)}
|
|
|
- loading={
|
|
|
- (message.preview || message.streaming) &&
|
|
|
- message.content.length === 0 &&
|
|
|
- !isUser
|
|
|
- }
|
|
|
- // onContextMenu={(e) => onRightClick(e, message)} // hard to use
|
|
|
- onDoubleClickCapture={() => {
|
|
|
- if (!isMobileScreen) return;
|
|
|
- setUserInput(getMessageTextContent(message));
|
|
|
- }}
|
|
|
- fontSize={fontSize}
|
|
|
- fontFamily={fontFamily}
|
|
|
- parentRef={scrollRef}
|
|
|
- defaultShow={i >= messages.length - 6}
|
|
|
- />
|
|
|
- {getMessageImages(message).length == 1 && (
|
|
|
- <img
|
|
|
- className={styles["chat-message-item-image"]}
|
|
|
- src={getMessageImages(message)[0]}
|
|
|
- alt=""
|
|
|
- />
|
|
|
- )}
|
|
|
- {getMessageImages(message).length > 1 && (
|
|
|
- <div
|
|
|
- className={styles["chat-message-item-images"]}
|
|
|
- style={
|
|
|
- {
|
|
|
- "--image-count":
|
|
|
- getMessageImages(message).length,
|
|
|
- } as React.CSSProperties
|
|
|
- }
|
|
|
- >
|
|
|
- {getMessageImages(message).map((image, index) => {
|
|
|
- return (
|
|
|
- <img
|
|
|
- className={
|
|
|
- styles["chat-message-item-image-multi"]
|
|
|
- }
|
|
|
- key={index}
|
|
|
- src={image}
|
|
|
- alt=""
|
|
|
- />
|
|
|
- );
|
|
|
- })}
|
|
|
+ {message?.audio_url && (
|
|
|
+ <div className={styles["chat-message-audio"]}>
|
|
|
+ <audio src={message.audio_url} controls />
|
|
|
</div>
|
|
|
)}
|
|
|
- </div>
|
|
|
- {message?.audio_url && (
|
|
|
- <div className={styles["chat-message-audio"]}>
|
|
|
- <audio src={message.audio_url} controls />
|
|
|
- </div>
|
|
|
- )}
|
|
|
|
|
|
- <div className={styles["chat-message-action-date"]}>
|
|
|
- {isContext
|
|
|
- ? Locale.Chat.IsContext
|
|
|
- : message.date.toLocaleString()}
|
|
|
+ <div className={styles["chat-message-action-date"]}>
|
|
|
+ {isContext
|
|
|
+ ? Locale.Chat.IsContext
|
|
|
+ : message.date.toLocaleString()}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- {shouldShowClearContextDivider && <ClearContextDivider />}
|
|
|
- </Fragment>
|
|
|
- );
|
|
|
- })}
|
|
|
+ {shouldShowClearContextDivider && <ClearContextDivider />}
|
|
|
+ </Fragment>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</div>
|
|
|
<div className={styles["chat-input-panel"]}>
|
|
|
<PromptHints
|