-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Mardown support (experimental) (#247)
* feat: Mardown support (experimental) * do not publish react-markdown yet * fix imports
- Loading branch information
Showing
17 changed files
with
1,006 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "assistant-ui/registry/assistant-ui/experimental/codeblock"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "assistant-ui/registry/assistant-ui/experimental/markdown-text"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "@/registry/assistant-ui/experimental/codeblock"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "@/registry/assistant-ui/experimental/markdown-text"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
packages/cli/registry/assistant-ui/experimental/codeblock.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
// Inspired by Chatbot-UI and modified to fit the needs of this project | ||
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx | ||
|
||
"use client"; | ||
|
||
import { type FC, memo, useState } from "react"; | ||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; | ||
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; | ||
|
||
import { Button } from "@/components/ui/button"; | ||
import { CheckIcon, CopyIcon, DownloadIcon } from "lucide-react"; | ||
|
||
interface Props { | ||
language: string; | ||
value: string; | ||
} | ||
|
||
interface languageMap { | ||
[key: string]: string | undefined; | ||
} | ||
|
||
export const programmingLanguages: languageMap = { | ||
javascript: ".js", | ||
python: ".py", | ||
java: ".java", | ||
c: ".c", | ||
cpp: ".cpp", | ||
"c++": ".cpp", | ||
"c#": ".cs", | ||
ruby: ".rb", | ||
php: ".php", | ||
swift: ".swift", | ||
"objective-c": ".m", | ||
kotlin: ".kt", | ||
typescript: ".ts", | ||
go: ".go", | ||
perl: ".pl", | ||
rust: ".rs", | ||
scala: ".scala", | ||
haskell: ".hs", | ||
lua: ".lua", | ||
shell: ".sh", | ||
sql: ".sql", | ||
html: ".html", | ||
css: ".css", | ||
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component | ||
}; | ||
|
||
export const generateRandomString = (length: number, lowercase = false) => { | ||
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0 | ||
let result = ""; | ||
for (let i = 0; i < length; i++) { | ||
result += chars.charAt(Math.floor(Math.random() * chars.length)); | ||
} | ||
return lowercase ? result.toLowerCase() : result; | ||
}; | ||
|
||
const CodeBlock: FC<Props> = memo(({ language, value }) => { | ||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); | ||
|
||
const downloadAsFile = () => { | ||
if (typeof window === "undefined") { | ||
return; | ||
} | ||
const fileExtension = programmingLanguages[language] || ".file"; | ||
const suggestedFileName = `file-${generateRandomString( | ||
3, | ||
true, | ||
)}${fileExtension}`; | ||
const fileName = window.prompt("Enter file name" || "", suggestedFileName); | ||
|
||
if (!fileName) { | ||
// User pressed cancel on prompt. | ||
return; | ||
} | ||
|
||
const blob = new Blob([value], { type: "text/plain" }); | ||
const url = URL.createObjectURL(blob); | ||
const link = document.createElement("a"); | ||
link.download = fileName; | ||
link.href = url; | ||
link.style.display = "none"; | ||
document.body.appendChild(link); | ||
link.click(); | ||
document.body.removeChild(link); | ||
URL.revokeObjectURL(url); | ||
}; | ||
|
||
const onCopy = () => { | ||
if (isCopied) return; | ||
copyToClipboard(value); | ||
}; | ||
|
||
return ( | ||
<div className="codeblock relative w-full bg-zinc-950 font-sans"> | ||
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100"> | ||
<span className="text-xs lowercase">{language}</span> | ||
<div className="flex items-center space-x-1"> | ||
<Button | ||
variant="ghost" | ||
className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0" | ||
onClick={downloadAsFile} | ||
size="icon" | ||
> | ||
<DownloadIcon className="size-4" /> | ||
<span className="sr-only">Download</span> | ||
</Button> | ||
<Button | ||
variant="ghost" | ||
size="icon" | ||
className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0" | ||
onClick={onCopy} | ||
> | ||
{isCopied ? ( | ||
<CheckIcon className="size-4" /> | ||
) : ( | ||
<CopyIcon className="size-4" /> | ||
)} | ||
<span className="sr-only">Copy code</span> | ||
</Button> | ||
</div> | ||
</div> | ||
<SyntaxHighlighter | ||
language={language} | ||
style={coldarkDark} | ||
PreTag="div" | ||
showLineNumbers | ||
customStyle={{ | ||
margin: 0, | ||
width: "100%", | ||
background: "transparent", | ||
padding: "1.5rem 1rem", | ||
}} | ||
lineNumberStyle={{ | ||
userSelect: "none", | ||
}} | ||
codeTagProps={{ | ||
style: { | ||
fontSize: "0.9rem", | ||
fontFamily: "var(--font-mono)", | ||
}, | ||
}} | ||
> | ||
{value} | ||
</SyntaxHighlighter> | ||
</div> | ||
); | ||
}); | ||
CodeBlock.displayName = "CodeBlock"; | ||
|
||
export { CodeBlock }; | ||
|
||
export interface useCopyToClipboardProps { | ||
timeout?: number; | ||
} | ||
|
||
export function useCopyToClipboard({ | ||
timeout = 2000, | ||
}: useCopyToClipboardProps) { | ||
const [isCopied, setIsCopied] = useState<boolean>(false); | ||
|
||
const copyToClipboard = (value: string) => { | ||
if (typeof window === "undefined" || !navigator.clipboard?.writeText) { | ||
return; | ||
} | ||
|
||
if (!value) { | ||
return; | ||
} | ||
|
||
navigator.clipboard.writeText(value).then(() => { | ||
setIsCopied(true); | ||
|
||
setTimeout(() => { | ||
setIsCopied(false); | ||
}, timeout); | ||
}); | ||
}; | ||
|
||
return { isCopied, copyToClipboard }; | ||
} |
49 changes: 49 additions & 0 deletions
49
packages/cli/registry/assistant-ui/experimental/markdown-text.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
"use client"; | ||
|
||
import { unstable_MarkdownTextPrimitive as MarkdownTextPrimitive } from "@assistant-ui/react-markdown"; | ||
import remarkGfm from "remark-gfm"; | ||
import remarkMath from "remark-math"; | ||
|
||
export const MarkdownText = () => { | ||
return ( | ||
<MarkdownTextPrimitive | ||
className="prose dark:prose-invert break-words prose-pre:p-0 prose-p:leading-relaxed" | ||
remarkPlugins={[remarkGfm, remarkMath]} | ||
components={{ | ||
p({ children }) { | ||
return <p className="mb-2 last:mb-0">{children}</p>; | ||
}, | ||
// code({ node, className, children, ...props }) { | ||
// if (children.length) { | ||
// if (children[0] == "▍") { | ||
// return ( | ||
// <span className="mt-1 animate-pulse cursor-default">▍</span> | ||
// ); | ||
// } | ||
|
||
// children[0] = (children[0] as string).replace("`▍`", "▍"); | ||
// } | ||
|
||
// const match = /language-(\w+)/.exec(className || ""); | ||
|
||
// if (inline) { | ||
// return ( | ||
// <code className={className} {...props}> | ||
// {children} | ||
// </code> | ||
// ); | ||
// } | ||
|
||
// return ( | ||
// <CodeBlock | ||
// key={Math.random()} | ||
// language={match?.[1] || ""} | ||
// value={String(children).replace(/\n$/, "")} | ||
// {...props} | ||
// /> | ||
// ); | ||
// }, | ||
}} | ||
/> | ||
); | ||
}; |
Oops, something went wrong.