mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-12 15:55:03 -05:00
Compare commits
3 Commits
fix/claude
...
toran/secr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d534b63f45 | ||
|
|
5af960ae3b | ||
|
|
4763e99307 |
106
rnd/autogpt_builder/src/components/ArtifactRenderer.tsx
Normal file
106
rnd/autogpt_builder/src/components/ArtifactRenderer.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
|
|
||||||
|
const ArtifactRenderer = ({ artifactData }) => {
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
const [iframeHeight, setIframeHeight] = useState('300px');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = Array.isArray(artifactData) && artifactData.length > 0 ? artifactData[0] : artifactData;
|
||||||
|
if (data && (data.type === 'image/svg+xml' || data.type === 'text/html')) {
|
||||||
|
const resizeIframe = () => {
|
||||||
|
if (iframeRef.current && iframeRef.current.contentWindow) {
|
||||||
|
const height = iframeRef.current.contentWindow.document.body.scrollHeight;
|
||||||
|
setIframeHeight(`${height + 20}px`); // Add a small buffer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resize on load and after a short delay (for any dynamic content)
|
||||||
|
if (iframeRef.current) {
|
||||||
|
iframeRef.current.onload = resizeIframe;
|
||||||
|
setTimeout(resizeIframe, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [artifactData]);
|
||||||
|
|
||||||
|
if (!artifactData) {
|
||||||
|
console.error("No artifact data received");
|
||||||
|
return <div>No artifact data received</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Array.isArray(artifactData) && artifactData.length > 0 ? artifactData[0] : artifactData;
|
||||||
|
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
console.error("Invalid artifact data structure:", artifactData);
|
||||||
|
return <div>Error: Invalid artifact data structure</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, title, content, language } = data;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
console.error("Artifact type is missing:", data);
|
||||||
|
return <div>Error: Artifact type is missing</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'image/png':
|
||||||
|
case 'image/jpeg':
|
||||||
|
case 'image/gif':
|
||||||
|
return <img src={content} alt={title} style={{ maxWidth: '100%', height: 'auto' }} />;
|
||||||
|
case 'application/vnd.agpt.code':
|
||||||
|
return (
|
||||||
|
<SyntaxHighlighter language={language || 'text'} style={tomorrow}>
|
||||||
|
{content}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
);
|
||||||
|
case 'text/markdown':
|
||||||
|
return <ReactMarkdown>{content}</ReactMarkdown>;
|
||||||
|
case 'text/html':
|
||||||
|
case 'image/svg+xml':
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
srcDoc={`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_blank">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
svg, img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>${content}</body>
|
||||||
|
</html>
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: iframeHeight,
|
||||||
|
border: 'none',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <p>Unsupported artifact type: {type}</p>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="artifact-renderer" style={{ width: '100%', overflow: 'hidden' }}>
|
||||||
|
<h3>{title || 'Untitled Artifact'}</h3>
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtifactRenderer;
|
||||||
@@ -23,6 +23,8 @@ import { history } from "./history";
|
|||||||
import NodeHandle from "./NodeHandle";
|
import NodeHandle from "./NodeHandle";
|
||||||
import { CustomEdgeData } from "./CustomEdge";
|
import { CustomEdgeData } from "./CustomEdge";
|
||||||
import { NodeGenericInputField } from "./node-input-components";
|
import { NodeGenericInputField } from "./node-input-components";
|
||||||
|
import ArtifactRenderer from './ArtifactRenderer';
|
||||||
|
import VideoRendererBlock from './VideoRendererBlock';
|
||||||
|
|
||||||
type ParsedKey = { key: string; index?: number };
|
type ParsedKey = { key: string; index?: number };
|
||||||
|
|
||||||
@@ -275,6 +277,44 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||||||
console.log("Copy node:", id);
|
console.log("Copy node:", id);
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const renderOutput = () => {
|
||||||
|
console.log("CustomNode renderOutput, full data:", data);
|
||||||
|
console.log("CustomNode renderOutput, output_data:", data.output_data);
|
||||||
|
|
||||||
|
switch (data.block_id) {
|
||||||
|
case "7a8b9c0d-1e2f-3g4h-5i6j-7k8l9m0n1o2p":
|
||||||
|
return <ArtifactRenderer artifactData={data.output_data.artifact_data} />;
|
||||||
|
case "a92a0017-2390-425f-b5a8-fb3c50c81400":
|
||||||
|
return <VideoRendererBlock data={data} />;
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="node-output" onClick={handleOutputClick}>
|
||||||
|
<p>
|
||||||
|
<strong>Status:</strong>{" "}
|
||||||
|
{typeof data.status === "object"
|
||||||
|
? JSON.stringify(data.status)
|
||||||
|
: data.status || "N/A"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Output Data:</strong>{" "}
|
||||||
|
{(() => {
|
||||||
|
const outputText =
|
||||||
|
typeof data.output_data === "object"
|
||||||
|
? JSON.stringify(data.output_data)
|
||||||
|
: data.output_data;
|
||||||
|
|
||||||
|
if (!outputText) return "No output data";
|
||||||
|
|
||||||
|
return outputText.length > 100
|
||||||
|
? `${outputText.slice(0, 100)}... Press To Read More`
|
||||||
|
: outputText;
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`custom-node dark-theme border rounded-xl shandow-md bg-white/[.8] ${data.status?.toLowerCase() ?? ""}`}
|
className={`custom-node dark-theme border rounded-xl shandow-md bg-white/[.8] ${data.status?.toLowerCase() ?? ""}`}
|
||||||
@@ -349,31 +389,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
|||||||
{data.outputSchema && generateOutputHandles(data.outputSchema)}
|
{data.outputSchema && generateOutputHandles(data.outputSchema)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isOutputOpen && (
|
{isOutputOpen && renderOutput()}
|
||||||
<div className="node-output" onClick={handleOutputClick}>
|
|
||||||
<p>
|
|
||||||
<strong>Status:</strong>{" "}
|
|
||||||
{typeof data.status === "object"
|
|
||||||
? JSON.stringify(data.status)
|
|
||||||
: data.status || "N/A"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Output Data:</strong>{" "}
|
|
||||||
{(() => {
|
|
||||||
const outputText =
|
|
||||||
typeof data.output_data === "object"
|
|
||||||
? JSON.stringify(data.output_data)
|
|
||||||
: data.output_data;
|
|
||||||
|
|
||||||
if (!outputText) return "No output data";
|
|
||||||
|
|
||||||
return outputText.length > 100
|
|
||||||
? `${outputText.slice(0, 100)}... Press To Read More`
|
|
||||||
: outputText;
|
|
||||||
})()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center pl-4 pb-4 mt-2.5">
|
<div className="flex items-center pl-4 pb-4 mt-2.5">
|
||||||
<Switch onCheckedChange={toggleOutput} />
|
<Switch onCheckedChange={toggleOutput} />
|
||||||
<span className="m-1 mr-4">Output</span>
|
<span className="m-1 mr-4">Output</span>
|
||||||
|
|||||||
45
rnd/autogpt_builder/src/components/VideoRendererBlock.tsx
Normal file
45
rnd/autogpt_builder/src/components/VideoRendererBlock.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface VideoRendererProps {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoRendererBlock: React.FC<VideoRendererProps> = ({ data }) => {
|
||||||
|
// Extract video URL from the correct location in the data structure
|
||||||
|
const videoUrl = data.hardcodedValues?.video_url ||
|
||||||
|
(Array.isArray(data.output_data?.video_url) && data.output_data.video_url[0]);
|
||||||
|
|
||||||
|
if (!videoUrl || typeof videoUrl !== 'string') {
|
||||||
|
return <div>Invalid or missing video URL</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getYouTubeVideoId = (url: string) => {
|
||||||
|
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||||
|
const match = url.match(regExp);
|
||||||
|
return (match && match[7].length === 11) ? match[7] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const videoId = getYouTubeVideoId(videoUrl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', padding: '10px' }}>
|
||||||
|
{videoId ? (
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="315"
|
||||||
|
src={`https://www.youtube.com/embed/${videoId}`}
|
||||||
|
frameBorder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
) : (
|
||||||
|
<video controls width="100%" height="315">
|
||||||
|
<source src={videoUrl} type="video/mp4" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoRendererBlock;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||||
|
from autogpt_server.data.model import SchemaField
|
||||||
|
import re
|
||||||
|
import base64
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
class ArtifactRendererBlock(Block):
|
||||||
|
class Input(BlockSchema):
|
||||||
|
artifact_string: str = SchemaField(description="The input string containing an AGPT artifact to be rendered.")
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
artifact_data: dict = SchemaField(description="Processed artifact data for frontend rendering.")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="7a8b9c0d-1e2f-3g4h-5i6j-7k8l9m0n1o2p",
|
||||||
|
description="Processes an AGPT artifact for visual rendering within the block.",
|
||||||
|
categories={BlockCategory.TEXT, BlockCategory.BASIC},
|
||||||
|
input_schema=ArtifactRendererBlock.Input,
|
||||||
|
output_schema=ArtifactRendererBlock.Output,
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_artifact(self, artifact_string):
|
||||||
|
pattern = r'<agptArtifact\s+(.*?)>(.*?)</agptArtifact>'
|
||||||
|
match = re.search(pattern, artifact_string, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
attributes = dict(re.findall(r'(\w+)="([^"]*)"', match.group(1)))
|
||||||
|
content = match.group(2).strip()
|
||||||
|
return attributes, content
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def process_artifact(self, attributes, content):
|
||||||
|
artifact_type = attributes.get('type', '')
|
||||||
|
title = attributes.get('title', 'Untitled Artifact')
|
||||||
|
identifier = attributes.get('identifier', '')
|
||||||
|
language = attributes.get('language', '')
|
||||||
|
|
||||||
|
processed_data = {
|
||||||
|
'type': artifact_type,
|
||||||
|
'title': title,
|
||||||
|
'identifier': identifier,
|
||||||
|
'language': language,
|
||||||
|
'content': content
|
||||||
|
}
|
||||||
|
|
||||||
|
if artifact_type.startswith('image/'):
|
||||||
|
processed_data['content'] = f"data:{artifact_type};base64,{content}"
|
||||||
|
elif artifact_type == 'text/markdown':
|
||||||
|
# Send markdown as plain text, don't convert to HTML
|
||||||
|
processed_data['content'] = content
|
||||||
|
elif artifact_type == 'application/vnd.agpt.code':
|
||||||
|
# Keep the content as is for code snippets
|
||||||
|
pass
|
||||||
|
elif artifact_type == 'text/html' or artifact_type == 'image/svg+xml':
|
||||||
|
# Keep HTML and SVG content as is
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
processed_data['content'] = content
|
||||||
|
|
||||||
|
return processed_data
|
||||||
|
|
||||||
|
def run(self, input_data: Input) -> BlockOutput:
|
||||||
|
attributes, content = self.parse_artifact(input_data.artifact_string)
|
||||||
|
if attributes and content:
|
||||||
|
processed_data = self.process_artifact(attributes, content)
|
||||||
|
yield "artifact_data", processed_data
|
||||||
|
else:
|
||||||
|
yield "artifact_data", {"error": "Invalid artifact format"}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||||
|
from autogpt_server.data.model import SchemaField
|
||||||
|
|
||||||
|
class VideoRendererBlock(Block):
|
||||||
|
class Input(BlockSchema):
|
||||||
|
video_url: str = SchemaField(description="The URL of the video to be rendered.")
|
||||||
|
|
||||||
|
class Output(BlockSchema):
|
||||||
|
video_url: str = SchemaField(description="The URL of the video to be rendered.")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
id="a92a0017-2390-425f-b5a8-fb3c50c81400",
|
||||||
|
description="Renders a video from a given URL within the block.",
|
||||||
|
input_schema=VideoRendererBlock.Input,
|
||||||
|
output_schema=VideoRendererBlock.Output
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self, input_data: Input) -> BlockOutput:
|
||||||
|
yield "video_url", input_data.video_url
|
||||||
Reference in New Issue
Block a user