mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-08-14 21:50:49 +00:00
feat(UI): adding MVP playground UI (#2828)
# What does this PR do? I've been tinkering a little with a simple chat playground in the UI, so I'm opening the PR with what's kind of a WIP. If you look at the first commit, that includes the big part of the changes. The rest of the files changed come from adding installing the `shadcn` components. Note this is missing a lot; e.g., - sessions - document upload - audio (the shadcn components install these by default from https://shadcn-chatbot-kit.vercel.app/docs/components/chat) I still need to wire up a lot more to make it actually fully functional but it does basic chat using the LS Typescript Client. Basic demo: <img width="1329" height="1430" alt="Image" src="https://github.com/user-attachments/assets/917a2096-36d4-4925-b83b-f1f2cda98698" /> <img width="1319" height="1424" alt="Image" src="https://github.com/user-attachments/assets/fab1583b-1c72-4bf3-baf2-405aee13c6bb" /> <!-- If resolving an issue, uncomment and update the line below --> <!-- Closes #[issue-number] --> ## Test Plan <!-- Describe the tests you ran to verify your changes with result summaries. *Provide clear instructions so the plan can be easily re-executed.* --> --------- Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
This commit is contained in:
parent
d6ae2b0f47
commit
f3d5459647
32 changed files with 4876 additions and 31 deletions
195
llama_stack/ui/components/chat-playground/markdown-renderer.tsx
Normal file
195
llama_stack/ui/components/chat-playground/markdown-renderer.tsx
Normal file
|
@ -0,0 +1,195 @@
|
|||
import React, { Suspense } from "react"
|
||||
import Markdown from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CopyButton } from "@/components/ui/copy-button"
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
children: string
|
||||
}
|
||||
|
||||
export function MarkdownRenderer({ children }: MarkdownRendererProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Markdown remarkPlugins={[remarkGfm]} components={COMPONENTS}>
|
||||
{children}
|
||||
</Markdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface HighlightedPre extends React.HTMLAttributes<HTMLPreElement> {
|
||||
children: string
|
||||
language: string
|
||||
}
|
||||
|
||||
const HighlightedPre = React.memo(
|
||||
async ({ children, language, ...props }: HighlightedPre) => {
|
||||
const { codeToTokens, bundledLanguages } = await import("shiki")
|
||||
|
||||
if (!(language in bundledLanguages)) {
|
||||
return <pre {...props}>{children}</pre>
|
||||
}
|
||||
|
||||
const { tokens } = await codeToTokens(children, {
|
||||
lang: language as keyof typeof bundledLanguages,
|
||||
defaultColor: false,
|
||||
themes: {
|
||||
light: "github-light",
|
||||
dark: "github-dark",
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<pre {...props}>
|
||||
<code>
|
||||
{tokens.map((line, lineIndex) => (
|
||||
<>
|
||||
<span key={lineIndex}>
|
||||
{line.map((token, tokenIndex) => {
|
||||
const style =
|
||||
typeof token.htmlStyle === "string"
|
||||
? undefined
|
||||
: token.htmlStyle
|
||||
|
||||
return (
|
||||
<span
|
||||
key={tokenIndex}
|
||||
className="text-shiki-light bg-shiki-light-bg dark:text-shiki-dark dark:bg-shiki-dark-bg"
|
||||
style={style}
|
||||
>
|
||||
{token.content}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
{lineIndex !== tokens.length - 1 && "\n"}
|
||||
</>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
)
|
||||
HighlightedPre.displayName = "HighlightedCode"
|
||||
|
||||
interface CodeBlockProps extends React.HTMLAttributes<HTMLPreElement> {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
language: string
|
||||
}
|
||||
|
||||
const CodeBlock = ({
|
||||
children,
|
||||
className,
|
||||
language,
|
||||
...restProps
|
||||
}: CodeBlockProps) => {
|
||||
const code =
|
||||
typeof children === "string"
|
||||
? children
|
||||
: childrenTakeAllStringContents(children)
|
||||
|
||||
const preClass = cn(
|
||||
"overflow-x-scroll rounded-md border bg-background/50 p-4 font-mono text-sm [scrollbar-width:none]",
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/code relative mb-4">
|
||||
<Suspense
|
||||
fallback={
|
||||
<pre className={preClass} {...restProps}>
|
||||
{children}
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
<HighlightedPre language={language} className={preClass}>
|
||||
{code}
|
||||
</HighlightedPre>
|
||||
</Suspense>
|
||||
|
||||
<div className="invisible absolute right-2 top-2 flex space-x-1 rounded-lg p-1 opacity-0 transition-all duration-200 group-hover/code:visible group-hover/code:opacity-100">
|
||||
<CopyButton content={code} copyMessage="Copied code to clipboard" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function childrenTakeAllStringContents(element: any): string {
|
||||
if (typeof element === "string") {
|
||||
return element
|
||||
}
|
||||
|
||||
if (element?.props?.children) {
|
||||
let children = element.props.children
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
return children
|
||||
.map((child) => childrenTakeAllStringContents(child))
|
||||
.join("")
|
||||
} else {
|
||||
return childrenTakeAllStringContents(children)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
const COMPONENTS = {
|
||||
h1: withClass("h1", "text-2xl font-semibold"),
|
||||
h2: withClass("h2", "font-semibold text-xl"),
|
||||
h3: withClass("h3", "font-semibold text-lg"),
|
||||
h4: withClass("h4", "font-semibold text-base"),
|
||||
h5: withClass("h5", "font-medium"),
|
||||
strong: withClass("strong", "font-semibold"),
|
||||
a: withClass("a", "text-primary underline underline-offset-2"),
|
||||
blockquote: withClass("blockquote", "border-l-2 border-primary pl-4"),
|
||||
code: ({ children, className, node, ...rest }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
return match ? (
|
||||
<CodeBlock className={className} language={match[1]} {...rest}>
|
||||
{children}
|
||||
</CodeBlock>
|
||||
) : (
|
||||
<code
|
||||
className={cn(
|
||||
"font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:bg-background/50 [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5"
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
pre: ({ children }: any) => children,
|
||||
ol: withClass("ol", "list-decimal space-y-2 pl-6"),
|
||||
ul: withClass("ul", "list-disc space-y-2 pl-6"),
|
||||
li: withClass("li", "my-1.5"),
|
||||
table: withClass(
|
||||
"table",
|
||||
"w-full border-collapse overflow-y-auto rounded-md border border-foreground/20"
|
||||
),
|
||||
th: withClass(
|
||||
"th",
|
||||
"border border-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
),
|
||||
td: withClass(
|
||||
"td",
|
||||
"border border-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
),
|
||||
tr: withClass("tr", "m-0 border-t p-0 even:bg-muted"),
|
||||
p: withClass("p", "whitespace-pre-wrap"),
|
||||
hr: withClass("hr", "border-foreground/20"),
|
||||
}
|
||||
|
||||
function withClass(Tag: keyof JSX.IntrinsicElements, classes: string) {
|
||||
const Component = ({ node, ...props }: any) => (
|
||||
<Tag className={classes} {...props} />
|
||||
)
|
||||
Component.displayName = Tag
|
||||
return Component
|
||||
}
|
||||
|
||||
export default MarkdownRenderer
|
Loading…
Add table
Add a link
Reference in a new issue