feat: style change

This commit is contained in:
Hk-Gosuto 2023-12-04 21:33:27 +08:00
parent 796abf83ce
commit 4c46de7d1d
5 changed files with 452 additions and 319 deletions

View File

@ -12,6 +12,7 @@ export type ChatModel = ModelType;
export interface RequestMessage {
role: MessageRole;
content: string;
image_url?: string;
}
export interface LLMConfig {

View File

@ -539,6 +539,35 @@
bottom: 32px;
}
.chat-input-image {
background-color: var(--primary);
color: white;
position: absolute;
right: 28px;
bottom: 78px;
display: flex;
align-items: flex-start;
border-radius: 4px;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.chat-input-image-close {
fill: black;
border: none;
align-items: center;
justify-content: center;
display: flex;
margin: 0px;
padding: 0px;
background-color: white;
width: 22px;
height: 48px;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
@ -547,4 +576,8 @@
.chat-input-send {
bottom: 30px;
}
.chat-input-image {
bottom: 30px;
}
}

View File

@ -29,6 +29,7 @@ import ConfirmIcon from "../icons/confirm.svg";
import CancelIcon from "../icons/cancel.svg";
import EnablePluginIcon from "../icons/plugin_enable.svg";
import DisablePluginIcon from "../icons/plugin_disable.svg";
import UploadIcon from "../icons/upload.svg";
import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg";
@ -92,6 +93,7 @@ import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks";
import Image from "next/image";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@ -331,8 +333,10 @@ function ClearContextDivider() {
function ChatAction(props: {
text: string;
icon: JSX.Element;
icon?: JSX.Element;
innerNode?: JSX.Element;
onClick: () => void;
style?: React.CSSProperties;
}) {
const iconRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLDivElement>(null);
@ -357,23 +361,29 @@ function ChatAction(props: {
className={`${styles["chat-input-action"]} clickable`}
onClick={() => {
props.onClick();
setTimeout(updateWidth, 1);
iconRef ? setTimeout(updateWidth, 1) : undefined;
}}
onMouseEnter={updateWidth}
onTouchStart={updateWidth}
onMouseEnter={props.icon ? updateWidth : undefined}
onTouchStart={props.icon ? updateWidth : undefined}
style={
{
"--icon-width": `${width.icon}px`,
"--full-width": `${width.full}px`,
} as React.CSSProperties
props.icon
? ({
"--icon-width": `${width.icon}px`,
"--full-width": `${width.full}px`,
...props.style,
} as React.CSSProperties)
: props.style
}
>
<div ref={iconRef} className={styles["icon"]}>
{props.icon}
</div>
<div className={styles["text"]} ref={textRef}>
{props.icon ? (
<div ref={iconRef} className={styles["icon"]}>
{props.icon}
</div>
) : null}
<div className={props.icon ? styles["text"] : undefined} ref={textRef}>
{props.text}
</div>
{props.innerNode}
</div>
);
}
@ -412,6 +422,7 @@ export function ChatActions(props: {
showPromptModal: () => void;
scrollToBottom: () => void;
showPromptHints: () => void;
imageSelected: (img: any) => void;
hitBottom: boolean;
}) {
const config = useAppConfig();
@ -440,6 +451,25 @@ export function ChatActions(props: {
const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll();
function selectImage() {
document.getElementById("chat-image-file-select-upload")?.click();
}
const onImageSelected = (e: any) => {
const file = e.target.files[0];
const filename = file.name;
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const base64 = reader.result;
props.imageSelected({
filename,
base64,
});
};
e.target.value = null;
};
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const allModels = useAllModels();
@ -536,6 +566,21 @@ export function ChatActions(props: {
/>
)}
<ChatAction
onClick={selectImage}
text="选择图片"
icon={<UploadIcon />}
innerNode={
<input
type="file"
accept=".png,.jpg,.webp,.jpeg"
id="chat-image-file-select-upload"
style={{ display: "none" }}
onChange={onImageSelected}
/>
}
/>
{showModelSelector && (
<Selector
defaultSelectedValue={currentModel}
@ -649,6 +694,7 @@ function _Chat() {
const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState("");
const [userImage, setUserImage] = useState<any>();
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const { scrollRef, setAutoScroll, scrollDomToBottom } = useScrollToBottom();
@ -722,7 +768,7 @@ function _Chat() {
}
};
const doSubmit = (userInput: string) => {
const doSubmit = (userInput: string, userImage?: any) => {
if (userInput.trim() === "") return;
const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) {
@ -736,6 +782,7 @@ function _Chat() {
localStorage.setItem(LAST_INPUT_KEY, userInput);
setUserInput("");
setPromptHints([]);
setUserImage(null);
if (!isMobileScreen) inputRef.current?.focus();
setAutoScroll(true);
};
@ -937,6 +984,7 @@ function _Chat() {
...createMessage({
role: "user",
content: userInput,
image_url: userImage?.base64,
}),
preview: true,
},
@ -949,6 +997,7 @@ function _Chat() {
isLoading,
session.messages,
userInput,
userImage?.base64,
]);
const [msgRenderIndex, _setMsgRenderIndex] = useState(
@ -1075,6 +1124,8 @@ function _Chat() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const textareaMinHeight = userImage ? 121 : 68;
return (
<div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region>
@ -1280,6 +1331,7 @@ function _Chat() {
)}
<div className={styles["chat-message-item"]}>
<Markdown
imageBase64={isUser && userImage && userImage.base64}
content={message.content}
loading={
(message.preview || message.streaming) &&
@ -1296,7 +1348,19 @@ function _Chat() {
defaultShow={i >= messages.length - 6}
/>
</div>
{!isUser && message.model == "gpt-4-vision-preview" && (
<div
className={[
styles["chat-message-actions"],
styles["column-flex"],
].join(" ")}
>
<div
style={{ marginTop: "6px" }}
className={styles["chat-input-actions"]}
></div>
</div>
)}
<div className={styles["chat-message-action-date"]}>
{isContext
? Locale.Chat.IsContext
@ -1328,6 +1392,9 @@ function _Chat() {
setUserInput("/");
onSearch("");
}}
imageSelected={(img: any) => {
setUserImage(img);
}}
/>
<div className={styles["chat-input-panel-inner"]}>
<textarea
@ -1343,14 +1410,39 @@ function _Chat() {
autoFocus={autoFocus}
style={{
fontSize: config.fontSize,
minHeight: textareaMinHeight,
}}
/>
{userImage && (
<div className={styles["chat-input-image"]}>
<div
style={{ position: "relative", width: "48px", height: "48px" }}
>
<Image
src={userImage.base64}
alt={userImage.filename}
title={userImage.filename}
layout="fill"
objectFit="cover"
objectPosition="center"
/>
</div>
<button
className={styles["chat-input-image-close"]}
onClick={() => {
setUserImage(null);
}}
>
X
</button>
</div>
)}
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}
className={styles["chat-input-send"]}
type="primary"
onClick={() => doSubmit(userInput)}
onClick={() => doSubmit(userInput, userImage)}
/>
</div>
</div>

View File

@ -116,38 +116,41 @@ function escapeDollarNumber(text: string) {
return escapedText;
}
function _MarkDownContent(props: { content: string }) {
function _MarkDownContent(props: { content: string; imageBase64?: string }) {
const escapedContent = useMemo(
() => escapeDollarNumber(props.content),
[props.content],
);
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
<div>
{props.imageBase64 && <img src={props.imageBase64} alt="" />}
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
p: (pProps) => <p {...pProps} dir="auto" />,
a: (aProps) => {
const href = aProps.href || "";
const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />;
},
],
]}
components={{
pre: PreCode,
p: (pProps) => <p {...pProps} dir="auto" />,
a: (aProps) => {
const href = aProps.href || "";
const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />;
},
}}
>
{escapedContent}
</ReactMarkdown>
}}
>
{escapedContent}
</ReactMarkdown>
</div>
);
}
@ -160,6 +163,7 @@ export function Markdown(
fontSize?: number;
parentRef?: RefObject<HTMLDivElement>;
defaultShow?: boolean;
imageBase64?: string;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
@ -178,7 +182,10 @@ export function Markdown(
{props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
<MarkdownContent
content={props.content}
imageBase64={props.imageBase64}
/>
)}
</div>
);

File diff suppressed because it is too large Load Diff