Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 42 additions & 61 deletions components/ai-elements/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { createContext, useContext, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useTheme } from "../theme-provider";

type CodeBlockContextType = {
code: string;
Expand All @@ -30,69 +31,49 @@ export const CodeBlock = ({
className,
children,
...props
}: CodeBlockProps) => (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"bg-background text-foreground relative w-full overflow-hidden rounded-md border",
className
)}
{...props}
>
<div className="relative">
<SyntaxHighlighter
className="overflow-hidden dark:hidden"
codeTagProps={{
className: "font-mono text-sm",
}}
customStyle={{
margin: 0,
padding: "1rem",
fontSize: "0.875rem",
background: "hsl(var(--background))",
color: "hsl(var(--foreground))",
}}
language={language}
lineNumberStyle={{
color: "hsl(var(--muted-foreground))",
paddingRight: "1rem",
minWidth: "2.5rem",
}}
showLineNumbers={showLineNumbers}
style={oneLight}
>
{code}
</SyntaxHighlighter>
<SyntaxHighlighter
className="hidden overflow-hidden dark:block"
codeTagProps={{
className: "font-mono text-sm",
}}
customStyle={{
margin: 0,
padding: "1rem",
fontSize: "0.875rem",
background: "hsl(var(--background))",
color: "hsl(var(--foreground))",
}}
language={language}
lineNumberStyle={{
color: "hsl(var(--muted-foreground))",
paddingRight: "1rem",
minWidth: "2.5rem",
}}
showLineNumbers={showLineNumbers}
style={oneDark}
>
{code}
</SyntaxHighlighter>
{children && (
<div className="absolute top-2 right-2 flex items-center gap-2">{children}</div>
}: CodeBlockProps) => {
const { theme } = useTheme();
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"bg-background text-foreground relative w-full overflow-hidden rounded-md border",
className
)}
{...props}
>
<div className="relative">
<SyntaxHighlighter
className="overflow-hidden"
codeTagProps={{
className: "font-mono text-sm",
}}
customStyle={{
margin: 0,
padding: "1rem",
fontSize: "0.875rem",
background: "hsl(var(--background))",
color: "hsl(var(--foreground))",
}}
language={language}
lineNumberStyle={{
color: "hsl(var(--muted-foreground))",
paddingRight: "1rem",
minWidth: "2.5rem",
}}
showLineNumbers={showLineNumbers}
style={theme === "light" ? oneLight : oneDark}
>
Comment on lines +35 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use resolved theme when selecting syntax style

When the app is set to “system”, theme stays "system" so this branch always falls back to oneDark, showing a dark code theme even for system-light users. Switch to the resolved theme so the highlight style matches the actual UI mode.

-  const { theme } = useTheme();
+  const { resolvedTheme } = useTheme();-            style={theme === "light" ? oneLight : oneDark}
+            style={resolvedTheme === "light" ? oneLight : oneDark}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { theme } = useTheme();
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"bg-background text-foreground relative w-full overflow-hidden rounded-md border",
className
)}
{...props}
>
<div className="relative">
<SyntaxHighlighter
className="overflow-hidden"
codeTagProps={{
className: "font-mono text-sm",
}}
customStyle={{
margin: 0,
padding: "1rem",
fontSize: "0.875rem",
background: "hsl(var(--background))",
color: "hsl(var(--foreground))",
}}
language={language}
lineNumberStyle={{
color: "hsl(var(--muted-foreground))",
paddingRight: "1rem",
minWidth: "2.5rem",
}}
showLineNumbers={showLineNumbers}
style={theme === "light" ? oneLight : oneDark}
>
const { resolvedTheme } = useTheme();
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"bg-background text-foreground relative w-full overflow-hidden rounded-md border",
className
)}
{...props}
>
<div className="relative">
<SyntaxHighlighter
className="overflow-hidden"
codeTagProps={{
className: "font-mono text-sm",
}}
customStyle={{
margin: 0,
padding: "1rem",
fontSize: "0.875rem",
background: "hsl(var(--background))",
color: "hsl(var(--foreground))",
}}
language={language}
lineNumberStyle={{
color: "hsl(var(--muted-foreground))",
paddingRight: "1rem",
minWidth: "2.5rem",
}}
showLineNumbers={showLineNumbers}
style={resolvedTheme === "light" ? oneLight : oneDark}
>
🤖 Prompt for AI Agents
In components/ai-elements/code-block.tsx around lines 35–66, the component
currently uses theme (which can be "system") to pick the SyntaxHighlighter style
causing a dark theme for system-light users; update the hook call to get the
resolved theme (e.g., const { theme, resolvedTheme } = useTheme()) and use
resolvedTheme when selecting the style (use oneLight when resolvedTheme ===
"light", otherwise oneDark), then remove reliance on raw "theme" for this
decision so the syntax highlighting matches the actual UI mode.

{code}
</SyntaxHighlighter>
{children && (
<div className="absolute top-2 right-2 flex items-center gap-2">{children}</div>
)}
</div>
</div>
</div>
</CodeBlockContext.Provider>
);
</CodeBlockContext.Provider>
);
};

export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void;
Expand Down
176 changes: 176 additions & 0 deletions components/block-viewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"use client";

import { ComponentErrorBoundary } from "@/components/error-boundary";
import { TooltipWrapper } from "@/components/tooltip-wrapper";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { cn } from "@/lib/utils";
import { Monitor, Smartphone, Tablet } from "lucide-react";
import React from "react";
import { ImperativePanelHandle } from "react-resizable-panels";

type BlockViewerContext = {
resizablePanelRef: React.RefObject<ImperativePanelHandle | null>;
toggleValue: string;
setToggleValue: (value: string) => void;
};

const BlockViewerContext = React.createContext<BlockViewerContext | null>(null);

function useBlockViewer() {
const context = React.useContext(BlockViewerContext);
if (!context) {
throw new Error("useBlockViewer must be used within a BlockViewerProvider.");
}
return context;
}

export function BlockViewerProvider({ children }: { children: React.ReactNode }) {
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null);
const [toggleValue, setToggleValue] = React.useState("100");
Comment on lines +29 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the ref typing to accept null.

React.useRef<ImperativePanelHandle>(null) fails under strictNullChecks because null isn’t assignable to that generic. Widen the type so the provider compiles.

-  const resizablePanelRef = React.useRef<ImperativePanelHandle>(null);
+  const resizablePanelRef = React.useRef<ImperativePanelHandle | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null);
const [toggleValue, setToggleValue] = React.useState("100");
const resizablePanelRef = React.useRef<ImperativePanelHandle | null>(null);
const [toggleValue, setToggleValue] = React.useState("100");
🤖 Prompt for AI Agents
In components/block-viewer.tsx around lines 29 to 30, the useRef is typed as
React.useRef<ImperativePanelHandle>(null) which fails under strictNullChecks;
change the ref type to allow null (e.g. use React.useRef<ImperativePanelHandle |
null>(null) or the equivalent MutableRefObject type) so the initial null value
is assignable and the provider compiles.


return (
<BlockViewerContext.Provider
value={{
resizablePanelRef,
toggleValue,
setToggleValue,
}}
>
{children}
</BlockViewerContext.Provider>
);
}

export function BlockViewer({
className,
name,
children,
...props
}: React.ComponentPropsWithoutRef<"div"> & {
name: string;
}) {
return (
<BlockViewerProvider>
<div
className={cn(
"group/block-view-wrapper bg-background @container isolate flex size-full min-w-0 flex-col overflow-clip",
className
)}
{...props}
>
<BlockViewerToolbar name={name} />
<BlockViewerDisplay name={name}>{children}</BlockViewerDisplay>
</div>
</BlockViewerProvider>
);
}

export function BlockViewerToolbar({
name,
toolbarControls,
}: {
name: string;
toolbarControls?: React.ReactNode;
}) {
const { resizablePanelRef, toggleValue, setToggleValue } = useBlockViewer();

return (
<div className="h-12 w-full border-b p-2">
<div className="flex size-full items-center justify-between gap-4">
<div className="flex-1">
{!!toolbarControls ? (
toolbarControls
) : (
<span className="text-sm font-medium capitalize">{name}</span>
)}
</div>

<div className="flex items-center justify-between max-lg:hidden">
<ToggleGroup
className="flex gap-0.5 rounded-md border p-0.5"
type="single"
value={toggleValue}
onValueChange={(value) => {
if (value && resizablePanelRef?.current) {
resizablePanelRef.current.resize(parseInt(value));
setToggleValue(value);
}
}}
>
<ToggleGroupItem value="100" className="size-6 p-1" asChild>
<TooltipWrapper label="Desktop view">
<Monitor className="size-3.5" />
</TooltipWrapper>
</ToggleGroupItem>

<ToggleGroupItem value="60" className="size-6 p-1" asChild>
<TooltipWrapper label="Tablet view">
<Tablet className="size-3.5" />
</TooltipWrapper>
</ToggleGroupItem>

<ToggleGroupItem value="30" className="size-6 p-1" asChild>
<TooltipWrapper label="Mobile view">
<Smartphone className="size-3.5" />
</TooltipWrapper>
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
</div>
);
}

export function BlockViewerDisplay({
name,
className,
children,
...props
}: React.ComponentPropsWithoutRef<"div"> & {
name: string;
}) {
const { resizablePanelRef, setToggleValue } = useBlockViewer();

// Auto-resize to full width when screen goes under lg breakpoint (1024px)
React.useEffect(() => {
const mql = window.matchMedia("(max-width: 1023px)");
const resizePanel = () => {
if (window.innerWidth < 1024 && resizablePanelRef?.current) {
resizablePanelRef.current.resize(100);
setToggleValue("100");
}
};

resizePanel();
mql.addEventListener("change", resizePanel);
return () => mql.removeEventListener("change", resizePanel);
}, [resizablePanelRef, setToggleValue]);

return (
<ComponentErrorBoundary name={name}>
<div
id={name}
data-name={name.toLowerCase()}
className={cn("grid w-full grow gap-4 overflow-clip", className)}
{...props}
>
<ResizablePanelGroup direction="horizontal" className="relative isolate z-10">
<div className="bg-muted absolute inset-0 right-3 [background-image:radial-gradient(var(--muted-foreground),transparent_1px)] [background-size:1rem_1rem] opacity-60 dark:opacity-35" />

<ResizablePanel
ref={resizablePanelRef}
className="bg-background relative lg:aspect-auto lg:min-w-[350px]"
defaultSize={100}
minSize={30}
>
{children}
</ResizablePanel>

<ResizableHandle className="after:bg-border relative inset-x-0 mx-auto hidden w-3 border-l bg-transparent after:absolute after:top-1/2 after:h-8 after:w-[4px] after:-translate-y-1/2 after:rounded-full after:transition-all hover:after:h-12 active:after:h-12 lg:block" />
<ResizablePanel defaultSize={0} minSize={0} />
</ResizablePanelGroup>
</div>
</ComponentErrorBoundary>
);
}
Loading