- {props.model?.startsWith("gpt-4") ? (
+ {props.model?.startsWith("gpt-4") ||
+ props.model?.startsWith("chatgpt-4o") ? (
) : (
diff --git a/app/components/error.tsx b/app/components/error.tsx
index c90997d11..4fcf759c1 100644
--- a/app/components/error.tsx
+++ b/app/components/error.tsx
@@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant";
import Locale from "../locales";
import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync";
+import { useChatStore } from "../store/chat";
interface IErrorBoundaryState {
hasError: boolean;
@@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component
{
try {
useSyncStore.getState().export();
} finally {
- localStorage.clear();
- location.reload();
+ useChatStore.getState().clearAllData();
}
}
diff --git a/app/components/home.tsx b/app/components/home.tsx
index 095cc6dd2..465ad0f1e 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -59,6 +59,17 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => ,
});
+const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
+ loading: () => ,
+});
+
+const SearchChat = dynamic(
+ async () => (await import("./search-chat")).SearchChatPage,
+ {
+ loading: () => ,
+ },
+);
+
const Sd = dynamic(async () => (await import("./sd")).Sd, {
loading: () => ,
});
@@ -174,6 +185,8 @@ function Screen() {
} />
} />
} />
+ } />
+ } />
} />
} />
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index 5515164c4..dc11c572d 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -8,14 +8,20 @@ import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
import { copyToClipboard, useWindowSize } from "../utils";
import mermaid from "mermaid";
-
+import Locale from "../locales";
import LoadingIcon from "../icons/three-dots.svg";
+import ReloadButtonIcon from "../icons/reload.svg";
import React from "react";
import { useDebouncedCallback } from "use-debounce";
import { showImageModal, FullScreen } from "./ui-lib";
-import { ArtifactsShareButton, HTMLPreview } from "./artifacts";
-import { Plugin } from "../constant";
+import {
+ ArtifactsShareButton,
+ HTMLPreview,
+ HTMLPreviewHander,
+} from "./artifacts";
import { useChatStore } from "../store";
+import { IconButton } from "./button";
+
export function Mermaid(props: { code: string }) {
const ref = useRef(null);
const [hasError, setHasError] = useState(false);
@@ -64,13 +70,12 @@ export function Mermaid(props: { code: string }) {
export function PreCode(props: { children: any }) {
const ref = useRef(null);
- const refText = ref.current?.innerText;
+ const previewRef = useRef(null);
const [mermaidCode, setMermaidCode] = useState("");
const [htmlCode, setHtmlCode] = useState("");
const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
- const plugins = session.mask?.plugin;
const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return;
@@ -79,6 +84,7 @@ export function PreCode(props: { children: any }) {
setMermaidCode((mermaidDom as HTMLElement).innerText);
}
const htmlDom = ref.current.querySelector("code.language-html");
+ const refText = ref.current.querySelector("code")?.innerText;
if (htmlDom) {
setHtmlCode((htmlDom as HTMLElement).innerText);
} else if (refText?.startsWith(" {
- setTimeout(renderArtifacts, 1);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [refText]);
-
- const enableArtifacts = useMemo(
- () => plugins?.includes(Plugin.Artifacts),
- [plugins],
- );
+ const enableArtifacts = session.mask?.enableArtifacts !== false;
//Wrap the paragraph for plain-text
useEffect(() => {
@@ -119,6 +117,7 @@ export function PreCode(props: { children: any }) {
codeElement.style.whiteSpace = "pre-wrap";
}
});
+ setTimeout(renderArtifacts, 1);
}
}, []);
@@ -145,7 +144,15 @@ export function PreCode(props: { children: any }) {
style={{ position: "absolute", right: 20, top: 10 }}
getCode={() => htmlCode}
/>
+ }
+ shadow
+ onClick={() => previewRef.current?.reload()}
+ />
(null);
const [collapsed, setCollapsed] = useState(true);
const [showToggle, setShowToggle] = useState(false);
@@ -175,6 +182,7 @@ function CustomCode(props: { children: any }) {
return (
<>
{props.children}
- {showToggle && collapsed && (
-
-
-
- )}
+ {showToggle && collapsed && (
+
+
+
+ )}
>
);
}
diff --git a/app/components/mask.tsx b/app/components/mask.tsx
index 8c17a544a..ee6c7da97 100644
--- a/app/components/mask.tsx
+++ b/app/components/mask.tsx
@@ -167,6 +167,22 @@ export function MaskConfig(props: {
>
+
+ {
+ props.updateMask((mask) => {
+ mask.enableArtifacts = e.currentTarget.checked;
+ });
+ }}
+ >
+
+
{!props.shouldSyncFromGlobal ? (
(
- () => localStorage.getItem("Mask-language") as Lang | undefined,
- );
- useEffect(() => {
- if (filterLang) {
- localStorage.setItem("Mask-language", filterLang);
- } else {
- localStorage.removeItem("Mask-language");
- }
- }, [filterLang]);
+ const filterLang = maskStore.language;
const allMasks = maskStore
.getAll()
@@ -526,9 +533,9 @@ export function MaskPage() {
onChange={(e) => {
const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) {
- setFilterLang(undefined);
+ maskStore.setLanguage(undefined);
} else {
- setFilterLang(value as Lang);
+ maskStore.setLanguage(value as Lang);
}
}}
>
diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss
new file mode 100644
index 000000000..a179e0a07
--- /dev/null
+++ b/app/components/plugin.module.scss
@@ -0,0 +1,16 @@
+.plugin-title {
+ font-weight: bolder;
+ font-size: 16px;
+ margin: 10px 0;
+}
+.plugin-content {
+ font-size: 14px;
+ font-family: inherit;
+ pre code {
+ max-height: 240px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ min-width: 300px;
+ }
+}
+
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
new file mode 100644
index 000000000..6f0b37107
--- /dev/null
+++ b/app/components/plugin.tsx
@@ -0,0 +1,393 @@
+import { useDebouncedCallback } from "use-debounce";
+import OpenAPIClientAxios from "openapi-client-axios";
+import yaml from "js-yaml";
+import { PLUGINS_REPO_URL } from "../constant";
+import { IconButton } from "./button";
+import { ErrorBoundary } from "./error";
+
+import styles from "./mask.module.scss";
+import pluginStyles from "./plugin.module.scss";
+
+import EditIcon from "../icons/edit.svg";
+import AddIcon from "../icons/add.svg";
+import CloseIcon from "../icons/close.svg";
+import DeleteIcon from "../icons/delete.svg";
+import EyeIcon from "../icons/eye.svg";
+import ConfirmIcon from "../icons/confirm.svg";
+import ReloadIcon from "../icons/reload.svg";
+import GithubIcon from "../icons/github.svg";
+
+import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
+import {
+ PasswordInput,
+ List,
+ ListItem,
+ Modal,
+ showConfirm,
+ showToast,
+} from "./ui-lib";
+import Locale from "../locales";
+import { useNavigate } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { getClientConfig } from "../config/client";
+
+export function PluginPage() {
+ const navigate = useNavigate();
+ const pluginStore = usePluginStore();
+
+ const allPlugins = pluginStore.getAll();
+ const [searchPlugins, setSearchPlugins] = useState([]);
+ const [searchText, setSearchText] = useState("");
+ const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
+
+ // refactored already, now it accurate
+ const onSearch = (text: string) => {
+ setSearchText(text);
+ if (text.length > 0) {
+ const result = allPlugins.filter(
+ (m) => m?.title.toLowerCase().includes(text.toLowerCase()),
+ );
+ setSearchPlugins(result);
+ } else {
+ setSearchPlugins(allPlugins);
+ }
+ };
+
+ const [editingPluginId, setEditingPluginId] = useState();
+ const editingPlugin = pluginStore.get(editingPluginId);
+ const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
+ const closePluginModal = () => setEditingPluginId(undefined);
+
+ const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
+ const content = e.target.innerText;
+ try {
+ const api = new OpenAPIClientAxios({
+ definition: yaml.load(content) as any,
+ });
+ api
+ .init()
+ .then(() => {
+ if (content != editingPlugin.content) {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.content = content;
+ const tool = FunctionToolService.add(plugin, true);
+ plugin.title = tool.api.definition.info.title;
+ plugin.version = tool.api.definition.info.version;
+ });
+ }
+ })
+ .catch((e) => {
+ console.error(e);
+ showToast(Locale.Plugin.EditModal.Error);
+ });
+ } catch (e) {
+ console.error(e);
+ showToast(Locale.Plugin.EditModal.Error);
+ }
+ }, 100).bind(null, editingPlugin);
+
+ const [loadUrl, setLoadUrl] = useState("");
+ const loadFromUrl = (loadUrl: string) =>
+ fetch(loadUrl)
+ .catch((e) => {
+ const p = new URL(loadUrl);
+ return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
+ headers: {
+ "X-Base-URL": p.origin,
+ },
+ });
+ })
+ .then((res) => res.text())
+ .then((content) => {
+ try {
+ return JSON.stringify(JSON.parse(content), null, " ");
+ } catch (e) {
+ return content;
+ }
+ })
+ .then((content) => {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.content = content;
+ const tool = FunctionToolService.add(plugin, true);
+ plugin.title = tool.api.definition.info.title;
+ plugin.version = tool.api.definition.info.version;
+ });
+ })
+ .catch((e) => {
+ showToast(Locale.Plugin.EditModal.Error);
+ });
+
+ return (
+
+
+
+
+
+ {Locale.Plugin.Page.Title}
+
+
+ {Locale.Plugin.Page.SubTitle(plugins.length)}
+
+
+
+
+
+
+ }
+ bordered
+ onClick={() => navigate(-1)}
+ />
+
+
+
+
+
+
+ onSearch(e.currentTarget.value)}
+ />
+
+ }
+ text={Locale.Plugin.Page.Create}
+ bordered
+ onClick={() => {
+ const createdPlugin = pluginStore.create();
+ setEditingPluginId(createdPlugin.id);
+ }}
+ />
+
+
+
+ {plugins.length == 0 && (
+
+ )}
+ {plugins.map((m) => (
+
+
+
+
+
+ {m.title}@{m.version}
+
+
+ {Locale.Plugin.Item.Info(
+ FunctionToolService.add(m).length,
+ )}
+
+
+
+
+ {m.builtin ? (
+ }
+ text={Locale.Plugin.Item.View}
+ onClick={() => setEditingPluginId(m.id)}
+ />
+ ) : (
+ }
+ text={Locale.Plugin.Item.Edit}
+ onClick={() => setEditingPluginId(m.id)}
+ />
+ )}
+ {!m.builtin && (
+ }
+ text={Locale.Plugin.Item.Delete}
+ onClick={async () => {
+ if (
+ await showConfirm(Locale.Plugin.Item.DeleteConfirm)
+ ) {
+ pluginStore.delete(m.id);
+ }
+ }}
+ />
+ )}
+
+
+ ))}
+
+
+
+
+ {editingPlugin && (
+
+
}
+ text={Locale.UI.Confirm}
+ key="export"
+ bordered
+ onClick={() => setEditingPluginId("")}
+ />,
+ ]}
+ >
+
+
+
+
+ {["bearer", "basic", "custom"].includes(
+ editingPlugin.authType as string,
+ ) && (
+
+
+
+ )}
+ {editingPlugin.authType == "custom" && (
+
+ {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.authHeader = e.target.value;
+ });
+ }}
+ >
+
+ )}
+ {["bearer", "basic", "custom"].includes(
+ editingPlugin.authType as string,
+ ) && (
+
+ {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.authToken = e.currentTarget.value;
+ });
+ }}
+ >
+
+ )}
+ {!getClientConfig()?.isApp && (
+
+ {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.usingProxy = e.currentTarget.checked;
+ });
+ }}
+ >
+
+ )}
+
+
+
+
+ setLoadUrl(e.currentTarget.value)}
+ >
+ }
+ text={Locale.Plugin.EditModal.Load}
+ bordered
+ onClick={() => loadFromUrl(loadUrl)}
+ />
+
+
+
+
+
+
+
+ }
+ >
+ {editingPluginTool?.tools.map((tool, index) => (
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/app/components/search-chat.tsx b/app/components/search-chat.tsx
new file mode 100644
index 000000000..7178865f5
--- /dev/null
+++ b/app/components/search-chat.tsx
@@ -0,0 +1,167 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import { ErrorBoundary } from "./error";
+import styles from "./mask.module.scss";
+import { useNavigate } from "react-router-dom";
+import { IconButton } from "./button";
+import CloseIcon from "../icons/close.svg";
+import EyeIcon from "../icons/eye.svg";
+import Locale from "../locales";
+import { Path } from "../constant";
+
+import { useChatStore } from "../store";
+
+type Item = {
+ id: number;
+ name: string;
+ content: string;
+};
+export function SearchChatPage() {
+ const navigate = useNavigate();
+
+ const chatStore = useChatStore();
+
+ const sessions = chatStore.sessions;
+ const selectSession = chatStore.selectSession;
+
+ const [searchResults, setSearchResults] = useState