diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 26716150..13105e84 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 3fde363f..c2046ffc 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 7765b77a..f9d86a55 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 4db5f573..3168641c 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 d2ddb7df..6e8b64e8 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 e02051c0..b9680912 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: (
+
+
+
+ ),
+ });
+}
+
+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 cb0cbbb1..07e87cbe 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 11b8b157..c744fd01 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",