mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-25 07:00:23 +09:00
feat: style change
This commit is contained in:
parent
796abf83ce
commit
4c46de7d1d
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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`,
|
? ({
|
||||||
"--full-width": `${width.full}px`,
|
"--icon-width": `${width.icon}px`,
|
||||||
} as React.CSSProperties
|
"--full-width": `${width.full}px`,
|
||||||
|
...props.style,
|
||||||
|
} as React.CSSProperties)
|
||||||
|
: props.style
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div ref={iconRef} className={styles["icon"]}>
|
{props.icon ? (
|
||||||
{props.icon}
|
<div ref={iconRef} className={styles["icon"]}>
|
||||||
</div>
|
{props.icon}
|
||||||
<div className={styles["text"]} ref={textRef}>
|
</div>
|
||||||
|
) : 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>
|
||||||
|
@ -116,38 +116,41 @@ 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 (
|
||||||
<ReactMarkdown
|
<div>
|
||||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
{props.imageBase64 && <img src={props.imageBase64} alt="" />}
|
||||||
rehypePlugins={[
|
<ReactMarkdown
|
||||||
RehypeKatex,
|
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||||
[
|
rehypePlugins={[
|
||||||
RehypeHighlight,
|
RehypeKatex,
|
||||||
{
|
[
|
||||||
detect: false,
|
RehypeHighlight,
|
||||||
ignoreMissing: true,
|
{
|
||||||
|
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={{
|
{escapedContent}
|
||||||
pre: PreCode,
|
</ReactMarkdown>
|
||||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
</div>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
Loading…
Reference in New Issue
Block a user