From 3a10f58b28ea3e485bc5abb45d96d18f0e7c3aee Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 23 Jul 2024 14:33:41 +0800 Subject: [PATCH 01/27] add preview html code --- app/components/markdown.tsx | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 1afd7de3b45..e870be87755 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -60,21 +60,49 @@ export function Mermaid(props: { code: string }) { ); } +export function HTMLPreview(props: { code: string }) { + const ref = useRef(null); + + return ( +
console.log("click")} + > + +
+ ); +} + export function PreCode(props: { children: any }) { const ref = useRef(null); const refText = ref.current?.innerText; const [mermaidCode, setMermaidCode] = useState(""); + const [htmlCode, setHtmlCode] = useState(""); - const renderMermaid = useDebouncedCallback(() => { + const renderArtifacts = useDebouncedCallback(() => { if (!ref.current) return; const mermaidDom = ref.current.querySelector("code.language-mermaid"); if (mermaidDom) { setMermaidCode((mermaidDom as HTMLElement).innerText); } + const htmlDom = ref.current.querySelector("code.language-html"); + if (htmlDom) { + setHtmlCode((htmlDom as HTMLElement).innerText); + } }, 600); useEffect(() => { - setTimeout(renderMermaid, 1); + setTimeout(renderArtifacts, 1); // eslint-disable-next-line react-hooks/exhaustive-deps }, [refText]); @@ -83,6 +111,7 @@ export function PreCode(props: { children: any }) { {mermaidCode.length > 0 && ( )} + {htmlCode.length > 0 && }
         
Date: Tue, 23 Jul 2024 14:39:31 +0800
Subject: [PATCH 02/27] add preview html code

---
 app/components/markdown.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index e870be87755..989e2889be5 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -77,7 +77,7 @@ export function HTMLPreview(props: { code: string }) {
         frameBorder={0}
         sandbox="allow-scripts"
         style={{ width: "100%", height: 400 }}
-        srcdoc={props.code}
+        srcDoc={props.code}
       >
     
   );

From 4199e17da0a9ab917fada1bdb09547bc5af899bf Mon Sep 17 00:00:00 2001
From: lloydzhou 
Date: Tue, 23 Jul 2024 17:44:15 +0800
Subject: [PATCH 03/27] auto height for html preview

---
 app/components/markdown.tsx | 46 +++++++++++++++++++++++++++++--------
 1 file changed, 37 insertions(+), 9 deletions(-)

diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index 989e2889be5..805af7b8b1a 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -13,6 +13,7 @@ import LoadingIcon from "../icons/three-dots.svg";
 import React from "react";
 import { useDebouncedCallback } from "use-debounce";
 import { showImageModal } from "./ui-lib";
+import { nanoid } from "nanoid";
 
 export function Mermaid(props: { code: string }) {
   const ref = useRef(null);
@@ -62,6 +63,31 @@ export function Mermaid(props: { code: string }) {
 
 export function HTMLPreview(props: { code: string }) {
   const ref = useRef(null);
+  const frameId = useRef(nanoid());
+  const [height, setHeight] = useState(600);
+  /*
+   * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
+   * 1. using srcdoc
+   * 2. using src with dataurl:
+   *    easy to share
+   *    length limit (Data URIs cannot be larger than 32,768 characters.)
+   */
+
+  useEffect(() => {
+    window.addEventListener("message", (e) => {
+      const { id, height } = e.data;
+      if (id == frameId.current) {
+        console.log("setHeight", height);
+        if (height < 600) {
+          setHeight(height + 40);
+        }
+      }
+    });
+  }, []);
+
+  const script = encodeURIComponent(
+    ``,
+  );
 
   return (
     
console.log("click")} + onClick={(e) => e.stopPropapation()} >
); @@ -108,10 +136,6 @@ export function PreCode(props: { children: any }) { return ( <> - {mermaidCode.length > 0 && ( - - )} - {htmlCode.length > 0 && }
         
         {props.children}
       
+ {mermaidCode.length > 0 && ( + + )} + {htmlCode.length > 0 && } ); } From fb60fbb217ca9764ce7f389919a4b5baaefb1b1b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 23 Jul 2024 17:51:44 +0800 Subject: [PATCH 04/27] auto height for html preview --- app/components/markdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 805af7b8b1a..a527a672eef 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -96,7 +96,7 @@ export function HTMLPreview(props: { code: string }) { cursor: "pointer", overflow: "auto", }} - onClick={(e) => e.stopPropapation()} + onClick={(e) => e.stopPropagation()} > + ); +} + +export function ArtifactShareButton({ getCode, id, style }) { + const [name, setName] = useState(id); + const [show, setShow] = useState(false); + const shareUrl = useMemo(() => + [location.origin, "#", Path.Artifact, "/", name].join(""), + ); + const upload = (code) => + fetch(ApiPath.Artifact, { + method: "POST", + body: getCode(), + }) + .then((res) => res.json()) + .then(({ id }) => { + if (id) { + setShow(true); + return setName(id); + } + throw Error(); + }) + .catch((e) => { + showToast(Locale.Export.Artifact.Error); + }); + return ( + <> +
+ } + bordered + title={Locale.Export.Artifact.Title} + onClick={() => { + upload(getCode()); + }} + /> +
+ {show && ( +
+ setShow(false)} + actions={[ + } + bordered + text={Locale.Export.Download} + onClick={() => { + downloadAs(getCode(), `${id}.html`).then(() => + setShow(false), + ); + }} + />, + } + bordered + text={Locale.Chat.Actions.Copy} + onClick={() => { + copyToClipboard(shareUrl).then(() => setShow(false)); + }} + />, + ]} + > + + +
+ )} + + ); +} + +export function Artifact() { + const { id } = useParams(); + const [code, setCode] = useState(""); + const { height } = useWindowSize(); + + useEffect(() => { + if (id) { + fetch(`${ApiPath.Artifact}?id=${id}`) + .then((res) => res.text()) + .then(setCode); + } + }, [id]); + + return ( +
+
+ + code} /> +
+ {code ? ( + + ) : ( + + )} +
+ ); +} diff --git a/app/components/home.tsx b/app/components/home.tsx index e127c65f8fd..11000f98533 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) { ); } +const Artifact = dynamic(async () => (await import("./artifact")).Artifact, { + loading: () => , +}); + const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); @@ -125,6 +129,7 @@ const loadAsyncGoogleFont = () => { function Screen() { const config = useAppConfig(); const location = useLocation(); + const isArtifact = location.pathname.includes(Path.Artifact); const isHome = location.pathname === Path.Home; const isAuth = location.pathname === Path.Auth; const isMobileScreen = useMobileScreen(); @@ -135,6 +140,14 @@ function Screen() { loadAsyncGoogleFont(); }, []); + if (isArtifact) { + return ( + + } /> + + ); + } + return (
(null); @@ -61,56 +61,6 @@ export function Mermaid(props: { code: string }) { ); } -export function HTMLPreview(props: { code: string }) { - const ref = useRef(null); - const frameId = useRef(nanoid()); - const [height, setHeight] = useState(600); - /* - * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an - * 1. using srcdoc - * 2. using src with dataurl: - * easy to share - * length limit (Data URIs cannot be larger than 32,768 characters.) - */ - - useEffect(() => { - window.addEventListener("message", (e) => { - const { id, height } = e.data; - if (id == frameId.current) { - console.log("setHeight", height); - if (height < 600) { - setHeight(height + 40); - } - } - }); - }, []); - - const script = encodeURIComponent( - ``, - ); - - return ( -
e.stopPropagation()} - > - -
- ); -} - export function PreCode(props: { children: any }) { const ref = useRef(null); const refText = ref.current?.innerText; @@ -151,7 +101,22 @@ export function PreCode(props: { children: any }) { {mermaidCode.length > 0 && ( )} - {htmlCode.length > 0 && } + {htmlCode.length > 0 && ( +
e.stopPropagation()} + > + htmlCode} + /> + +
+ )} ); } diff --git a/app/config/server.ts b/app/config/server.ts index d3ff9651d74..b87cba2fff1 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -158,6 +158,10 @@ export const getServerSideConfig = () => { alibabaUrl: process.env.ALIBABA_URL, alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), + cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID, + cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID, + cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY), + gtmId: process.env.GTM_ID, needCode: ACCESS_CODES.size > 0, diff --git a/app/constant.ts b/app/constant.ts index 2f4acb10439..3546cc9be9c 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -8,6 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; +export const PREVIEW_URL = "https://app.nextchat.dev"; export const DEFAULT_API_HOST = "https://api.nextchat.dev"; export const OPENAI_BASE_URL = "https://api.openai.com"; export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; @@ -31,6 +32,7 @@ export enum Path { NewChat = "/new-chat", Masks = "/masks", Auth = "/auth", + Artifact = "/artifact", } export enum ApiPath { @@ -42,6 +44,7 @@ export enum ApiPath { Baidu = "/api/baidu", ByteDance = "/api/bytedance", Alibaba = "/api/alibaba", + Artifact = "/api/artifact", } export enum SlotID { diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 43ea6763332..f08480f7cff 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -104,6 +104,10 @@ const cn = { Toast: "正在生成截图", Modal: "长按或右键保存图片", }, + Artifact: { + Title: "分享页面", + Error: "分享失败", + }, }, Select: { Search: "搜索消息", diff --git a/app/locales/en.ts b/app/locales/en.ts index 94c737550ec..dd3b9e733f8 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -106,6 +106,10 @@ const en: LocaleType = { Toast: "Capturing Image...", Modal: "Long press or right click to save image", }, + Artifact: { + Title: "Share Artifact", + Error: "Share Error", + }, }, Select: { Search: "Search", From e31bec3aff673a23cc3b113d1a9d32d9dceb0a58 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 24 Jul 2024 20:36:11 +0800 Subject: [PATCH 08/27] save artifact content to cloudflare workers kv --- app/components/artifact.tsx | 77 ++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index e08a713dc46..dfcd450646c 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -17,10 +17,12 @@ export function HTMLPreview(props: { code: string; autoHeight?: boolean; height?: number; + onLoad?: (title?: string) => void; }) { const ref = useRef(null); const frameId = useRef(nanoid()); const [iframeHeight, setIframeHeight] = useState(600); + const [title, setTitle] = useState(""); /* * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an * 1. using srcdoc @@ -31,9 +33,9 @@ export function HTMLPreview(props: { useEffect(() => { window.addEventListener("message", (e) => { - const { id, height } = e.data; + const { id, height, title } = e.data; + setTitle(title); if (id == frameId.current) { - console.log("setHeight", height); setIframeHeight(height); } }); @@ -65,32 +67,34 @@ export function HTMLPreview(props: { style={{ width: "100%", height }} // src={`data:text/html,${encodeURIComponent(srcDoc)}`} srcDoc={srcDoc} + onLoad={(e) => props?.onLoad(title)} > ); } -export function ArtifactShareButton({ getCode, id, style }) { +export function ArtifactShareButton({ getCode, id, style, fileName }) { const [name, setName] = useState(id); const [show, setShow] = useState(false); const shareUrl = useMemo(() => [location.origin, "#", Path.Artifact, "/", name].join(""), ); const upload = (code) => - fetch(ApiPath.Artifact, { - method: "POST", - body: getCode(), - }) - .then((res) => res.json()) - .then(({ id }) => { - if (id) { - setShow(true); - return setName(id); - } - throw Error(); - }) - .catch((e) => { - showToast(Locale.Export.Artifact.Error); - }); + id + ? Promise.resolve({ id }) + : fetch(ApiPath.Artifact, { + method: "POST", + body: getCode(), + }) + .then((res) => res.json()) + .then(({ id }) => { + if (id) { + return { id }; + } + throw Error(); + }) + .catch((e) => { + showToast(Locale.Export.Artifact.Error); + }); return ( <>
@@ -99,7 +103,10 @@ export function ArtifactShareButton({ getCode, id, style }) { bordered title={Locale.Export.Artifact.Title} onClick={() => { - upload(getCode()); + upload(getCode()).then(({ id }) => { + setShow(true); + setName(id); + }); }} />
@@ -115,7 +122,7 @@ export function ArtifactShareButton({ getCode, id, style }) { bordered text={Locale.Export.Download} onClick={() => { - downloadAs(getCode(), `${id}.html`).then(() => + downloadAs(getCode(), `${fileName || name}.html`).then(() => setShow(false), ); }} @@ -146,6 +153,8 @@ export function ArtifactShareButton({ getCode, id, style }) { export function Artifact() { const { id } = useParams(); const [code, setCode] = useState(""); + const [loading, setLoading] = useState(true); + const [fileName, setFileName] = useState(""); const { height } = useWindowSize(); useEffect(() => { @@ -167,23 +176,29 @@ export function Artifact() { >
- - code} /> + + } shadow /> + +
NextChat Artifact
+ code} fileName={fileName} />
- {code ? ( - - ) : ( - + {loading && } + {code && ( + { + setFileName(title); + setLoading(false); + }} + /> )}
); From ab9f5382b280d3c99a62c93fac0c650f89802221 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 24 Jul 2024 20:51:33 +0800 Subject: [PATCH 09/27] fix typescript --- app/api/artifact/route.ts | 4 ++-- app/components/artifact.tsx | 35 ++++++++++++++++++++++++----------- app/components/home.tsx | 2 +- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/app/api/artifact/route.ts b/app/api/artifact/route.ts index 4accf7c09a0..f647f26b254 100644 --- a/app/api/artifact/route.ts +++ b/app/api/artifact/route.ts @@ -4,7 +4,7 @@ import { getServerSideConfig } from "@/app/config/server"; async function handle(req: NextRequest, res: NextResponse) { const serverConfig = getServerSideConfig(); - const storeUrl = (key) => + const storeUrl = (key: string) => `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}/values/${key}`; const storeHeaders = () => ({ Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, @@ -32,7 +32,7 @@ async function handle(req: NextRequest, res: NextResponse) { } if (req.method === "GET") { const id = req?.nextUrl?.searchParams?.get("id"); - const res = await fetch(storeUrl(id), { + const res = await fetch(storeUrl(id as string), { headers: storeHeaders(), method: "GET", }); diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index dfcd450646c..24e62dfb1a8 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -67,23 +67,34 @@ export function HTMLPreview(props: { style={{ width: "100%", height }} // src={`data:text/html,${encodeURIComponent(srcDoc)}`} srcDoc={srcDoc} - onLoad={(e) => props?.onLoad(title)} + onLoad={(e) => props?.onLoad && props?.onLoad(title)} > ); } -export function ArtifactShareButton({ getCode, id, style, fileName }) { +export function ArtifactShareButton({ + getCode, + id, + style, + fileName, +}: { + getCode: () => string; + id?: string; + style?: any; + fileName?: string; +}) { const [name, setName] = useState(id); const [show, setShow] = useState(false); - const shareUrl = useMemo(() => - [location.origin, "#", Path.Artifact, "/", name].join(""), + const shareUrl = useMemo( + () => [location.origin, "#", Path.Artifact, "/", name].join(""), + [name], ); - const upload = (code) => + const upload = (code: string) => id ? Promise.resolve({ id }) : fetch(ApiPath.Artifact, { method: "POST", - body: getCode(), + body: code, }) .then((res) => res.json()) .then(({ id }) => { @@ -103,9 +114,11 @@ export function ArtifactShareButton({ getCode, id, style, fileName }) { bordered title={Locale.Export.Artifact.Title} onClick={() => { - upload(getCode()).then(({ id }) => { - setShow(true); - setName(id); + upload(getCode()).then((res) => { + if (res?.id) { + setShow(true); + setName(res?.id); + } }); }} /> @@ -168,7 +181,7 @@ export function Artifact() { return (
{ - setFileName(title); + setFileName(title as string); setLoading(false); }} /> diff --git a/app/components/home.tsx b/app/components/home.tsx index 11000f98533..4874480449f 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -143,7 +143,7 @@ function Screen() { if (isArtifact) { return ( - } /> + } /> ); } From b4bf11d648c4c9b9f390272b38b05e8fe193a24f Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 12:49:19 +0800 Subject: [PATCH 10/27] add loading icon when upload artifact content --- app/components/artifact.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index 24e62dfb1a8..b888027644f 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -7,6 +7,7 @@ import ExportIcon from "../icons/share.svg"; import CopyIcon from "../icons/copy.svg"; import DownloadIcon from "../icons/download.svg"; import GithubIcon from "../icons/github.svg"; +import LoadingButtonIcon from "../icons/loading.svg"; import Locale from "../locales"; import { Modal, showToast } from "./ui-lib"; import { copyToClipboard, downloadAs } from "../utils"; @@ -83,6 +84,7 @@ export function ArtifactShareButton({ style?: any; fileName?: string; }) { + const [loading, setLoading] = useState(false); const [name, setName] = useState(id); const [show, setShow] = useState(false); const shareUrl = useMemo( @@ -110,16 +112,19 @@ export function ArtifactShareButton({ <>
} + icon={loading ? : } bordered title={Locale.Export.Artifact.Title} onClick={() => { - upload(getCode()).then((res) => { - if (res?.id) { - setShow(true); - setName(res?.id); - } - }); + setLoading(true); + upload(getCode()) + .then((res) => { + if (res?.id) { + setShow(true); + setName(res?.id); + } + }) + .finally(() => setLoading(false)); }} />
From 044116c14c6bea72ca373986b7bebad888df52f2 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 13:29:39 +0800 Subject: [PATCH 11/27] add plugin selector on chat --- app/components/chat.tsx | 30 ++++++++++++++++++++++++++++++ app/components/ui-lib.tsx | 7 +++++-- app/constant.ts | 4 ++++ app/locales/cn.ts | 1 + app/locales/en.ts | 1 + app/store/mask.ts | 3 ++- 6 files changed, 43 insertions(+), 3 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 08bcd04fd56..b96609550b0 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -37,6 +37,7 @@ import AutoIcon from "../icons/auto.svg"; import BottomIcon from "../icons/bottom.svg"; import StopIcon from "../icons/pause.svg"; import RobotIcon from "../icons/robot.svg"; +import PluginIcon from "../icons/plugin.svg"; import { ChatMessage, @@ -89,6 +90,7 @@ import { REQUEST_TIMEOUT_MS, UNFINISHED_INPUT, ServiceProvider, + Plugin, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; @@ -476,6 +478,7 @@ export function ChatActions(props: { return model?.displayName ?? ""; }, [models, currentModel, currentProviderName]); const [showModelSelector, setShowModelSelector] = useState(false); + const [showPluginSelector, setShowPluginSelector] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false); useEffect(() => { @@ -620,6 +623,33 @@ export function ChatActions(props: { }} /> )} + + setShowPluginSelector(true)} + text={Locale.Plugin.Name} + icon={} + /> + {showPluginSelector && ( + setShowPluginSelector(false)} + onSelection={(s) => { + if (s.length === 0) return; + const plugin = s[0]; + chatStore.updateCurrentSession((session) => { + session.mask.plugin = s; + }); + showToast(plugin); + }} + /> + )}
); } diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index da700c0fb7c..186bce8d6f9 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -443,7 +443,7 @@ export function Selector(props: { subTitle?: string; value: T; }>; - defaultSelectedValue?: T; + defaultSelectedValue?: T[] | T; onSelection?: (selection: T[]) => void; onClose?: () => void; multiple?: boolean; @@ -453,7 +453,10 @@ export function Selector(props: {
{props.items.map((item, i) => { - const selected = props.defaultSelectedValue === item.value; + // @ts-ignore + const selected = props.multiple + ? props.defaultSelectedValue?.includes(item.value) + : props.defaultSelectedValue === item.value; return ( Date: Thu, 25 Jul 2024 13:34:59 +0800 Subject: [PATCH 12/27] update --- app/components/markdown.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index d5728bf5e28..fa8d12c407c 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -76,6 +76,8 @@ export function PreCode(props: { children: any }) { const htmlDom = ref.current.querySelector("code.language-html"); if (htmlDom) { setHtmlCode((htmlDom as HTMLElement).innerText); + } else if (refText?.startsWith(" Date: Thu, 25 Jul 2024 13:48:21 +0800 Subject: [PATCH 13/27] hotfix: ts check --- app/components/ui-lib.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 186bce8d6f9..8ac0f8e9dda 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -453,9 +453,9 @@ export function Selector(props: {
{props.items.map((item, i) => { - // @ts-ignore const selected = props.multiple - ? props.defaultSelectedValue?.includes(item.value) + ? // @ts-ignore + props.defaultSelectedValue?.includes(item.value) : props.defaultSelectedValue === item.value; return ( Date: Thu, 25 Jul 2024 14:15:16 +0800 Subject: [PATCH 14/27] hotfix: auto set height --- app/components/artifact.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index b888027644f..78e92820001 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -37,10 +37,12 @@ export function HTMLPreview(props: { const { id, height, title } = e.data; setTitle(title); if (id == frameId.current) { - setIframeHeight(height); + if (height != iframeHeight + 40) { + setIframeHeight(height); + } } }); - }, []); + }, [iframeHeight]); const height = useMemo(() => { const parentHeight = props.height || 600; From 763fc89b2983471d4133837e3c59faae38fd0602 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 15:10:17 +0800 Subject: [PATCH 15/27] add fullscreen button on artifact component --- app/components/markdown.tsx | 22 ++++++++++---------- app/components/ui-lib.tsx | 40 ++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index fa8d12c407c..0918e7c5b07 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -6,13 +6,13 @@ import RehypeKatex from "rehype-katex"; import RemarkGfm from "remark-gfm"; import RehypeHighlight from "rehype-highlight"; import { useRef, useState, RefObject, useEffect, useMemo } from "react"; -import { copyToClipboard } from "../utils"; +import { copyToClipboard, useWindowSize } from "../utils"; import mermaid from "mermaid"; import LoadingIcon from "../icons/three-dots.svg"; import React from "react"; import { useDebouncedCallback } from "use-debounce"; -import { showImageModal } from "./ui-lib"; +import { showImageModal, FullScreen } from "./ui-lib"; import { ArtifactShareButton, HTMLPreview } from "./artifact"; export function Mermaid(props: { code: string }) { @@ -66,6 +66,7 @@ export function PreCode(props: { children: any }) { const refText = ref.current?.innerText; const [mermaidCode, setMermaidCode] = useState(""); const [htmlCode, setHtmlCode] = useState(""); + const { height } = useWindowSize(); const renderArtifacts = useDebouncedCallback(() => { if (!ref.current) return; @@ -104,20 +105,17 @@ export function PreCode(props: { children: any }) { )} {htmlCode.length > 0 && ( -
e.stopPropagation()} - > + htmlCode} /> - -
+ + )} ); diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 8ac0f8e9dda..77223f5db5b 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -13,7 +13,13 @@ import MinIcon from "../icons/min.svg"; import Locale from "../locales"; import { createRoot } from "react-dom/client"; -import React, { HTMLProps, useEffect, useState } from "react"; +import React, { + HTMLProps, + useEffect, + useState, + useCallback, + useRef, +} from "react"; import { IconButton } from "./button"; export function Popover(props: { @@ -488,3 +494,35 @@ export function Selector(props: {
); } + +export function FullScreen(props: any) { + const { children, right = 10, top = 10, ...rest } = props; + const ref = useRef(); + const [fullScreen, setFullScreen] = useState(false); + const toggleFullscreen = useCallback(() => { + if (!document.fullscreenElement) { + ref.current?.requestFullscreen(); + } else { + document.exitFullscreen(); + } + }, []); + useEffect(() => { + document.addEventListener("fullscreenchange", (e) => { + if (e.target === ref.current) { + setFullScreen(!!document.fullscreenElement); + } + }); + }, []); + return ( +
+
+ : } + onClick={toggleFullscreen} + bordered + /> +
+ {children} +
+ ); +} From 7c1bc1f1a1b071a12d9045612be3bddeb283beb4 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 15:27:44 +0800 Subject: [PATCH 16/27] hotfix: auto set height --- app/components/artifact.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index 78e92820001..c06573b091e 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -37,9 +37,7 @@ export function HTMLPreview(props: { const { id, height, title } = e.data; setTitle(title); if (id == frameId.current) { - if (height != iframeHeight + 40) { - setIframeHeight(height); - } + setIframeHeight(height); } }); }, [iframeHeight]); @@ -47,7 +45,9 @@ export function HTMLPreview(props: { const height = useMemo(() => { const parentHeight = props.height || 600; if (props.autoHeight !== false) { - return iframeHeight > parentHeight ? parentHeight : iframeHeight + 40; + return iframeHeight + 40 > parentHeight + ? parentHeight + : iframeHeight + 40; } else { return parentHeight; } From d8afd1af88038d859b322fd55773c31d2cb6c4db Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 16:56:08 +0800 Subject: [PATCH 17/27] add expiration_ttl for kv storage --- app/api/artifact/route.ts | 27 +++++++++++++++++++++------ app/components/artifact.tsx | 11 ++++++++++- app/config/server.ts | 1 + 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/api/artifact/route.ts b/app/api/artifact/route.ts index f647f26b254..214fe76a182 100644 --- a/app/api/artifact/route.ts +++ b/app/api/artifact/route.ts @@ -4,18 +4,33 @@ import { getServerSideConfig } from "@/app/config/server"; async function handle(req: NextRequest, res: NextResponse) { const serverConfig = getServerSideConfig(); - const storeUrl = (key: string) => - `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}/values/${key}`; + const storeUrl = () => + `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`; const storeHeaders = () => ({ Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, }); if (req.method === "POST") { const clonedBody = await req.text(); const hashedCode = md5.hash(clonedBody).trim(); - const res = await fetch(storeUrl(hashedCode), { - headers: storeHeaders(), + const body = { + key: hashedCode, + value: clonedBody, + }; + try { + const ttl = parseInt(serverConfig.cloudflareKVTTL); + if (ttl > 60) { + body["expiration_ttl"] = ttl; + } + } catch (e) { + console.error(e); + } + const res = await fetch(`${storeUrl()}/bulk`, { + headers: { + ...storeHeaders(), + "Content-Type": "application/json", + }, method: "PUT", - body: clonedBody, + body: JSON.stringify([body]), }); const result = await res.json(); console.log("save data", result); @@ -32,7 +47,7 @@ async function handle(req: NextRequest, res: NextResponse) { } if (req.method === "GET") { const id = req?.nextUrl?.searchParams?.get("id"); - const res = await fetch(storeUrl(id as string), { + const res = await fetch(`${storeUrl()}/values/${id}`, { headers: storeHeaders(), method: "GET", }); diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index c06573b091e..9258157f6c2 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -180,8 +180,17 @@ export function Artifact() { useEffect(() => { if (id) { fetch(`${ApiPath.Artifact}?id=${id}`) + .then((res) => { + if (res.status > 300) { + throw Error("can not get content"); + } + return res; + }) .then((res) => res.text()) - .then(setCode); + .then(setCode) + .catch((e) => { + showToast(Locale.Export.Artifact.Error); + }); } }, [id]); diff --git a/app/config/server.ts b/app/config/server.ts index b87cba2fff1..507aea2859d 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -161,6 +161,7 @@ export const getServerSideConfig = () => { cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID, cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID, cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY), + cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL, gtmId: process.env.GTM_ID, From 21ef9a4567ec4f61a4d0db26f0e23815bb0f7924 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Thu, 25 Jul 2024 17:37:21 +0800 Subject: [PATCH 18/27] feat: artifacts style --- .gitignore | 2 ++ app/components/artifact.module.scss | 12 +++++++++ app/components/artifact.tsx | 25 +++++++++--------- app/components/markdown.tsx | 4 +-- app/components/ui-lib.module.scss | 1 + app/components/ui-lib.tsx | 39 +++++++++++++++++++++-------- 6 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 app/components/artifact.module.scss diff --git a/.gitignore b/.gitignore index a24c6e047d5..2ff556f646e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ dev *.key *.key.pub + +masks.json diff --git a/app/components/artifact.module.scss b/app/components/artifact.module.scss new file mode 100644 index 00000000000..1e166f41846 --- /dev/null +++ b/app/components/artifact.module.scss @@ -0,0 +1,12 @@ +.artifact { + display: block; + width: 100%; + height: 100%; + position: relative; +} + +.artifact-iframe { + width: 100%; + border: var(--border-in-light); + border-radius: 6px; +} diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index c06573b091e..64a6ad652e5 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -13,6 +13,7 @@ import { Modal, showToast } from "./ui-lib"; import { copyToClipboard, downloadAs } from "../utils"; import { Path, ApiPath, REPO_URL } from "@/app/constant"; import { Loading } from "./home"; +import styles from "./artifact.module.scss"; export function HTMLPreview(props: { code: string; @@ -61,17 +62,22 @@ export function HTMLPreview(props: { return props.code + script; }, [props.code]); + const handleOnLoad = () => { + if (props?.onLoad) { + props.onLoad(title); + } + }; + return ( + onLoad={handleOnLoad} + /> ); } @@ -186,14 +192,7 @@ export function Artifact() { }, [id]); return ( -
+
)} {htmlCode.length > 0 && ( - + htmlCode} /> void; + onClick?: (e: React.MouseEvent) => void; }) { return (
(props: { onClose?: () => void; multiple?: boolean; }) { + const [selectedValues, setSelectedValues] = useState( + Array.isArray(props.defaultSelectedValue) + ? props.defaultSelectedValue + : props.defaultSelectedValue !== undefined + ? [props.defaultSelectedValue] + : [], + ); + + const handleSelection = ( + e: React.MouseEvent, + value: T, + ) => { + if (props.multiple) { + e.stopPropagation(); + const newSelectedValues = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + setSelectedValues(newSelectedValues); + props.onSelection?.(newSelectedValues); + } else { + setSelectedValues([value]); + props.onSelection?.([value]); + props.onClose?.(); + } + }; + return (
props.onClose?.()}>
{props.items.map((item, i) => { - const selected = props.multiple - ? // @ts-ignore - props.defaultSelectedValue?.includes(item.value) - : props.defaultSelectedValue === item.value; + const selected = selectedValues.includes(item.value); return ( { - props.onSelection?.([item.value]); - props.onClose?.(); - }} + onClick={(e) => handleSelection(e, item.value)} > {selected ? (
(props: {
); } - export function FullScreen(props: any) { const { children, right = 10, top = 10, ...rest } = props; const ref = useRef(); From 6a083b24c4caa8faaf3dad6cf5f9bb9054e50ef7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 19:22:18 +0800 Subject: [PATCH 19/27] fix typescript error --- app/api/artifact/route.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/api/artifact/route.ts b/app/api/artifact/route.ts index 214fe76a182..ab8485913b6 100644 --- a/app/api/artifact/route.ts +++ b/app/api/artifact/route.ts @@ -12,12 +12,16 @@ async function handle(req: NextRequest, res: NextResponse) { if (req.method === "POST") { const clonedBody = await req.text(); const hashedCode = md5.hash(clonedBody).trim(); - const body = { + const body: { + key: string; + value: string; + expiration_ttl?: Number; + } = { key: hashedCode, value: clonedBody, }; try { - const ttl = parseInt(serverConfig.cloudflareKVTTL); + const ttl = parseInt(serverConfig.cloudflareKVTTL as string); if (ttl > 60) { body["expiration_ttl"] = ttl; } From 556d563ba0f8f4b23c1244bfb9e4d14abd4dccb0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 19:31:16 +0800 Subject: [PATCH 20/27] update --- app/components/artifact.tsx | 10 +++++++--- app/components/ui-lib.tsx | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index 9258157f6c2..0c3ee84a854 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -33,14 +33,18 @@ export function HTMLPreview(props: { */ useEffect(() => { - window.addEventListener("message", (e) => { + const handleMessage = (e) => { const { id, height, title } = e.data; setTitle(title); if (id == frameId.current) { setIframeHeight(height); } - }); - }, [iframeHeight]); + }; + window.addEventListener("message", handleMessage); + return () => { + window.removeEventListener("message", handleMessage); + }; + }, []); const height = useMemo(() => { const parentHeight = props.height || 600; diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index a23be825179..aff524b1201 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -528,11 +528,15 @@ export function FullScreen(props: any) { } }, []); useEffect(() => { - document.addEventListener("fullscreenchange", (e) => { + const handleScreenChange = (e) => { if (e.target === ref.current) { setFullScreen(!!document.fullscreenElement); } - }); + }; + document.addEventListener("fullscreenchange", handleScreenChange); + return () => { + document.removeEventListener("fullscreenchange", handleScreenChange); + }; }, []); return (
From 5ec0311f84cb000adba477775c12e88a9f68c7a5 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 25 Jul 2024 19:38:18 +0800 Subject: [PATCH 21/27] fix typescript error --- app/api/artifact/route.ts | 2 +- app/components/artifact.tsx | 2 +- app/components/ui-lib.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/artifact/route.ts b/app/api/artifact/route.ts index ab8485913b6..4707e795f0a 100644 --- a/app/api/artifact/route.ts +++ b/app/api/artifact/route.ts @@ -15,7 +15,7 @@ async function handle(req: NextRequest, res: NextResponse) { const body: { key: string; value: string; - expiration_ttl?: Number; + expiration_ttl?: number; } = { key: hashedCode, value: clonedBody, diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index 0c3ee84a854..bab68ee516b 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -33,7 +33,7 @@ export function HTMLPreview(props: { */ useEffect(() => { - const handleMessage = (e) => { + const handleMessage = (e: any) => { const { id, height, title } = e.data; setTitle(title); if (id == frameId.current) { diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index aff524b1201..2356ee254ab 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -528,7 +528,7 @@ export function FullScreen(props: any) { } }, []); useEffect(() => { - const handleScreenChange = (e) => { + const handleScreenChange = (e: any) => { if (e.target === ref.current) { setFullScreen(!!document.fullscreenElement); } From c27ef6ffbf94be6bab2f6ba7cc9237b1125627a2 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Thu, 25 Jul 2024 23:29:29 +0800 Subject: [PATCH 22/27] feat: artifacts style --- app/components/artifact.module.scss | 22 +++++++++++++-- app/components/artifact.tsx | 42 +++++++++++++---------------- app/components/chat.tsx | 5 ++-- app/components/markdown.tsx | 13 +++++++-- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/app/components/artifact.module.scss b/app/components/artifact.module.scss index 1e166f41846..2909008b0b5 100644 --- a/app/components/artifact.module.scss +++ b/app/components/artifact.module.scss @@ -1,8 +1,26 @@ .artifact { - display: block; + display: flex; width: 100%; height: 100%; - position: relative; + flex-direction: column; + &-header { + display: flex; + align-items: center; + height: 36px; + padding: 20px; + background: var(--second); + } + &-title { + flex: 1; + text-align: center; + font-weight: bold; + font-size: 24px; + } + &-content { + flex-grow: 1; + padding: 0 20px 20px 20px; + background-color: var(--second); + } } .artifact-iframe { diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx index b378cba37ad..3006fe0120a 100644 --- a/app/components/artifact.tsx +++ b/app/components/artifact.tsx @@ -18,7 +18,7 @@ import styles from "./artifact.module.scss"; export function HTMLPreview(props: { code: string; autoHeight?: boolean; - height?: number; + height?: number | string; onLoad?: (title?: string) => void; }) { const ref = useRef(null); @@ -185,7 +185,6 @@ export function Artifact() { const [code, setCode] = useState(""); const [loading, setLoading] = useState(true); const [fileName, setFileName] = useState(""); - const { height } = useWindowSize(); useEffect(() => { if (id) { @@ -205,33 +204,28 @@ export function Artifact() { }, [id]); return ( -
-
+
+
} shadow /> -
NextChat Artifact
+
NextChat Artifact
code} fileName={fileName} />
- {loading && } - {code && ( - { - setFileName(title as string); - setLoading(false); - }} - /> - )} +
+ {loading && } + {code && ( + { + setFileName(title as string); + setLoading(false); + }} + /> + )} +
); } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6bfd99b5394..33956e6bc56 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -641,12 +641,13 @@ export function ChatActions(props: { ]} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { - if (s.length === 0) return; const plugin = s[0]; chatStore.updateCurrentSession((session) => { session.mask.plugin = s; }); - showToast(plugin); + if (plugin) { + showToast(plugin); + } }} /> )} diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 219c4c86879..36c7429028f 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -14,7 +14,8 @@ import React from "react"; import { useDebouncedCallback } from "use-debounce"; import { showImageModal, FullScreen } from "./ui-lib"; import { ArtifactShareButton, HTMLPreview } from "./artifact"; - +import { Plugin } from "../constant"; +import { useChatStore } from "../store"; export function Mermaid(props: { code: string }) { const ref = useRef(null); const [hasError, setHasError] = useState(false); @@ -67,6 +68,9 @@ export function PreCode(props: { children: any }) { 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; @@ -87,6 +91,11 @@ export function PreCode(props: { children: any }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [refText]); + const enableArtifacts = useMemo( + () => plugins?.includes(Plugin.Artifact), + [plugins], + ); + return ( <>
@@ -104,7 +113,7 @@ export function PreCode(props: { children: any }) {
       {mermaidCode.length > 0 && (
         
       )}
-      {htmlCode.length > 0 && (
+      {htmlCode.length > 0 && enableArtifacts && (
         
           
Date: Fri, 26 Jul 2024 11:21:51 +0800
Subject: [PATCH 23/27] fix: ts error

---
 app/components/artifact.tsx | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx
index 3006fe0120a..2cb44d6cd47 100644
--- a/app/components/artifact.tsx
+++ b/app/components/artifact.tsx
@@ -48,14 +48,12 @@ export function HTMLPreview(props: {
   }, []);
 
   const height = useMemo(() => {
-    const parentHeight = props.height || 600;
-    if (props.autoHeight !== false) {
-      return iframeHeight + 40 > parentHeight
-        ? parentHeight
-        : iframeHeight + 40;
-    } else {
-      return parentHeight;
+    if (!props.autoHeight) return props.height || 600;
+    if (typeof props.height === "string") {
+      return props.height;
     }
+    const parentHeight = props.height || 600;
+    return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40;
   }, [props.autoHeight, props.height, iframeHeight]);
 
   const srcDoc = useMemo(() => {

From f2d2622172fa8b081f5e44f7c3655ffcb4969ed6 Mon Sep 17 00:00:00 2001
From: Dogtiti <499960698@qq.com>
Date: Fri, 26 Jul 2024 13:49:15 +0800
Subject: [PATCH 24/27] fix: uploading loading

---
 app/components/artifact.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/components/artifact.tsx b/app/components/artifact.tsx
index 2cb44d6cd47..295a4230e09 100644
--- a/app/components/artifact.tsx
+++ b/app/components/artifact.tsx
@@ -126,6 +126,7 @@ export function ArtifactShareButton({
           bordered
           title={Locale.Export.Artifact.Title}
           onClick={() => {
+            if (loading) return;
             setLoading(true);
             upload(getCode())
               .then((res) => {

From 6737f016f5c3d4ddc01a7b29ebb3b5df17138b96 Mon Sep 17 00:00:00 2001
From: Dogtiti <499960698@qq.com>
Date: Fri, 26 Jul 2024 15:50:26 +0800
Subject: [PATCH 25/27] chore: artifact => artifacts

---
 app/api/{artifact => artifacts}/route.ts      |  0
 ...fact.module.scss => artifacts.module.scss} |  4 +--
 .../{artifact.tsx => artifacts.tsx}           | 36 ++++++++++---------
 app/components/chat.tsx                       |  4 +--
 app/components/home.tsx                       |  6 ++--
 app/components/markdown.tsx                   |  6 ++--
 app/constant.ts                               |  6 ++--
 app/locales/cn.ts                             |  4 +--
 app/locales/en.ts                             |  6 ++--
 9 files changed, 38 insertions(+), 34 deletions(-)
 rename app/api/{artifact => artifacts}/route.ts (100%)
 rename app/components/{artifact.module.scss => artifacts.module.scss} (93%)
 rename app/components/{artifact.tsx => artifacts.tsx} (87%)

diff --git a/app/api/artifact/route.ts b/app/api/artifacts/route.ts
similarity index 100%
rename from app/api/artifact/route.ts
rename to app/api/artifacts/route.ts
diff --git a/app/components/artifact.module.scss b/app/components/artifacts.module.scss
similarity index 93%
rename from app/components/artifact.module.scss
rename to app/components/artifacts.module.scss
index 2909008b0b5..7ea572a9055 100644
--- a/app/components/artifact.module.scss
+++ b/app/components/artifacts.module.scss
@@ -1,4 +1,4 @@
-.artifact {
+.artifacts {
   display: flex;
   width: 100%;
   height: 100%;
@@ -23,7 +23,7 @@
   }
 }
 
-.artifact-iframe {
+.artifacts-iframe {
   width: 100%;
   border: var(--border-in-light);
   border-radius: 6px;
diff --git a/app/components/artifact.tsx b/app/components/artifacts.tsx
similarity index 87%
rename from app/components/artifact.tsx
rename to app/components/artifacts.tsx
index 295a4230e09..326891e736a 100644
--- a/app/components/artifact.tsx
+++ b/app/components/artifacts.tsx
@@ -13,7 +13,7 @@ import { Modal, showToast } from "./ui-lib";
 import { copyToClipboard, downloadAs } from "../utils";
 import { Path, ApiPath, REPO_URL } from "@/app/constant";
 import { Loading } from "./home";
-import styles from "./artifact.module.scss";
+import styles from "./artifacts.module.scss";
 
 export function HTMLPreview(props: {
   code: string;
@@ -72,7 +72,7 @@ export function HTMLPreview(props: {
 
   return (