-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
3,308 additions
and
910 deletions.
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 |
---|---|---|
@@ -1,3 +1,13 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
"extends": [ | ||
"next/core-web-vitals", | ||
"eslint:recommended", | ||
"plugin:@typescript-eslint/recommended" | ||
], | ||
"rules": { | ||
"eqeqeq": "error", | ||
"no-unused-vars": "error", | ||
"no-console": "warn", | ||
"@typescript-eslint/no-explicit-any": "error" | ||
} | ||
} |
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,257 @@ | ||
"use client"; | ||
|
||
import { Button, Collapsible, ContentMessage, Spinner } from "@dust-tt/sparkle"; | ||
import { | ||
VisualizationRPCCommand, | ||
VisualizationRPCRequest, | ||
} from "@dust-tt/types"; | ||
import * as papaparseAll from "papaparse"; | ||
import * as reactAll from "react"; | ||
import React from "react"; | ||
import { useEffect, useMemo, useState } from "react"; | ||
import { importCode, Runner } from "react-runner"; | ||
import {} from "react-runner"; | ||
import * as rechartsAll from "recharts"; | ||
|
||
function isFileResult(res: unknown): res is { file: File } { | ||
return ( | ||
typeof res === "object" && | ||
res !== null && | ||
"file" in res && | ||
res.file instanceof File | ||
); | ||
} | ||
|
||
// This is a hook provided to the code generator model to fetch a file from the conversation. | ||
function useFile(actionId: string, fileId: string) { | ||
const [file, setFile] = useState<File | null>(null); | ||
const actionIdParsed = useMemo(() => parseInt(actionId, 10), [actionId]); | ||
|
||
useEffect(() => { | ||
if (!fileId) { | ||
return; | ||
} | ||
|
||
const getFileContent = async () => { | ||
const getFile = makeIframeMessagePassingFunction< | ||
{ fileId: string }, | ||
{ code: string } | ||
>("getFile", actionIdParsed); | ||
|
||
const res = await getFile({ fileId }); | ||
if (!isFileResult(res)) { | ||
return; | ||
} | ||
|
||
const { file } = res; | ||
|
||
setFile(file); | ||
}; | ||
|
||
getFileContent(); | ||
}, [actionIdParsed, fileId]); | ||
|
||
return file; | ||
} | ||
|
||
// This function creates a function that sends a command to the host window with templated Input and Output types. | ||
function makeIframeMessagePassingFunction<Params, Answer>( | ||
methodName: VisualizationRPCCommand, | ||
actionId: number | ||
) { | ||
return (params?: Params) => { | ||
return new Promise<Answer>((resolve, reject) => { | ||
const messageUniqueId = Math.random().toString(); | ||
const listener = (event: MessageEvent) => { | ||
if (event.data.messageUniqueId === messageUniqueId) { | ||
if (event.data.error) { | ||
reject(event.data.error); | ||
} else { | ||
resolve(event.data.result); | ||
} | ||
window.removeEventListener("message", listener); | ||
} | ||
}; | ||
window.addEventListener("message", listener); | ||
window.top?.postMessage( | ||
{ | ||
command: methodName, | ||
messageUniqueId, | ||
actionId, | ||
params, | ||
} satisfies VisualizationRPCRequest, | ||
"*" | ||
); | ||
}); | ||
}; | ||
} | ||
|
||
// This component renders the generated code. | ||
// It gets the generated code via message passing to the host window. | ||
export function VisualizationWrapper({ actionId }: { actionId: string }) { | ||
const [code, setCode] = useState<string | null>(null); | ||
const [errored, setErrored] = useState<Error | null>(null); | ||
const useFileWrapped = (fileId: string) => useFile(actionId, fileId); | ||
|
||
useEffect(() => { | ||
// Get the code to execute. | ||
const getCodeToExecute = makeIframeMessagePassingFunction< | ||
{ actionId: string }, | ||
{ code: string } | ||
>("getCodeToExecute", parseInt(actionId, 10)); | ||
|
||
const fetchCode = async () => { | ||
try { | ||
const result = await getCodeToExecute({ actionId }); | ||
const regex = /<visualization[^>]*>\s*([\s\S]*?)\s*<\/visualization>/; | ||
let extractedCode: string | null = null; | ||
const match = result.code.match(regex); | ||
if (match && match[1]) { | ||
extractedCode = match[1]; | ||
setCode(extractedCode); | ||
} else { | ||
setErrored(new Error("No visualization code found")); | ||
} | ||
} catch (error) { | ||
console.error(error); | ||
} | ||
}; | ||
|
||
fetchCode(); | ||
}, [actionId]); | ||
|
||
// This retry function sends the "retry" instruction to the host window, to retry an agent message | ||
// in case the generated code does not work or is not satisfying. | ||
const retry = useMemo(() => { | ||
return makeIframeMessagePassingFunction("retry", parseInt(actionId, 10)); | ||
}, [actionId]); | ||
|
||
if (errored) { | ||
return <VisualizationError error={errored} retry={() => retry()} />; | ||
} | ||
|
||
if (!code) { | ||
return <Spinner variant="color" size="xxl" />; | ||
} | ||
|
||
const generatedCodeScope = { | ||
recharts: rechartsAll, | ||
react: reactAll, | ||
papaparse: papaparseAll, | ||
"@dust/react-hooks": { useFile: useFileWrapped }, | ||
}; | ||
|
||
const scope = { | ||
import: { | ||
recharts: rechartsAll, | ||
react: reactAll, | ||
// Here we expose the code generated as a module to be imported by the wrapper code below. | ||
"@dust/generated-code": importCode(code, { import: generatedCodeScope }), | ||
}, | ||
}; | ||
|
||
// This code imports and renders the generated code. | ||
const wrapperCode = ` | ||
() => { | ||
import Comp from '@dust/generated-code'; | ||
return (<Comp />); | ||
} | ||
`; | ||
|
||
return ( | ||
<Runner | ||
code={wrapperCode} | ||
scope={scope} | ||
onRendered={(error) => { | ||
if (error) { | ||
setErrored(error); | ||
} | ||
}} | ||
/> | ||
); | ||
} | ||
|
||
// This is the component to render when an error occurs. | ||
function VisualizationError({ | ||
error, | ||
retry, | ||
}: { | ||
error: Error; | ||
retry: () => void; | ||
}) { | ||
return ( | ||
<> | ||
<div className="flex w-full flex-col items-center justify-center gap-4"> | ||
<div> | ||
<ContentMessage title="Error" variant="pink"> | ||
We encountered an error while running the code generated above. You | ||
can try again by clicking the button below. | ||
<Collapsible> | ||
<Collapsible.Button label="Show details" /> | ||
<Collapsible.Panel> | ||
<div className="s-flex s-h-16 s-w-full s-items-center s-justify-center s-bg-slate-200"> | ||
Error messsage: | ||
{error.message} | ||
</div> | ||
</Collapsible.Panel> | ||
</Collapsible> | ||
</ContentMessage> | ||
</div> | ||
<div> | ||
<Button label="Retry" onClick={retry} /> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
type ErrorBoundaryProps = { | ||
actionId: string; | ||
}; | ||
|
||
type ErrorBoundaryState = { | ||
hasError: boolean; | ||
error: unknown; | ||
activeTab: "code" | "runtime"; | ||
}; | ||
|
||
// This is the error boundary component that wraps the VisualizationWrapper component. | ||
// It needs to be a class component for error handling to work. | ||
export class VisualizationWrapperWithErrorHandling extends React.Component< | ||
ErrorBoundaryProps, | ||
ErrorBoundaryState | ||
> { | ||
constructor(props: ErrorBoundaryProps) { | ||
super(props); | ||
this.state = { hasError: false, error: null, activeTab: "code" }; | ||
} | ||
|
||
static getDerivedStateFromError() { | ||
// Update state so the next render will show the fallback UI. | ||
return { hasError: true }; | ||
} | ||
|
||
componentDidCatch(error: unknown) { | ||
this.setState({ hasError: true, error }); | ||
} | ||
|
||
render() { | ||
if (this.state.hasError) { | ||
// You can render any custom fallback UI | ||
let error: Error; | ||
if (this.state.error instanceof Error) { | ||
error = this.state.error; | ||
} else { | ||
error = new Error("Unknown error"); | ||
} | ||
const retry = makeIframeMessagePassingFunction( | ||
"retry", | ||
parseInt(this.props.actionId, 10) | ||
); | ||
return <VisualizationError error={error} retry={() => retry} />; | ||
} | ||
|
||
return <VisualizationWrapper actionId={this.props.actionId} />; | ||
} | ||
} |
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,22 @@ | ||
import type { Metadata } from "next"; | ||
import { Inter } from "next/font/google"; | ||
import "../globals.css"; | ||
|
||
const inter = Inter({ subsets: ["latin"] }); | ||
|
||
export const metadata: Metadata = { | ||
title: "Dust Visualization", | ||
description: "Dust Visualization", | ||
}; | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
return ( | ||
<html lang="en"> | ||
<body className={inter.className}>{children}</body> | ||
</html> | ||
); | ||
} |
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,16 @@ | ||
import { VisualizationIframeContentWithErrorHandling } from "@viz/components/VisualizationIframeContent"; | ||
|
||
type IframeProps = { | ||
wId: string; | ||
aId: string; | ||
}; | ||
|
||
export default function Iframe({ | ||
searchParams, | ||
}: { | ||
searchParams: IframeProps; | ||
}) { | ||
return ( | ||
<VisualizationIframeContentWithErrorHandling actionId={searchParams.aId} /> | ||
); | ||
} |
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 |
---|---|---|
@@ -1,4 +1,25 @@ | ||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = {}; | ||
const isDev = process.env.NODE_ENV === "development"; | ||
|
||
const nextConfig = { | ||
async headers() { | ||
return [ | ||
{ | ||
source: "/:path*", | ||
headers: [ | ||
{ | ||
key: "Access-Control-Allow-Origin", | ||
value: isDev ? "http://localhost:3000" : "https://dust.tt", | ||
}, | ||
], | ||
}, | ||
// Allow CORS for static files. | ||
{ | ||
source: "/_next/static/:path*", | ||
headers: [{ key: "Access-Control-Allow-Origin", value: "*" }], | ||
}, | ||
]; | ||
}, | ||
}; | ||
|
||
export default nextConfig; |
Oops, something went wrong.