docs: changes to AI assistant icon + add button to code blocks (#11227)

* docs: changes to AI assistant icon + add button to code blocks

* fix code blocks in questions

* collapse code block in answer

* styling fixes

* fix new line at cursor position
This commit is contained in:
Shahed Nasser
2025-01-30 14:29:22 +02:00
committed by GitHub
parent 386ce4b857
commit be33a56290
21 changed files with 294 additions and 97 deletions
@@ -12,7 +12,7 @@ export const AiAssistantChatWindowFooter = () => {
)}
>
<span className="text-medusa-fg-muted">Chat is cleared on refresh</span>
<span className="h-full w-px bg-medusa-border-base"></span>
<span className="h-docs_0.75 w-px bg-medusa-border-base"></span>
<div className="flex items-center gap-docs_0.5">
<span className="text-medusa-fg-subtle">Line break</span>
<div className="flex items-center gap-[5px]">
@@ -23,7 +23,9 @@ export const AiAssistantChatWindowHeader = () => {
<Tooltip
tooltipChildren={
<>
This site is protected by reCAPTCHA and the{" "}
This site is protected by reCAPTCHA and
<br />
the{" "}
<Link href="https://policies.google.com/privacy">
Google Privacy Policy
</Link>{" "}
@@ -32,6 +34,7 @@ export const AiAssistantChatWindowHeader = () => {
</>
}
clickable={true}
tooltipClassName={"!text-compact-small-plus"}
>
<ShieldCheck className="text-medusa-fg-muted" />
</Tooltip>
@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from "react"
import clsx from "clsx"
import { useAiAssistantChat } from "../../../../providers/AiAssistant/Chat"
import { ArrowUpCircleSolid } from "@medusajs/icons"
import { useAiAssistant } from "../../../../providers"
export const AiAssistantChatWindowInput = () => {
const {
@@ -12,6 +13,7 @@ export const AiAssistantChatWindowInput = () => {
loading,
getThreadItems,
} = useAiAssistantChat()
const { chatOpened } = useAiAssistant()
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
@@ -33,7 +35,17 @@ export const AiAssistantChatWindowInput = () => {
return
}
if (e.shiftKey) {
setQuestion((prev) => `${prev}\n`)
const { selectionStart, selectionEnd } = e.currentTarget
setQuestion(
(prev) =>
`${prev.substring(0, selectionStart)}\n${prev.substring(selectionEnd)}`
)
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = inputRef.current.selectionEnd =
selectionStart + 1
}
}, 0)
} else {
onSubmit()
}
@@ -52,6 +64,7 @@ export const AiAssistantChatWindowInput = () => {
useEffect(() => {
adjustTextareaHeight()
inputRef.current?.focus()
}, [question])
const handleTouch = (e: React.TouchEvent<HTMLTextAreaElement>) => {
@@ -61,6 +74,19 @@ export const AiAssistantChatWindowInput = () => {
})
}
useEffect(() => {
if (!chatOpened || !inputRef.current) {
return
}
const isCursorAtEnd =
inputRef.current.selectionStart === inputRef.current.value.length
if (isCursorAtEnd) {
inputRef.current.scrollTop = inputRef.current.scrollHeight
}
}, [chatOpened, inputRef.current])
return (
<div
className={clsx(
@@ -1,6 +1,13 @@
import clsx from "clsx"
import React from "react"
import { AiAssistantIcon, DotsLoading, MarkdownContent } from "@/components"
import {
AiAssistantIcon,
CodeMdx,
CodeMdxProps,
DotsLoading,
MarkdownContent,
MDXComponents,
} from "@/components"
import { AiAssistantThreadItemActions } from "./Actions"
import { AiAssistantThread } from "../../../providers/AiAssistant/Chat"
@@ -27,7 +34,7 @@ export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => {
"txt-small text-medusa-fg-base",
item.type === "question" && [
"rounded-docs_xl bg-medusa-tag-neutral-bg",
"px-docs_0.75 py-docs_0.5",
"px-docs_0.75 py-docs_0.5 max-w-full",
],
item.type !== "question" && "flex-1",
item.type === "answer" && "text-pretty flex-1 max-w-[calc(100%-20px)]"
@@ -38,6 +45,39 @@ export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => {
className="[&>*:last-child]:mb-0"
allowedElements={["br", "p", "code", "pre"]}
unwrapDisallowed={true}
components={{
...MDXComponents,
code: (props: CodeMdxProps) => {
return (
<CodeMdx
{...props}
noCopy
noReport
forceNoTitle
noAskAi
inlineCodeProps={{
...props.inlineCodeProps,
className: "!text-wrap !break-words",
variant: "grey-bg",
}}
collapsibleLines="11"
codeBlockProps={{
className: clsx(
"rounded-docs_lg p-[5px]",
props.className
),
wrapperClassName: "rounded-docs_lg",
innerClassName: "border rounded-docs_lg",
overrideColors: {
bg: "bg-medusa-contrast-bg-subtle",
innerBg: "bg-medusa-contrast-bg-subtle",
innerBorder: "border-medusa-contrast-border-bot",
},
}}
/>
)
},
}}
>
{item.content}
</MarkdownContent>
@@ -45,7 +85,15 @@ export const AiAssistantThreadItem = ({ item }: AiAssistantThreadItemProps) => {
{item.type === "answer" && (
<div className="flex flex-col gap-docs_0.75">
{!item.question_id && item.content.length === 0 && <DotsLoading />}
<MarkdownContent className="[&>*:last-child]:mb-0">
<MarkdownContent
className="[&>*:last-child]:mb-0"
components={{
...MDXComponents,
code: (props: CodeMdxProps) => {
return <CodeMdx {...props} noReport noAskAi />
},
}}
>
{item.content}
</MarkdownContent>
{item.question_id && <AiAssistantThreadItemActions item={item} />}
@@ -1,6 +1,6 @@
"use client"
import React, { useMemo, useState } from "react"
import React from "react"
import { Button } from "../../Button"
import { Tooltip } from "../../Tooltip"
import { Kbd } from "../../Kbd"
@@ -9,17 +9,13 @@ import { useAiAssistant, useSearch, useSiteConfig } from "../../../providers"
import { useKeyboardShortcut } from "../../../hooks"
import Image from "next/image"
const AI_ASSISTANT_ICON = "/images/ai-assistent-luminosity.png"
// const AI_ASSISTANT_ICON = "/images/ai-assistent-luminosity.png"
const AI_ASSISTANT_ICON_ACTIVE = "/images/ai-assistent.png"
export const AiAssistantTriggerButton = () => {
const [hovered, setHovered] = useState(false)
const { config } = useSiteConfig()
const { chatOpened, setChatOpened } = useAiAssistant()
const { setChatOpened } = useAiAssistant()
const { setIsOpen } = useSearch()
const isActive = useMemo(() => {
return hovered || chatOpened
}, [hovered, chatOpened])
const osShortcut = getOsShortcut()
useKeyboardShortcut({
@@ -35,36 +31,29 @@ export const AiAssistantTriggerButton = () => {
return (
<Tooltip
render={() => (
<span className="flex gap-[6px] items-center">
<span className="text-compact-x-small-plus text-medusa-fg-base">
Ask AI
</span>
<span className="flex gap-[5px] items-center">
<Kbd className="bg-medusa-bg-field-component border-medusa-border-strong w-[18px] h-[18px] inline-block">
{osShortcut}
</Kbd>
<Kbd className="bg-medusa-bg-field-component border-medusa-border-strong w-[18px] h-[18px] inline-block">
i
</Kbd>
</span>
<span className="flex gap-[5px] items-center">
<Kbd className="bg-medusa-bg-field-component border-medusa-border-strong w-[18px] h-[18px] inline-block">
{osShortcut}
</Kbd>
<Kbd className="bg-medusa-bg-field-component border-medusa-border-strong w-[18px] h-[18px] inline-block">
i
</Kbd>
</span>
)}
>
<Button
variant="transparent-clear"
className="!p-[6.5px]"
onMouseOver={() => setHovered(true)}
onMouseOut={() => setHovered(false)}
onTouchStart={() => setHovered(true)}
onTouchEnd={() => setHovered(false)}
onClick={() => setChatOpened((prev) => !prev)}
>
<Image
src={`${config.basePath}${isActive ? AI_ASSISTANT_ICON_ACTIVE : AI_ASSISTANT_ICON}`}
src={`${config.basePath}${AI_ASSISTANT_ICON_ACTIVE}`}
width={15}
height={15}
alt="AI Assistant"
/>
<span className="hidden md:inline-block text-medusa-fg-subtle">
Ask AI
</span>
</Button>
</Tooltip>
)
@@ -0,0 +1,59 @@
"use client"
import React from "react"
import { useAiAssistant, useSiteConfig } from "../../../../providers"
import { useAiAssistantChat } from "../../../../providers/AiAssistant/Chat"
import clsx from "clsx"
import { Tooltip } from "../../../Tooltip"
import Image from "next/image"
export type CodeBlockCopyActionProps = {
source: string
inHeader: boolean
}
export const CodeBlockAskAiAction = ({
source,
inHeader,
}: CodeBlockCopyActionProps) => {
const { setChatOpened } = useAiAssistant()
const { setQuestion, loading } = useAiAssistantChat()
const { config } = useSiteConfig()
const handleClick = () => {
if (loading) {
return
}
setQuestion(`\`\`\`tsx\n${source.trim()}\n\`\`\`\n\nExplain the code above`)
setChatOpened(true)
}
return (
<Tooltip
text="Ask AI"
tooltipClassName="font-base"
className={clsx("group")}
innerClassName={clsx(
inHeader && "flex",
"h-fit rounded-docs_sm",
"group-hover:bg-medusa-contrast-bg-base-hover group-focus:bg-medusa-contrast-bg-base-hover"
)}
>
<span
className={clsx(
!inHeader && "p-[6px]",
inHeader && "p-[4.5px]",
"cursor-pointer"
)}
onClick={handleClick}
>
<Image
src={`${config.basePath}/images/ai-assistent-luminosity.png`}
width={15}
height={15}
alt="Ask AI"
/>
</span>
</Tooltip>
)
}
@@ -1,3 +1,5 @@
"use client"
import React, { useEffect, useState } from "react"
import { CopyButton } from "../../../.."
import clsx from "clsx"
@@ -6,6 +6,7 @@ import { Link, Tooltip } from "@/components"
import { ExclamationCircle, PlaySolid } from "@medusajs/icons"
import { GITHUB_ISSUES_LINK } from "@/constants"
import { CodeBlockCopyAction } from "./Copy"
import { CodeBlockAskAiAction } from "./AskAi"
export type CodeBlockActionsProps = {
source: string
@@ -74,6 +75,7 @@ export const CodeBlockActions = ({
]
)}
>
<CodeBlockAskAiAction source={source} inHeader={inHeader} />
{canShowApiTesting && (
<Tooltip
text="Test API"
@@ -38,9 +38,13 @@ export type CodeBlockMetaFields = {
noCopy?: boolean
noReport?: boolean
noLineNumbers?: boolean
noAskAi?: boolean
collapsibleLines?: string
expandButtonLabel?: string
isTerminal?: boolean
forceNoTitle?: boolean
collapsed?: boolean
wrapperClassName?: string
} & CodeBlockHeaderMeta
export type CodeBlockStyle = "loud" | "subtle" | "inline"
@@ -48,14 +52,11 @@ export type CodeBlockStyle = "loud" | "subtle" | "inline"
export type CodeBlockProps = {
source: string
lang?: string
wrapperClassName?: string
innerClassName?: string
className?: string
collapsed?: boolean
blockStyle?: CodeBlockStyle
children?: React.ReactNode
style?: React.HTMLAttributes<HTMLDivElement>["style"]
forceNoTitle?: boolean
animateTokenHighlights?: boolean
overrideColors?: {
bg?: string
@@ -277,6 +278,7 @@ export const CodeBlock = ({
getCollapsedLinesElm,
getNonCollapsedLinesElm,
type: collapsibleType,
isCollapsible,
...collapsibleResult
} = useCollapsibleCodeLines({
collapsibleLinesStr: collapsibleLines,
@@ -462,7 +464,7 @@ export const CodeBlock = ({
isSingleLine={tokens.length <= 1}
/>
)}
{collapsibleType === "end" && (
{collapsibleType === "end" && isCollapsible(tokens) && (
<>
<CodeBlockCollapsibleFade
type={collapsibleType}
@@ -2,7 +2,9 @@ import React from "react"
import {
CodeBlock,
CodeBlockMetaFields,
CodeBlockProps,
InlineCode,
InlineCodeProps,
MermaidDiagram,
} from "@/components"
import { Npm2YarnCode } from "../Npm2YarnCode"
@@ -10,13 +12,21 @@ import { Npm2YarnCode } from "../Npm2YarnCode"
export type CodeMdxProps = {
className?: string
children?: React.ReactNode
inlineCodeProps?: Partial<InlineCodeProps>
codeBlockProps?: Partial<CodeBlockProps>
} & CodeBlockMetaFields
// due to how mdx handles code blocks
// it is required that a code block specify a language
// to be considered a block. Otherwise, it will be
// considered as inline code
export const CodeMdx = ({ className, children, ...rest }: CodeMdxProps) => {
export const CodeMdx = ({
className,
children,
inlineCodeProps = {},
codeBlockProps = {},
...rest
}: CodeMdxProps) => {
if (!children) {
return <></>
}
@@ -33,8 +43,15 @@ export const CodeMdx = ({ className, children, ...rest }: CodeMdxProps) => {
} else if (match[1] === "mermaid") {
return <MermaidDiagram diagramContent={codeContent} />
}
return <CodeBlock source={codeContent} lang={match[1]} {...rest} />
return (
<CodeBlock
source={codeContent}
lang={match[1]}
{...codeBlockProps}
{...rest}
/>
)
}
return <InlineCode>{codeContent}</InlineCode>
return <InlineCode {...inlineCodeProps}>{codeContent}</InlineCode>
}
@@ -4,9 +4,14 @@ import React from "react"
import clsx from "clsx"
import { CopyButton } from "@/components"
export type InlineCodeProps = React.ComponentProps<"code">
export type InlineCodeProps = React.ComponentProps<"code"> & {
variant?: "default" | "grey-bg"
}
export const InlineCode = (props: InlineCodeProps) => {
export const InlineCode = ({
variant = "default",
...props
}: InlineCodeProps) => {
return (
<CopyButton
text={props.children as string}
@@ -18,11 +23,18 @@ export const InlineCode = (props: InlineCodeProps) => {
<code
{...props}
className={clsx(
"border-medusa-tag-neutral-border border",
"text-medusa-tag-neutral-text",
"bg-medusa-tag-neutral-bg font-monospace text-code-label rounded-docs_sm py-0 px-[6px]",
"group-active:bg-medusa-bg-subtle-pressed group-focus:bg-medusa-bg-subtle-pressed",
"group-hover:bg-medusa-tag-neutral-bg-hover",
"text-medusa-tag-neutral-text border",
"font-monospace text-code-label rounded-docs_sm py-0 px-[5px]",
variant === "default" && [
"bg-medusa-tag-neutral-bg group-hover:bg-medusa-tag-neutral-bg-hover",
"group-active:bg-medusa-bg-subtle-pressed group-focus:bg-medusa-bg-subtle-pressed",
"border-medusa-tag-neutral-border",
],
variant === "grey-bg" && [
"bg-medusa-bg-switch-off group-hover:bg-medusa-bg-switch-off-hover",
"group-active:bg-medusa-bg-switch-off-hover group-focus:bg-medusa-switch-off-hover",
"border-medusa-border-strong",
],
props.className
)}
/>