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 { export interface RequestMessage {
role: MessageRole; role: MessageRole;
content: string; content: string;
image_url?: string;
} }
export interface LLMConfig { export interface LLMConfig {

View File

@ -539,6 +539,35 @@
bottom: 32px; 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) { @media only screen and (max-width: 600px) {
.chat-input { .chat-input {
font-size: 16px; font-size: 16px;
@ -547,4 +576,8 @@
.chat-input-send { .chat-input-send {
bottom: 30px; 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 CancelIcon from "../icons/cancel.svg";
import EnablePluginIcon from "../icons/plugin_enable.svg"; import EnablePluginIcon from "../icons/plugin_enable.svg";
import DisablePluginIcon from "../icons/plugin_disable.svg"; import DisablePluginIcon from "../icons/plugin_disable.svg";
import UploadIcon from "../icons/upload.svg";
import LightIcon from "../icons/light.svg"; import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg"; import DarkIcon from "../icons/dark.svg";
@ -92,6 +93,7 @@ import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter"; import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import Image from "next/image";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
@ -331,8 +333,10 @@ function ClearContextDivider() {
function ChatAction(props: { function ChatAction(props: {
text: string; text: string;
icon: JSX.Element; icon?: JSX.Element;
innerNode?: JSX.Element;
onClick: () => void; onClick: () => void;
style?: React.CSSProperties;
}) { }) {
const iconRef = useRef<HTMLDivElement>(null); const iconRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
@ -357,23 +361,29 @@ function ChatAction(props: {
className={`${styles["chat-input-action"]} clickable`} className={`${styles["chat-input-action"]} clickable`}
onClick={() => { onClick={() => {
props.onClick(); props.onClick();
setTimeout(updateWidth, 1); iconRef ? setTimeout(updateWidth, 1) : undefined;
}} }}
onMouseEnter={updateWidth} onMouseEnter={props.icon ? updateWidth : undefined}
onTouchStart={updateWidth} onTouchStart={props.icon ? updateWidth : undefined}
style={ style={
{ props.icon
? ({
"--icon-width": `${width.icon}px`, "--icon-width": `${width.icon}px`,
"--full-width": `${width.full}px`, "--full-width": `${width.full}px`,
} as React.CSSProperties ...props.style,
} as React.CSSProperties)
: props.style
} }
> >
{props.icon ? (
<div ref={iconRef} className={styles["icon"]}> <div ref={iconRef} className={styles["icon"]}>
{props.icon} {props.icon}
</div> </div>
<div className={styles["text"]} ref={textRef}> ) : null}
<div className={props.icon ? styles["text"] : undefined} ref={textRef}>
{props.text} {props.text}
</div> </div>
{props.innerNode}
</div> </div>
); );
} }
@ -412,6 +422,7 @@ export function ChatActions(props: {
showPromptModal: () => void; showPromptModal: () => void;
scrollToBottom: () => void; scrollToBottom: () => void;
showPromptHints: () => void; showPromptHints: () => void;
imageSelected: (img: any) => void;
hitBottom: boolean; hitBottom: boolean;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
@ -440,6 +451,25 @@ export function ChatActions(props: {
const couldStop = ChatControllerPool.hasPending(); const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll(); 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 // switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model; const currentModel = chatStore.currentSession().mask.modelConfig.model;
const allModels = useAllModels(); 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 && ( {showModelSelector && (
<Selector <Selector
defaultSelectedValue={currentModel} defaultSelectedValue={currentModel}
@ -649,6 +694,7 @@ function _Chat() {
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState(""); const [userInput, setUserInput] = useState("");
const [userImage, setUserImage] = useState<any>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler(); const { submitKey, shouldSubmit } = useSubmitHandler();
const { scrollRef, setAutoScroll, scrollDomToBottom } = useScrollToBottom(); 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; if (userInput.trim() === "") return;
const matchCommand = chatCommands.match(userInput); const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) { if (matchCommand.matched) {
@ -736,6 +782,7 @@ function _Chat() {
localStorage.setItem(LAST_INPUT_KEY, userInput); localStorage.setItem(LAST_INPUT_KEY, userInput);
setUserInput(""); setUserInput("");
setPromptHints([]); setPromptHints([]);
setUserImage(null);
if (!isMobileScreen) inputRef.current?.focus(); if (!isMobileScreen) inputRef.current?.focus();
setAutoScroll(true); setAutoScroll(true);
}; };
@ -937,6 +984,7 @@ function _Chat() {
...createMessage({ ...createMessage({
role: "user", role: "user",
content: userInput, content: userInput,
image_url: userImage?.base64,
}), }),
preview: true, preview: true,
}, },
@ -949,6 +997,7 @@ function _Chat() {
isLoading, isLoading,
session.messages, session.messages,
userInput, userInput,
userImage?.base64,
]); ]);
const [msgRenderIndex, _setMsgRenderIndex] = useState( const [msgRenderIndex, _setMsgRenderIndex] = useState(
@ -1075,6 +1124,8 @@ function _Chat() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const textareaMinHeight = userImage ? 121 : 68;
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region> <div className="window-header" data-tauri-drag-region>
@ -1280,6 +1331,7 @@ function _Chat() {
)} )}
<div className={styles["chat-message-item"]}> <div className={styles["chat-message-item"]}>
<Markdown <Markdown
imageBase64={isUser && userImage && userImage.base64}
content={message.content} content={message.content}
loading={ loading={
(message.preview || message.streaming) && (message.preview || message.streaming) &&
@ -1296,7 +1348,19 @@ function _Chat() {
defaultShow={i >= messages.length - 6} defaultShow={i >= messages.length - 6}
/> />
</div> </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"]}> <div className={styles["chat-message-action-date"]}>
{isContext {isContext
? Locale.Chat.IsContext ? Locale.Chat.IsContext
@ -1328,6 +1392,9 @@ function _Chat() {
setUserInput("/"); setUserInput("/");
onSearch(""); onSearch("");
}} }}
imageSelected={(img: any) => {
setUserImage(img);
}}
/> />
<div className={styles["chat-input-panel-inner"]}> <div className={styles["chat-input-panel-inner"]}>
<textarea <textarea
@ -1343,14 +1410,39 @@ function _Chat() {
autoFocus={autoFocus} autoFocus={autoFocus}
style={{ style={{
fontSize: config.fontSize, 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 <IconButton
icon={<SendWhiteIcon />} icon={<SendWhiteIcon />}
text={Locale.Chat.Send} text={Locale.Chat.Send}
className={styles["chat-input-send"]} className={styles["chat-input-send"]}
type="primary" type="primary"
onClick={() => doSubmit(userInput)} onClick={() => doSubmit(userInput, userImage)}
/> />
</div> </div>
</div> </div>

View File

@ -116,13 +116,15 @@ function escapeDollarNumber(text: string) {
return escapedText; return escapedText;
} }
function _MarkDownContent(props: { content: string }) { function _MarkDownContent(props: { content: string; imageBase64?: string }) {
const escapedContent = useMemo( const escapedContent = useMemo(
() => escapeDollarNumber(props.content), () => escapeDollarNumber(props.content),
[props.content], [props.content],
); );
return ( return (
<div>
{props.imageBase64 && <img src={props.imageBase64} alt="" />}
<ReactMarkdown <ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]} remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[ rehypePlugins={[
@ -148,6 +150,7 @@ function _MarkDownContent(props: { content: string }) {
> >
{escapedContent} {escapedContent}
</ReactMarkdown> </ReactMarkdown>
</div>
); );
} }
@ -160,6 +163,7 @@ export function Markdown(
fontSize?: number; fontSize?: number;
parentRef?: RefObject<HTMLDivElement>; parentRef?: RefObject<HTMLDivElement>;
defaultShow?: boolean; defaultShow?: boolean;
imageBase64?: string;
} & React.DOMAttributes<HTMLDivElement>, } & React.DOMAttributes<HTMLDivElement>,
) { ) {
const mdRef = useRef<HTMLDivElement>(null); const mdRef = useRef<HTMLDivElement>(null);
@ -178,7 +182,10 @@ export function Markdown(
{props.loading ? ( {props.loading ? (
<LoadingIcon /> <LoadingIcon />
) : ( ) : (
<MarkdownContent content={props.content} /> <MarkdownContent
content={props.content}
imageBase64={props.imageBase64}
/>
)} )}
</div> </div>
); );

File diff suppressed because it is too large Load Diff