Merge pull request #5 from ConnectAI-E/feature/artifacts-style
Feature/artifacts style
This commit is contained in:
commit
a0f0b4ff9e
|
@ -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;
|
||||||
|
}
|
|
@ -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,34 +204,21 @@ 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>
|
||||||
|
<div className={styles["artifact-content"]}>
|
||||||
{loading && <Loading />}
|
{loading && <Loading />}
|
||||||
{code && (
|
{code && (
|
||||||
<HTMLPreview
|
<HTMLPreview
|
||||||
code={code}
|
code={code}
|
||||||
autoHeight={false}
|
autoHeight={false}
|
||||||
height={height - 36}
|
height={"100%"}
|
||||||
onLoad={(title) => {
|
onLoad={(title) => {
|
||||||
setFileName(title as string);
|
setFileName(title as string);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -234,5 +226,6 @@ export function Artifact() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
if (plugin) {
|
||||||
showToast(plugin);
|
showToast(plugin);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -309,6 +309,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
|
min-width: 300px;
|
||||||
.list {
|
.list {
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
Loading…
Reference in New Issue