Merge pull request #5 from ConnectAI-E/feature/artifacts-style

Feature/artifacts style
This commit is contained in:
Lloyd Zhou 2024-07-25 23:36:34 +08:00 committed by GitHub
commit a0f0b4ff9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 106 additions and 53 deletions

View File

@ -0,0 +1,30 @@
.artifact {
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);
}
}
.artifact-iframe {
width: 100%;
border: var(--border-in-light);
border-radius: 6px;
}

View File

@ -13,11 +13,12 @@ import { Modal, showToast } from "./ui-lib";
import { copyToClipboard, downloadAs } from "../utils"; import { copyToClipboard, downloadAs } from "../utils";
import { Path, ApiPath, REPO_URL } from "@/app/constant"; import { Path, ApiPath, REPO_URL } from "@/app/constant";
import { Loading } from "./home"; import { Loading } from "./home";
import styles from "./artifact.module.scss";
export function HTMLPreview(props: { export function HTMLPreview(props: {
code: string; code: string;
autoHeight?: boolean; autoHeight?: boolean;
height?: number; height?: number | string;
onLoad?: (title?: string) => void; onLoad?: (title?: string) => void;
}) { }) {
const ref = useRef<HTMLIFrameElement>(null); const ref = useRef<HTMLIFrameElement>(null);
@ -65,17 +66,22 @@ export function HTMLPreview(props: {
return props.code + script; return props.code + script;
}, [props.code]); }, [props.code]);
const handleOnLoad = () => {
if (props?.onLoad) {
props.onLoad(title);
}
};
return ( return (
<iframe <iframe
className={styles["artifact-iframe"]}
id={frameId.current} id={frameId.current}
ref={ref} ref={ref}
frameBorder={0}
sandbox="allow-forms allow-modals allow-scripts" sandbox="allow-forms allow-modals allow-scripts"
style={{ width: "100%", height }} style={{ height }}
// src={`data:text/html,${encodeURIComponent(srcDoc)}`}
srcDoc={srcDoc} srcDoc={srcDoc}
onLoad={(e) => props?.onLoad && props?.onLoad(title)} onLoad={handleOnLoad}
></iframe> />
); );
} }
@ -179,7 +185,6 @@ export function Artifact() {
const [code, setCode] = useState(""); const [code, setCode] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [fileName, setFileName] = useState(""); const [fileName, setFileName] = useState("");
const { height } = useWindowSize();
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@ -199,40 +204,28 @@ export function Artifact() {
}, [id]); }, [id]);
return ( return (
<div <div className={styles["artifact"]}>
style={{ <div className={styles["artifact-header"]}>
display: "block",
width: "100%",
height: "100%",
position: "relative",
}}
>
<div
style={{
height: 36,
display: "flex",
alignItems: "center",
padding: 12,
}}
>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer"> <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton bordered icon={<GithubIcon />} shadow /> <IconButton bordered icon={<GithubIcon />} shadow />
</a> </a>
<div style={{ flex: 1, textAlign: "center" }}>NextChat Artifact</div> <div className={styles["artifact-title"]}>NextChat Artifact</div>
<ArtifactShareButton id={id} getCode={() => code} fileName={fileName} /> <ArtifactShareButton id={id} getCode={() => code} fileName={fileName} />
</div> </div>
{loading && <Loading />} <div className={styles["artifact-content"]}>
{code && ( {loading && <Loading />}
<HTMLPreview {code && (
code={code} <HTMLPreview
autoHeight={false} code={code}
height={height - 36} autoHeight={false}
onLoad={(title) => { height={"100%"}
setFileName(title as string); onLoad={(title) => {
setLoading(false); setFileName(title as string);
}} setLoading(false);
/> }}
)} />
)}
</div>
</div> </div>
); );
} }

View File

@ -641,12 +641,13 @@ export function ChatActions(props: {
]} ]}
onClose={() => setShowPluginSelector(false)} onClose={() => setShowPluginSelector(false)}
onSelection={(s) => { onSelection={(s) => {
if (s.length === 0) return;
const plugin = s[0]; const plugin = s[0];
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
session.mask.plugin = s; session.mask.plugin = s;
}); });
showToast(plugin); if (plugin) {
showToast(plugin);
}
}} }}
/> />
)} )}

View File

@ -14,7 +14,8 @@ import React from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { showImageModal, FullScreen } from "./ui-lib"; import { showImageModal, FullScreen } from "./ui-lib";
import { ArtifactShareButton, HTMLPreview } from "./artifact"; import { ArtifactShareButton, HTMLPreview } from "./artifact";
import { Plugin } from "../constant";
import { useChatStore } from "../store";
export function Mermaid(props: { code: string }) { export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
@ -67,6 +68,9 @@ export function PreCode(props: { children: any }) {
const [mermaidCode, setMermaidCode] = useState(""); const [mermaidCode, setMermaidCode] = useState("");
const [htmlCode, setHtmlCode] = useState(""); const [htmlCode, setHtmlCode] = useState("");
const { height } = useWindowSize(); const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
const plugins = session.mask?.plugin;
const renderArtifacts = useDebouncedCallback(() => { const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return; if (!ref.current) return;
@ -87,6 +91,11 @@ export function PreCode(props: { children: any }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [refText]); }, [refText]);
const enableArtifacts = useMemo(
() => plugins?.includes(Plugin.Artifact),
[plugins],
);
return ( return (
<> <>
<pre ref={ref}> <pre ref={ref}>
@ -104,10 +113,10 @@ export function PreCode(props: { children: any }) {
{mermaidCode.length > 0 && ( {mermaidCode.length > 0 && (
<Mermaid code={mermaidCode} key={mermaidCode} /> <Mermaid code={mermaidCode} key={mermaidCode} />
)} )}
{htmlCode.length > 0 && ( {htmlCode.length > 0 && enableArtifacts && (
<FullScreen className="no-dark html" right={60}> <FullScreen className="no-dark html" right={70}>
<ArtifactShareButton <ArtifactShareButton
style={{ position: "absolute", right: 10, top: 10 }} style={{ position: "absolute", right: 20, top: 10 }}
getCode={() => htmlCode} getCode={() => htmlCode}
/> />
<HTMLPreview <HTMLPreview

View File

@ -309,6 +309,7 @@
} }
&-content { &-content {
min-width: 300px;
.list { .list {
max-height: 90vh; max-height: 90vh;
overflow-x: hidden; overflow-x: hidden;

View File

@ -55,7 +55,7 @@ export function ListItem(props: {
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
icon?: JSX.Element; icon?: JSX.Element;
className?: string; className?: string;
onClick?: (event: MouseEvent) => void; onClick?: (e: MouseEvent) => void;
vertical?: boolean; vertical?: boolean;
}) { }) {
return ( return (
@ -470,15 +470,35 @@ export function Selector<T>(props: {
onClose?: () => void; onClose?: () => void;
multiple?: boolean; 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 ( return (
<div className={styles["selector"]} onClick={() => props.onClose?.()}> <div className={styles["selector"]} onClick={() => props.onClose?.()}>
<div className={styles["selector-content"]}> <div className={styles["selector-content"]}>
<List> <List>
{props.items.map((item, i) => { {props.items.map((item, i) => {
const selected = props.multiple const selected = selectedValues.includes(item.value);
? // @ts-ignore
props.defaultSelectedValue?.includes(item.value)
: props.defaultSelectedValue === item.value;
return ( return (
<ListItem <ListItem
className={`${styles["selector-item"]} ${ className={`${styles["selector-item"]} ${
@ -487,11 +507,11 @@ export function Selector<T>(props: {
key={i} key={i}
title={item.title} title={item.title}
subTitle={item.subTitle} subTitle={item.subTitle}
onClick={(event) => { onClick={(e) => {
event.stopPropagation(); if (item.disable) {
if (!item.disable) { e.stopPropagation();
props.onSelection?.([item.value]); } else {
props.onClose?.(); handleSelection(e, item.value);
} }
}} }}
> >
@ -515,7 +535,6 @@ export function Selector<T>(props: {
</div> </div>
); );
} }
export function FullScreen(props: any) { export function FullScreen(props: any) {
const { children, right = 10, top = 10, ...rest } = props; const { children, right = 10, top = 10, ...rest } = props;
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();