diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 26716150631..13105e8433c 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -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"; @@ -404,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 (
@@ -485,10 +487,28 @@ export function ChatActions(props: { /> setShowModelSelector(true)} text={currentModel} icon={} /> + + {showModelSelector && ( + ({ + 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]); + }} + /> + )}
); } diff --git a/app/components/exporter.module.scss b/app/components/exporter.module.scss index 3fde363f009..c2046ffc09d 100644 --- a/app/components/exporter.module.scss +++ b/app/components/exporter.module.scss @@ -186,7 +186,7 @@ box-shadow: var(--card-shadow); border: var(--border-in-light); - * { + *:not(li) { overflow: hidden; } } diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 7765b77aa1a..f9d86a552c6 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -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: () => , @@ -369,6 +379,7 @@ export function ImagePreviewer(props: { const previewRef = useRef(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`; diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 4db5f573b66..3168641c733 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -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(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) { diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index d2ddb7df83e..6e8b64e81ec 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -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); + } + } + } + } +} diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index e02051c01b7..b9680912388 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -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 ( -
+
{props.icon &&
{props.icon}
}
@@ -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 ( -
+
{props.title}
-
- +
+
setMax(!isMax)} + > + {isMax ? : } +
+
+ +
@@ -394,3 +419,54 @@ export function showPrompt(content: any, value = "", rows = 3) { ); }); } + +export function showImageModal(img: string) { + showModal({ + title: Locale.Export.Image.Modal, + children: ( +
+ preview +
+ ), + }); +} + +export function Selector(props: { + items: Array<{ + title: string; + subTitle?: string; + value: T; + }>; + onSelection?: (selection: T[]) => void; + onClose?: () => void; + multiple?: boolean; +}) { + return ( +
props.onClose?.()}> +
+ + {props.items.map((item, i) => { + return ( + { + props.onSelection?.([item.value]); + props.onClose?.(); + }} + > + ); + })} + +
+
+ ); +} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index cb0cbbb17b9..07e87cbe655 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -86,6 +86,10 @@ const cn = { Select: "选取", Preview: "预览", }, + Image: { + Toast: "正在生成截图", + Modal: "长按或右键保存图片", + }, }, Select: { Search: "搜索消息", diff --git a/app/locales/en.ts b/app/locales/en.ts index 11b8b157282..c744fd01314 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -87,6 +87,10 @@ const en: LocaleType = { Select: "Select", Preview: "Preview", }, + Image: { + Toast: "Capturing Image...", + Modal: "Long press or right click to save image", + }, }, Select: { Search: "Search",