Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
supportedArchitectures:
Copy link
Member

Choose a reason for hiding this comment

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

ooc, why did you add this?

Copy link
Author

Choose a reason for hiding this comment

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

ah, this is a stray from troubleshooting my dev environment. removing!

cpu:
- x64
- arm64
os:
- darwin
- linux
- win32
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const createArtifactContent = (
title: toolCall?.title,
code: toolCall?.artifact,
language: toolCall?.language as ProgrammingLanguageOptions,
isValidReact: toolCall?.isValidReact
};
}

Expand Down
6 changes: 6 additions & 0 deletions apps/agents/src/open-canvas/nodes/rewrite-artifact/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@ export const OPTIONALLY_UPDATE_ARTIFACT_META_SCHEMA = z
.describe(
"The language of the code artifact. This should be populated with the programming language if the user is requesting code to be written, or 'other', in all other cases."
),
isValidReact: z
.boolean()
.optional()
.describe(
"Whether or not the generated code is valid React code. Only populate this field if generating code."
),
})
.describe("Update the artifact meta information, if necessary.");
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const createNewArtifactContent = ({
currentArtifactContent
) as ProgrammingLanguageOptions,
code: newContent,
isValidReact: artifactMetaToolCall.isValidReact
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const rewriteCodeArtifactTheme = async (
// Ensure the new artifact's language is updated, if necessary
language: state.portLanguage || currentArtifactContent.language,
code: artifactContentText,
isValidReact: currentArtifactContent.isValidReact
};

const newArtifact: ArtifactV3 = {
Expand Down
5 changes: 4 additions & 1 deletion apps/agents/src/open-canvas/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const DEFAULT_CODE_PROMPT_RULES = `- Do NOT include triple backticks when generating code. The code should be in plain text.`;
const DEFAULT_CODE_PROMPT_RULES = `
- If writing React code with style information, remember to put all CSS in a style element.
- Do NOT include triple backticks when generating code. The code should be in plain text.
`;

const APP_CONTEXT = `
<app-context>
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"react-dom": "^18",
"react-icons": "^5.3.0",
"react-json-view": "^1.21.3",
"react-live": "^4.1.8",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.1.7",
"react-syntax-highlighter": "^15.5.0",
Expand Down
39 changes: 39 additions & 0 deletions apps/web/src/components/artifacts/CodePreviewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ArtifactCodeV3 } from "@opencanvas/shared/types";
import { LiveProvider, LivePreview, LiveError } from "react-live";
import { cn } from "@/lib/utils";
import { getPreviewCode } from "@/lib/get_preview_code";
import { motion } from "framer-motion";

export interface CodePreviewerProps {
isExpanded: boolean;
artifact: ArtifactCodeV3;
}

export function CodePreviewer({ isExpanded, artifact }: CodePreviewerProps) {
const cleanedCode = getPreviewCode(artifact.code);

return (
<motion.div
layout
animate={{
flex: isExpanded ? 0.5 : 0,
opacity: isExpanded ? 1 : 0,
}}
initial={false}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className={cn(
"border-l-[1px] border-gray-200 h-screen overflow-hidden",
isExpanded ? "px-5" : ""
)}
>
<div className="w-full h-full">
{isExpanded && (
<LiveProvider noInline code={`${cleanedCode}`}>
<LivePreview />
<LiveError className="text-red-800 bg-red-100 rounded p-4" />
</LiveProvider>
)}
</div>
</motion.div>
);
}
123 changes: 100 additions & 23 deletions apps/web/src/components/artifacts/CodeRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ArtifactCodeV3 } from "@opencanvas/shared/types";
import React, { MutableRefObject, useEffect } from "react";
import React, {
Dispatch,
MutableRefObject,
SetStateAction,
useEffect,
useState,
} from "react";
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { cpp } from "@codemirror/lang-cpp";
Expand All @@ -19,7 +25,10 @@ import { cn } from "@/lib/utils";
import { CopyText } from "./components/CopyText";
import { getArtifactContent } from "@opencanvas/shared/utils/artifacts";
import { useGraphContext } from "@/contexts/GraphContext";

import { motion } from "framer-motion";
import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button";
import { PanelRightOpen, PanelRightClose } from "lucide-react";
import { CodePreviewer } from "./CodePreviewer";
export interface CodeRendererProps {
editorRef: MutableRefObject<EditorView | null>;
isHovering: boolean;
Expand Down Expand Up @@ -58,6 +67,46 @@ const getLanguageExtension = (language: string) => {
}
};

export interface ToggleCodePreviewProps {
isCodePreviewVisible: boolean;
setIsCodePreviewVisible: Dispatch<SetStateAction<boolean>>;
codePreviewDisabled: boolean;
isStreaming: boolean;
}

function ToggleCodePreview({
isCodePreviewVisible,
setIsCodePreviewVisible,
codePreviewDisabled,
}: ToggleCodePreviewProps) {
const tooltipContent = codePreviewDisabled
? "Code preview is only supported for valid React code"
: `${isCodePreviewVisible ? "Hide" : "Show"} code preview`;

return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
>
<TooltipIconButton
tooltip={tooltipContent}
variant="outline"
delayDuration={400}
disabled={codePreviewDisabled}
onClick={() => setIsCodePreviewVisible((p) => !p)}
>
{isCodePreviewVisible ? (
<PanelRightClose className="w-5 h-5 text-gray-600" />
) : (
<PanelRightOpen className="w-5 h-5 text-gray-600" />
)}
</TooltipIconButton>
</motion.div>
);
}

export function CodeRendererComponent(props: Readonly<CodeRendererProps>) {
const { graphData } = useGraphContext();
const {
Expand All @@ -68,13 +117,20 @@ export function CodeRendererComponent(props: Readonly<CodeRendererProps>) {
setArtifactContent,
setUpdateRenderedArtifactRequired,
} = graphData;
const [isCodePreviewVisible, setIsCodePreviewVisible] = useState(false);

useEffect(() => {
if (updateRenderedArtifactRequired) {
setUpdateRenderedArtifactRequired(false);
}
}, [updateRenderedArtifactRequired]);

useEffect(() => {
if (isStreaming) {
setIsCodePreviewVisible(false);
}
}, [isStreaming]);

if (!artifact) {
return null;
}
Expand All @@ -89,7 +145,7 @@ export function CodeRendererComponent(props: Readonly<CodeRendererProps>) {
const isEditable = !isStreaming;

return (
<div className="relative">
<motion.div layout className="flex flex-row w-full overflow-hidden">
<style jsx global>{`
.pulse-code .cm-content {
animation: codePulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
Expand All @@ -105,27 +161,48 @@ export function CodeRendererComponent(props: Readonly<CodeRendererProps>) {
}
}
`}</style>
{props.isHovering && (
<div className="absolute top-0 right-4 z-10">
<CopyText currentArtifactContent={artifactContent} />
</div>
)}
<CodeMirror
editable={isEditable}
className={cn(
"w-full min-h-full",
styles.codeMirrorCustom,
isStreaming && !firstTokenReceived ? "pulse-code" : ""
)}
value={cleanContent(artifactContent.code)}
height="800px"
extensions={extensions}
onChange={(c) => setArtifactContent(artifactContent.index, c)}
onCreateEditor={(view) => {
props.editorRef.current = view;
<motion.div
layout
className="relative overflow-hidden"
animate={{
flex: isCodePreviewVisible ? 0.5 : 1,
}}
/>
</div>
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
{props.isHovering && (
<div className="absolute flex gap-2 top-2 right-4 z-10">
<CopyText currentArtifactContent={artifactContent} />
{!isStreaming && <ToggleCodePreview
isCodePreviewVisible={isCodePreviewVisible}
setIsCodePreviewVisible={setIsCodePreviewVisible}
codePreviewDisabled={!artifactContent.isValidReact}
isStreaming={isStreaming}
/>}
</div>
)}
<CodeMirror
editable={isEditable}
className={cn(
"w-full min-h-full",
styles.codeMirrorCustom,
isStreaming && !firstTokenReceived ? "pulse-code" : ""
)}
value={cleanContent(artifactContent.code)}
height="800px"
extensions={extensions}
onChange={(c) => setArtifactContent(artifactContent.index, c)}
onCreateEditor={(view) => {
props.editorRef.current = view;
}}
/>
</motion.div>
{!isStreaming && artifactContent.isValidReact && (
<CodePreviewer
artifact={artifactContent}
isExpanded={isCodePreviewVisible}
/>
)}
</motion.div>
);
}

Expand Down
22 changes: 12 additions & 10 deletions apps/web/src/components/ui/assistant-ui/tooltip-icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,18 @@ export const TooltipIconButton = forwardRef<
<TooltipProvider>
<Tooltip delayDuration={delayDuration ?? 700}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
{...rest}
className={cn("size-6 p-1", className)}
ref={ref}
>
{children}
<span className="sr-only">{tooltip}</span>
</Button>
<span>
<Button
variant="ghost"
size="icon"
{...rest}
className={cn("size-6 p-1", className)}
ref={ref}
>
{children}
<span className="sr-only">{tooltip}</span>
</Button>
</span>
</TooltipTrigger>
<TooltipContent side={side}>{tooltip}</TooltipContent>
</Tooltip>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/contexts/GraphContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
type: artifactType,
title: prevCurrentContent.title,
language: artifactLanguage,
isValidReact: isArtifactCodeContent(artifact) ? artifact.isValidReact : undefined
},
prevCurrentContent,
newArtifactIndex,
Expand Down Expand Up @@ -1154,6 +1155,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
type: artifactType,
title: prevCurrentContent.title,
language: artifactLanguage,
isValidReact: isArtifactCodeContent(artifact) ? artifact.isValidReact : undefined
},
prevCurrentContent,
newArtifactIndex,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/contexts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const createNewGeneratedArtifactFromTool = (
title: artifactTool.title || "",
code: artifactTool.artifact || "",
language: artifactTool.language as ProgrammingLanguageOptions,
isValidReact: artifactTool.isValidReact
};
}
};
Expand Down Expand Up @@ -272,6 +273,7 @@ export const updateRewrittenArtifact = ({
index: currentIndex,
language: artifactLanguage as ProgrammingLanguageOptions,
code: newArtifactContent,
isValidReact: rewriteArtifactMeta.isValidReact
},
];
} else {
Expand Down
37 changes: 37 additions & 0 deletions apps/web/src/lib/get_preview_code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const reactImportRegex =
/^import\s+(React(?:,\s*\{[^}]+\})?|\{[^}]+\})\s+from\s+['"]react['"];?/gm;

export const getPreviewCode = (code: string): string => {
const matches = Array.from(code.matchAll(reactImportRegex));
const namedBindings = new Set<string>();

// Strip any import statements from the generated code
for (const match of matches) {
const imported = match[1];

if (imported.includes("{")) {
const names = imported
.replace(/React,?/, "")
.replace(/[{}]/g, "")
.split(",")
.map((name) => name.trim());

names.forEach((n) => namedBindings.add(n));
}
}

let transformed = code.replace(reactImportRegex, "").trim();

namedBindings.forEach((name) => {
const usageRegex = new RegExp(`\\b${name}\\b`, "g");
transformed = transformed.replace(usageRegex, `React.${name}`);
});

// Replace "export default X" with "render(X)" to display
transformed = transformed.replace(
/export\s+default\s+([a-zA-Z_$][\w$]*)\s*;/,
"render($1);"
);

return transformed;
};
3 changes: 3 additions & 0 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface ArtifactToolResponse {
title?: string;
language?: string;
type?: string;
isValidReact?: boolean;
}

export type RewriteArtifactMetaToolResponse =
Expand All @@ -66,6 +67,7 @@ export type RewriteArtifactMetaToolResponse =
type: "code";
title: string;
language: ProgrammingLanguageOptions;
isValidReact?: boolean
};

export type LanguageOptions =
Expand Down Expand Up @@ -116,6 +118,7 @@ export interface ArtifactCodeV3 {
title: string;
language: ProgrammingLanguageOptions;
code: string;
isValidReact?: boolean;
}

export interface ArtifactV3 {
Expand Down
Loading