소스 검색

share page markdown

Jay V 8 달 전
부모
커밋
65b2cf73d7

+ 3 - 0
bun.lock

@@ -65,6 +65,7 @@
         "astro-sst": "3.1.4",
         "diff": "8.0.2",
         "luxon": "3.6.1",
+        "marked": "15.0.12",
         "rehype-autolink-headings": "7.1.0",
         "sharp": "0.32.5",
         "shiki": "3.4.2",
@@ -935,6 +936,8 @@
 
     "markdown-table": ["[email protected]", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
 
+    "marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
+
     "math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
 
     "mdast-util-definitions": ["[email protected]", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],

+ 1 - 0
packages/web/package.json

@@ -21,6 +21,7 @@
     "astro-sst": "3.1.4",
     "diff": "8.0.2",
     "luxon": "3.6.1",
+    "marked": "15.0.12",
     "rehype-autolink-headings": "7.1.0",
     "sharp": "0.32.5",
     "shiki": "3.4.2",

+ 21 - 0
packages/web/src/components/MarkdownView.tsx

@@ -0,0 +1,21 @@
+import { type JSX, splitProps, createResource } from "solid-js"
+import { marked } from "marked"
+import styles from "./markdownview.module.css"
+
+interface MarkdownViewProps extends JSX.HTMLAttributes<HTMLDivElement> {
+  markdown: string
+}
+
+function MarkdownView(props: MarkdownViewProps) {
+  const [local, rest] = splitProps(props, ["markdown"])
+  const [html] = createResource(async () => {
+    return marked.parse(local.markdown)
+  })
+
+  return (
+    <div innerHTML={html()} class={styles["markdown-body"]} {...rest} />
+  )
+}
+
+export default MarkdownView
+

+ 57 - 2
packages/web/src/components/Share.tsx

@@ -12,6 +12,7 @@ import {
   createSignal,
 } from "solid-js"
 import { DateTime } from "luxon"
+import { createStore, reconcile } from "solid-js/store"
 import { IconOpenAI, IconGemini, IconAnthropic } from "./icons/custom"
 import {
   IconCpuChip,
@@ -32,9 +33,9 @@ import {
 } from "./icons"
 import DiffView from "./DiffView"
 import CodeBlock from "./CodeBlock"
+import MarkdownView from "./MarkdownView"
 import styles from "./share.module.css"
 import { type UIMessage } from "ai"
-import { createStore, reconcile } from "solid-js/store"
 
 const MIN_DURATION = 2
 
@@ -268,6 +269,60 @@ function TextPart(props: TextPartProps) {
   )
 }
 
+interface MarkdownPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
+  text: string
+  expand?: boolean
+}
+function MarkdownPart(props: MarkdownPartProps) {
+  const [local, rest] = splitProps(props, ["text", "expand"])
+  const [expanded, setExpanded] = createSignal(false)
+  const [overflowed, setOverflowed] = createSignal(false)
+  let divEl: HTMLDivElement | undefined
+
+  function checkOverflow() {
+    if (divEl && !local.expand) {
+      setOverflowed(divEl.scrollHeight > divEl.clientHeight + 1)
+    }
+  }
+
+  onMount(() => {
+    checkOverflow()
+    window.addEventListener("resize", checkOverflow)
+  })
+
+  createEffect(() => {
+    local.text
+    setTimeout(checkOverflow, 0)
+  })
+
+  onCleanup(() => {
+    window.removeEventListener("resize", checkOverflow)
+  })
+
+  return (
+    <div
+      class={styles["message-markdown"]}
+      data-expanded={expanded() || local.expand === true}
+      {...rest}
+    >
+      <MarkdownView
+        data-elment-markdown
+        markdown={local.text}
+        ref={(el) => (divEl = el)}
+      />
+      {((!local.expand && overflowed()) || expanded()) && (
+        <button
+          type="button"
+          data-element-button-text
+          onClick={() => setExpanded((e) => !e)}
+        >
+          {expanded() ? "Show less" : "Show more"}
+        </button>
+      )}
+    </div>
+  )
+}
+
 interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
   text: string
   desc?: string
@@ -682,7 +737,7 @@ export default function Share(props: { api: string }) {
                                 <div></div>
                               </div>
                               <div data-section="content">
-                                <TextPart
+                                <MarkdownPart
                                   text={part().text}
                                   expand={isLastPart()}
                                 />

+ 40 - 0
packages/web/src/components/markdownview.module.css

@@ -0,0 +1,40 @@
+.markdown-body {
+  font-size: 0.875rem;
+  line-height: 1.5;
+
+  p,
+  blockquote,
+  ul,
+  ol,
+  dl,
+  table,
+  pre {
+    margin-bottom: 1rem;
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
+    margin-bottom: 0.5rem;
+  }
+
+  & > *:last-child {
+    margin-bottom: 0;
+  }
+
+  code {
+    font-weight: 500;
+
+    &::before {
+      content: "`";
+      font-weight: 600;
+    }
+    &::after {
+      content: "`";
+      font-weight: 600;
+    }
+  }
+}

+ 31 - 0
packages/web/src/components/share.module.css

@@ -351,6 +351,7 @@
     }
   }
 
+  [data-part-type="tool-write"],
   [data-part-type="tool-read"],
   [data-part-type="tool-fetch"] {
     [data-part-tool-result] {
@@ -534,6 +535,36 @@
   }
 }
 
+.message-markdown {
+  background-color: var(--sl-color-bg-surface);
+  padding: 0.5rem calc(0.5rem + 3px);
+  border-radius: 0.25rem;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 1rem;
+
+  button {
+    flex: 0 0 auto;
+    padding: 2px 0;
+    font-size: 0.75rem;
+  }
+
+  &[data-expanded="true"] {
+    [data-elment-markdown] {
+      display: block;
+    }
+  }
+  &[data-expanded="false"] {
+    [data-elment-markdown] {
+      display: -webkit-box;
+      -webkit-box-orient: vertical;
+      -webkit-line-clamp: 3;
+      overflow: hidden;
+    }
+  }
+}
+
 .diff-code-block {
   pre {
     line-height: 1.25;