Merge pull request #5092 from ConnectAI-E/feature-artifacts
[Artifacts] add preview html code
This commit is contained in:
commit
b27f394995
|
@ -0,0 +1,73 @@
|
|||
import md5 from "spark-md5";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
|
||||
async function handle(req: NextRequest, res: NextResponse) {
|
||||
const serverConfig = getServerSideConfig();
|
||||
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 body: {
|
||||
key: string;
|
||||
value: string;
|
||||
expiration_ttl?: number;
|
||||
} = {
|
||||
key: hashedCode,
|
||||
value: clonedBody,
|
||||
};
|
||||
try {
|
||||
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
|
||||
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: JSON.stringify([body]),
|
||||
});
|
||||
const result = await res.json();
|
||||
console.log("save data", result);
|
||||
if (result?.success) {
|
||||
return NextResponse.json(
|
||||
{ code: 0, id: hashedCode, result },
|
||||
{ status: res.status },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: true, msg: "Save data error" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (req.method === "GET") {
|
||||
const id = req?.nextUrl?.searchParams?.get("id");
|
||||
const res = await fetch(`${storeUrl()}/values/${id}`, {
|
||||
headers: storeHeaders(),
|
||||
method: "GET",
|
||||
});
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: res.headers,
|
||||
});
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: true, msg: "Invalid request" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
|
||||
export const runtime = "edge";
|
|
@ -0,0 +1,31 @@
|
|||
.artifacts {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.artifacts-iframe {
|
||||
width: 100%;
|
||||
border: var(--border-in-light);
|
||||
border-radius: 6px;
|
||||
background-color: var(--gray);
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
import { useEffect, useState, useRef, useMemo } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useWindowSize } from "@/app/utils";
|
||||
import { IconButton } from "./button";
|
||||
import { nanoid } from "nanoid";
|
||||
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";
|
||||
import { Path, ApiPath, REPO_URL } from "@/app/constant";
|
||||
import { Loading } from "./home";
|
||||
import styles from "./artifacts.module.scss";
|
||||
|
||||
export function HTMLPreview(props: {
|
||||
code: string;
|
||||
autoHeight?: boolean;
|
||||
height?: number | string;
|
||||
onLoad?: (title?: string) => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const frameId = useRef<string>(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
|
||||
* 2. using src with dataurl:
|
||||
* easy to share
|
||||
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (e: any) => {
|
||||
const { id, height, title } = e.data;
|
||||
setTitle(title);
|
||||
if (id == frameId.current) {
|
||||
setIframeHeight(height);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const height = useMemo(() => {
|
||||
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(() => {
|
||||
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
||||
if (props.code.includes("</head>")) {
|
||||
props.code.replace("</head>", "</head>" + script);
|
||||
}
|
||||
return props.code + script;
|
||||
}, [props.code]);
|
||||
|
||||
const handleOnLoad = () => {
|
||||
if (props?.onLoad) {
|
||||
props.onLoad(title);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className={styles["artifacts-iframe"]}
|
||||
id={frameId.current}
|
||||
ref={ref}
|
||||
sandbox="allow-forms allow-modals allow-scripts"
|
||||
style={{ height }}
|
||||
srcDoc={srcDoc}
|
||||
onLoad={handleOnLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArtifactsShareButton({
|
||||
getCode,
|
||||
id,
|
||||
style,
|
||||
fileName,
|
||||
}: {
|
||||
getCode: () => string;
|
||||
id?: string;
|
||||
style?: any;
|
||||
fileName?: string;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [name, setName] = useState(id);
|
||||
const [show, setShow] = useState(false);
|
||||
const shareUrl = useMemo(
|
||||
() => [location.origin, "#", Path.Artifacts, "/", name].join(""),
|
||||
[name],
|
||||
);
|
||||
const upload = (code: string) =>
|
||||
id
|
||||
? Promise.resolve({ id })
|
||||
: fetch(ApiPath.Artifacts, {
|
||||
method: "POST",
|
||||
body: code,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ id }) => {
|
||||
if (id) {
|
||||
return { id };
|
||||
}
|
||||
throw Error();
|
||||
})
|
||||
.catch((e) => {
|
||||
showToast(Locale.Export.Artifacts.Error);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="window-action-button" style={style}>
|
||||
<IconButton
|
||||
icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
|
||||
bordered
|
||||
title={Locale.Export.Artifacts.Title}
|
||||
onClick={() => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
upload(getCode())
|
||||
.then((res) => {
|
||||
if (res?.id) {
|
||||
setShow(true);
|
||||
setName(res?.id);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{show && (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Export.Artifacts.Title}
|
||||
onClose={() => setShow(false)}
|
||||
actions={[
|
||||
<IconButton
|
||||
key="download"
|
||||
icon={<DownloadIcon />}
|
||||
bordered
|
||||
text={Locale.Export.Download}
|
||||
onClick={() => {
|
||||
downloadAs(getCode(), `${fileName || name}.html`).then(() =>
|
||||
setShow(false),
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
<IconButton
|
||||
key="copy"
|
||||
icon={<CopyIcon />}
|
||||
bordered
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
onClick={() => {
|
||||
copyToClipboard(shareUrl).then(() => setShow(false));
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<a target="_blank" href={shareUrl}>
|
||||
{shareUrl}
|
||||
</a>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Artifacts() {
|
||||
const { id } = useParams();
|
||||
const [code, setCode] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fileName, setFileName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetch(`${ApiPath.Artifacts}?id=${id}`)
|
||||
.then((res) => {
|
||||
if (res.status > 300) {
|
||||
throw Error("can not get content");
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then(setCode)
|
||||
.catch((e) => {
|
||||
showToast(Locale.Export.Artifacts.Error);
|
||||
});
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className={styles["artifacts"]}>
|
||||
<div className={styles["artifacts-header"]}>
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton bordered icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
|
||||
<ArtifactsShareButton
|
||||
id={id}
|
||||
getCode={() => code}
|
||||
fileName={fileName}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["artifacts-content"]}>
|
||||
{loading && <Loading />}
|
||||
{code && (
|
||||
<HTMLPreview
|
||||
code={code}
|
||||
autoHeight={false}
|
||||
height={"100%"}
|
||||
onLoad={(title) => {
|
||||
setFileName(title as string);
|
||||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -90,6 +90,7 @@ import {
|
|||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
ServiceProvider,
|
||||
Plugin,
|
||||
} from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||
|
@ -477,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(() => {
|
||||
|
@ -588,12 +590,6 @@ export function ChatActions(props: {
|
|||
icon={<RobotIcon />}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
onClick={() => showToast(Locale.WIP)}
|
||||
text={Locale.Plugin.Name}
|
||||
icon={<PluginIcon />}
|
||||
/>
|
||||
|
||||
{showModelSelector && (
|
||||
<Selector
|
||||
defaultSelectedValue={`${currentModel}@${currentProviderName}`}
|
||||
|
@ -627,6 +623,34 @@ export function ChatActions(props: {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChatAction
|
||||
onClick={() => setShowPluginSelector(true)}
|
||||
text={Locale.Plugin.Name}
|
||||
icon={<PluginIcon />}
|
||||
/>
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
multiple
|
||||
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
|
||||
items={[
|
||||
{
|
||||
title: Locale.Plugin.Artifacts,
|
||||
value: Plugin.Artifacts,
|
||||
},
|
||||
]}
|
||||
onClose={() => setShowPluginSelector(false)}
|
||||
onSelection={(s) => {
|
||||
const plugin = s[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.plugin = s;
|
||||
});
|
||||
if (plugin) {
|
||||
showToast(plugin);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) {
|
|||
);
|
||||
}
|
||||
|
||||
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
@ -137,6 +141,7 @@ export function WindowContent(props: { children: React.ReactNode }) {
|
|||
function Screen() {
|
||||
const config = useAppConfig();
|
||||
const location = useLocation();
|
||||
const isArtifact = location.pathname.includes(Path.Artifacts);
|
||||
const isHome = location.pathname === Path.Home;
|
||||
const isAuth = location.pathname === Path.Auth;
|
||||
const isSd = location.pathname === Path.Sd;
|
||||
|
@ -150,6 +155,13 @@ function Screen() {
|
|||
loadAsyncGoogleFont();
|
||||
}, []);
|
||||
|
||||
if (isArtifact) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/artifacts/:id" element={<Artifacts />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
const renderContent = () => {
|
||||
if (isAuth) return <AuthPage />;
|
||||
if (isSd) return <Sd />;
|
||||
|
|
|
@ -6,14 +6,16 @@ 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 { ArtifactsShareButton, HTMLPreview } from "./artifacts";
|
||||
import { Plugin } from "../constant";
|
||||
import { useChatStore } from "../store";
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
@ -64,25 +66,38 @@ export function PreCode(props: { children: any }) {
|
|||
const ref = useRef<HTMLPreElement>(null);
|
||||
const refText = ref.current?.innerText;
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
const [htmlCode, setHtmlCode] = useState("");
|
||||
const { height } = useWindowSize();
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const plugins = session.mask?.plugin;
|
||||
|
||||
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);
|
||||
} else if (refText?.startsWith("<!DOCTYPE")) {
|
||||
setHtmlCode(refText);
|
||||
}
|
||||
}, 600);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(renderMermaid, 1);
|
||||
setTimeout(renderArtifacts, 1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refText]);
|
||||
|
||||
const enableArtifacts = useMemo(
|
||||
() => plugins?.includes(Plugin.Artifacts),
|
||||
[plugins],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
<pre ref={ref}>
|
||||
<span
|
||||
className="copy-code-button"
|
||||
|
@ -95,6 +110,22 @@ export function PreCode(props: { children: any }) {
|
|||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
{htmlCode.length > 0 && enableArtifacts && (
|
||||
<FullScreen className="no-dark html" right={70}>
|
||||
<ArtifactsShareButton
|
||||
style={{ position: "absolute", right: 20, top: 10 }}
|
||||
getCode={() => htmlCode}
|
||||
/>
|
||||
<HTMLPreview
|
||||
code={htmlCode}
|
||||
autoHeight={!document.fullscreenElement}
|
||||
height={!document.fullscreenElement ? 600 : height}
|
||||
/>
|
||||
</FullScreen>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -309,6 +309,7 @@
|
|||
}
|
||||
|
||||
&-content {
|
||||
min-width: 300px;
|
||||
.list {
|
||||
max-height: 90vh;
|
||||
overflow-x: hidden;
|
||||
|
|
|
@ -19,6 +19,8 @@ import React, {
|
|||
MouseEvent,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
|
@ -53,7 +55,7 @@ export function ListItem(props: {
|
|||
children?: JSX.Element | JSX.Element[];
|
||||
icon?: JSX.Element;
|
||||
className?: string;
|
||||
onClick?: (event: MouseEvent) => void;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
vertical?: boolean;
|
||||
}) {
|
||||
return (
|
||||
|
@ -463,17 +465,40 @@ export function Selector<T>(props: {
|
|||
value: T;
|
||||
disable?: boolean;
|
||||
}>;
|
||||
defaultSelectedValue?: T;
|
||||
defaultSelectedValue?: T[] | T;
|
||||
onSelection?: (selection: T[]) => void;
|
||||
onClose?: () => void;
|
||||
multiple?: boolean;
|
||||
}) {
|
||||
const [selectedValues, setSelectedValues] = useState<T[]>(
|
||||
Array.isArray(props.defaultSelectedValue)
|
||||
? props.defaultSelectedValue
|
||||
: props.defaultSelectedValue !== undefined
|
||||
? [props.defaultSelectedValue]
|
||||
: [],
|
||||
);
|
||||
|
||||
const handleSelection = (e: 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 (
|
||||
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
|
||||
<div className={styles["selector-content"]}>
|
||||
<List>
|
||||
{props.items.map((item, i) => {
|
||||
const selected = props.defaultSelectedValue === item.value;
|
||||
const selected = selectedValues.includes(item.value);
|
||||
return (
|
||||
<ListItem
|
||||
className={`${styles["selector-item"]} ${
|
||||
|
@ -482,11 +507,11 @@ export function Selector<T>(props: {
|
|||
key={i}
|
||||
title={item.title}
|
||||
subTitle={item.subTitle}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (!item.disable) {
|
||||
props.onSelection?.([item.value]);
|
||||
props.onClose?.();
|
||||
onClick={(e) => {
|
||||
if (item.disable) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
handleSelection(e, item.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -510,3 +535,38 @@ export function Selector<T>(props: {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
export function FullScreen(props: any) {
|
||||
const { children, right = 10, top = 10, ...rest } = props;
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
ref.current?.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const handleScreenChange = (e: any) => {
|
||||
if (e.target === ref.current) {
|
||||
setFullScreen(!!document.fullscreenElement);
|
||||
}
|
||||
};
|
||||
document.addEventListener("fullscreenchange", handleScreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleScreenChange);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div ref={ref} style={{ position: "relative" }} {...rest}>
|
||||
<div style={{ position: "absolute", right, top }}>
|
||||
<IconButton
|
||||
icon={fullScreen ? <MinIcon /> : <MaxIcon />}
|
||||
onClick={toggleFullscreen}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -168,6 +168,11 @@ 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),
|
||||
cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
|
||||
|
||||
gtmId: process.env.GTM_ID,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
|
|
|
@ -35,6 +35,7 @@ export enum Path {
|
|||
Auth = "/auth",
|
||||
Sd = "/sd",
|
||||
SdNew = "/sd-new",
|
||||
Artifacts = "/artifacts",
|
||||
}
|
||||
|
||||
export enum ApiPath {
|
||||
|
@ -47,6 +48,7 @@ export enum ApiPath {
|
|||
ByteDance = "/api/bytedance",
|
||||
Alibaba = "/api/alibaba",
|
||||
Stability = "/api/stability",
|
||||
Artifacts = "/api/artifacts",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
|
@ -59,6 +61,10 @@ export enum FileName {
|
|||
Prompts = "prompts.json",
|
||||
}
|
||||
|
||||
export enum Plugin {
|
||||
Artifacts = "artifacts",
|
||||
}
|
||||
|
||||
export enum StoreKey {
|
||||
Chat = "chat-next-web-store",
|
||||
Access = "access-control",
|
||||
|
|
|
@ -104,6 +104,10 @@ const cn = {
|
|||
Toast: "正在生成截图",
|
||||
Modal: "长按或右键保存图片",
|
||||
},
|
||||
Artifacts: {
|
||||
Title: "分享页面",
|
||||
Error: "分享失败",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "搜索消息",
|
||||
|
@ -457,6 +461,7 @@ const cn = {
|
|||
},
|
||||
Plugin: {
|
||||
Name: "插件",
|
||||
Artifacts: "Artifacts",
|
||||
},
|
||||
Discovery: {
|
||||
Name: "发现",
|
||||
|
|
|
@ -106,6 +106,10 @@ const en: LocaleType = {
|
|||
Toast: "Capturing Image...",
|
||||
Modal: "Long press or right click to save image",
|
||||
},
|
||||
Artifacts: {
|
||||
Title: "Share Artifacts",
|
||||
Error: "Share Error",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Search",
|
||||
|
@ -464,6 +468,7 @@ const en: LocaleType = {
|
|||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
Artifacts: "Artifacts",
|
||||
},
|
||||
Discovery: {
|
||||
Name: "Discovery",
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
|
|||
import { getLang, Lang } from "../locales";
|
||||
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
||||
import { ModelConfig, useAppConfig } from "./config";
|
||||
import { StoreKey } from "../constant";
|
||||
import { StoreKey, Plugin } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
|
@ -17,6 +17,7 @@ export type Mask = {
|
|||
modelConfig: ModelConfig;
|
||||
lang: Lang;
|
||||
builtin: boolean;
|
||||
plugin?: Plugin[];
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_STATE = {
|
||||
|
@ -37,6 +38,7 @@ export const createEmptyMask = () =>
|
|||
lang: getLang(),
|
||||
builtin: false,
|
||||
createdAt: Date.now(),
|
||||
plugin: [Plugin.Artifacts],
|
||||
}) as Mask;
|
||||
|
||||
export const useMaskStore = createPersistStore(
|
||||
|
|
Loading…
Reference in New Issue