This commit is contained in:
GH Action - Upstream Sync
2023-07-06 01:25:51 +00:00
30 changed files with 712 additions and 90 deletions

View File

@@ -61,7 +61,14 @@ import Locale from "../locales";
import { IconButton } from "./button";
import styles from "./chat.module.scss";
import { ListItem, Modal, showConfirm, showPrompt, showToast } from "./ui-lib";
import {
ListItem,
Modal,
Selector,
showConfirm,
showPrompt,
showToast,
} from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom";
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
import { Avatar } from "./emoji";
@@ -172,10 +179,29 @@ function PromptToast(props: {
function useSubmitHandler() {
const config = useAppConfig();
const submitKey = config.submitKey;
const isComposing = useRef(false);
useEffect(() => {
const onCompositionStart = () => {
isComposing.current = true;
};
const onCompositionEnd = () => {
isComposing.current = false;
};
window.addEventListener("compositionstart", onCompositionStart);
window.addEventListener("compositionend", onCompositionEnd);
return () => {
window.removeEventListener("compositionstart", onCompositionStart);
window.removeEventListener("compositionend", onCompositionEnd);
};
}, []);
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== "Enter") return false;
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current))
return false;
return (
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
@@ -385,16 +411,11 @@ export function ChatActions(props: {
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
function nextModel() {
const models = config.models.filter((m) => m.available).map((m) => m.name);
const modelIndex = models.indexOf(currentModel);
const nextIndex = (modelIndex + 1) % models.length;
const nextModel = models[nextIndex];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.model = nextModel as ModelType;
session.mask.syncGlobalConfig = false;
});
}
const models = useMemo(
() => config.models.filter((m) => m.available).map((m) => m.name),
[config.models],
);
const [showModelSelector, setShowModelSelector] = useState(false);
return (
<div className={styles["chat-input-actions"]}>
@@ -466,10 +487,28 @@ export function ChatActions(props: {
/>
<ChatAction
onClick={nextModel}
onClick={() => setShowModelSelector(true)}
text={currentModel}
icon={<RobotIcon />}
/>
{showModelSelector && (
<Selector
items={models.map((m) => ({
title: m,
value: m,
}))}
onClose={() => setShowModelSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.model = s[0] as ModelType;
session.mask.syncGlobalConfig = false;
});
showToast(s[0]);
}}
/>
)}
</div>
);
}

View File

@@ -186,7 +186,7 @@
box-shadow: var(--card-shadow);
border: var(--border-in-light);
* {
*:not(li) {
overflow: hidden;
}
}

View File

@@ -1,7 +1,16 @@
/* eslint-disable @next/next/no-img-element */
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import Locale from "../locales";
import styles from "./exporter.module.scss";
import { List, ListItem, Modal, Select, showToast } from "./ui-lib";
import {
List,
ListItem,
Modal,
Select,
showImageModal,
showModal,
showToast,
} from "./ui-lib";
import { IconButton } from "./button";
import { copyToClipboard, downloadAs, useMobileScreen } from "../utils";
@@ -23,6 +32,7 @@ import { DEFAULT_MASK_AVATAR } from "../store/mask";
import { api } from "../client/api";
import { prettyObject } from "../utils/format";
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
import { getClientConfig } from "../config/client";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -369,6 +379,7 @@ export function ImagePreviewer(props: {
const previewRef = useRef<HTMLDivElement>(null);
const copy = () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
toBlob(dom).then((blob) => {
@@ -393,17 +404,15 @@ export function ImagePreviewer(props: {
const isMobile = useMobileScreen();
const download = () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
toPng(dom)
.then((blob) => {
if (!blob) return;
if (isMobile) {
const image = new Image();
image.src = blob;
const win = window.open("");
win?.document.write(image.outerHTML);
if (isMobile || getClientConfig()?.isApp) {
showImageModal(blob);
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;

View File

@@ -12,6 +12,7 @@ import mermaid from "mermaid";
import LoadingIcon from "../icons/three-dots.svg";
import React from "react";
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
import { showImageModal } from "./ui-lib";
export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null);
@@ -37,11 +38,13 @@ export function Mermaid(props: { code: string }) {
if (!svg) return;
const text = new XMLSerializer().serializeToString(svg);
const blob = new Blob([text], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const win = window.open(url);
if (win) {
win.onload = () => URL.revokeObjectURL(url);
}
console.log(blob);
// const url = URL.createObjectURL(blob);
// const win = window.open(url);
// if (win) {
// win.onload = () => URL.revokeObjectURL(url);
// }
showImageModal(URL.createObjectURL(blob));
}
if (hasError) {

View File

@@ -30,7 +30,7 @@ import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useEffect, useState } from "react";
import { downloadAs, readFromFile } from "../utils";
import { copyToClipboard, downloadAs, readFromFile } from "../utils";
import { Updater } from "../typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
@@ -65,6 +65,11 @@ export function MaskConfig(props: {
});
};
const copyMaskLink = () => {
const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
copyToClipboard(maskLink);
};
const globalConfig = useAppConfig();
return (
@@ -125,6 +130,20 @@ export function MaskConfig(props: {
}}
></input>
</ListItem>
{!props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Share.Title}
subTitle={Locale.Mask.Config.Share.SubTitle}
>
<IconButton
icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink}
/>
</ListItem>
) : null}
{props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Sync.Title}

View File

@@ -15,6 +15,7 @@ import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask";
import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks";
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
const xmin = Math.max(aRect.x, bRect.x);
@@ -93,14 +94,17 @@ export function NewChat() {
const { state } = useLocation();
const startChat = (mask?: Mask) => {
chatStore.newSession(mask);
setTimeout(() => navigate(Path.Chat), 1);
setTimeout(() => {
chatStore.newSession(mask);
navigate(Path.Chat);
}, 10);
};
useCommand({
mask: (id) => {
try {
const mask = maskStore.get(parseInt(id));
const intId = parseInt(id);
const mask = maskStore.get(intId) ?? BUILTIN_MASK_STORE.get(intId);
startChat(mask ?? undefined);
} catch {
console.error("[New Chat] failed to create chat from mask id=", id);

View File

@@ -542,10 +542,12 @@ export function Settings() {
}
></input>
</ListItem>
</List>
<List>
<ListItem
title={Locale.Settings.Mask.Title}
subTitle={Locale.Settings.Mask.SubTitle}
title={Locale.Settings.Mask.Splash.Title}
subTitle={Locale.Settings.Mask.Splash.SubTitle}
>
<input
type="checkbox"
@@ -559,6 +561,22 @@ export function Settings() {
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Mask.Builtin.Title}
subTitle={Locale.Settings.Mask.Builtin.SubTitle}
>
<input
type="checkbox"
checked={config.hideBuiltinMasks}
onChange={(e) =>
updateConfig(
(config) =>
(config.hideBuiltinMasks = e.currentTarget.checked),
)
}
></input>
</ListItem>
</List>
<List>

View File

@@ -62,6 +62,7 @@
box-shadow: var(--card-shadow);
margin-bottom: 20px;
animation: slide-in ease 0.3s;
background: var(--white);
}
.list .list-item:last-child {
@@ -79,6 +80,19 @@
--modal-padding: 20px;
&-max {
width: 95vw;
max-width: unset;
height: 95vh;
display: flex;
flex-direction: column;
.modal-content {
max-height: unset !important;
flex-grow: 1;
}
}
.modal-header {
padding: var(--modal-padding);
display: flex;
@@ -91,11 +105,19 @@
font-size: 16px;
}
.modal-close-btn {
cursor: pointer;
.modal-header-actions {
display: flex;
&:hover {
filter: brightness(1.2);
.modal-header-action {
cursor: pointer;
&:not(:last-child) {
margin-right: 20px;
}
&:hover {
filter: brightness(1.2);
}
}
}
}
@@ -249,3 +271,34 @@
border: 1px solid var(--primary);
}
}
.selector {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
&-content {
.list {
overflow: hidden;
.list-item {
cursor: pointer;
background-color: var(--white);
&:hover {
filter: brightness(0.95);
}
&:active {
filter: brightness(0.9);
}
}
}
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @next/next/no-img-element */
import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
@@ -6,6 +7,8 @@ import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg";
import ConfirmIcon from "../icons/confirm.svg";
import CancelIcon from "../icons/cancel.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import Locale from "../locales";
@@ -44,9 +47,13 @@ export function ListItem(props: {
children?: JSX.Element | JSX.Element[];
icon?: JSX.Element;
className?: string;
onClick?: () => void;
}) {
return (
<div className={styles["list-item"] + ` ${props.className || ""}`}>
<div
className={styles["list-item"] + ` ${props.className || ""}`}
onClick={props.onClick}
>
<div className={styles["list-header"]}>
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
<div className={styles["list-item-title"]}>
@@ -93,6 +100,7 @@ interface ModalProps {
title: string;
children?: any;
actions?: JSX.Element[];
defaultMax?: boolean;
onClose?: () => void;
}
export function Modal(props: ModalProps) {
@@ -111,13 +119,30 @@ export function Modal(props: ModalProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [isMax, setMax] = useState(!!props.defaultMax);
return (
<div className={styles["modal-container"]}>
<div
className={
styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}`
}
>
<div className={styles["modal-header"]}>
<div className={styles["modal-title"]}>{props.title}</div>
<div className={styles["modal-close-btn"]} onClick={props.onClose}>
<CloseIcon />
<div className={styles["modal-header-actions"]}>
<div
className={styles["modal-header-action"]}
onClick={() => setMax(!isMax)}
>
{isMax ? <MinIcon /> : <MaxIcon />}
</div>
<div
className={styles["modal-header-action"]}
onClick={props.onClose}
>
<CloseIcon />
</div>
</div>
</div>
@@ -394,3 +419,54 @@ export function showPrompt(content: any, value = "", rows = 3) {
);
});
}
export function showImageModal(img: string) {
showModal({
title: Locale.Export.Image.Modal,
children: (
<div>
<img
src={img}
alt="preview"
style={{
maxWidth: "100%",
}}
></img>
</div>
),
});
}
export function Selector<T>(props: {
items: Array<{
title: string;
subTitle?: string;
value: T;
}>;
onSelection?: (selection: T[]) => void;
onClose?: () => void;
multiple?: boolean;
}) {
return (
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
<div className={styles["selector-content"]}>
<List>
{props.items.map((item, i) => {
return (
<ListItem
className={styles["selector-item"]}
key={i}
title={item.title}
subTitle={item.subTitle}
onClick={() => {
props.onSelection?.([item.value]);
props.onClose?.();
}}
></ListItem>
);
})}
</List>
</div>
</div>
);
}