Sfoglia il codice sorgente

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

Feature/artifacts style
Lloyd Zhou 1 anno fa
parent
commit
a0f0b4ff9e

+ 30 - 0
app/components/artifact.module.scss

@@ -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;
+}

+ 29 - 36
app/components/artifact.tsx

@@ -13,11 +13,12 @@ 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;
   autoHeight?: boolean;
-  height?: number;
+  height?: number | string;
   onLoad?: (title?: string) => void;
 }) {
   const ref = useRef<HTMLIFrameElement>(null);
@@ -65,17 +66,22 @@ export function HTMLPreview(props: {
     return props.code + script;
   }, [props.code]);
 
+  const handleOnLoad = () => {
+    if (props?.onLoad) {
+      props.onLoad(title);
+    }
+  };
+
   return (
     <iframe
+      className={styles["artifact-iframe"]}
       id={frameId.current}
       ref={ref}
-      frameBorder={0}
       sandbox="allow-forms allow-modals allow-scripts"
-      style={{ width: "100%", height }}
-      // src={`data:text/html,${encodeURIComponent(srcDoc)}`}
+      style={{ height }}
       srcDoc={srcDoc}
-      onLoad={(e) => props?.onLoad && props?.onLoad(title)}
-    ></iframe>
+      onLoad={handleOnLoad}
+    />
   );
 }
 
@@ -179,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) {
@@ -199,40 +204,28 @@ export function Artifact() {
   }, [id]);
 
   return (
-    <div
-      style={{
-        display: "block",
-        width: "100%",
-        height: "100%",
-        position: "relative",
-      }}
-    >
-      <div
-        style={{
-          height: 36,
-          display: "flex",
-          alignItems: "center",
-          padding: 12,
-        }}
-      >
+    <div className={styles["artifact"]}>
+      <div className={styles["artifact-header"]}>
         <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
           <IconButton bordered icon={<GithubIcon />} shadow />
         </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} />
       </div>
-      {loading && <Loading />}
-      {code && (
-        <HTMLPreview
-          code={code}
-          autoHeight={false}
-          height={height - 36}
-          onLoad={(title) => {
-            setFileName(title as string);
-            setLoading(false);
-          }}
-        />
-      )}
+      <div className={styles["artifact-content"]}>
+        {loading && <Loading />}
+        {code && (
+          <HTMLPreview
+            code={code}
+            autoHeight={false}
+            height={"100%"}
+            onLoad={(title) => {
+              setFileName(title as string);
+              setLoading(false);
+            }}
+          />
+        )}
+      </div>
     </div>
   );
 }

+ 3 - 2
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);
+            }
           }}
         />
       )}

+ 13 - 4
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<HTMLDivElement>(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 (
     <>
       <pre ref={ref}>
@@ -104,10 +113,10 @@ export function PreCode(props: { children: any }) {
       {mermaidCode.length > 0 && (
         <Mermaid code={mermaidCode} key={mermaidCode} />
       )}
-      {htmlCode.length > 0 && (
-        <FullScreen className="no-dark html" right={60}>
+      {htmlCode.length > 0 && enableArtifacts && (
+        <FullScreen className="no-dark html" right={70}>
           <ArtifactShareButton
-            style={{ position: "absolute", right: 10, top: 10 }}
+            style={{ position: "absolute", right: 20, top: 10 }}
             getCode={() => htmlCode}
           />
           <HTMLPreview

+ 1 - 0
app/components/ui-lib.module.scss

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

+ 30 - 11
app/components/ui-lib.tsx

@@ -55,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 (
@@ -470,15 +470,35 @@ export function Selector<T>(props: {
   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.multiple
-              ? // @ts-ignore
-                props.defaultSelectedValue?.includes(item.value)
-              : props.defaultSelectedValue === item.value;
+            const selected = selectedValues.includes(item.value);
             return (
               <ListItem
                 className={`${styles["selector-item"]} ${
@@ -487,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);
                   }
                 }}
               >
@@ -515,7 +535,6 @@ export function Selector<T>(props: {
     </div>
   );
 }
-
 export function FullScreen(props: any) {
   const { children, right = 10, top = 10, ...rest } = props;
   const ref = useRef<HTMLDivElement>();