mirror of
https://github.com/microsoft/autogen.git
synced 2026-02-15 18:35:03 -05:00
v1 of AutoGen Studio on AgentChat (#4097)
* add skeleton worflow manager * add test notebook * update test nb * add sample team spec * refactor requirements to agentchat and ext * add base provider to return agentchat agents from json spec * initial api refactor, update dbmanager * api refactor * refactor tests * ags api tutorial update * ui refactor * general refactor * minor refactor updates * backend api refaactor * ui refactor and update * implement v1 for streaming connection with ui updates * backend refactor * ui refactor * minor ui tweak * minor refactor and tweaks * general refactor * update tests * sync uv.lock with main * uv lock update
This commit is contained in:
@@ -1,873 +0,0 @@
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
Cog8ToothIcon,
|
||||
XMarkIcon,
|
||||
ClipboardIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import Icon from "./icons";
|
||||
import { Modal, Table, Tooltip, theme } from "antd";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import Papa from "papaparse";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { truncateText } from "./utils";
|
||||
|
||||
const { useToken } = theme;
|
||||
interface CodeProps {
|
||||
node?: any;
|
||||
inline?: any;
|
||||
className?: any;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
children?: ReactNode;
|
||||
title?: string | ReactNode;
|
||||
subtitle?: string | ReactNode;
|
||||
count?: number;
|
||||
active?: boolean;
|
||||
cursor?: string;
|
||||
icon?: ReactNode;
|
||||
padding?: string;
|
||||
className?: string;
|
||||
open?: boolean;
|
||||
hoverable?: boolean;
|
||||
onClick?: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const SectionHeader = ({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
count,
|
||||
icon,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<div id="section-header" className="mb-4">
|
||||
<h1 className="text-primary text-2xl">
|
||||
{/* {count !== null && <span className="text-accent mr-1">{count}</span>} */}
|
||||
{icon && <>{icon}</>}
|
||||
{title}
|
||||
{count !== null && (
|
||||
<span className="text-accent mr-1 ml-2 text-xs">{count}</span>
|
||||
)}
|
||||
</h1>
|
||||
{subtitle && <span className="inline-block">{subtitle}</span>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconButton = ({
|
||||
onClick,
|
||||
icon,
|
||||
className,
|
||||
active = false,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<span
|
||||
id="icon-button"
|
||||
role={"button"}
|
||||
onClick={onClick}
|
||||
className={`inline-block mr-2 hover:text-accent transition duration-300 ${className} ${
|
||||
active ? "border-accent border rounded text-accent" : ""
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const LaunchButton = ({
|
||||
children,
|
||||
onClick,
|
||||
className = "p-3 px-5 ",
|
||||
}: any) => {
|
||||
return (
|
||||
<button
|
||||
id="launch-button"
|
||||
role={"button"}
|
||||
className={` focus:ring ring-accent ring-l-none rounded cursor-pointer hover:brightness-110 bg-accent transition duration-500 text-white ${className} `}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecondaryButton = ({ children, onClick, className }: any) => {
|
||||
return (
|
||||
<button
|
||||
id="secondary-button"
|
||||
role={"button"}
|
||||
className={` ${className} focus:ring ring-accent p-2 px-5 rounded cursor-pointer hover:brightness-90 bg-secondary transition duration-500 text-primary`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
hoverable = true,
|
||||
active,
|
||||
cursor = "cursor-pointer",
|
||||
className = "p-3",
|
||||
onClick,
|
||||
}: IProps) => {
|
||||
let border = active
|
||||
? "border-accent"
|
||||
: "border-secondary hover:border-accent ";
|
||||
border = hoverable ? border : "border-secondary";
|
||||
|
||||
return (
|
||||
<button
|
||||
id="card"
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
role={"button"}
|
||||
className={`${border} border-2 bg-secondary group ${className} w-full text-left rounded ${cursor} transition duration-300`}
|
||||
>
|
||||
<div className="mt- text-sm text-secondary h-full break-words">
|
||||
{title && (
|
||||
<div className="text-accent rounded font-semibold text-xs pb-1">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div>{subtitle}</div>
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollapseBox = ({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
className = " p-3",
|
||||
open = false,
|
||||
}: IProps) => {
|
||||
const [isOpen, setIsOpen] = React.useState<boolean>(open);
|
||||
const chevronClass = "h-4 cursor-pointer inline-block mr-1";
|
||||
return (
|
||||
<div
|
||||
id="collapse-box"
|
||||
onMouseDown={(e) => {
|
||||
if (e.detail > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className="bordper border-secondary rounded"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className={`cursor-pointer bg-secondary p-2 rounded ${
|
||||
isOpen ? "rounded-b-none " : " "
|
||||
}"}`}
|
||||
>
|
||||
{isOpen && <ChevronUpIcon className={chevronClass} />}
|
||||
{!isOpen && <ChevronDownIcon className={chevronClass} />}
|
||||
|
||||
<span className=" inline-block -mt-2 mb-2 text-xs">
|
||||
{" "}
|
||||
{/* {isOpen ? "hide" : "show"} section | */}
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className={`${className} bg-tertiary rounded rounded-t-none`}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HighLight = ({ children }: IProps) => {
|
||||
return <span id="highlight" className="border-b border-accent">{children}</span>;
|
||||
};
|
||||
|
||||
export const LoadBox = ({
|
||||
subtitle,
|
||||
className = "my-2 text-accent ",
|
||||
}: IProps) => {
|
||||
return (
|
||||
<div id="load-box" className={`${className} `}>
|
||||
<span className="mr-2 ">
|
||||
{" "}
|
||||
<Icon size={5} icon="loading" />
|
||||
</span>{" "}
|
||||
{subtitle}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingBar = ({ children }: IProps) => {
|
||||
return (
|
||||
<>
|
||||
<div id="loading-bar" className="rounded bg-secondary p-3">
|
||||
<span className="inline-block h-6 w-6 relative mr-2">
|
||||
<Cog8ToothIcon className="animate-ping text-accent absolute inline-flex h-full w-full rounded-ful opacity-75" />
|
||||
<Cog8ToothIcon className="relative text-accent animate-spin inline-flex rounded-full h-6 w-6" />
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="loadbar rounded-b"></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MessageBox = ({ title, children, className }: IProps) => {
|
||||
const messageBox = useRef<HTMLDivElement>(null);
|
||||
|
||||
const closeMessage = () => {
|
||||
if (messageBox.current) {
|
||||
messageBox.current.remove();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="message-box"
|
||||
ref={messageBox}
|
||||
className={`${className} p-3 rounded bg-secondary transition duration-1000 ease-in-out overflow-hidden`}
|
||||
>
|
||||
{" "}
|
||||
<div className="flex gap-2 mb-2">
|
||||
<div className="flex-1">
|
||||
{/* <span className="mr-2 text-accent">
|
||||
<InformationCircleIcon className="h-6 w-6 inline-block" />
|
||||
</span>{" "} */}
|
||||
<span className="font-semibold text-primary text-base">{title}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
closeMessage();
|
||||
}}
|
||||
className=" border border-secondary bg-secondary brightness-125 hover:brightness-100 cursor-pointer transition duration-200 inline-block px-1 pb-1 rounded text-primary"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4 inline-block" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GroupView = ({
|
||||
children,
|
||||
title,
|
||||
className = "text-primary bg-primary ",
|
||||
}: any) => {
|
||||
return (
|
||||
<div id="group-view" className={`rounded mt-4 border-secondary ${className}`}>
|
||||
<div className="mt-4 p-2 rounded border relative">
|
||||
<div className={`absolute -top-3 inline-block ${className}`}>
|
||||
{title}
|
||||
</div>
|
||||
<div className="mt-2"> {children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandView = ({
|
||||
children,
|
||||
icon = null,
|
||||
className = "",
|
||||
title = "Detail View",
|
||||
}: any) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
let windowAspect = 1;
|
||||
if (typeof window !== "undefined") {
|
||||
windowAspect = window.innerWidth / window.innerHeight;
|
||||
}
|
||||
const minImageWidth = 400;
|
||||
return (
|
||||
<div
|
||||
id="expand-view"
|
||||
style={{
|
||||
minHeight: "100px",
|
||||
}}
|
||||
className={`h-full rounded mb-6 border-secondary ${className}`}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
className="text-xs mb-2 h-full w-full break-words"
|
||||
>
|
||||
{icon ? icon : children}
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
title={title}
|
||||
width={800}
|
||||
open={isOpen}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
footer={null}
|
||||
>
|
||||
{/* <ResizableBox
|
||||
// handle={<span className="text-accent">resize</span>}
|
||||
lockAspectRatio={false}
|
||||
handle={
|
||||
<div className="absolute right-0 bottom-0 cursor-se-resize font-semibold boprder p-3 bg-secondary">
|
||||
<ArrowDownRightIcon className="h-4 w-4 inline-block" />
|
||||
</div>
|
||||
}
|
||||
width={800}
|
||||
height={minImageWidth * windowAspect}
|
||||
minConstraints={[minImageWidth, minImageWidth * windowAspect]}
|
||||
maxConstraints={[900, 900 * windowAspect]}
|
||||
className="overflow-auto w-full rounded select-none "
|
||||
> */}
|
||||
{children}
|
||||
{/* </ResizableBox> */}
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingOverlay = ({ children, loading }: IProps) => {
|
||||
return (
|
||||
<>
|
||||
{loading && (
|
||||
<>
|
||||
<div
|
||||
id="loading-overlay"
|
||||
className="absolute inset-0 bg-secondary flex pointer-events-none"
|
||||
style={{ opacity: 0.5 }}
|
||||
>
|
||||
{/* Overlay background */}
|
||||
</div>
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{ pointerEvents: "none" }}
|
||||
>
|
||||
{/* Center BounceLoader without inheriting the opacity */}
|
||||
<BounceLoader />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="relative">{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarkdownView = ({
|
||||
data,
|
||||
className = "",
|
||||
showCode = true,
|
||||
}: {
|
||||
data: string;
|
||||
className?: string;
|
||||
showCode?: boolean;
|
||||
}) => {
|
||||
function processString(inputString: string): string {
|
||||
// TODO: Had to add this temp measure while debugging. Why is it null?
|
||||
if (!inputString) {
|
||||
console.log("inputString is null!")
|
||||
}
|
||||
inputString = inputString && inputString.replace(/\n/g, " \n");
|
||||
const markdownPattern = /```markdown\s+([\s\S]*?)\s+```/g;
|
||||
return inputString?.replace(markdownPattern, (match, content) => content);
|
||||
}
|
||||
const [showCopied, setShowCopied] = React.useState(false);
|
||||
|
||||
const CodeView = ({ props, children, language }: any) => {
|
||||
const [codeVisible, setCodeVisible] = React.useState(showCode);
|
||||
return (
|
||||
<div>
|
||||
<div className=" flex ">
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setCodeVisible(!codeVisible);
|
||||
}}
|
||||
className=" flex-1 mr-4 "
|
||||
>
|
||||
{!codeVisible && (
|
||||
<div className=" text-white hover:text-accent duration-300">
|
||||
<ChevronDownIcon className="inline-block w-5 h-5" />
|
||||
<span className="text-xs"> show</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{codeVisible && (
|
||||
<div className=" text-white hover:text-accent duration-300">
|
||||
{" "}
|
||||
<ChevronUpIcon className="inline-block w-5 h-5" />
|
||||
<span className="text-xs"> hide</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="flex-1"></div> */}
|
||||
<div>
|
||||
{showCopied && (
|
||||
<div className="inline-block text-sm text-white">
|
||||
{" "}
|
||||
🎉 Copied!{" "}
|
||||
</div>
|
||||
)}
|
||||
<ClipboardIcon
|
||||
role={"button"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data);
|
||||
// message.success("Code copied to clipboard");
|
||||
setShowCopied(true);
|
||||
setTimeout(() => {
|
||||
setShowCopied(false);
|
||||
}, 3000);
|
||||
}}
|
||||
className=" inline-block duration-300 text-white hover:text-accent w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{codeVisible && (
|
||||
<SyntaxHighlighter
|
||||
{...props}
|
||||
style={atomDark}
|
||||
language={language}
|
||||
className="rounded w-full"
|
||||
PreTag="div"
|
||||
wrapLongLines={true}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="markdown-view"
|
||||
className={` w-full chatbox prose dark:prose-invert text-primary rounded ${className}`}
|
||||
>
|
||||
<ReactMarkdown
|
||||
className=" w-full"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }: CodeProps) {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const language = match ? match[1] : "text";
|
||||
return !inline && match ? (
|
||||
<CodeView props={props} children={children} language={language} />
|
||||
) : (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{processString(data)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICodeProps {
|
||||
code: string;
|
||||
language: string;
|
||||
title?: string;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string | undefined;
|
||||
wrapLines?: boolean;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
minHeight?: string;
|
||||
}
|
||||
|
||||
export const CodeBlock = ({
|
||||
code,
|
||||
language = "python",
|
||||
showLineNumbers = false,
|
||||
className = " ",
|
||||
wrapLines = false,
|
||||
maxHeight = "400px",
|
||||
minHeight = "auto",
|
||||
}: ICodeProps) => {
|
||||
const codeString = code;
|
||||
|
||||
const [showCopied, setShowCopied] = React.useState(false);
|
||||
return (
|
||||
<div id="code-block" className="relative">
|
||||
<div className=" rounded absolute right-5 top-4 z-10 ">
|
||||
<div className="relative border border-transparent w-full h-full">
|
||||
<div
|
||||
style={{ zIndex: -1 }}
|
||||
className="w-full absolute top-0 h-full bg-gray-900 hover:bg-opacity-0 duration-300 bg-opacity-50 rounded"
|
||||
></div>
|
||||
<div className=" ">
|
||||
{showCopied && (
|
||||
<div className="inline-block px-2 pl-3 text-white">
|
||||
{" "}
|
||||
🎉 Copied!{" "}
|
||||
</div>
|
||||
)}
|
||||
<ClipboardIcon
|
||||
role={"button"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(codeString);
|
||||
// message.success("Code copied to clipboard");
|
||||
setShowCopied(true);
|
||||
setTimeout(() => {
|
||||
setShowCopied(false);
|
||||
}, 6000);
|
||||
}}
|
||||
className="m-2 inline-block duration-300 text-white hover:text-accent w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="codeDivBox"
|
||||
className={`rounded w-full overflow-auto overflow-y-scroll scroll ${className}`}
|
||||
style={{ maxHeight: maxHeight, minHeight: minHeight }}
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
id="codeDiv"
|
||||
className="rounded-sm h-full break-all"
|
||||
language={language}
|
||||
showLineNumbers={showLineNumbers}
|
||||
style={atomDark}
|
||||
wrapLines={wrapLines}
|
||||
wrapLongLines={wrapLines}
|
||||
>
|
||||
{codeString}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Controls Row
|
||||
export const ControlRowView = ({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
control,
|
||||
className,
|
||||
truncateLength = 20,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
value: string | number | boolean;
|
||||
control: any;
|
||||
className?: string;
|
||||
truncateLength?: number;
|
||||
}) => {
|
||||
return (
|
||||
<div id="control-row-view" className={`${className}`}>
|
||||
<div>
|
||||
<span className="text-primary inline-block">{title} </span>
|
||||
<span className="text-xs ml-1 text-accent -mt-2 inline-block">
|
||||
{truncateText(value + "", truncateLength)}
|
||||
</span>{" "}
|
||||
<Tooltip title={description}>
|
||||
<InformationCircleIcon className="text-gray-400 inline-block w-4 h-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{control}
|
||||
<div className="bordper-b border-secondary border-dashed pb-2 mxp-2"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BounceLoader = ({
|
||||
className,
|
||||
title = "",
|
||||
}: {
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div id="bounce-loader" className="inline-block">
|
||||
<div className="inline-flex gap-2">
|
||||
<span className=" rounded-full bg-accent h-2 w-2 inline-block"></span>
|
||||
<span className="animate-bounce rounded-full bg-accent h-3 w-3 inline-block"></span>
|
||||
<span className=" rounded-full bg-accent h-2 w-2 inline-block"></span>
|
||||
</div>
|
||||
<span className=" text-sm">{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageLoader = ({
|
||||
src,
|
||||
className = "",
|
||||
}: {
|
||||
src: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<div id="image-loader" className="w-full rounded relative">
|
||||
{isLoading && (
|
||||
<div className="absolute h-24 inset-0 flex items-center justify-center">
|
||||
<BounceLoader title=" loading .." />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
alt="Dynamic content"
|
||||
src={src}
|
||||
className={`w-full rounded ${
|
||||
isLoading ? "opacity-0" : "opacity-100"
|
||||
} ${className}`}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type DataRow = { [key: string]: any };
|
||||
export const CsvLoader = ({
|
||||
csvUrl,
|
||||
className,
|
||||
}: {
|
||||
csvUrl: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [data, setData] = useState<DataRow[]>([]);
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [pageSize, setPageSize] = useState<number>(50);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch(csvUrl);
|
||||
const csvString = await response.text();
|
||||
const parsedData = Papa.parse(csvString, {
|
||||
header: true,
|
||||
dynamicTyping: true,
|
||||
skipEmptyLines: true,
|
||||
});
|
||||
setData(parsedData.data as DataRow[]);
|
||||
|
||||
// Use the keys of the first object for column headers
|
||||
const firstRow = parsedData.data[0] as DataRow; // Type assertion
|
||||
const columnHeaders: any[] = Object.keys(firstRow).map((key) => {
|
||||
const val = {
|
||||
title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the key for the title
|
||||
dataIndex: key,
|
||||
key: key,
|
||||
};
|
||||
if (typeof firstRow[key] === "number") {
|
||||
return {
|
||||
...val,
|
||||
sorter: (a: DataRow, b: DataRow) => a[key] - b[key],
|
||||
};
|
||||
}
|
||||
return val;
|
||||
});
|
||||
setColumns(columnHeaders);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching CSV data:", error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [csvUrl]);
|
||||
|
||||
// calculate x scroll, based on number of columns
|
||||
const scrollX = columns.length * 150;
|
||||
|
||||
return (
|
||||
<div id="csv-loader" className={`CsvLoader ${className}`}>
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
pagination={{ pageSize: pageSize }}
|
||||
scroll={{ y: 450, x: scrollX }}
|
||||
onChange={(pagination) => {
|
||||
setPageSize(pagination.pageSize || 50);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeLoader = ({
|
||||
url,
|
||||
className,
|
||||
}: {
|
||||
url: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetch(url)
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
setCode(data);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div id="code-loader" className={`w-full rounded relative ${className}`}>
|
||||
{isLoading && (
|
||||
<div className="absolute h-24 inset-0 flex items-center justify-center">
|
||||
<BounceLoader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && <CodeBlock code={code || ""} language={"python"} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PdfViewer = ({ url }: { url: string }) => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Assuming the URL is directly usable as the source for the <object> tag
|
||||
setLoading(false);
|
||||
// Note: No need to handle the creation and cleanup of a blob URL or converting file content as it's not provided anymore.
|
||||
}, [url]);
|
||||
|
||||
// Render the PDF viewer
|
||||
return (
|
||||
<div id="pdf-viewer" className="h-full">
|
||||
{loading && <p>Loading PDF...</p>}
|
||||
{!loading && (
|
||||
<object
|
||||
className="w-full rounded"
|
||||
data={url}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
style={{ height: "calc(90vh - 200px)" }}
|
||||
>
|
||||
<p>PDF cannot be displayed.</p>
|
||||
</object>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MonacoEditor = ({
|
||||
value,
|
||||
editorRef,
|
||||
language,
|
||||
onChange,
|
||||
minimap = true,
|
||||
}: {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
editorRef: any;
|
||||
language: string;
|
||||
minimap?: boolean;
|
||||
}) => {
|
||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||
const onEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor;
|
||||
setIsEditorReady(true);
|
||||
};
|
||||
return (
|
||||
<div id="monaco-editor" className="h-full rounded">
|
||||
<Editor
|
||||
height="100%"
|
||||
className="h-full rounded"
|
||||
defaultLanguage={language}
|
||||
defaultValue={value}
|
||||
value={value}
|
||||
onChange={(value: string | undefined) => {
|
||||
if (onChange && value) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
onMount={onEditorDidMount}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
wordWrap: "on",
|
||||
wrappingIndent: "indent",
|
||||
wrappingStrategy: "advanced",
|
||||
minimap: {
|
||||
enabled: minimap,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardHoverBar = ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
icon: any;
|
||||
hoverText: string;
|
||||
onClick: (e: any) => void;
|
||||
}[];
|
||||
}) => {
|
||||
const itemRows = items.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
key={"cardhoverrow" + i}
|
||||
id={`card-hover-bar-item-${i}`}
|
||||
role="button"
|
||||
className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<Tooltip title={item.hoverText}>
|
||||
<item.icon className=" w-5, h-5 cursor-pointer inline-block" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
id="card-hover-bar"
|
||||
onMouseEnter={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className=" mt-2 text-right opacity-0 group-hover:opacity-100 "
|
||||
>
|
||||
{itemRows}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentRow = ({ message }: { message: any }) => {
|
||||
return (
|
||||
<GroupView
|
||||
title={
|
||||
<div className="rounded p-1 px-2 inline-block text-xs bg-secondary">
|
||||
<span className="font-semibold">{message.sender}</span> ( to{" "}
|
||||
{message.recipient} )
|
||||
</div>
|
||||
}
|
||||
className="m"
|
||||
>
|
||||
<MarkdownView data={message.message?.content} className="text-sm" />
|
||||
</GroupView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
import React from "react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import {
|
||||
BellIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ChevronDown,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Menu as MenuIcon,
|
||||
} from "lucide-react";
|
||||
import { Tooltip } from "antd";
|
||||
import { appContext } from "../hooks/provider";
|
||||
import { useConfigStore } from "../hooks/store";
|
||||
|
||||
type ContentHeaderProps = {
|
||||
title?: string;
|
||||
onMobileMenuToggle: () => void;
|
||||
isMobileMenuOpen: boolean;
|
||||
};
|
||||
|
||||
const classNames = (...classes: (string | undefined | boolean)[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
const ContentHeader = ({
|
||||
title,
|
||||
onMobileMenuToggle,
|
||||
isMobileMenuOpen,
|
||||
}: ContentHeaderProps) => {
|
||||
const { darkMode, setDarkMode, user, logout } = React.useContext(appContext);
|
||||
const { sidebar, setSidebarState } = useConfigStore();
|
||||
const { isExpanded } = sidebar;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-40 bg-primary border-b border-secondary">
|
||||
<div className="flex h-16 items-center gap-x-4 px-4">
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
onClick={onMobileMenuToggle}
|
||||
className="md:hidden p-2 rounded-md hover:bg-secondary text-secondary hover:text-accent transition-colors"
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
<MenuIcon className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
{/* Desktop Sidebar Toggle - Hidden on Mobile */}
|
||||
<div className="hidden md:block">
|
||||
<Tooltip title={isExpanded ? "Close Sidebar" : "Open Sidebar"}>
|
||||
<button
|
||||
onClick={() => setSidebarState({ isExpanded: !isExpanded })}
|
||||
className={classNames(
|
||||
"p-2 rounded-md hover:bg-secondary",
|
||||
"hover:text-accent text-secondary transition-colors",
|
||||
"focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50"
|
||||
)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<PanelLeftClose strokeWidth={1.5} className="h-6 w-6" />
|
||||
) : (
|
||||
<PanelLeftOpen strokeWidth={1.5} className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||
{/* Search */}
|
||||
<div className="flex flex-1 items-center">
|
||||
<form className="hidden relative flex flex-1">
|
||||
<label htmlFor="search-field" className="sr-only">
|
||||
Search
|
||||
</label>
|
||||
<MagnifyingGlassIcon className="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-secondary" />
|
||||
<input
|
||||
id="search-field"
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
className="block h-full w-full border-0 bg-primary py-0 pl-8 pr-0 text-primary placeholder:text-secondary focus:ring-0 sm:text-sm"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Right side header items */}
|
||||
<div className="flex items-center gap-x-4 lg:gap-x-6 ml-auto">
|
||||
{/* Dark Mode Toggle */}
|
||||
<button
|
||||
onClick={() =>
|
||||
setDarkMode(darkMode === "dark" ? "light" : "dark")
|
||||
}
|
||||
className="text-secondary hover:text-primary"
|
||||
>
|
||||
{darkMode === "dark" ? (
|
||||
<MoonIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<SunIcon className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Notifications */}
|
||||
<button className="text-secondary hidden hover:text-primary">
|
||||
<BellIcon className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="hidden lg:block lg:h-6 lg:w-px lg:bg-secondary" />
|
||||
|
||||
{/* User Menu */}
|
||||
{user && (
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center">
|
||||
{user.avatar_url ? (
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={user.avatar_url}
|
||||
alt={user.name}
|
||||
/>
|
||||
) : (
|
||||
<div className="border-2 bg-accent h-8 w-8 rounded-full flex items-center justify-center text-white">
|
||||
{user.name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden lg:flex lg:items-center">
|
||||
<span className="ml-4 text-sm text-primary">
|
||||
{user.name}
|
||||
</span>
|
||||
<ChevronDown className="ml-2 h-5 w-5 text-secondary" />
|
||||
</span>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-primary py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => logout()}
|
||||
className={`${
|
||||
active ? "bg-secondary" : ""
|
||||
} block px-4 py-2 text-sm text-primary`}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentHeader;
|
||||
@@ -17,7 +17,7 @@ const Footer = () => {
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div className=" mt-4 text-primary p-3 border-t border-secondary flex ">
|
||||
<div className=" text-primary p-3 border-t border-secondary flex ">
|
||||
<div className="text-xs flex-1">
|
||||
Maintained by the AutoGen{" "}
|
||||
<a
|
||||
|
||||
@@ -208,20 +208,62 @@ const Icon = ({ icon = "app", size = 4, className = "" }: Props) => {
|
||||
className={` ${sizeClass} inline-block `}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 93 90"
|
||||
viewBox="0 0 290 264"
|
||||
>
|
||||
<path
|
||||
d="M44.0471 0H31.0006C28.3399 0 25.997 1.75225 25.2451 4.30449L2.26702 82.3045C1.13478 86.1479 4.01575 90 8.02249 90H21.776C24.4553 90 26.8098 88.2236 27.5454 85.6473L49.8165 7.64732C50.9108 3.81465 48.0329 0 44.0471 0Z"
|
||||
fill="#3F9447"
|
||||
d="M112.233 104.244C106.455 104.242 100.778 105.756 95.7684 108.635C90.7586 111.513 86.5912 115.655 83.6823 120.647L0 264H38.456C44.2308 263.999 49.9048 262.486 54.9137 259.613C59.9226 256.739 64.0919 252.603 67.0068 247.618L86.2185 214.713L97.1033 196.079L141.807 119.548L150.733 104.244H112.233Z"
|
||||
fill="url(#paint0_linear_149_456)"
|
||||
/>
|
||||
<path
|
||||
d="M61.8267 39H51.7524C49.9425 39 48.3581 40.2153 47.8891 41.9634L36.3514 84.9634C35.6697 87.5042 37.5841 90 40.2148 90H50.644C52.4654 90 54.0568 88.7695 54.5153 87.0068L65.6979 44.0068C66.3568 41.4731 64.4446 39 61.8267 39Z"
|
||||
fill="#D9D9D9"
|
||||
d="M111.547 33.2857L130.813 0L212.939 144.278C215.795 149.3 217.285 154.982 217.262 160.759C217.239 166.535 215.704 172.205 212.809 177.205L193.532 210.49L111.417 66.2122C108.559 61.1911 107.068 55.5088 107.091 49.7316C107.114 43.9544 108.65 38.284 111.547 33.2857Z"
|
||||
fill="url(#paint1_linear_149_456)"
|
||||
/>
|
||||
<path
|
||||
d="M90.1629 84.234L77.2698 58.0311C77.0912 57.668 76.8537 57.337 76.5672 57.0514C74.5514 55.0426 71.1154 55.9917 70.4166 58.7504L63.7622 85.0177C63.1219 87.5453 65.0322 90 67.6397 90H86.5738C89.5362 90 91.4707 86.8921 90.1629 84.234Z"
|
||||
fill="#3F9447"
|
||||
d="M289.285 245.714H123.281C117.498 245.714 111.815 244.199 106.8 241.319C101.785 238.439 97.6121 234.295 94.6976 229.3L86.1748 214.714L97.0596 196.079H241.348C247.134 196.075 252.82 197.588 257.837 200.468C262.855 203.349 267.029 207.495 269.943 212.493L289.285 245.714Z"
|
||||
fill="url(#paint2_linear_149_456)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_149_456"
|
||||
x1="116.173"
|
||||
y1="107.901"
|
||||
x2="37.3131"
|
||||
y2="255.118"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2314CC" />
|
||||
<stop offset="0.22" stopColor="#234CE4" />
|
||||
<stop offset="1" stopColor="#4081FF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_149_456"
|
||||
x1="200.792"
|
||||
y1="184.508"
|
||||
x2="134.199"
|
||||
y2="47.8169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#7215D4" />
|
||||
<stop offset="0.11" stopColor="#7554D5" />
|
||||
<stop offset="0.56" stopColor="#9E8AE9" />
|
||||
<stop offset="1" stopColor="#CC99FF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_149_456"
|
||||
x1="107.651"
|
||||
y1="220.896"
|
||||
x2="271.108"
|
||||
y2="220.896"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2E31F0" />
|
||||
<stop offset="0.2" stopColor="#4081FF" />
|
||||
<stop offset="0.39" stopColor="#848EE5" />
|
||||
<stop offset="0.49" stopColor="#8183E2" />
|
||||
<stop offset="0.65" stopColor="#7866DA" />
|
||||
<stop offset="0.75" stopColor="#7251D4" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import * as React from "react";
|
||||
import Header from "./header";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
import { X } from "lucide-react";
|
||||
import { appContext } from "../hooks/provider";
|
||||
import { useConfigStore } from "../hooks/store";
|
||||
import Footer from "./footer";
|
||||
|
||||
/// import ant css
|
||||
import "antd/dist/reset.css";
|
||||
import SideBar from "./sidebar";
|
||||
import ContentHeader from "./contentheader";
|
||||
|
||||
const classNames = (...classes: (string | undefined | boolean)[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@@ -23,38 +29,96 @@ const Layout = ({
|
||||
showHeader = true,
|
||||
restricted = false,
|
||||
}: Props) => {
|
||||
const layoutContent = (
|
||||
<div
|
||||
// style={{ height: "calc(100vh - 64px)" }}
|
||||
className={` h-full flex flex-col`}
|
||||
>
|
||||
{showHeader && <Header meta={meta} link={link} />}
|
||||
<div className="flex-1 text-primary ">
|
||||
<title>{meta?.title + " | " + title}</title>
|
||||
<div className=" h-full text-primary">{children}</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
const { darkMode } = React.useContext(appContext);
|
||||
const { sidebar } = useConfigStore();
|
||||
const { isExpanded } = sidebar;
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
|
||||
// Close mobile menu on route change
|
||||
React.useEffect(() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
}, [link]);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.getElementsByTagName("html")[0].className = `${
|
||||
darkMode === "dark" ? "dark bg-primary" : "light bg-primary"
|
||||
} `;
|
||||
}`;
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<appContext.Consumer>
|
||||
{(context: any) => {
|
||||
if (restricted) {
|
||||
return <div className="h-full ">{context.user && layoutContent}</div>;
|
||||
} else {
|
||||
return layoutContent;
|
||||
}
|
||||
}}
|
||||
</appContext.Consumer>
|
||||
const layoutContent = (
|
||||
<div className="min-h-screen flex">
|
||||
{/* Mobile menu */}
|
||||
<Dialog
|
||||
as="div"
|
||||
open={isMobileMenuOpen}
|
||||
onClose={() => setIsMobileMenuOpen(false)}
|
||||
className="relative z-50 md:hidden"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||
|
||||
{/* Mobile Sidebar Container */}
|
||||
<div className="fixed inset-0 flex">
|
||||
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
|
||||
<div className="absolute right-0 top-0 flex w-16 justify-center pt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="text-secondary"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<X className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<SideBar link={link} meta={meta} isMobile={true} />
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* Desktop sidebar */}
|
||||
<div className="hidden md:flex md:flex-col md:fixed md:inset-y-0">
|
||||
<SideBar link={link} meta={meta} isMobile={false} />
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div
|
||||
className={classNames(
|
||||
"flex-1 flex flex-col min-h-screen",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"md:pl-16",
|
||||
isExpanded ? "md:pl-72" : "md:pl-16"
|
||||
)}
|
||||
>
|
||||
{showHeader && (
|
||||
<ContentHeader
|
||||
title={title}
|
||||
isMobileMenuOpen={isMobileMenuOpen}
|
||||
onMobileMenuToggle={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main className="flex-1 p-2 text-primary">{children}</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Handle restricted content
|
||||
if (restricted) {
|
||||
return (
|
||||
<appContext.Consumer>
|
||||
{(context: any) => {
|
||||
if (context.user) {
|
||||
return layoutContent;
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</appContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
return layoutContent;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from "react";
|
||||
import { Link } from "gatsby";
|
||||
import { useConfigStore } from "../hooks/store";
|
||||
import { Tooltip } from "antd";
|
||||
import { Blocks, Settings, MessagesSquare } from "lucide-react";
|
||||
import Icon from "./icons";
|
||||
|
||||
const navigation = [
|
||||
// { name: "Build", href: "/build", icon: Blocks },
|
||||
{ name: "Playground", href: "/", icon: MessagesSquare },
|
||||
];
|
||||
|
||||
const classNames = (...classes: (string | undefined | boolean)[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
type SidebarProps = {
|
||||
link: string;
|
||||
meta?: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
isMobile: boolean;
|
||||
};
|
||||
|
||||
const Sidebar = ({ link, meta, isMobile }: SidebarProps) => {
|
||||
const { sidebar } = useConfigStore();
|
||||
const { isExpanded } = sidebar;
|
||||
|
||||
// Always show full sidebar in mobile view
|
||||
const showFull = isMobile || isExpanded;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex grow flex-col gap-y-5 overflow-y-auto border-r border-secondary bg-primary",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
showFull ? "w-72 px-6" : "w-16 px-2"
|
||||
)}
|
||||
>
|
||||
{/* App Logo/Title */}
|
||||
<div
|
||||
className={`flex h-16 items-center ${showFull ? "gap-x-3" : "ml-2"}`}
|
||||
>
|
||||
<div className="w-8 text-right text-accent">
|
||||
<Icon icon="app" size={8} />
|
||||
</div>
|
||||
{showFull && (
|
||||
<div className="flex flex-col" style={{ minWidth: "200px" }}>
|
||||
<span className="text-base font-semibold text-primary">
|
||||
{meta?.title}
|
||||
</span>
|
||||
<span className="text-xs text-secondary">{meta?.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
{/* Main Navigation */}
|
||||
<li>
|
||||
<ul
|
||||
role="list"
|
||||
className={classNames(
|
||||
"-mx-2 space-y-1",
|
||||
!showFull && "items-center"
|
||||
)}
|
||||
>
|
||||
{navigation.map((item) => {
|
||||
const isActive = item.href === link;
|
||||
const IconComponent = item.icon;
|
||||
|
||||
const navLink = (
|
||||
<Link
|
||||
to={item.href}
|
||||
className={classNames(
|
||||
isActive
|
||||
? "text-accent"
|
||||
: "text-primary hover:text-accent hover:bg-secondary",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-medium",
|
||||
!showFull && "justify-center"
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
className={classNames(
|
||||
isActive
|
||||
? "text-accent"
|
||||
: "text-secondary group-hover:text-accent",
|
||||
"h-6 w-6 shrink-0"
|
||||
)}
|
||||
/>
|
||||
{showFull && item.name}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={item.name}>
|
||||
{!showFull && !isMobile ? (
|
||||
<Tooltip title={item.name} placement="right">
|
||||
{navLink}
|
||||
</Tooltip>
|
||||
) : (
|
||||
navLink
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{/* Settings at bottom */}
|
||||
<li
|
||||
className={classNames(
|
||||
"mt-auto -mx-2 mb-4",
|
||||
!showFull && "flex justify-center"
|
||||
)}
|
||||
>
|
||||
{!showFull && !isMobile ? (
|
||||
<Tooltip title="Settings" placement="right">
|
||||
<Link
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-medium",
|
||||
"text-primary hover:text-accent hover:bg-secondary",
|
||||
!showFull && "justify-center"
|
||||
)}
|
||||
>
|
||||
<Settings className="h-6 w-6 shrink-0 text-secondary group-hover:text-accent" />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Link
|
||||
to="/settings"
|
||||
className="group flex gap-x-3 rounded-md p-2 text-sm font-medium text-primary hover:text-accent hover:bg-secondary"
|
||||
>
|
||||
<Settings className="h-6 w-6 shrink-0 text-secondary group-hover:text-accent" />
|
||||
{showFull && "Settings"}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,127 +0,0 @@
|
||||
export type NotificationType = "success" | "info" | "warning" | "error";
|
||||
|
||||
export interface IMessage {
|
||||
user_id: string;
|
||||
role: string;
|
||||
content: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
session_id?: number;
|
||||
connection_id?: string;
|
||||
workflow_id?: number;
|
||||
meta?: any;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface IStatus {
|
||||
message: string;
|
||||
status: boolean;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface IChatMessage {
|
||||
text: string;
|
||||
sender: "user" | "bot";
|
||||
meta?: any;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface ILLMConfig {
|
||||
config_list: Array<IModelConfig>;
|
||||
timeout?: number;
|
||||
cache_seed?: number | null;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
}
|
||||
|
||||
export interface IAgentConfig {
|
||||
name: string;
|
||||
llm_config?: ILLMConfig | false;
|
||||
human_input_mode: string;
|
||||
max_consecutive_auto_reply: number;
|
||||
system_message: string | "";
|
||||
is_termination_msg?: boolean | string;
|
||||
default_auto_reply?: string | null;
|
||||
code_execution_config?: "none" | "local" | "docker";
|
||||
description?: string;
|
||||
|
||||
admin_name?: string;
|
||||
messages?: Array<IMessage>;
|
||||
max_round?: number;
|
||||
speaker_selection_method?: string;
|
||||
allow_repeat_speaker?: boolean;
|
||||
}
|
||||
|
||||
export interface IAgent {
|
||||
type?: "assistant" | "userproxy" | "groupchat";
|
||||
config: IAgentConfig;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
id?: number;
|
||||
skills?: Array<ISkill>;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface IWorkflow {
|
||||
name: string;
|
||||
description: string;
|
||||
sender?: IAgent;
|
||||
receiver?: IAgent;
|
||||
type?: "autonomous" | "sequential";
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
summary_method?: "none" | "last" | "llm";
|
||||
id?: number;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface IModelConfig {
|
||||
model: string;
|
||||
api_key?: string;
|
||||
api_version?: string;
|
||||
base_url?: string;
|
||||
api_type?: "open_ai" | "azure" | "google" | "anthropic" | "mistral";
|
||||
user_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
description?: string;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface IMetadataFile {
|
||||
name: string;
|
||||
path: string;
|
||||
extension: string;
|
||||
content: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IChatSession {
|
||||
id?: number;
|
||||
user_id: string;
|
||||
workflow_id?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IGalleryItem {
|
||||
id: number;
|
||||
messages: Array<IMessage>;
|
||||
session: IChatSession;
|
||||
tags: Array<string>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ISkill {
|
||||
name: string;
|
||||
content: string;
|
||||
secrets?: any[];
|
||||
libraries?: string[];
|
||||
id?: number;
|
||||
description?: string;
|
||||
user_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface IStatus {
|
||||
message: string;
|
||||
status: boolean;
|
||||
data?: any;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
export interface RequestUsage {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
}
|
||||
|
||||
export interface MessageConfig {
|
||||
source: string;
|
||||
content: string;
|
||||
models_usage?: RequestUsage;
|
||||
}
|
||||
|
||||
export interface DBModel {
|
||||
id?: number;
|
||||
user_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Message extends DBModel {
|
||||
config: MessageConfig;
|
||||
session_id: number;
|
||||
run_id: string;
|
||||
}
|
||||
|
||||
export interface Session extends DBModel {
|
||||
name: string;
|
||||
team_id?: string;
|
||||
}
|
||||
|
||||
export interface TeamConfig {
|
||||
name: string;
|
||||
participants: AgentConfig[];
|
||||
team_type: TeamTypes;
|
||||
model_client?: ModelConfig;
|
||||
termination_condition?: TerminationConfig;
|
||||
}
|
||||
|
||||
export interface Team extends DBModel {
|
||||
config: TeamConfig;
|
||||
}
|
||||
|
||||
export type ModelTypes = "OpenAIChatCompletionClient";
|
||||
|
||||
export type AgentTypes = "AssistantAgent" | "CodingAssistantAgent";
|
||||
|
||||
export type TeamTypes = "RoundRobinGroupChat" | "SelectorGroupChat";
|
||||
|
||||
export type TerminationTypes =
|
||||
| "MaxMessageTermination"
|
||||
| "StopMessageTermination"
|
||||
| "TextMentionTermination";
|
||||
|
||||
export interface ModelConfig {
|
||||
model: string;
|
||||
model_type: ModelTypes;
|
||||
api_key?: string;
|
||||
base_url?: string;
|
||||
}
|
||||
|
||||
export interface ToolConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AgentConfig {
|
||||
name: string;
|
||||
agent_type: AgentTypes;
|
||||
system_message?: string;
|
||||
model_client?: ModelConfig;
|
||||
tools?: ToolConfig[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TerminationConfig {
|
||||
termination_type: TerminationTypes;
|
||||
max_messages?: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface TaskResult {
|
||||
messages: MessageConfig[];
|
||||
usage: string;
|
||||
duration: number;
|
||||
stop_reason?: string;
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
IAgent,
|
||||
IAgentConfig,
|
||||
ILLMConfig,
|
||||
IModelConfig,
|
||||
ISkill,
|
||||
IStatus,
|
||||
IWorkflow,
|
||||
} from "./types";
|
||||
import { IStatus } from "./types";
|
||||
|
||||
export const getServerUrl = () => {
|
||||
return process.env.GATSBY_API_URL || "/api";
|
||||
@@ -100,10 +92,6 @@ export function fetchJSON(
|
||||
onFinal();
|
||||
});
|
||||
}
|
||||
export const capitalize = (s: string) => {
|
||||
if (typeof s !== "string") return "";
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
|
||||
export function eraseCookie(name: string) {
|
||||
document.cookie = name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
@@ -116,435 +104,6 @@ export function truncateText(text: string, length = 50) {
|
||||
return text;
|
||||
}
|
||||
|
||||
export const getCaretCoordinates = () => {
|
||||
let caretX, caretY;
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection?.rangeCount !== 0) {
|
||||
const range = selection.getRangeAt(0).cloneRange();
|
||||
range.collapse(false);
|
||||
const rect = range.getClientRects()[0];
|
||||
if (rect) {
|
||||
caretX = rect.left;
|
||||
caretY = rect.top;
|
||||
}
|
||||
}
|
||||
return { caretX, caretY };
|
||||
};
|
||||
|
||||
export const getPrefixSuffix = (container: any) => {
|
||||
let prefix = "";
|
||||
let suffix = "";
|
||||
if (window.getSelection) {
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
let range = sel.getRangeAt(0).cloneRange();
|
||||
range.collapse(true);
|
||||
range.setStart(container!, 0);
|
||||
prefix = range.toString();
|
||||
|
||||
range = sel.getRangeAt(0).cloneRange();
|
||||
range.collapse(true);
|
||||
range.setEnd(container, container.childNodes.length);
|
||||
|
||||
suffix = range.toString();
|
||||
console.log("prefix", prefix);
|
||||
console.log("suffix", suffix);
|
||||
}
|
||||
}
|
||||
return { prefix, suffix };
|
||||
};
|
||||
|
||||
export const uid = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
|
||||
export const setCaretToEnd = (element: HTMLElement) => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
range.selectNodeContents(element);
|
||||
range.collapse(false);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
element.focus();
|
||||
};
|
||||
|
||||
// return a color between a start and end color using a percentage
|
||||
export const ColorTween = (
|
||||
startColor: string,
|
||||
endColor: string,
|
||||
percent: number
|
||||
) => {
|
||||
// example startColor = "#ff0000" endColor = "#0000ff" percent = 0.5
|
||||
const start = {
|
||||
r: parseInt(startColor.substring(1, 3), 16),
|
||||
g: parseInt(startColor.substring(3, 5), 16),
|
||||
b: parseInt(startColor.substring(5, 7), 16),
|
||||
};
|
||||
const end = {
|
||||
r: parseInt(endColor.substring(1, 3), 16),
|
||||
g: parseInt(endColor.substring(3, 5), 16),
|
||||
b: parseInt(endColor.substring(5, 7), 16),
|
||||
};
|
||||
const r = Math.floor(start.r + (end.r - start.r) * percent);
|
||||
const g = Math.floor(start.g + (end.g - start.g) * percent);
|
||||
const b = Math.floor(start.b + (end.b - start.b) * percent);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
export const guid = () => {
|
||||
var w = () => {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
};
|
||||
return `${w()}${w()}-${w()}-${w()}-${w()}-${w()}${w()}${w()}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a string and returns the first n characters followed by asterisks.
|
||||
* @param {string} str - The string to obscure
|
||||
* @param {number} n - Number of characters to show before obscuring
|
||||
* @returns {string} The obscured string with first n characters in clear text
|
||||
*/
|
||||
export const obscureString = (str: string, n: number = 3) => {
|
||||
if (n < 0 || n > str.length) {
|
||||
console.log("n cannot be less than 0 or greater than the string length.");
|
||||
return str;
|
||||
}
|
||||
// First n characters in clear text
|
||||
var clearText = str.substring(0, n);
|
||||
// Remaining characters replaced with asterisks
|
||||
var obscured = clearText + "*".repeat(str.length - n);
|
||||
|
||||
return obscured;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a number of seconds into a human-readable string representing the duration in days, hours, minutes, and seconds.
|
||||
* @param {number} seconds - The number of seconds to convert.
|
||||
* @returns {string} A well-formatted duration string.
|
||||
*/
|
||||
export const formatDuration = (seconds: number) => {
|
||||
const units = [
|
||||
{ label: " day", seconds: 86400 },
|
||||
{ label: " hr", seconds: 3600 },
|
||||
{ label: " min", seconds: 60 },
|
||||
{ label: " sec", seconds: 1 },
|
||||
];
|
||||
|
||||
let remainingSeconds = seconds;
|
||||
const parts = [];
|
||||
|
||||
for (const { label, seconds: unitSeconds } of units) {
|
||||
const count = Math.floor(remainingSeconds / unitSeconds);
|
||||
if (count > 0) {
|
||||
parts.push(count + (count > 1 ? label + "s" : label));
|
||||
remainingSeconds -= count * unitSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(" ") : "0 sec";
|
||||
};
|
||||
|
||||
export const sampleModelConfig = (modelType: string = "open_ai") => {
|
||||
const openaiConfig: IModelConfig = {
|
||||
model: "gpt-4-1106-preview",
|
||||
api_type: "open_ai",
|
||||
description: "OpenAI GPT-4 model",
|
||||
};
|
||||
const azureConfig: IModelConfig = {
|
||||
model: "gpt-4",
|
||||
api_type: "azure",
|
||||
api_version: "v1",
|
||||
base_url: "https://youazureendpoint.azure.com/",
|
||||
description: "Azure model",
|
||||
};
|
||||
|
||||
const googleConfig: IModelConfig = {
|
||||
model: "gemini-1.0-pro",
|
||||
api_type: "google",
|
||||
description: "Google Gemini Model model",
|
||||
};
|
||||
|
||||
const anthropicConfig: IModelConfig = {
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
api_type: "anthropic",
|
||||
description: "Claude 3.5 Sonnet model",
|
||||
};
|
||||
|
||||
const mistralConfig: IModelConfig = {
|
||||
model: "mistral",
|
||||
api_type: "mistral",
|
||||
description: "Mistral model",
|
||||
};
|
||||
|
||||
switch (modelType) {
|
||||
case "open_ai":
|
||||
return openaiConfig;
|
||||
case "azure":
|
||||
return azureConfig;
|
||||
case "google":
|
||||
return googleConfig;
|
||||
case "anthropic":
|
||||
return anthropicConfig;
|
||||
case "mistral":
|
||||
return mistralConfig;
|
||||
default:
|
||||
return openaiConfig;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRandomIntFromDateAndSalt = (salt: number = 43444) => {
|
||||
const currentDate = new Date();
|
||||
const seed = currentDate.getTime() + salt;
|
||||
const randomValue = Math.sin(seed) * 10000;
|
||||
const randomInt = Math.floor(randomValue) % 100;
|
||||
return randomInt;
|
||||
};
|
||||
|
||||
export const getSampleWorkflow = (workflow_type: string = "autonomous") => {
|
||||
const autonomousWorkflow: IWorkflow = {
|
||||
name: "Default Chat Workflow",
|
||||
description: "Autonomous Workflow",
|
||||
type: "autonomous",
|
||||
summary_method: "llm",
|
||||
};
|
||||
const sequentialWorkflow: IWorkflow = {
|
||||
name: "Default Sequential Workflow",
|
||||
description: "Sequential Workflow",
|
||||
type: "sequential",
|
||||
summary_method: "llm",
|
||||
};
|
||||
|
||||
if (workflow_type === "autonomous") {
|
||||
return autonomousWorkflow;
|
||||
} else if (workflow_type === "sequential") {
|
||||
return sequentialWorkflow;
|
||||
} else {
|
||||
return autonomousWorkflow;
|
||||
}
|
||||
};
|
||||
|
||||
export const sampleAgentConfig = (agent_type: string = "assistant") => {
|
||||
const llm_config: ILLMConfig = {
|
||||
config_list: [],
|
||||
temperature: 0.1,
|
||||
timeout: 600,
|
||||
cache_seed: null,
|
||||
max_tokens: 4000,
|
||||
};
|
||||
|
||||
const userProxyConfig: IAgentConfig = {
|
||||
name: "userproxy",
|
||||
human_input_mode: "NEVER",
|
||||
description: "User Proxy",
|
||||
max_consecutive_auto_reply: 25,
|
||||
system_message: "You are a helpful assistant.",
|
||||
default_auto_reply: "TERMINATE",
|
||||
llm_config: false,
|
||||
code_execution_config: "local",
|
||||
};
|
||||
const userProxyFlowSpec: IAgent = {
|
||||
type: "userproxy",
|
||||
config: userProxyConfig,
|
||||
};
|
||||
|
||||
const assistantConfig: IAgentConfig = {
|
||||
name: "primary_assistant",
|
||||
description: "Primary Assistant",
|
||||
llm_config: llm_config,
|
||||
human_input_mode: "NEVER",
|
||||
max_consecutive_auto_reply: 25,
|
||||
code_execution_config: "none",
|
||||
system_message:
|
||||
"You are a helpful AI assistant. Solve tasks using your coding and language skills. In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the user to execute. 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. Reply 'TERMINATE' in the end when everything is done.",
|
||||
};
|
||||
|
||||
const assistantFlowSpec: IAgent = {
|
||||
type: "assistant",
|
||||
config: assistantConfig,
|
||||
};
|
||||
|
||||
const groupChatAssistantConfig = Object.assign(
|
||||
{
|
||||
admin_name: "groupchat_assistant",
|
||||
messages: [],
|
||||
max_round: 10,
|
||||
speaker_selection_method: "auto",
|
||||
allow_repeat_speaker: false,
|
||||
},
|
||||
assistantConfig
|
||||
);
|
||||
groupChatAssistantConfig.name = "groupchat_assistant";
|
||||
groupChatAssistantConfig.system_message =
|
||||
"You are a helpful assistant skilled at cordinating a group of other assistants to solve a task. ";
|
||||
groupChatAssistantConfig.description = "Group Chat Assistant";
|
||||
|
||||
const groupChatFlowSpec: IAgent = {
|
||||
type: "groupchat",
|
||||
config: groupChatAssistantConfig,
|
||||
};
|
||||
|
||||
if (agent_type === "userproxy") {
|
||||
return userProxyFlowSpec;
|
||||
} else if (agent_type === "assistant") {
|
||||
return assistantFlowSpec;
|
||||
} else if (agent_type === "groupchat") {
|
||||
return groupChatFlowSpec;
|
||||
} else {
|
||||
return assistantFlowSpec;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSampleSkill = () => {
|
||||
const content = `
|
||||
from typing import List
|
||||
import uuid
|
||||
import requests # to perform HTTP requests
|
||||
from pathlib import Path
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
def generate_and_save_images(query: str, image_size: str = "1024x1024") -> List[str]:
|
||||
"""
|
||||
Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk. Use the code below anytime there is a request to create an image.
|
||||
|
||||
:param query: A natural language description of the image to be generated.
|
||||
:param image_size: The size of the image to be generated. (default is "1024x1024")
|
||||
:return: A list of filenames for the saved images.
|
||||
"""
|
||||
|
||||
client = OpenAI() # Initialize the OpenAI client
|
||||
response = client.images.generate(model="dall-e-3", prompt=query, n=1, size=image_size) # Generate images
|
||||
|
||||
# List to store the file names of saved images
|
||||
saved_files = []
|
||||
|
||||
# Check if the response is successful
|
||||
if response.data:
|
||||
for image_data in response.data:
|
||||
# Generate a random UUID as the file name
|
||||
file_name = str(uuid.uuid4()) + ".png" # Assuming the image is a PNG
|
||||
file_path = Path(file_name)
|
||||
|
||||
img_url = image_data.url
|
||||
img_response = requests.get(img_url)
|
||||
if img_response.status_code == 200:
|
||||
# Write the binary content to a file
|
||||
with open(file_path, "wb") as img_file:
|
||||
img_file.write(img_response.content)
|
||||
print(f"Image saved to {file_path}")
|
||||
saved_files.append(str(file_path))
|
||||
else:
|
||||
print(f"Failed to download the image from {img_url}")
|
||||
else:
|
||||
print("No image data found in the response!")
|
||||
|
||||
# Return the list of saved files
|
||||
return saved_files
|
||||
|
||||
|
||||
# Example usage of the function:
|
||||
# generate_and_save_images("A cute baby sea otter")
|
||||
`;
|
||||
|
||||
const skill: ISkill = {
|
||||
name: "generate_and_save_images",
|
||||
description: "Generate and save images based on a user's query.",
|
||||
content: content,
|
||||
};
|
||||
|
||||
return skill;
|
||||
};
|
||||
|
||||
export const timeAgo = (
|
||||
dateString: string,
|
||||
returnFormatted: boolean = false
|
||||
): string => {
|
||||
// if dateStr is empty, return empty string
|
||||
if (!dateString) {
|
||||
return "";
|
||||
}
|
||||
// Parse the date string into a Date object
|
||||
const timestamp = new Date(dateString);
|
||||
|
||||
// Check for invalid date
|
||||
if (isNaN(timestamp.getTime())) {
|
||||
throw new Error("Invalid date string provided.");
|
||||
}
|
||||
|
||||
// Get the current time
|
||||
const now = new Date();
|
||||
|
||||
// Calculate the difference in milliseconds
|
||||
const timeDifference = now.getTime() - timestamp.getTime();
|
||||
|
||||
// Convert time difference to minutes and hours
|
||||
const minutesAgo = Math.floor(timeDifference / (1000 * 60));
|
||||
const hoursAgo = Math.floor(minutesAgo / 60);
|
||||
|
||||
// Format the date into a readable format e.g. "November 27, 2021, 3:45 PM"
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
};
|
||||
const formattedDate = timestamp.toLocaleDateString(undefined, options);
|
||||
|
||||
if (returnFormatted) {
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
// Determine the time difference string
|
||||
let timeAgoStr: string;
|
||||
if (minutesAgo < 1) {
|
||||
timeAgoStr = "just now";
|
||||
} else if (minutesAgo < 60) {
|
||||
// Less than an hour ago, display minutes
|
||||
timeAgoStr = `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`;
|
||||
} else if (hoursAgo < 24) {
|
||||
// Less than a day ago, display hours
|
||||
timeAgoStr = `${hoursAgo} ${hoursAgo === 1 ? "hour" : "hours"} ago`;
|
||||
} else {
|
||||
// More than a day ago, display the formatted date
|
||||
timeAgoStr = formattedDate;
|
||||
}
|
||||
|
||||
// Return the final readable string
|
||||
return timeAgoStr;
|
||||
};
|
||||
|
||||
export const examplePrompts = [
|
||||
{
|
||||
title: "Stock Price",
|
||||
prompt:
|
||||
"Plot a chart of NVDA and TESLA stock price for 2023. Save the result to a file named nvda_tesla.png",
|
||||
},
|
||||
{
|
||||
title: "Sine Wave",
|
||||
prompt:
|
||||
"Write a python script to plot a sine wave and save it to disc as a png file sine_wave.png",
|
||||
},
|
||||
{
|
||||
title: "Markdown",
|
||||
prompt:
|
||||
"List out the top 5 rivers in africa and their length and return that as a markdown table. Do not try to write any code, just write the table",
|
||||
},
|
||||
{
|
||||
title: "Paint",
|
||||
prompt:
|
||||
"paint a picture of a glass of ethiopian coffee, freshly brewed in a tall glass cup, on a table right in front of a lush green forest scenery",
|
||||
},
|
||||
{
|
||||
title: "Travel",
|
||||
prompt:
|
||||
"Plan a 2 day trip to hawaii. Limit to 3 activities per day, be as brief as possible!",
|
||||
},
|
||||
];
|
||||
|
||||
export const fetchVersion = () => {
|
||||
const versionUrl = getServerUrl() + "/version";
|
||||
return fetch(versionUrl)
|
||||
@@ -557,128 +116,3 @@ export const fetchVersion = () => {
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively sanitizes JSON objects by replacing specific keys with a given value.
|
||||
* @param {JsonValue} data - The JSON data to be sanitized.
|
||||
* @param {string[]} keys - An array of keys to be replaced in the JSON object.
|
||||
* @param {string} replacement - The value to use as replacement for the specified keys.
|
||||
* @returns {JsonValue} - The sanitized JSON data.
|
||||
*/
|
||||
export const sanitizeConfig = (
|
||||
data: any,
|
||||
keys: string[] = ["api_key", "id", "created_at", "updated_at", "secrets"]
|
||||
): any => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) => sanitizeConfig(item, keys));
|
||||
} else if (typeof data === "object" && data !== null) {
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
delete data[key];
|
||||
} else {
|
||||
data[key] = sanitizeConfig(data[key], keys);
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the input text against the regex '^[a-zA-Z0-9_-]{1,64}$' and returns an object with
|
||||
* status, message, and sanitizedText. Status is boolean indicating whether input text is valid,
|
||||
* message provides information about the outcome, and sanitizedText contains a valid version
|
||||
* of the input text or the original text if it was already valid.
|
||||
*
|
||||
* @param text - The input string to be checked and sanitized.
|
||||
* @returns An object containing a status, a message, and sanitizedText.
|
||||
*/
|
||||
export const checkAndSanitizeInput = (
|
||||
text: string
|
||||
): { status: boolean; message: string; sanitizedText: string } => {
|
||||
// Create a regular expression pattern to match valid characters
|
||||
const regexPattern: RegExp = /^[a-zA-Z0-9_-]{1,64}$/;
|
||||
let status: boolean = true;
|
||||
let message: string;
|
||||
let sanitizedText: string;
|
||||
|
||||
// Check if the input text matches the pattern
|
||||
if (regexPattern.test(text)) {
|
||||
// Text already adheres to the pattern
|
||||
message = `The text '${text}' is valid.`;
|
||||
sanitizedText = text;
|
||||
} else {
|
||||
// The text does not match; sanitize the input
|
||||
status = false;
|
||||
sanitizedText = text.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
||||
message = `'${text}' is invalid. Consider using '${sanitizedText}' instead.`;
|
||||
}
|
||||
|
||||
return { status, message, sanitizedText };
|
||||
};
|
||||
|
||||
export const isValidConfig = (
|
||||
jsonObj: any,
|
||||
templateObj: any,
|
||||
diffThreshold: number = 4
|
||||
): {
|
||||
status: boolean;
|
||||
message: string;
|
||||
} => {
|
||||
// Check if both parameters are indeed objects and not null
|
||||
if (
|
||||
typeof jsonObj !== "object" ||
|
||||
jsonObj === null ||
|
||||
Array.isArray(jsonObj) ||
|
||||
typeof templateObj !== "object" ||
|
||||
templateObj === null ||
|
||||
Array.isArray(templateObj)
|
||||
) {
|
||||
return {
|
||||
status: false,
|
||||
message:
|
||||
"Invalid input: One or both parameters are not objects, or are null or arrays.",
|
||||
};
|
||||
}
|
||||
|
||||
const jsonKeys = new Set(Object.keys(jsonObj));
|
||||
const templateKeys = new Set(Object.keys(templateObj));
|
||||
|
||||
if (jsonKeys.size !== templateKeys.size) {
|
||||
if (Math.abs(jsonKeys.size - templateKeys.size) > diffThreshold) {
|
||||
return {
|
||||
status: false,
|
||||
message:
|
||||
"Configuration does not match template: Number of keys differ.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of templateKeys) {
|
||||
if (!jsonKeys.has(key)) {
|
||||
return {
|
||||
status: false,
|
||||
message: `Configuration does not match template: Missing key '${key}' in configuration.`,
|
||||
};
|
||||
}
|
||||
|
||||
// If the value is an object, recursively validate
|
||||
if (
|
||||
typeof templateObj[key] === "object" &&
|
||||
templateObj[key] !== null &&
|
||||
!Array.isArray(templateObj[key])
|
||||
) {
|
||||
const result = isValidConfig(jsonObj[key], templateObj[key]);
|
||||
if (!result.status) {
|
||||
return {
|
||||
status: false,
|
||||
message: `Configuration error in nested key '${key}': ${result.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: true,
|
||||
message: "Configuration is valid.",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
DocumentDuplicateIcon,
|
||||
InformationCircleIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Dropdown, MenuProps, Modal, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { IAgent, IStatus } from "../../types";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import {
|
||||
fetchJSON,
|
||||
getServerUrl,
|
||||
sanitizeConfig,
|
||||
timeAgo,
|
||||
truncateText,
|
||||
} from "../../utils";
|
||||
import { BounceLoader, Card, CardHoverBar, LoadingOverlay } from "../../atoms";
|
||||
import { AgentViewer } from "./utils/agentconfig";
|
||||
|
||||
const AgentsView = ({}: any) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const listAgentsUrl = `${serverUrl}/agents?user_id=${user?.email}`;
|
||||
|
||||
const [agents, setAgents] = React.useState<IAgent[] | null>([]);
|
||||
const [selectedAgent, setSelectedAgent] = React.useState<IAgent | null>(null);
|
||||
|
||||
const [showNewAgentModal, setShowNewAgentModal] = React.useState(false);
|
||||
|
||||
const [showAgentModal, setShowAgentModal] = React.useState(false);
|
||||
|
||||
const sampleAgent = {
|
||||
config: {
|
||||
name: "sample_agent",
|
||||
description: "Sample agent description",
|
||||
human_input_mode: "NEVER",
|
||||
max_consecutive_auto_reply: 3,
|
||||
system_message: "",
|
||||
},
|
||||
};
|
||||
const [newAgent, setNewAgent] = React.useState<IAgent | null>(sampleAgent);
|
||||
|
||||
const deleteAgent = (agent: IAgent) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
const deleteAgentUrl = `${serverUrl}/agents/delete?user_id=${user?.email}&agent_id=${agent.id}`;
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: user?.email,
|
||||
agent: agent,
|
||||
}),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
fetchAgents();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(deleteAgentUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const fetchAgents = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
setAgents(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(listAgentsUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
// console.log("fetching messages", messages);
|
||||
fetchAgents();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const agentRows = (agents || []).map((agent: IAgent, i: number) => {
|
||||
const cardItems = [
|
||||
{
|
||||
title: "Download",
|
||||
icon: ArrowDownTrayIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
// download workflow as workflow.name.json
|
||||
const element = document.createElement("a");
|
||||
const sanitizedAgent = sanitizeConfig(agent);
|
||||
const file = new Blob([JSON.stringify(sanitizedAgent)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `agent_${agent.config.name}.json`;
|
||||
document.body.appendChild(element); // Required for this to work in FireFox
|
||||
element.click();
|
||||
},
|
||||
hoverText: "Download",
|
||||
},
|
||||
{
|
||||
title: "Make a Copy",
|
||||
icon: DocumentDuplicateIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
let newAgent = { ...sanitizeConfig(agent) };
|
||||
newAgent.config.name = `${agent.config.name}_copy`;
|
||||
console.log("newAgent", newAgent);
|
||||
setNewAgent(newAgent);
|
||||
setShowNewAgentModal(true);
|
||||
},
|
||||
hoverText: "Make a Copy",
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
icon: TrashIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
deleteAgent(agent);
|
||||
},
|
||||
hoverText: "Delete",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<li
|
||||
role="listitem"
|
||||
key={"agentrow" + i}
|
||||
className=" "
|
||||
style={{ width: "200px" }}
|
||||
>
|
||||
<Card
|
||||
className="h-full p-2 cursor-pointer"
|
||||
title={
|
||||
<div className=" ">
|
||||
{truncateText(agent.config.name || "", 25)}
|
||||
</div>
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedAgent(agent);
|
||||
setShowAgentModal(true);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ minHeight: "65px" }}
|
||||
aria-hidden="true"
|
||||
className="my-2 break-words"
|
||||
>
|
||||
<div className="text-xs mb-2">{agent.type}</div>{" "}
|
||||
{truncateText(agent.config.description || "", 70)}
|
||||
</div>
|
||||
<div
|
||||
aria-label={`Updated ${timeAgo(agent.updated_at || "")}`}
|
||||
className="text-xs"
|
||||
>
|
||||
{timeAgo(agent.updated_at || "")}
|
||||
</div>
|
||||
<CardHoverBar items={cardItems} />
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const AgentModal = ({
|
||||
agent,
|
||||
setAgent,
|
||||
showAgentModal,
|
||||
setShowAgentModal,
|
||||
handler,
|
||||
}: {
|
||||
agent: IAgent | null;
|
||||
setAgent: (agent: IAgent | null) => void;
|
||||
showAgentModal: boolean;
|
||||
setShowAgentModal: (show: boolean) => void;
|
||||
handler?: (agent: IAgent | null) => void;
|
||||
}) => {
|
||||
const [localAgent, setLocalAgent] = React.useState<IAgent | null>(agent);
|
||||
|
||||
const closeModal = () => {
|
||||
setShowAgentModal(false);
|
||||
if (handler) {
|
||||
handler(localAgent);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<>Agent Configuration</>}
|
||||
width={800}
|
||||
open={showAgentModal}
|
||||
onOk={() => {
|
||||
closeModal();
|
||||
}}
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
}}
|
||||
footer={[]}
|
||||
>
|
||||
{agent && (
|
||||
<AgentViewer
|
||||
agent={localAgent || agent}
|
||||
setAgent={setLocalAgent}
|
||||
close={closeModal}
|
||||
/>
|
||||
)}
|
||||
{/* {JSON.stringify(localAgent)} */}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const uploadAgent = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => {
|
||||
const contents = e.target.result;
|
||||
if (contents) {
|
||||
try {
|
||||
const agent = JSON.parse(contents);
|
||||
// TBD validate that it is a valid agent
|
||||
if (!agent.config) {
|
||||
throw new Error(
|
||||
"Invalid agent file. An agent must have a config"
|
||||
);
|
||||
}
|
||||
setNewAgent(agent);
|
||||
setShowNewAgentModal(true);
|
||||
} catch (err) {
|
||||
message.error(
|
||||
"Invalid agent file. Please upload a valid agent file."
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const agentsMenuItems: MenuProps["items"] = [
|
||||
// {
|
||||
// type: "divider",
|
||||
// },
|
||||
{
|
||||
key: "uploadagent",
|
||||
label: (
|
||||
<div>
|
||||
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
|
||||
Upload Agent
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const agentsMenuItemOnClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "uploadagent") {
|
||||
uploadAgent();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-primary ">
|
||||
<AgentModal
|
||||
agent={selectedAgent}
|
||||
setAgent={setSelectedAgent}
|
||||
setShowAgentModal={setShowAgentModal}
|
||||
showAgentModal={showAgentModal}
|
||||
handler={(agent: IAgent | null) => {
|
||||
fetchAgents();
|
||||
}}
|
||||
/>
|
||||
|
||||
<AgentModal
|
||||
agent={newAgent || sampleAgent}
|
||||
setAgent={setNewAgent}
|
||||
setShowAgentModal={setShowNewAgentModal}
|
||||
showAgentModal={showNewAgentModal}
|
||||
handler={(agent: IAgent | null) => {
|
||||
fetchAgents();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mb-2 relative">
|
||||
<div className=" rounded ">
|
||||
<div className="flex mt-2 pb-2 mb-2 border-b">
|
||||
<div className="flex-1 font-semibold mb-2 ">
|
||||
{" "}
|
||||
Agents ({agentRows.length}){" "}
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
menu={{
|
||||
items: agentsMenuItems,
|
||||
onClick: agentsMenuItemOnClick,
|
||||
}}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
onClick={() => {
|
||||
setShowNewAgentModal(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 inline-block mr-1" />
|
||||
New Agent
|
||||
</Dropdown.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs mb-2 pb-1 ">
|
||||
{" "}
|
||||
Configure an agent that can reused in your agent workflow .
|
||||
<div>
|
||||
Tip: You can also create a Group of Agents ( New Agent -
|
||||
GroupChat) which can have multiple agents in it.
|
||||
</div>
|
||||
</div>
|
||||
{agents && agents.length > 0 && (
|
||||
<div className="w-full relative">
|
||||
<LoadingOverlay loading={loading} />
|
||||
<ul className=" flex flex-wrap gap-3">{agentRows}</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agents && agents.length === 0 && !loading && (
|
||||
<div className="text-sm border mt-4 rounded text-secondary p-2">
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
|
||||
No agents found. Please create a new agent.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className=" w-full text-center">
|
||||
{" "}
|
||||
<BounceLoader />{" "}
|
||||
<span className="inline-block"> loading .. </span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentsView;
|
||||
@@ -1,81 +0,0 @@
|
||||
import * as React from "react";
|
||||
import SkillsView from "./skills";
|
||||
import AgentsView from "./agents";
|
||||
import WorkflowView from "./workflow";
|
||||
import { Tabs } from "antd";
|
||||
import {
|
||||
BugAntIcon,
|
||||
CpuChipIcon,
|
||||
Square2StackIcon,
|
||||
Square3Stack3DIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import ModelsView from "./models";
|
||||
|
||||
const BuildView = () => {
|
||||
return (
|
||||
<div className=" ">
|
||||
{/* <div className="mb-4 text-2xl">Build </div> */}
|
||||
<div className="mb-6 text-sm hidden text-secondary">
|
||||
{" "}
|
||||
Create skills, agents and workflows for building multiagent capabilities{" "}
|
||||
</div>
|
||||
|
||||
<div className="mb-4 text-primary">
|
||||
{" "}
|
||||
<Tabs
|
||||
tabBarStyle={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
defaultActiveKey="4"
|
||||
tabPosition="left"
|
||||
items={[
|
||||
{
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Skills
|
||||
</div>
|
||||
),
|
||||
key: "1",
|
||||
children: <SkillsView />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<CpuChipIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Models
|
||||
</div>
|
||||
),
|
||||
key: "2",
|
||||
children: <ModelsView />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Square2StackIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Agents
|
||||
</>
|
||||
),
|
||||
key: "3",
|
||||
children: <AgentsView />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Square3Stack3DIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Workflows
|
||||
</>
|
||||
),
|
||||
key: "4",
|
||||
children: <WorkflowView />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildView;
|
||||
@@ -1,403 +0,0 @@
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
DocumentDuplicateIcon,
|
||||
InformationCircleIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Dropdown, MenuProps, Modal, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { IModelConfig, IStatus } from "../../types";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import {
|
||||
fetchJSON,
|
||||
getServerUrl,
|
||||
sanitizeConfig,
|
||||
timeAgo,
|
||||
truncateText,
|
||||
} from "../../utils";
|
||||
import { BounceLoader, Card, CardHoverBar, LoadingOverlay } from "../../atoms";
|
||||
import { ModelConfigView } from "./utils/modelconfig";
|
||||
|
||||
const ModelsView = ({}: any) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const listModelsUrl = `${serverUrl}/models?user_id=${user?.email}`;
|
||||
const createModelUrl = `${serverUrl}/models`;
|
||||
const testModelUrl = `${serverUrl}/models/test`;
|
||||
|
||||
const defaultModel: IModelConfig = {
|
||||
model: "gpt-4-1106-preview",
|
||||
description: "Sample OpenAI GPT-4 model",
|
||||
user_id: user?.email,
|
||||
};
|
||||
|
||||
const [models, setModels] = React.useState<IModelConfig[] | null>([]);
|
||||
const [selectedModel, setSelectedModel] = React.useState<IModelConfig | null>(
|
||||
null
|
||||
);
|
||||
const [newModel, setNewModel] = React.useState<IModelConfig | null>(
|
||||
defaultModel
|
||||
);
|
||||
|
||||
const [showNewModelModal, setShowNewModelModal] = React.useState(false);
|
||||
const [showModelModal, setShowModelModal] = React.useState(false);
|
||||
|
||||
const deleteModel = (model: IModelConfig) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
const deleteModelUrl = `${serverUrl}/models/delete?user_id=${user?.email}&model_id=${model.id}`;
|
||||
const payLoad = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
fetchModels();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(deleteModelUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const fetchModels = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
setModels(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(listModelsUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const createModel = (model: IModelConfig) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
model.user_id = user?.email;
|
||||
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
const updatedModels = [data.data].concat(models || []);
|
||||
setModels(updatedModels);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(createModelUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
// console.log("fetching messages", messages);
|
||||
fetchModels();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const modelRows = (models || []).map((model: IModelConfig, i: number) => {
|
||||
const cardItems = [
|
||||
{
|
||||
title: "Download",
|
||||
icon: ArrowDownTrayIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
// download workflow as workflow.name.json
|
||||
const element = document.createElement("a");
|
||||
const sanitizedSkill = sanitizeConfig(model);
|
||||
const file = new Blob([JSON.stringify(sanitizedSkill)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `model_${model.model}.json`;
|
||||
document.body.appendChild(element); // Required for this to work in FireFox
|
||||
element.click();
|
||||
},
|
||||
hoverText: "Download",
|
||||
},
|
||||
{
|
||||
title: "Make a Copy",
|
||||
icon: DocumentDuplicateIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
let newModel = { ...sanitizeConfig(model) };
|
||||
newModel.model = `${model.model}_copy`;
|
||||
setNewModel(newModel);
|
||||
setShowNewModelModal(true);
|
||||
},
|
||||
hoverText: "Make a Copy",
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
icon: TrashIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
deleteModel(model);
|
||||
},
|
||||
hoverText: "Delete",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<li
|
||||
role="listitem"
|
||||
key={"modelrow" + i}
|
||||
className=" "
|
||||
style={{ width: "200px" }}
|
||||
>
|
||||
<Card
|
||||
className="h-full p-2 cursor-pointer"
|
||||
title={
|
||||
<div className=" ">{truncateText(model.model || "", 20)}</div>
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedModel(model);
|
||||
setShowModelModal(true);
|
||||
}}
|
||||
>
|
||||
<div style={{ minHeight: "65px" }} className="my-2 break-words">
|
||||
{" "}
|
||||
{truncateText(model.description || model.model || "", 70)}
|
||||
</div>
|
||||
<div
|
||||
aria-label={`Updated ${timeAgo(model.updated_at || "")} `}
|
||||
className="text-xs"
|
||||
>
|
||||
{timeAgo(model.updated_at || "")}
|
||||
</div>
|
||||
<CardHoverBar items={cardItems} />
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const ModelModal = ({
|
||||
model,
|
||||
setModel,
|
||||
showModelModal,
|
||||
setShowModelModal,
|
||||
handler,
|
||||
}: {
|
||||
model: IModelConfig;
|
||||
setModel: (model: IModelConfig | null) => void;
|
||||
showModelModal: boolean;
|
||||
setShowModelModal: (show: boolean) => void;
|
||||
handler?: (agent: IModelConfig) => void;
|
||||
}) => {
|
||||
const [localModel, setLocalModel] = React.useState<IModelConfig>(model);
|
||||
|
||||
const closeModal = () => {
|
||||
setModel(null);
|
||||
setShowModelModal(false);
|
||||
if (handler) {
|
||||
handler(model);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<>
|
||||
Model Specification{" "}
|
||||
<span className="text-accent font-normal">{model?.model}</span>{" "}
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
open={showModelModal}
|
||||
footer={[]}
|
||||
onOk={() => {
|
||||
closeModal();
|
||||
}}
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{model && (
|
||||
<ModelConfigView
|
||||
model={localModel}
|
||||
setModel={setLocalModel}
|
||||
close={closeModal}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const uploadModel = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => {
|
||||
const contents = e.target.result;
|
||||
if (contents) {
|
||||
try {
|
||||
const model = JSON.parse(contents);
|
||||
if (model) {
|
||||
setNewModel(model);
|
||||
setShowNewModelModal(true);
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("Invalid model file");
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const modelsMenuItems: MenuProps["items"] = [
|
||||
// {
|
||||
// type: "divider",
|
||||
// },
|
||||
{
|
||||
key: "uploadmodel",
|
||||
label: (
|
||||
<div>
|
||||
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
|
||||
Upload Model
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const modelsMenuItemOnClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "uploadmodel") {
|
||||
uploadModel();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-primary ">
|
||||
{selectedModel && (
|
||||
<ModelModal
|
||||
model={selectedModel}
|
||||
setModel={setSelectedModel}
|
||||
setShowModelModal={setShowModelModal}
|
||||
showModelModal={showModelModal}
|
||||
handler={(model: IModelConfig | null) => {
|
||||
fetchModels();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ModelModal
|
||||
model={newModel || defaultModel}
|
||||
setModel={setNewModel}
|
||||
setShowModelModal={setShowNewModelModal}
|
||||
showModelModal={showNewModelModal}
|
||||
handler={(model: IModelConfig | null) => {
|
||||
fetchModels();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mb-2 relative">
|
||||
<div className=" rounded ">
|
||||
<div className="flex mt-2 pb-2 mb-2 border-b">
|
||||
<div className="flex-1 font-semibold mb-2 ">
|
||||
{" "}
|
||||
Models ({modelRows.length}){" "}
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
menu={{
|
||||
items: modelsMenuItems,
|
||||
onClick: modelsMenuItemOnClick,
|
||||
}}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
onClick={() => {
|
||||
setShowNewModelModal(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 inline-block mr-1" />
|
||||
New Model
|
||||
</Dropdown.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs mb-2 pb-1 ">
|
||||
{" "}
|
||||
Create model configurations that can be reused in your agents and
|
||||
workflows. {selectedModel?.model}
|
||||
</div>
|
||||
{models && models.length > 0 && (
|
||||
<div className="w-full relative">
|
||||
<LoadingOverlay loading={loading} />
|
||||
<ul className=" flex flex-wrap gap-3">{modelRows}</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{models && models.length === 0 && !loading && (
|
||||
<div className="text-sm border mt-4 rounded text-secondary p-2">
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
|
||||
No models found. Please create a new model which can be reused
|
||||
with agents.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className=" w-full text-center">
|
||||
{" "}
|
||||
<BounceLoader />{" "}
|
||||
<span className="inline-block"> loading .. </span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsView;
|
||||
@@ -1,380 +0,0 @@
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
CodeBracketIcon,
|
||||
CodeBracketSquareIcon,
|
||||
DocumentDuplicateIcon,
|
||||
InformationCircleIcon,
|
||||
KeyIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Button, Input, Modal, message, MenuProps, Dropdown, Tabs } from "antd";
|
||||
import * as React from "react";
|
||||
import { ISkill, IStatus } from "../../types";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import {
|
||||
fetchJSON,
|
||||
getSampleSkill,
|
||||
getServerUrl,
|
||||
sanitizeConfig,
|
||||
timeAgo,
|
||||
truncateText,
|
||||
} from "../../utils";
|
||||
import {
|
||||
BounceLoader,
|
||||
Card,
|
||||
CardHoverBar,
|
||||
LoadingOverlay,
|
||||
MonacoEditor,
|
||||
} from "../../atoms";
|
||||
import { SkillSelector } from "./utils/selectors";
|
||||
import { SkillConfigView } from "./utils/skillconfig";
|
||||
|
||||
const SkillsView = ({}: any) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const listSkillsUrl = `${serverUrl}/skills?user_id=${user?.email}`;
|
||||
const saveSkillsUrl = `${serverUrl}/skills`;
|
||||
|
||||
const [skills, setSkills] = React.useState<ISkill[] | null>([]);
|
||||
const [selectedSkill, setSelectedSkill] = React.useState<any>(null);
|
||||
|
||||
const [showSkillModal, setShowSkillModal] = React.useState(false);
|
||||
const [showNewSkillModal, setShowNewSkillModal] = React.useState(false);
|
||||
|
||||
const sampleSkill = getSampleSkill();
|
||||
const [newSkill, setNewSkill] = React.useState<ISkill | null>(sampleSkill);
|
||||
|
||||
const deleteSkill = (skill: ISkill) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const deleteSkillUrl = `${serverUrl}/skills/delete?user_id=${user?.email}&skill_id=${skill.id}`;
|
||||
const payLoad = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: user?.email,
|
||||
skill: skill,
|
||||
}),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
fetchSkills();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(deleteSkillUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const fetchSkills = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
// message.success(data.message);
|
||||
console.log("skills", data.data);
|
||||
setSkills(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(listSkillsUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
// console.log("fetching messages", messages);
|
||||
fetchSkills();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const skillRows = (skills || []).map((skill: ISkill, i: number) => {
|
||||
const cardItems = [
|
||||
{
|
||||
title: "Download",
|
||||
icon: ArrowDownTrayIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
// download workflow as workflow.name.json
|
||||
const element = document.createElement("a");
|
||||
const sanitizedSkill = sanitizeConfig(skill);
|
||||
const file = new Blob([JSON.stringify(sanitizedSkill)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `skill_${skill.name}.json`;
|
||||
document.body.appendChild(element); // Required for this to work in FireFox
|
||||
element.click();
|
||||
},
|
||||
hoverText: "Download",
|
||||
},
|
||||
{
|
||||
title: "Make a Copy",
|
||||
icon: DocumentDuplicateIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
let newSkill = { ...sanitizeConfig(skill) };
|
||||
newSkill.name = `${skill.name}_copy`;
|
||||
setNewSkill(newSkill);
|
||||
setShowNewSkillModal(true);
|
||||
},
|
||||
hoverText: "Make a Copy",
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
icon: TrashIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
deleteSkill(skill);
|
||||
},
|
||||
hoverText: "Delete",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<li key={"skillrow" + i} className=" " style={{ width: "200px" }}>
|
||||
<div>
|
||||
{" "}
|
||||
<Card
|
||||
className="h-full p-2 cursor-pointer group"
|
||||
title={truncateText(skill.name, 25)}
|
||||
onClick={() => {
|
||||
setSelectedSkill(skill);
|
||||
setShowSkillModal(true);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ minHeight: "65px" }}
|
||||
className="my-2 break-words"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{" "}
|
||||
{skill.description
|
||||
? truncateText(skill.description || "", 70)
|
||||
: truncateText(skill.content || "", 70)}
|
||||
</div>
|
||||
<div
|
||||
aria-label={`Updated ${timeAgo(skill.updated_at || "")}`}
|
||||
className="text-xs"
|
||||
>
|
||||
{timeAgo(skill.updated_at || "")}
|
||||
</div>
|
||||
<CardHoverBar items={cardItems} />
|
||||
</Card>
|
||||
<div className="text-right mt-2"></div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const SkillModal = ({
|
||||
skill,
|
||||
setSkill,
|
||||
showSkillModal,
|
||||
setShowSkillModal,
|
||||
handler,
|
||||
}: {
|
||||
skill: ISkill | null;
|
||||
setSkill: any;
|
||||
showSkillModal: boolean;
|
||||
setShowSkillModal: any;
|
||||
handler: any;
|
||||
}) => {
|
||||
const editorRef = React.useRef<any | null>(null);
|
||||
const [localSkill, setLocalSkill] = React.useState<ISkill | null>(skill);
|
||||
|
||||
const closeModal = () => {
|
||||
setSkill(null);
|
||||
setShowSkillModal(false);
|
||||
if (handler) {
|
||||
handler(skill);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<>
|
||||
Skill Specification{" "}
|
||||
<span className="text-accent font-normal">{localSkill?.name}</span>{" "}
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
open={showSkillModal}
|
||||
onCancel={() => {
|
||||
setShowSkillModal(false);
|
||||
}}
|
||||
footer={[]}
|
||||
>
|
||||
{localSkill && (
|
||||
<SkillConfigView
|
||||
skill={localSkill}
|
||||
setSkill={setLocalSkill}
|
||||
close={closeModal}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const uploadSkill = () => {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = ".json";
|
||||
fileInput.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result;
|
||||
if (content) {
|
||||
try {
|
||||
const skill = JSON.parse(content as string);
|
||||
if (skill) {
|
||||
setNewSkill(skill);
|
||||
setShowNewSkillModal(true);
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("Invalid skill file");
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
const skillsMenuItems: MenuProps["items"] = [
|
||||
// {
|
||||
// type: "divider",
|
||||
// },
|
||||
{
|
||||
key: "uploadskill",
|
||||
label: (
|
||||
<div>
|
||||
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
|
||||
Upload Skill
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const skillsMenuItemOnClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "uploadskill") {
|
||||
uploadSkill();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" text-primary ">
|
||||
<SkillModal
|
||||
skill={selectedSkill}
|
||||
setSkill={setSelectedSkill}
|
||||
showSkillModal={showSkillModal}
|
||||
setShowSkillModal={setShowSkillModal}
|
||||
handler={(skill: ISkill) => {
|
||||
fetchSkills();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SkillModal
|
||||
skill={newSkill || sampleSkill}
|
||||
setSkill={setNewSkill}
|
||||
showSkillModal={showNewSkillModal}
|
||||
setShowSkillModal={setShowNewSkillModal}
|
||||
handler={(skill: ISkill) => {
|
||||
fetchSkills();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mb-2 relative">
|
||||
<div className="">
|
||||
<div className="flex mt-2 pb-2 mb-2 border-b">
|
||||
<ul className="flex-1 font-semibold mb-2 ">
|
||||
{" "}
|
||||
Skills ({skillRows.length}){" "}
|
||||
</ul>
|
||||
<div>
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
menu={{
|
||||
items: skillsMenuItems,
|
||||
onClick: skillsMenuItemOnClick,
|
||||
}}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
onClick={() => {
|
||||
setShowNewSkillModal(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 inline-block mr-1" />
|
||||
New Skill
|
||||
</Dropdown.Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mb-2 pb-1 ">
|
||||
{" "}
|
||||
Skills are python functions that agents can use to solve tasks.{" "}
|
||||
</div>
|
||||
{skills && skills.length > 0 && (
|
||||
<div
|
||||
// style={{ height: "400px" }}
|
||||
className="w-full relative"
|
||||
>
|
||||
<LoadingOverlay loading={loading} />
|
||||
<div className=" flex flex-wrap gap-3">{skillRows}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{skills && skills.length === 0 && !loading && (
|
||||
<div className="text-sm border mt-4 rounded text-secondary p-2">
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
|
||||
No skills found. Please create a new skill.
|
||||
</div>
|
||||
)}
|
||||
{loading && (
|
||||
<div className=" w-full text-center">
|
||||
{" "}
|
||||
<BounceLoader />{" "}
|
||||
<span className="inline-block"> loading .. </span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsView;
|
||||
@@ -1,517 +0,0 @@
|
||||
import React from "react";
|
||||
import { CollapseBox, ControlRowView } from "../../../atoms";
|
||||
import { checkAndSanitizeInput, fetchJSON, getServerUrl } from "../../../utils";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Slider,
|
||||
Tabs,
|
||||
message,
|
||||
theme,
|
||||
} from "antd";
|
||||
import {
|
||||
BugAntIcon,
|
||||
CpuChipIcon,
|
||||
UserGroupIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import {
|
||||
AgentSelector,
|
||||
AgentTypeSelector,
|
||||
ModelSelector,
|
||||
SkillSelector,
|
||||
} from "./selectors";
|
||||
import { IAgent, ILLMConfig } from "../../../types";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const AgentConfigView = ({
|
||||
agent,
|
||||
setAgent,
|
||||
close,
|
||||
}: {
|
||||
agent: IAgent;
|
||||
setAgent: (agent: IAgent) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const nameValidation = checkAndSanitizeInput(agent?.config?.name);
|
||||
const [error, setError] = React.useState<any>(null);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const createAgentUrl = `${serverUrl}/agents`;
|
||||
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
|
||||
|
||||
const onControlChange = (value: any, key: string) => {
|
||||
// if (key === "llm_config") {
|
||||
// if (value.config_list.length === 0) {
|
||||
// value = false;
|
||||
// }
|
||||
// }
|
||||
const updatedAgent = {
|
||||
...agent,
|
||||
config: { ...agent.config, [key]: value },
|
||||
};
|
||||
|
||||
setAgent(updatedAgent);
|
||||
setControlChanged(true);
|
||||
};
|
||||
|
||||
const llm_config: ILLMConfig = agent?.config?.llm_config || {
|
||||
config_list: [],
|
||||
temperature: 0.1,
|
||||
max_tokens: 4000,
|
||||
};
|
||||
|
||||
const createAgent = (agent: IAgent) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
|
||||
console.log("agent", agent);
|
||||
agent.user_id = user?.email;
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(agent),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
console.log("agents", data.data);
|
||||
const newAgent = data.data;
|
||||
setAgent(newAgent);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
// setNewAgent(sampleAgent);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
const onFinal = () => {
|
||||
setLoading(false);
|
||||
setControlChanged(false);
|
||||
};
|
||||
|
||||
fetchJSON(createAgentUrl, payLoad, onSuccess, onError, onFinal);
|
||||
};
|
||||
|
||||
const hasChanged =
|
||||
(!controlChanged || !nameValidation.status) && agent?.id !== undefined;
|
||||
|
||||
return (
|
||||
<div className="text-primary">
|
||||
<Form>
|
||||
<div
|
||||
className={`grid gap-3 ${
|
||||
agent.type === "groupchat" ? "grid-cols-2" : "grid-cols-1"
|
||||
}`}
|
||||
>
|
||||
<div className="">
|
||||
<ControlRowView
|
||||
title="Agent Name"
|
||||
className=""
|
||||
description="Name of the agent"
|
||||
value={agent?.config?.name}
|
||||
control={
|
||||
<>
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Agent Name"
|
||||
value={agent?.config?.name}
|
||||
onChange={(e) => {
|
||||
onControlChange(e.target.value, "name");
|
||||
}}
|
||||
/>
|
||||
{!nameValidation.status && (
|
||||
<div className="text-xs text-red-500 mt-2">
|
||||
{nameValidation.message}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Agent Description"
|
||||
className="mt-4"
|
||||
description="Description of the agent, used by other agents
|
||||
(e.g. the GroupChatManager) to decide when to call upon this agent. (Default: system_message)"
|
||||
value={agent.config.description || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Agent Description"
|
||||
value={agent.config.description || ""}
|
||||
onChange={(e) => {
|
||||
onControlChange(e.target.value, "description");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Max Consecutive Auto Reply"
|
||||
className="mt-4"
|
||||
description="Max consecutive auto reply messages before termination."
|
||||
value={agent.config?.max_consecutive_auto_reply}
|
||||
control={
|
||||
<Slider
|
||||
min={1}
|
||||
max={agent.type === "groupchat" ? 600 : 30}
|
||||
defaultValue={agent.config.max_consecutive_auto_reply}
|
||||
step={1}
|
||||
onChange={(value: any) => {
|
||||
onControlChange(value, "max_consecutive_auto_reply");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Human Input Mode"
|
||||
description="Defines when to request human input"
|
||||
value={agent.config.human_input_mode}
|
||||
control={
|
||||
<Select
|
||||
className="mt-2 w-full"
|
||||
defaultValue={agent.config.human_input_mode}
|
||||
onChange={(value: any) => {
|
||||
onControlChange(value, "human_input_mode");
|
||||
}}
|
||||
options={
|
||||
[
|
||||
{ label: "NEVER", value: "NEVER" },
|
||||
{ label: "TERMINATE", value: "TERMINATE" },
|
||||
{ label: "ALWAYS", value: "ALWAYS" },
|
||||
] as any
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="System Message"
|
||||
className="mt-4"
|
||||
description="Free text to control agent behavior"
|
||||
value={agent.config.system_message}
|
||||
control={
|
||||
<TextArea
|
||||
className="mt-2 w-full"
|
||||
value={agent.config.system_message}
|
||||
rows={3}
|
||||
onChange={(e) => {
|
||||
onControlChange(e.target.value, "system_message");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
{" "}
|
||||
<CollapseBox
|
||||
className="bg-secondary mt-4"
|
||||
open={false}
|
||||
title="Advanced Options"
|
||||
>
|
||||
<ControlRowView
|
||||
title="Temperature"
|
||||
className="mt-4"
|
||||
description="Defines the randomness of the agent's response."
|
||||
value={llm_config.temperature}
|
||||
control={
|
||||
<Slider
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
defaultValue={llm_config.temperature || 0.1}
|
||||
onChange={(value: any) => {
|
||||
const llm_config = {
|
||||
...agent.config.llm_config,
|
||||
temperature: value,
|
||||
};
|
||||
onControlChange(llm_config, "llm_config");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Agent Default Auto Reply"
|
||||
className="mt-4"
|
||||
description="Default auto reply when no code execution or llm-based reply is generated."
|
||||
value={agent.config.default_auto_reply || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Agent Description"
|
||||
value={agent.config.default_auto_reply || ""}
|
||||
onChange={(e) => {
|
||||
onControlChange(e.target.value, "default_auto_reply");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Max Tokens"
|
||||
description="Max tokens generated by LLM used in the agent's response."
|
||||
value={llm_config.max_tokens}
|
||||
className="mt-4"
|
||||
control={
|
||||
<Slider
|
||||
min={100}
|
||||
max={50000}
|
||||
defaultValue={llm_config.max_tokens || 1000}
|
||||
onChange={(value: any) => {
|
||||
const llm_config = {
|
||||
...agent.config.llm_config,
|
||||
max_tokens: value,
|
||||
};
|
||||
onControlChange(llm_config, "llm_config");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ControlRowView
|
||||
title="Code Execution Config"
|
||||
className="mt-4"
|
||||
description="Determines if and where code execution is done."
|
||||
value={agent.config.code_execution_config || "none"}
|
||||
control={
|
||||
<Select
|
||||
className="mt-2 w-full"
|
||||
defaultValue={
|
||||
agent.config.code_execution_config || "none"
|
||||
}
|
||||
onChange={(value: any) => {
|
||||
onControlChange(value, "code_execution_config");
|
||||
}}
|
||||
options={
|
||||
[
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Local", value: "local" },
|
||||
{ label: "Docker", value: "docker" },
|
||||
] as any
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</CollapseBox>
|
||||
</div>
|
||||
</div>
|
||||
{/* ====================== Group Chat Config ======================= */}
|
||||
{agent.type === "groupchat" && (
|
||||
<div>
|
||||
<ControlRowView
|
||||
title="Speaker Selection Method"
|
||||
description="How the next speaker is selected"
|
||||
className=""
|
||||
value={agent?.config?.speaker_selection_method || "auto"}
|
||||
control={
|
||||
<Select
|
||||
className="mt-2 w-full"
|
||||
defaultValue={
|
||||
agent?.config?.speaker_selection_method || "auto"
|
||||
}
|
||||
onChange={(value: any) => {
|
||||
if (agent?.config) {
|
||||
onControlChange(value, "speaker_selection_method");
|
||||
}
|
||||
}}
|
||||
options={
|
||||
[
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "Round Robin", value: "round_robin" },
|
||||
{ label: "Random", value: "random" },
|
||||
] as any
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Admin Name"
|
||||
className="mt-4"
|
||||
description="Name of the admin of the group chat"
|
||||
value={agent.config.admin_name || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Agent Description"
|
||||
value={agent.config.admin_name || ""}
|
||||
onChange={(e) => {
|
||||
onControlChange(e.target.value, "admin_name");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Max Rounds"
|
||||
className="mt-4"
|
||||
description="Max rounds before termination."
|
||||
value={agent.config?.max_round || 10}
|
||||
control={
|
||||
<Slider
|
||||
min={10}
|
||||
max={600}
|
||||
defaultValue={agent.config.max_round}
|
||||
step={1}
|
||||
onChange={(value: any) => {
|
||||
onControlChange(value, "max_round");
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Allow Repeat Speaker"
|
||||
className="mt-4"
|
||||
description="Allow the same speaker to speak multiple times in a row"
|
||||
value={agent.config?.allow_repeat_speaker || false}
|
||||
control={
|
||||
<Select
|
||||
className="mt-2 w-full"
|
||||
defaultValue={agent.config.allow_repeat_speaker}
|
||||
onChange={(value: any) => {
|
||||
onControlChange(value, "allow_repeat_speaker");
|
||||
}}
|
||||
options={
|
||||
[
|
||||
{ label: "True", value: true },
|
||||
{ label: "False", value: false },
|
||||
] as any
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="w-full mt-4 text-right">
|
||||
{" "}
|
||||
{!hasChanged && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
createAgent(agent);
|
||||
setAgent(agent);
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
{agent.id ? "Update Agent" : "Create Agent"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="ml-2"
|
||||
key="close"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentViewer = ({
|
||||
agent,
|
||||
setAgent,
|
||||
close,
|
||||
}: {
|
||||
agent: IAgent | null;
|
||||
setAgent: (newAgent: IAgent) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
let items = [
|
||||
{
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Agent Configuration
|
||||
</div>
|
||||
),
|
||||
key: "1",
|
||||
children: (
|
||||
<div>
|
||||
{!agent?.type && (
|
||||
<AgentTypeSelector agent={agent} setAgent={setAgent} />
|
||||
)}
|
||||
|
||||
{agent?.type && agent && (
|
||||
<AgentConfigView agent={agent} setAgent={setAgent} close={close} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (agent) {
|
||||
if (agent?.id) {
|
||||
if (agent.type && agent.type === "groupchat") {
|
||||
items.push({
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<UserGroupIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Agents
|
||||
</div>
|
||||
),
|
||||
key: "2",
|
||||
children: <AgentSelector agentId={agent?.id} />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<CpuChipIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Models
|
||||
</div>
|
||||
),
|
||||
key: "3",
|
||||
children: <ModelSelector agentId={agent?.id} />,
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: (
|
||||
<>
|
||||
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Skills
|
||||
</>
|
||||
),
|
||||
key: "4",
|
||||
children: <SkillSelector agentId={agent?.id} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-primary">
|
||||
{/* <RenderView viewIndex={currentViewIndex} /> */}
|
||||
<Tabs
|
||||
tabBarStyle={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
defaultActiveKey="1"
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,207 +0,0 @@
|
||||
import { Button, Modal, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { IWorkflow } from "../../../types";
|
||||
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
checkAndSanitizeInput,
|
||||
fetchJSON,
|
||||
getServerUrl,
|
||||
sanitizeConfig,
|
||||
} from "../../../utils";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { CodeBlock } from "../../../atoms";
|
||||
|
||||
export const ExportWorkflowModal = ({
|
||||
workflow,
|
||||
show,
|
||||
setShow,
|
||||
}: {
|
||||
workflow: IWorkflow | null;
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
}) => {
|
||||
const serverUrl = getServerUrl();
|
||||
const { user } = React.useContext(appContext);
|
||||
|
||||
const [error, setError] = React.useState<any>(null);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [workflowDetails, setWorkflowDetails] = React.useState<any>(null);
|
||||
|
||||
const getWorkflowCode = (workflow: IWorkflow) => {
|
||||
const workflowCode = `from autogenstudio import WorkflowManager
|
||||
# load workflow from exported json workflow file.
|
||||
workflow_manager = WorkflowManager(workflow="path/to/your/workflow_.json")
|
||||
|
||||
# run the workflow on a task
|
||||
task_query = "What is the height of the Eiffel Tower?. Dont write code, just respond to the question."
|
||||
workflow_manager.run(message=task_query)`;
|
||||
return workflowCode;
|
||||
};
|
||||
|
||||
const getCliWorkflowCode = (workflow: IWorkflow) => {
|
||||
const workflowCode = `autogenstudio serve --workflow=workflow.json --port=5000
|
||||
`;
|
||||
return workflowCode;
|
||||
};
|
||||
|
||||
const getGunicornWorkflowCode = (workflow: IWorkflow) => {
|
||||
const workflowCode = `gunicorn -w $((2 * $(getconf _NPROCESSORS_ONLN) + 1)) --timeout 12600 -k uvicorn.workers.UvicornWorker autogenstudio.web.app:app --bind `;
|
||||
|
||||
return workflowCode;
|
||||
};
|
||||
|
||||
const fetchWorkFlow = (workflow: IWorkflow) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
const downloadWorkflowUrl = `${serverUrl}/workflows/export/${workflow.id}?user_id=${user?.email}`;
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
setWorkflowDetails(data.data);
|
||||
console.log("workflow details", data.data);
|
||||
|
||||
const sanitized_name =
|
||||
checkAndSanitizeInput(workflow.name).sanitizedText || workflow.name;
|
||||
const file_name = `workflow_${sanitized_name}.json`;
|
||||
const workflowData = sanitizeConfig(data.data);
|
||||
const file = new Blob([JSON.stringify(workflowData)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const downloadUrl = URL.createObjectURL(file);
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadUrl;
|
||||
a.download = file_name;
|
||||
a.click();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(downloadWorkflowUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (workflow && workflow.id && show) {
|
||||
// fetchWorkFlow(workflow.id);
|
||||
console.log("workflow modal ... component loaded", workflow);
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<>
|
||||
Export Workflow
|
||||
<span className="text-accent font-normal ml-2">
|
||||
{workflow?.name}
|
||||
</span>{" "}
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
open={show}
|
||||
onOk={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
footer={[]}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
{" "}
|
||||
You can use the following steps to start integrating your workflow
|
||||
into your application.{" "}
|
||||
</div>
|
||||
{workflow && workflow.id && (
|
||||
<>
|
||||
<div className="flex mt-2 gap-3">
|
||||
<div>
|
||||
<div className="text-sm mt-2 mb-2 pb-1 font-bold">Step 1</div>
|
||||
<div className="mt-2 mb-2 pb-1 text-xs">
|
||||
Download your workflow as a JSON file by clicking the button
|
||||
below.
|
||||
</div>
|
||||
|
||||
<div className="text-sm mt-2 mb-2 pb-1">
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
fetchWorkFlow(workflow);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
<ArrowDownTrayIcon className="h-4 w-4 inline-block ml-2 -mt-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm mt-2 mb-2 pb-1 font-bold">Step 2</div>
|
||||
<div className=" mt-2 mb-2 pb-1 text-xs">
|
||||
Copy the following code snippet and paste it into your
|
||||
application to run your workflow on a task.
|
||||
</div>
|
||||
<div className="text-sm mt-2 mb-2 pb-1">
|
||||
<CodeBlock
|
||||
className="text-xs"
|
||||
code={getWorkflowCode(workflow)}
|
||||
language="python"
|
||||
wrapLines={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm mt-2 mb-2 pb-1 font-bold">
|
||||
Step 3 (Deploy)
|
||||
</div>
|
||||
<div className=" mt-2 mb-2 pb-1 text-xs">
|
||||
You can also deploy your workflow as an API endpoint using the
|
||||
autogenstudio python CLI.
|
||||
</div>
|
||||
|
||||
<div className="text-sm mt-2 mb-2 pb-1">
|
||||
<CodeBlock
|
||||
className="text-xs"
|
||||
code={getCliWorkflowCode(workflow)}
|
||||
language="bash"
|
||||
wrapLines={true}
|
||||
/>
|
||||
|
||||
<div className="text-xs mt-2">
|
||||
Note: this will start a endpoint on port 5000. You can change
|
||||
the port by changing the port number. You can also scale this
|
||||
using multiple workers (e.g., via an application server like
|
||||
gunicorn) or wrap it in a docker container and deploy on a
|
||||
cloud provider like Azure.
|
||||
</div>
|
||||
|
||||
<CodeBlock
|
||||
className="text-xs"
|
||||
code={getGunicornWorkflowCode(workflow)}
|
||||
language="bash"
|
||||
wrapLines={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,388 +0,0 @@
|
||||
import React from "react";
|
||||
import { fetchJSON, getServerUrl, sampleModelConfig } from "../../../utils";
|
||||
import { Button, Input, message, theme } from "antd";
|
||||
import {
|
||||
CpuChipIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { IModelConfig, IStatus } from "../../../types";
|
||||
import { Card, ControlRowView } from "../../../atoms";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
|
||||
const ModelTypeSelector = ({
|
||||
model,
|
||||
setModel,
|
||||
}: {
|
||||
model: IModelConfig;
|
||||
setModel: (newModel: IModelConfig) => void;
|
||||
}) => {
|
||||
const modelTypes = [
|
||||
{
|
||||
label: "OpenAI",
|
||||
value: "open_ai",
|
||||
description: "OpenAI or other endpoints that implement the OpenAI API",
|
||||
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
|
||||
hint: "In addition to OpenAI models, You can also use OSS models via tools like Ollama, vLLM, LMStudio etc. that provide OpenAI compatible endpoint.",
|
||||
},
|
||||
{
|
||||
label: "Azure OpenAI",
|
||||
value: "azure",
|
||||
description: "Azure OpenAI endpoint",
|
||||
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
|
||||
hint: "Azure OpenAI endpoint",
|
||||
},
|
||||
{
|
||||
label: "Gemini",
|
||||
value: "google",
|
||||
description: "Gemini",
|
||||
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
|
||||
hint: "Gemini",
|
||||
},
|
||||
{
|
||||
label: "Claude",
|
||||
value: "anthropic",
|
||||
description: "Anthropic Claude",
|
||||
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
|
||||
hint: "Anthropic Claude models",
|
||||
},
|
||||
{
|
||||
label: "Mistral",
|
||||
value: "mistral",
|
||||
description: "Mistral",
|
||||
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
|
||||
hint: "Mistral models",
|
||||
},
|
||||
];
|
||||
|
||||
const [selectedType, setSelectedType] = React.useState<string | undefined>(
|
||||
model?.api_type
|
||||
);
|
||||
|
||||
const modelTypeRows = modelTypes.map((modelType: any, i: number) => {
|
||||
return (
|
||||
<li
|
||||
onMouseEnter={() => {
|
||||
setSelectedHint(modelType.hint);
|
||||
}}
|
||||
role="listitem"
|
||||
key={"modeltype" + i}
|
||||
className="w-36"
|
||||
>
|
||||
<Card
|
||||
active={selectedType === modelType.value}
|
||||
className="h-full p-2 cursor-pointer"
|
||||
title={<div className=" ">{modelType.label}</div>}
|
||||
onClick={() => {
|
||||
setSelectedType(modelType.value);
|
||||
if (model) {
|
||||
const sampleModel = sampleModelConfig(modelType.value);
|
||||
setModel(sampleModel);
|
||||
// setAgent(sampleAgent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ minHeight: "35px" }} className="my-2 break-words ">
|
||||
{" "}
|
||||
<div className="mb-2">{modelType.icon}</div>
|
||||
<span className="text-secondary tex-sm">
|
||||
{" "}
|
||||
{modelType.description}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const [selectedHint, setSelectedHint] = React.useState<string>("open_ai");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-3">Select Model Type</div>
|
||||
<ul className="inline-flex gap-2">{modelTypeRows}</ul>
|
||||
|
||||
<div className="text-xs mt-4">
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1 -mt-1" />
|
||||
{selectedHint}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ModelConfigMainView = ({
|
||||
model,
|
||||
setModel,
|
||||
close,
|
||||
}: {
|
||||
model: IModelConfig;
|
||||
setModel: (newModel: IModelConfig) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [modelStatus, setModelStatus] = React.useState<IStatus | null>(null);
|
||||
const serverUrl = getServerUrl();
|
||||
const { user } = React.useContext(appContext);
|
||||
const testModelUrl = `${serverUrl}/models/test`;
|
||||
const createModelUrl = `${serverUrl}/models`;
|
||||
|
||||
// const [model, setmodel] = React.useState<IModelConfig | null>(
|
||||
// model
|
||||
// );
|
||||
const testModel = (model: IModelConfig) => {
|
||||
setModelStatus(null);
|
||||
setLoading(true);
|
||||
model.user_id = user?.email;
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
setModelStatus(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
setModelStatus(data);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(testModelUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
const createModel = (model: IModelConfig) => {
|
||||
setLoading(true);
|
||||
model.user_id = user?.email;
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
setModel(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
const onFinal = () => {
|
||||
setLoading(false);
|
||||
setControlChanged(false);
|
||||
};
|
||||
fetchJSON(createModelUrl, payLoad, onSuccess, onError, onFinal);
|
||||
};
|
||||
|
||||
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
|
||||
|
||||
const updateModelConfig = (key: string, value: string) => {
|
||||
if (model) {
|
||||
const updatedModelConfig = { ...model, [key]: value };
|
||||
// setmodel(updatedModelConfig);
|
||||
setModel(updatedModelConfig);
|
||||
}
|
||||
setControlChanged(true);
|
||||
};
|
||||
|
||||
const hasChanged = !controlChanged && model.id !== undefined;
|
||||
|
||||
return (
|
||||
<div className="relative ">
|
||||
<div className="text-sm my-2">
|
||||
Enter parameters for your{" "}
|
||||
<span className="mx-1 text-accent">{model.api_type}</span> model.
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<ControlRowView
|
||||
title="Model"
|
||||
className=""
|
||||
description="Model name"
|
||||
value={model?.model || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2 w-full"
|
||||
value={model?.model}
|
||||
onChange={(e) => {
|
||||
updateModelConfig("model", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Base URL"
|
||||
className=""
|
||||
description="Base URL for Model Endpoint"
|
||||
value={model?.base_url || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2 w-full"
|
||||
value={model?.base_url}
|
||||
onChange={(e) => {
|
||||
updateModelConfig("base_url", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ControlRowView
|
||||
title="API Key"
|
||||
className=""
|
||||
description="API Key"
|
||||
value={model?.api_key || ""}
|
||||
truncateLength={5}
|
||||
control={
|
||||
<Input.Password
|
||||
className="mt-2 w-full"
|
||||
value={model?.api_key}
|
||||
onChange={(e) => {
|
||||
updateModelConfig("api_key", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{model?.api_type == "azure" && (
|
||||
<ControlRowView
|
||||
title="API Version"
|
||||
className=" "
|
||||
description="API Version, required by Azure Models"
|
||||
value={model?.api_version || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2 w-full"
|
||||
value={model?.api_version}
|
||||
onChange={(e) => {
|
||||
updateModelConfig("api_version", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ControlRowView
|
||||
title="Description"
|
||||
className="mt-4"
|
||||
description="Description of the model"
|
||||
value={model?.description || ""}
|
||||
control={
|
||||
<TextArea
|
||||
className="mt-2 w-full"
|
||||
value={model?.description}
|
||||
onChange={(e) => {
|
||||
updateModelConfig("description", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{model?.api_type === "azure" && (
|
||||
<div className="mt-4 text-xs">
|
||||
Note: For Azure OAI models, you will need to specify all fields.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modelStatus && (
|
||||
<div
|
||||
className={`text-sm border mt-4 rounded text-secondary p-2 ${
|
||||
modelStatus.status ? "border-accent" : " border-red-500 "
|
||||
}`}
|
||||
>
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
|
||||
{modelStatus.message}
|
||||
|
||||
{/* <span className="block"> Note </span> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full mt-4 text-right">
|
||||
<Button
|
||||
key="test"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
if (model) {
|
||||
testModel(model);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test Model
|
||||
</Button>
|
||||
|
||||
{!hasChanged && (
|
||||
<Button
|
||||
className="ml-2"
|
||||
key="save"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (model) {
|
||||
createModel(model);
|
||||
setModel(model);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{model?.id ? "Update Model" : "Save Model"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="ml-2"
|
||||
key="close"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelConfigView = ({
|
||||
model,
|
||||
setModel,
|
||||
close,
|
||||
}: {
|
||||
model: IModelConfig;
|
||||
setModel: (newModel: IModelConfig) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-primary">
|
||||
<div>
|
||||
{!model?.api_type && (
|
||||
<ModelTypeSelector model={model} setModel={setModel} />
|
||||
)}
|
||||
|
||||
{model?.api_type && model && (
|
||||
<ModelConfigMainView
|
||||
model={model}
|
||||
setModel={setModel}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,295 +0,0 @@
|
||||
import React from "react";
|
||||
import { fetchJSON, getServerUrl, sampleModelConfig } from "../../../utils";
|
||||
import { Button, Input, message, theme } from "antd";
|
||||
import {
|
||||
CpuChipIcon,
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
InformationCircleIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ISkill, IStatus } from "../../../types";
|
||||
import { Card, ControlRowView, MonacoEditor } from "../../../atoms";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
|
||||
const SecretsEditor = ({
|
||||
secrets = [],
|
||||
updateSkillConfig,
|
||||
}: {
|
||||
secrets: { secret: string; value: string }[];
|
||||
updateSkillConfig: (key: string, value: any) => void;
|
||||
}) => {
|
||||
const [editingIndex, setEditingIndex] = React.useState<number | null>(null);
|
||||
const [newSecret, setNewSecret] = React.useState<string>("");
|
||||
const [newValue, setNewValue] = React.useState<string>("");
|
||||
|
||||
const toggleEditing = (index: number) => {
|
||||
setEditingIndex(editingIndex === index ? null : index);
|
||||
};
|
||||
|
||||
const handleAddSecret = () => {
|
||||
if (newSecret && newValue) {
|
||||
const updatedSecrets = [
|
||||
...secrets,
|
||||
{ secret: newSecret, value: newValue },
|
||||
];
|
||||
updateSkillConfig("secrets", updatedSecrets);
|
||||
setNewSecret("");
|
||||
setNewValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSecret = (index: number) => {
|
||||
const updatedSecrets = secrets.filter((_, i) => i !== index);
|
||||
updateSkillConfig("secrets", updatedSecrets);
|
||||
};
|
||||
|
||||
const handleSecretChange = (index: number, key: string, value: string) => {
|
||||
const updatedSecrets = secrets.map((item, i) =>
|
||||
i === index ? { ...item, [key]: value } : item
|
||||
);
|
||||
updateSkillConfig("secrets", updatedSecrets);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
{secrets && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{secrets.map((secret, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input
|
||||
value={secret.secret}
|
||||
disabled={editingIndex !== index}
|
||||
onChange={(e) =>
|
||||
handleSecretChange(index, "secret", e.target.value)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input.Password
|
||||
value={secret.value}
|
||||
visibilityToggle
|
||||
disabled={editingIndex !== index}
|
||||
onChange={(e) =>
|
||||
handleSecretChange(index, "value", e.target.value)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
icon={
|
||||
editingIndex === index ? (
|
||||
<EyeSlashIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5" />
|
||||
)
|
||||
}
|
||||
onClick={() => toggleEditing(index)}
|
||||
/>
|
||||
<Button
|
||||
icon={<TrashIcon className="h-5 w-5" />}
|
||||
onClick={() => handleRemoveSecret(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Input
|
||||
placeholder="New Secret"
|
||||
value={newSecret}
|
||||
onChange={(e) => setNewSecret(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input.Password
|
||||
placeholder="New Value"
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
icon={<PlusIcon className="h-5 w-5" />}
|
||||
onClick={handleAddSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SkillConfigView = ({
|
||||
skill,
|
||||
setSkill,
|
||||
close,
|
||||
}: {
|
||||
skill: ISkill;
|
||||
setSkill: (newModel: ISkill) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const serverUrl = getServerUrl();
|
||||
const { user } = React.useContext(appContext);
|
||||
const testModelUrl = `${serverUrl}/skills/test`;
|
||||
const createSkillUrl = `${serverUrl}/skills`;
|
||||
|
||||
const createSkill = (skill: ISkill) => {
|
||||
setLoading(true);
|
||||
skill.user_id = user?.email;
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(skill),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
setSkill(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
const onFinal = () => {
|
||||
setLoading(false);
|
||||
setControlChanged(false);
|
||||
};
|
||||
fetchJSON(createSkillUrl, payLoad, onSuccess, onError, onFinal);
|
||||
};
|
||||
|
||||
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
|
||||
|
||||
const updateSkillConfig = (key: string, value: string) => {
|
||||
if (skill) {
|
||||
const updatedSkill = { ...skill, [key]: value };
|
||||
// setSkill(updatedModelConfig);
|
||||
setSkill(updatedSkill);
|
||||
}
|
||||
setControlChanged(true);
|
||||
};
|
||||
|
||||
const hasChanged = !controlChanged && skill.id !== undefined;
|
||||
const editorRef = React.useRef<any | null>(null);
|
||||
|
||||
return (
|
||||
<div className="relative ">
|
||||
{skill && (
|
||||
<div style={{ minHeight: "65vh" }}>
|
||||
<div className="flex gap-3">
|
||||
<div className="h-ful flex-1 ">
|
||||
<div className="mb-2 h-full" style={{ minHeight: "65vh" }}>
|
||||
<div className="h-full mt-2" style={{ height: "65vh" }}>
|
||||
<MonacoEditor
|
||||
value={skill?.content}
|
||||
language="python"
|
||||
editorRef={editorRef}
|
||||
onChange={(value: string) => {
|
||||
updateSkillConfig("content", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-72 ">
|
||||
<div className="">
|
||||
<ControlRowView
|
||||
title="Name"
|
||||
className=""
|
||||
description="Skill name, should match function name"
|
||||
value={skill?.name || ""}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2 w-full"
|
||||
value={skill?.name}
|
||||
onChange={(e) => {
|
||||
updateSkillConfig("name", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Description"
|
||||
className="mt-4"
|
||||
description="Description of the skill"
|
||||
value={skill?.description || ""}
|
||||
control={
|
||||
<TextArea
|
||||
className="mt-2 w-full"
|
||||
value={skill?.description}
|
||||
onChange={(e) => {
|
||||
updateSkillConfig("description", e.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Secrets"
|
||||
className="mt-4"
|
||||
description="Environment variables"
|
||||
value=""
|
||||
control={
|
||||
<SecretsEditor
|
||||
secrets={skill?.secrets || []}
|
||||
updateSkillConfig={updateSkillConfig}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full mt-4 text-right">
|
||||
{/* <Button
|
||||
key="test"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
if (skill) {
|
||||
testModel(skill);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test Model
|
||||
</Button> */}
|
||||
|
||||
{!hasChanged && (
|
||||
<Button
|
||||
className="ml-2"
|
||||
key="save"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (skill) {
|
||||
createSkill(skill);
|
||||
setSkill(skill);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{skill?.id ? "Update Skill" : "Save Skill"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="ml-2"
|
||||
key="close"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,279 +0,0 @@
|
||||
import React from "react";
|
||||
import { IWorkflow, IStatus, IChatSession } from "../../../types";
|
||||
import { ControlRowView } from "../../../atoms";
|
||||
import {
|
||||
fetchJSON,
|
||||
getRandomIntFromDateAndSalt,
|
||||
getServerUrl,
|
||||
} from "../../../utils";
|
||||
import { Button, Drawer, Input, Select, Tabs, message, theme } from "antd";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { BugAntIcon, UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { WorkflowAgentSelector, WorkflowTypeSelector } from "./selectors";
|
||||
import ChatBox from "../../playground/chatbox";
|
||||
|
||||
export const WorkflowViewConfig = ({
|
||||
workflow,
|
||||
setWorkflow,
|
||||
close,
|
||||
}: {
|
||||
workflow: IWorkflow;
|
||||
setWorkflow: (newFlowConfig: IWorkflow) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<IStatus | null>(null);
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const createWorkflowUrl = `${serverUrl}/workflows`;
|
||||
|
||||
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
|
||||
const [localWorkflow, setLocalWorkflow] = React.useState<IWorkflow>(workflow);
|
||||
|
||||
const updateFlowConfig = (key: string, value: string) => {
|
||||
// When an updatedFlowConfig is created using localWorkflow, if the contents of FlowConfigViewer Modal are changed after the Agent Specification Modal is updated, the updated contents of the Agent Specification Modal are not saved. Fixed to localWorkflow->flowConfig. Fixed a bug.
|
||||
const updatedFlowConfig = { ...workflow, [key]: value };
|
||||
|
||||
setLocalWorkflow(updatedFlowConfig);
|
||||
setWorkflow(updatedFlowConfig);
|
||||
setControlChanged(true);
|
||||
};
|
||||
|
||||
const createWorkflow = (workflow: IWorkflow) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
workflow.user_id = user?.email;
|
||||
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(workflow),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
const newWorkflow = data.data;
|
||||
setWorkflow(newWorkflow);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
// setNewAgent(sampleAgent);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
const onFinal = () => {
|
||||
setLoading(false);
|
||||
setControlChanged(false);
|
||||
};
|
||||
|
||||
fetchJSON(createWorkflowUrl, payLoad, onSuccess, onError, onFinal);
|
||||
};
|
||||
|
||||
const hasChanged = !controlChanged && workflow.id !== undefined;
|
||||
const [drawerOpen, setDrawerOpen] = React.useState<boolean>(false);
|
||||
|
||||
const openDrawer = () => {
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
setDrawerOpen(false);
|
||||
};
|
||||
|
||||
const dummySession: IChatSession = {
|
||||
user_id: user?.email || "test_session_user_id",
|
||||
workflow_id: workflow?.id,
|
||||
name: "test_session",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div className="mb-2">{flowConfig.name}</div> */}
|
||||
<div>
|
||||
<ControlRowView
|
||||
title="Workflow Name"
|
||||
className="mt-4 mb-2"
|
||||
description="Name of the workflow"
|
||||
value={localWorkflow.name}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2 w-full"
|
||||
value={localWorkflow.name}
|
||||
onChange={(e) => updateFlowConfig("name", e.target.value)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Workflow Description"
|
||||
className="mt-4 mb-2"
|
||||
description="Description of the workflow"
|
||||
value={localWorkflow.description}
|
||||
control={
|
||||
<Input
|
||||
className="mt-2 w-full"
|
||||
value={localWorkflow.description}
|
||||
onChange={(e) => updateFlowConfig("description", e.target.value)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ControlRowView
|
||||
title="Summary Method"
|
||||
description="Defines the method to summarize the conversation"
|
||||
value={localWorkflow.summary_method || "last"}
|
||||
control={
|
||||
<Select
|
||||
className="mt-2 w-full"
|
||||
defaultValue={localWorkflow.summary_method || "last"}
|
||||
onChange={(value: any) =>
|
||||
updateFlowConfig("summary_method", value)
|
||||
}
|
||||
options={
|
||||
[
|
||||
{ label: "last", value: "last" },
|
||||
{ label: "none", value: "none" },
|
||||
{ label: "llm", value: "llm" },
|
||||
] as any
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full mt-4 text-right">
|
||||
{" "}
|
||||
{!hasChanged && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
createWorkflow(localWorkflow);
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
{workflow.id ? "Update Workflow" : "Create Workflow"}
|
||||
</Button>
|
||||
)}
|
||||
{workflow?.id && (
|
||||
<Button
|
||||
className="ml-2 text-primary"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
Test Workflow
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="ml-2"
|
||||
key="close text-primary"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
title={<div>{workflow?.name || "Test Workflow"}</div>}
|
||||
size="large"
|
||||
onClose={closeDrawer}
|
||||
open={drawerOpen}
|
||||
>
|
||||
<div className="h-full ">
|
||||
{drawerOpen && (
|
||||
<ChatBox
|
||||
initMessages={[]}
|
||||
session={dummySession}
|
||||
heightOffset={100}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorflowViewer = ({
|
||||
workflow,
|
||||
setWorkflow,
|
||||
close,
|
||||
}: {
|
||||
workflow: IWorkflow;
|
||||
setWorkflow: (workflow: IWorkflow) => void;
|
||||
close: () => void;
|
||||
}) => {
|
||||
let items = [
|
||||
{
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Workflow Configuration
|
||||
</div>
|
||||
),
|
||||
key: "1",
|
||||
children: (
|
||||
<div>
|
||||
{!workflow?.type && (
|
||||
<WorkflowTypeSelector
|
||||
workflow={workflow}
|
||||
setWorkflow={setWorkflow}
|
||||
/>
|
||||
)}
|
||||
|
||||
{workflow?.type && workflow && (
|
||||
<WorkflowViewConfig
|
||||
workflow={workflow}
|
||||
setWorkflow={setWorkflow}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (workflow) {
|
||||
if (workflow?.id) {
|
||||
items.push({
|
||||
label: (
|
||||
<div className="w-full ">
|
||||
{" "}
|
||||
<UserGroupIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Agents
|
||||
</div>
|
||||
),
|
||||
key: "2",
|
||||
children: (
|
||||
<>
|
||||
<WorkflowAgentSelector workflow={workflow} />{" "}
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
|
||||
return (
|
||||
<div className="text-primary">
|
||||
<Tabs
|
||||
tabBarStyle={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
defaultActiveKey="1"
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,428 +0,0 @@
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
CodeBracketSquareIcon,
|
||||
DocumentDuplicateIcon,
|
||||
InformationCircleIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
UserGroupIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Dropdown, MenuProps, Modal, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { IWorkflow, IStatus } from "../../types";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import {
|
||||
fetchJSON,
|
||||
getServerUrl,
|
||||
sanitizeConfig,
|
||||
timeAgo,
|
||||
truncateText,
|
||||
} from "../../utils";
|
||||
import { BounceLoader, Card, CardHoverBar, LoadingOverlay } from "../../atoms";
|
||||
import { WorflowViewer } from "./utils/workflowconfig";
|
||||
import { ExportWorkflowModal } from "./utils/export";
|
||||
|
||||
const WorkflowView = ({}: any) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const listWorkflowsUrl = `${serverUrl}/workflows?user_id=${user?.email}`;
|
||||
const saveWorkflowsUrl = `${serverUrl}/workflows`;
|
||||
|
||||
const [workflows, setWorkflows] = React.useState<IWorkflow[] | null>([]);
|
||||
const [selectedWorkflow, setSelectedWorkflow] =
|
||||
React.useState<IWorkflow | null>(null);
|
||||
const [selectedExportWorkflow, setSelectedExportWorkflow] =
|
||||
React.useState<IWorkflow | null>(null);
|
||||
|
||||
const sampleWorkflow: IWorkflow = {
|
||||
name: "Sample Agent Workflow",
|
||||
description: "Sample Agent Workflow",
|
||||
};
|
||||
const [newWorkflow, setNewWorkflow] = React.useState<IWorkflow | null>(
|
||||
sampleWorkflow
|
||||
);
|
||||
|
||||
const [showWorkflowModal, setShowWorkflowModal] = React.useState(false);
|
||||
const [showNewWorkflowModal, setShowNewWorkflowModal] = React.useState(false);
|
||||
|
||||
const fetchWorkFlow = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
setWorkflows(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(listWorkflowsUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const deleteWorkFlow = (workflow: IWorkflow) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const deleteWorkflowsUrl = `${serverUrl}/workflows/delete?user_id=${user?.email}&workflow_id=${workflow.id}`;
|
||||
const payLoad = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: user?.email,
|
||||
workflow: workflow,
|
||||
}),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
fetchWorkFlow();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(deleteWorkflowsUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
// console.log("fetching messages", messages);
|
||||
fetchWorkFlow();
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedWorkflow) {
|
||||
setShowWorkflowModal(true);
|
||||
}
|
||||
}, [selectedWorkflow]);
|
||||
|
||||
const [showExportModal, setShowExportModal] = React.useState(false);
|
||||
|
||||
const workflowRows = (workflows || []).map(
|
||||
(workflow: IWorkflow, i: number) => {
|
||||
const cardItems = [
|
||||
{
|
||||
title: "Export",
|
||||
icon: CodeBracketSquareIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
setSelectedExportWorkflow(workflow);
|
||||
setShowExportModal(true);
|
||||
},
|
||||
hoverText: "Export",
|
||||
},
|
||||
{
|
||||
title: "Download",
|
||||
icon: ArrowDownTrayIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
// download workflow as workflow.name.json
|
||||
const element = document.createElement("a");
|
||||
const sanitizedWorkflow = sanitizeConfig(workflow);
|
||||
const file = new Blob([JSON.stringify(sanitizedWorkflow)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `workflow_${workflow.name}.json`;
|
||||
document.body.appendChild(element); // Required for this to work in FireFox
|
||||
element.click();
|
||||
},
|
||||
hoverText: "Download",
|
||||
},
|
||||
{
|
||||
title: "Make a Copy",
|
||||
icon: DocumentDuplicateIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
let newWorkflow = { ...sanitizeConfig(workflow) };
|
||||
newWorkflow.name = `${workflow.name}_copy`;
|
||||
setNewWorkflow(newWorkflow);
|
||||
setShowNewWorkflowModal(true);
|
||||
},
|
||||
hoverText: "Make a Copy",
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
icon: TrashIcon,
|
||||
onClick: (e: any) => {
|
||||
e.stopPropagation();
|
||||
deleteWorkFlow(workflow);
|
||||
},
|
||||
hoverText: "Delete",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<li
|
||||
key={"workflowrow" + i}
|
||||
className="block h-full"
|
||||
style={{ width: "200px" }}
|
||||
>
|
||||
<Card
|
||||
className=" block p-2 cursor-pointer"
|
||||
title={<div className=" ">{truncateText(workflow.name, 25)}</div>}
|
||||
onClick={() => {
|
||||
setSelectedWorkflow(workflow);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ minHeight: "65px" }}
|
||||
className="break-words my-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="text-xs mb-2">{workflow.type}</div>{" "}
|
||||
{truncateText(workflow.description, 70)}
|
||||
</div>
|
||||
<div
|
||||
aria-label={`Updated ${timeAgo(workflow.updated_at || "")} ago`}
|
||||
className="text-xs"
|
||||
>
|
||||
{timeAgo(workflow.updated_at || "")}
|
||||
</div>
|
||||
|
||||
<CardHoverBar items={cardItems} />
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const WorkflowModal = ({
|
||||
workflow,
|
||||
setWorkflow,
|
||||
showModal,
|
||||
setShowModal,
|
||||
handler,
|
||||
}: {
|
||||
workflow: IWorkflow | null;
|
||||
setWorkflow?: (workflow: IWorkflow | null) => void;
|
||||
showModal: boolean;
|
||||
setShowModal: (show: boolean) => void;
|
||||
handler?: (workflow: IWorkflow) => void;
|
||||
}) => {
|
||||
const [localWorkflow, setLocalWorkflow] = React.useState<IWorkflow | null>(
|
||||
workflow
|
||||
);
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
if (handler) {
|
||||
handler(localWorkflow as IWorkflow);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<>
|
||||
Workflow Specification{" "}
|
||||
<span className="text-accent font-normal">
|
||||
{localWorkflow?.name}
|
||||
</span>{" "}
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
open={showModal}
|
||||
onOk={() => {
|
||||
closeModal();
|
||||
}}
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
}}
|
||||
footer={[]}
|
||||
>
|
||||
<>
|
||||
{localWorkflow && (
|
||||
<WorflowViewer
|
||||
workflow={localWorkflow}
|
||||
setWorkflow={setLocalWorkflow}
|
||||
close={closeModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const uploadWorkflow = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => {
|
||||
const contents = e.target.result;
|
||||
if (contents) {
|
||||
try {
|
||||
const workflow = JSON.parse(contents);
|
||||
// TBD validate that it is a valid workflow
|
||||
setNewWorkflow(workflow);
|
||||
setShowNewWorkflowModal(true);
|
||||
} catch (err) {
|
||||
message.error("Invalid workflow file");
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const workflowTypes: MenuProps["items"] = [
|
||||
// {
|
||||
// key: "twoagents",
|
||||
// label: (
|
||||
// <div>
|
||||
// {" "}
|
||||
// <UsersIcon className="w-5 h-5 inline-block mr-2" />
|
||||
// Two Agents
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// key: "groupchat",
|
||||
// label: (
|
||||
// <div>
|
||||
// <UserGroupIcon className="w-5 h-5 inline-block mr-2" />
|
||||
// Group Chat
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// type: "divider",
|
||||
// },
|
||||
{
|
||||
key: "uploadworkflow",
|
||||
label: (
|
||||
<div>
|
||||
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
|
||||
Upload Workflow
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const showWorkflow = (config: IWorkflow) => {
|
||||
setSelectedWorkflow(config);
|
||||
setShowWorkflowModal(true);
|
||||
};
|
||||
|
||||
const workflowTypesOnClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "uploadworkflow") {
|
||||
uploadWorkflow();
|
||||
return;
|
||||
}
|
||||
showWorkflow(sampleWorkflow);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" text-primary ">
|
||||
<WorkflowModal
|
||||
workflow={selectedWorkflow}
|
||||
setWorkflow={setSelectedWorkflow}
|
||||
showModal={showWorkflowModal}
|
||||
setShowModal={setShowWorkflowModal}
|
||||
handler={(workflow: IWorkflow) => {
|
||||
fetchWorkFlow();
|
||||
}}
|
||||
/>
|
||||
|
||||
<WorkflowModal
|
||||
workflow={newWorkflow}
|
||||
showModal={showNewWorkflowModal}
|
||||
setShowModal={setShowNewWorkflowModal}
|
||||
handler={(workflow: IWorkflow) => {
|
||||
fetchWorkFlow();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ExportWorkflowModal
|
||||
workflow={selectedExportWorkflow}
|
||||
show={showExportModal}
|
||||
setShow={setShowExportModal}
|
||||
/>
|
||||
|
||||
<div className="mb-2 relative">
|
||||
<div className=" rounded ">
|
||||
<div className="flex mt-2 pb-2 mb-2 border-b">
|
||||
<div className="flex-1 font-semibold mb-2 ">
|
||||
{" "}
|
||||
Workflows ({workflowRows.length}){" "}
|
||||
</div>
|
||||
<div className=" ">
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
menu={{ items: workflowTypes, onClick: workflowTypesOnClick }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
onClick={() => {
|
||||
showWorkflow(sampleWorkflow);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 inline-block mr-1" />
|
||||
New Workflow
|
||||
</Dropdown.Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mb-2 pb-1 ">
|
||||
{" "}
|
||||
Configure an agent workflow that can be used to handle tasks.
|
||||
</div>
|
||||
{workflows && workflows.length > 0 && (
|
||||
<div
|
||||
// style={{ minHeight: "500px" }}
|
||||
className="w-full relative"
|
||||
>
|
||||
<LoadingOverlay loading={loading} />
|
||||
<ul className="flex flex-wrap gap-3">{workflowRows}</ul>
|
||||
</div>
|
||||
)}
|
||||
{workflows && workflows.length === 0 && !loading && (
|
||||
<div className="text-sm border mt-4 rounded text-secondary p-2">
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
|
||||
No workflows found. Please create a new workflow.
|
||||
</div>
|
||||
)}
|
||||
{loading && (
|
||||
<div className=" w-full text-center">
|
||||
{" "}
|
||||
<BounceLoader />{" "}
|
||||
<span className="inline-block"> loading .. </span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowView;
|
||||
@@ -1,207 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import { fetchJSON, getServerUrl, timeAgo, truncateText } from "../../utils";
|
||||
import { IGalleryItem, IStatus } from "../../types";
|
||||
import { Button, message } from "antd";
|
||||
import { BounceLoader, Card } from "../../atoms";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { navigate } from "gatsby";
|
||||
import ChatBox from "../playground/chatbox";
|
||||
|
||||
const GalleryView = ({ location }: any) => {
|
||||
const serverUrl = getServerUrl();
|
||||
const { user } = React.useContext(appContext);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [gallery, setGallery] = React.useState<null | IGalleryItem[]>(null);
|
||||
const [currentGallery, setCurrentGallery] =
|
||||
React.useState<null | IGalleryItem>(null);
|
||||
const listGalleryUrl = `${serverUrl}/gallery?user_id=${user?.email}`;
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
const [currentGalleryId, setCurrentGalleryId] = React.useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// get gallery id from url
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const galleryId = urlParams.get("id");
|
||||
|
||||
if (galleryId) {
|
||||
// Fetch gallery details using the galleryId
|
||||
fetchGallery(galleryId);
|
||||
setCurrentGalleryId(galleryId);
|
||||
} else {
|
||||
// Redirect to an error page or home page if the id is not found
|
||||
// navigate("/");
|
||||
fetchGallery(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchGallery = (galleryId: string | null) => {
|
||||
const fetchGalleryUrl = galleryId
|
||||
? `${serverUrl}/gallery?gallery_id=${galleryId}`
|
||||
: listGalleryUrl;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
// message.success(data.message);
|
||||
console.log("gallery", data);
|
||||
if (galleryId) {
|
||||
// Set the currently viewed gallery item
|
||||
setCurrentGallery(data.data[0]);
|
||||
} else {
|
||||
setGallery(data.data);
|
||||
}
|
||||
// Set the list of gallery items
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(fetchGalleryUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const GalleryContent = ({ item }: { item: IGalleryItem }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 text-sm">
|
||||
This session contains {item.messages.length} messages and was created{" "}
|
||||
{timeAgo(item.timestamp)}
|
||||
</div>
|
||||
<div className="">
|
||||
<ChatBox initMessages={item.messages} editable={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TagsView = ({ tags }: { tags: string[] }) => {
|
||||
const tagsView = tags.map((tag: string, index: number) => {
|
||||
return (
|
||||
<div key={"tag" + index} className="mr-2 inline-block">
|
||||
<span className="text-xs bg-secondary border px-3 p-1 rounded">
|
||||
{tag}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return <div className="flex flex-wrap">{tagsView}</div>;
|
||||
};
|
||||
|
||||
const galleryRows = gallery?.map((item: IGalleryItem, index: number) => {
|
||||
const isSelected = currentGallery?.id === item.id;
|
||||
return (
|
||||
<div key={"galleryrow" + index} className="">
|
||||
<Card
|
||||
active={isSelected}
|
||||
onClick={() => {
|
||||
setCurrentGallery(item);
|
||||
// add to history
|
||||
navigate(`/gallery?id=${item.id}`);
|
||||
}}
|
||||
className="h-full p-2 cursor-pointer"
|
||||
title={truncateText(item.messages[0]?.content || "", 20)}
|
||||
>
|
||||
<div className="my-2">
|
||||
{" "}
|
||||
{truncateText(item.messages[0]?.content || "", 80)}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{item.messages.length} message{item.messages.length > 1 && "s"}
|
||||
</div>
|
||||
<div className="my-2 border-t border-dashed w-full pt-2 inline-flex gap-2 ">
|
||||
<TagsView tags={item.tags} />{" "}
|
||||
</div>
|
||||
<div className="text-xs">{timeAgo(item.timestamp)}</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className=" ">
|
||||
<div className="mb-4 text-2xl">Gallery</div>
|
||||
|
||||
{/* back to gallery button */}
|
||||
|
||||
{currentGallery && (
|
||||
<div className="mb-4 w-full">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setCurrentGallery(null);
|
||||
// add to history
|
||||
navigate(`/gallery?_=${Date.now()}`);
|
||||
if (currentGalleryId) {
|
||||
fetchGallery(null);
|
||||
setCurrentGalleryId(null);
|
||||
}
|
||||
}}
|
||||
className="bg-primary text-white px-2 py-1 rounded"
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4 inline mr-1" />
|
||||
Back to gallery
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!currentGallery && (
|
||||
<>
|
||||
<div>
|
||||
View a collection of AutoGen agent specifications and sessions{" "}
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{galleryRows}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gallery && gallery.length === 0 && (
|
||||
<div className="text-sm border rounded text-secondary p-2">
|
||||
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
|
||||
No gallery items found. Please create a chat session and publish to
|
||||
gallery.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentGallery && (
|
||||
<div className="mt-4 border-t pt-2">
|
||||
<GalleryContent item={currentGallery} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="w-full text-center boder mt-4">
|
||||
<div>
|
||||
{" "}
|
||||
<BounceLoader />
|
||||
</div>
|
||||
loading gallery
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryView;
|
||||
@@ -0,0 +1,449 @@
|
||||
import * as React from "react";
|
||||
import { message } from "antd";
|
||||
import { getServerUrl } from "../../../utils";
|
||||
import { SessionManager } from "../../shared/session/manager";
|
||||
import { IStatus } from "../../../types/app";
|
||||
import { Message } from "../../../types/datamodel";
|
||||
import { useConfigStore } from "../../../../hooks/store";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import ChatInput from "./chatinput";
|
||||
import { ModelUsage, SocketMessage, ThreadState, ThreadStatus } from "./types";
|
||||
import { MessageList } from "./messagelist";
|
||||
import TeamManager from "../../shared/team/manager";
|
||||
|
||||
const logo = require("../../../../images/landing/welcome.svg").default;
|
||||
|
||||
export default function ChatView({
|
||||
initMessages,
|
||||
}: {
|
||||
initMessages: Message[];
|
||||
}) {
|
||||
const serverUrl = getServerUrl();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
const [messages, setMessages] = React.useState<Message[]>(initMessages);
|
||||
const [threadMessages, setThreadMessages] = React.useState<
|
||||
Record<string, ThreadState>
|
||||
>({});
|
||||
const chatContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const { session, sessions } = useConfigStore();
|
||||
const [activeSockets, setActiveSockets] = React.useState<
|
||||
Record<string, WebSocket>
|
||||
>({});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (chatContainerRef.current) {
|
||||
chatContainerRef.current.scrollTo({
|
||||
top: chatContainerRef.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}, [messages, threadMessages]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
Object.values(activeSockets).forEach((socket) => socket.close());
|
||||
};
|
||||
}, [activeSockets]);
|
||||
|
||||
const getBaseUrl = (url: string): string => {
|
||||
try {
|
||||
// Remove protocol (http:// or https://)
|
||||
let baseUrl = url.replace(/(^\w+:|^)\/\//, "");
|
||||
|
||||
// Handle both localhost and production cases
|
||||
if (baseUrl.startsWith("localhost")) {
|
||||
// For localhost, keep the port if it exists
|
||||
baseUrl = baseUrl.replace("/api", "");
|
||||
} else if (baseUrl === "/api") {
|
||||
// For production where url is just '/api'
|
||||
baseUrl = window.location.host;
|
||||
} else {
|
||||
// For other cases, remove '/api' and trailing slash
|
||||
baseUrl = baseUrl.replace("/api", "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
} catch (error) {
|
||||
console.error("Error processing server URL:", error);
|
||||
throw new Error("Invalid server URL configuration");
|
||||
}
|
||||
};
|
||||
|
||||
const createRun = async (sessionId: number): Promise<string> => {
|
||||
const payload = { session_id: sessionId, user_id: user?.email || "" };
|
||||
|
||||
const response = await fetch(`${serverUrl}/runs`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create run");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data.run_id;
|
||||
};
|
||||
|
||||
const startRun = async (runId: string, query: string) => {
|
||||
const messagePayload = {
|
||||
user_id: user?.email,
|
||||
session_id: session?.id,
|
||||
config: {
|
||||
content: query,
|
||||
source: "user",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(`${serverUrl}/runs/${runId}/start`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(messagePayload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to start run");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
interface RequestUsage {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
}
|
||||
|
||||
const connectWebSocket = (runId: string, query: string) => {
|
||||
const baseUrl = getBaseUrl(serverUrl);
|
||||
// Determine if we should use ws:// or wss:// based on current protocol
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${wsProtocol}//${baseUrl}/api/ws/runs/${runId}`;
|
||||
|
||||
console.log("Connecting to WebSocket URL:", wsUrl); // For debugging
|
||||
|
||||
const socket = new WebSocket(wsUrl);
|
||||
let isClosing = false;
|
||||
|
||||
const closeSocket = () => {
|
||||
if (!isClosing && socket.readyState !== WebSocket.CLOSED) {
|
||||
isClosing = true;
|
||||
socket.close();
|
||||
setActiveSockets((prev) => {
|
||||
const newSockets = { ...prev };
|
||||
delete newSockets[runId];
|
||||
return newSockets;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
socket.onopen = async () => {
|
||||
try {
|
||||
setActiveSockets((prev) => ({
|
||||
...prev,
|
||||
[runId]: socket,
|
||||
}));
|
||||
|
||||
setThreadMessages((prev) => ({
|
||||
...prev,
|
||||
[runId]: {
|
||||
messages: [],
|
||||
status: "streaming",
|
||||
isExpanded: true,
|
||||
},
|
||||
}));
|
||||
|
||||
setMessages((prev: Message[]) =>
|
||||
prev.map((msg: Message) => {
|
||||
if (msg.run_id === runId && msg.config.source === "bot") {
|
||||
return {
|
||||
...msg,
|
||||
config: {
|
||||
...msg.config,
|
||||
content: "Starting...",
|
||||
},
|
||||
};
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
);
|
||||
|
||||
// Start the run only after socket is connected
|
||||
await startRun(runId, query);
|
||||
} catch (error) {
|
||||
console.error("Error starting run:", error);
|
||||
message.error("Failed to start run");
|
||||
closeSocket();
|
||||
|
||||
setThreadMessages((prev) => ({
|
||||
...prev,
|
||||
[runId]: {
|
||||
...prev[runId],
|
||||
status: "error",
|
||||
isExpanded: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const message: SocketMessage = JSON.parse(event.data);
|
||||
|
||||
switch (message.type) {
|
||||
case "message":
|
||||
setThreadMessages((prev) => {
|
||||
const currentThread = prev[runId] || {
|
||||
messages: [],
|
||||
status: "streaming",
|
||||
isExpanded: true,
|
||||
};
|
||||
|
||||
const models_usage: ModelUsage | undefined = message.data
|
||||
?.models_usage
|
||||
? {
|
||||
prompt_tokens: message.data.models_usage.prompt_tokens,
|
||||
completion_tokens:
|
||||
message.data.models_usage.completion_tokens,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const newMessage = {
|
||||
source: message.data?.source || "",
|
||||
content: message.data?.content || "",
|
||||
models_usage,
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[runId]: {
|
||||
...currentThread,
|
||||
messages: [...currentThread.messages, newMessage],
|
||||
status: "streaming",
|
||||
},
|
||||
};
|
||||
});
|
||||
break;
|
||||
|
||||
case "result":
|
||||
case "completion":
|
||||
setThreadMessages((prev) => {
|
||||
const currentThread = prev[runId];
|
||||
if (!currentThread) return prev;
|
||||
|
||||
const finalMessage = message.data?.task_result?.messages
|
||||
?.filter((msg: any) => msg.content !== "TERMINATE")
|
||||
.pop();
|
||||
|
||||
const status: ThreadStatus = message.status || "complete";
|
||||
// Capture completion reason from task_result
|
||||
const reason =
|
||||
message.data?.task_result?.stop_reason ||
|
||||
(message.error ? `Error: ${message.error}` : undefined);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[runId]: {
|
||||
...currentThread,
|
||||
status: status,
|
||||
reason: reason,
|
||||
isExpanded: true,
|
||||
finalResult: finalMessage,
|
||||
messages: currentThread.messages,
|
||||
},
|
||||
};
|
||||
});
|
||||
closeSocket();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = (event) => {
|
||||
console.log(
|
||||
`WebSocket closed for run ${runId}. Code: ${event.code}, Reason: ${event.reason}`
|
||||
);
|
||||
|
||||
if (!isClosing) {
|
||||
setActiveSockets((prev) => {
|
||||
const newSockets = { ...prev };
|
||||
delete newSockets[runId];
|
||||
return newSockets;
|
||||
});
|
||||
|
||||
setThreadMessages((prev) => {
|
||||
const thread = prev[runId];
|
||||
if (thread && thread.status === "streaming") {
|
||||
return {
|
||||
...prev,
|
||||
[runId]: {
|
||||
...thread,
|
||||
status: "complete",
|
||||
reason: event.reason || "Connection closed",
|
||||
},
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
message.error("WebSocket connection error");
|
||||
|
||||
setThreadMessages((prev) => {
|
||||
const thread = prev[runId];
|
||||
if (!thread) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[runId]: {
|
||||
...thread,
|
||||
status: "error",
|
||||
reason: "WebSocket connection error occurred",
|
||||
isExpanded: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
closeSocket();
|
||||
};
|
||||
|
||||
return socket;
|
||||
};
|
||||
|
||||
const cancelRun = async (runId: string) => {
|
||||
const socket = activeSockets[runId];
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: "stop" }));
|
||||
|
||||
setThreadMessages((prev) => ({
|
||||
...prev,
|
||||
[runId]: {
|
||||
...prev[runId],
|
||||
status: "cancelled",
|
||||
reason: "Cancelled by user",
|
||||
isExpanded: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const runTask = async (query: string) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
if (!session?.id) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let runId: string | null = null;
|
||||
|
||||
try {
|
||||
runId = (await createRun(session.id)) + "";
|
||||
|
||||
const userMessage: Message = {
|
||||
config: {
|
||||
content: query,
|
||||
source: "user",
|
||||
},
|
||||
session_id: session.id,
|
||||
run_id: runId,
|
||||
};
|
||||
|
||||
const botMessage: Message = {
|
||||
config: {
|
||||
content: "Thinking...",
|
||||
source: "bot",
|
||||
},
|
||||
session_id: session.id,
|
||||
run_id: runId,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage, botMessage]);
|
||||
connectWebSocket(runId, query); // Now passing query to connectWebSocket
|
||||
} catch (err) {
|
||||
console.error("Error:", err);
|
||||
message.error("Error during request processing");
|
||||
|
||||
if (runId) {
|
||||
if (activeSockets[runId]) {
|
||||
activeSockets[runId].close();
|
||||
}
|
||||
|
||||
setThreadMessages((prev) => ({
|
||||
...prev,
|
||||
[runId!]: {
|
||||
...prev[runId!],
|
||||
status: "error",
|
||||
isExpanded: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
setError({
|
||||
status: false,
|
||||
message: err instanceof Error ? err.message : "Unknown error occurred",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// session changed
|
||||
if (session) {
|
||||
setMessages([]);
|
||||
setThreadMessages({});
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<div className="text-primary h-[calc(100vh-195px)] bg-primary relative rounded flex-1 scroll">
|
||||
<div className="flex gap-4 w-full">
|
||||
<div className="flex-1">
|
||||
<SessionManager />
|
||||
</div>
|
||||
<TeamManager />
|
||||
</div>
|
||||
<div className="flex flex-col h-full">
|
||||
<div
|
||||
className="flex-1 overflow-y-auto scroll relative min-h-0"
|
||||
ref={chatContainerRef}
|
||||
>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
threadMessages={threadMessages}
|
||||
setThreadMessages={setThreadMessages}
|
||||
onRetry={runTask}
|
||||
onCancel={cancelRun}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sessions?.length === 0 ? (
|
||||
<div className="flex h-[calc(100%-100px)] flex-col items-center justify-center w-full">
|
||||
<div className="mt-4 text-sm text-secondary text-center">
|
||||
<img src={logo} alt="Welcome" className="w-72 h-72 mb-4" />
|
||||
Welcome! Create a session to get started!
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{session && (
|
||||
<div className="flex-shrink-0">
|
||||
<ChatInput onSubmit={runTask} loading={loading} error={error} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
PaperAirplaneIcon,
|
||||
Cog6ToothIcon,
|
||||
ExclamationTriangleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import * as React from "react";
|
||||
import { IStatus } from "../../../types/app";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSubmit: (text: string) => void;
|
||||
loading: boolean;
|
||||
error: IStatus | null;
|
||||
}
|
||||
export default function ChatInput({
|
||||
onSubmit,
|
||||
loading,
|
||||
error,
|
||||
}: ChatInputProps) {
|
||||
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
const [previousLoading, setPreviousLoading] = React.useState(loading);
|
||||
const [text, setText] = React.useState("");
|
||||
|
||||
const textAreaDefaultHeight = "64px";
|
||||
|
||||
// Handle textarea auto-resize
|
||||
React.useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.style.height = textAreaDefaultHeight;
|
||||
const scrollHeight = textAreaRef.current.scrollHeight;
|
||||
textAreaRef.current.style.height = `${scrollHeight}px`;
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
// Clear input when loading changes from true to false (meaning the response is complete)
|
||||
React.useEffect(() => {
|
||||
if (previousLoading && !loading && !error) {
|
||||
resetInput();
|
||||
}
|
||||
setPreviousLoading(loading);
|
||||
}, [loading, error, previousLoading]);
|
||||
|
||||
const resetInput = () => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.value = "";
|
||||
textAreaRef.current.style.height = textAreaDefaultHeight;
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setText(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (textAreaRef.current?.value && !loading) {
|
||||
const query = textAreaRef.current.value;
|
||||
onSubmit(query);
|
||||
// Don't reset immediately - wait for response to complete
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2 w-full">
|
||||
<div
|
||||
className={`mt-2 rounded shadow-sm flex mb-1 ${
|
||||
loading ? "opacity-50 pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
<form
|
||||
className="flex-1 relative"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
id="queryInput"
|
||||
name="queryInput"
|
||||
ref={textAreaRef}
|
||||
defaultValue={"what is the height of the eiffel tower"}
|
||||
onChange={handleTextChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex items-center w-full resize-none text-gray-600 rounded border border-accent bg-white p-2 pl-5 pr-16"
|
||||
style={{
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
minHeight: "50px",
|
||||
}}
|
||||
placeholder="Type your message here..."
|
||||
disabled={loading}
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleSubmit}
|
||||
style={{ width: "45px", height: "35px" }}
|
||||
className="absolute right-3 bottom-2 bg-accent hover:brightness-75 transition duration-300 rounded cursor-pointer flex justify-center items-center"
|
||||
>
|
||||
{!loading ? (
|
||||
<div className="inline-block">
|
||||
<PaperAirplaneIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-block">
|
||||
<Cog6ToothIcon className="text-white animate-spin rounded-full h-6 w-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{error && !error.status && (
|
||||
<div className="p-2 border rounded mt-4 text-orange-500 text-sm">
|
||||
<ExclamationTriangleIcon className="h-5 text-orange-500 inline-block mr-2" />
|
||||
{error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { ChevronDown, ChevronRight, RotateCcw, User, Bot } from "lucide-react";
|
||||
import { ThreadView } from "./thread";
|
||||
import { MessageListProps } from "./types";
|
||||
|
||||
export const MessageList: React.FC<MessageListProps> = ({
|
||||
messages,
|
||||
threadMessages,
|
||||
setThreadMessages,
|
||||
onRetry,
|
||||
onCancel,
|
||||
loading,
|
||||
}) => {
|
||||
const messageListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const messagePairs = React.useMemo(() => {
|
||||
const pairs = [];
|
||||
for (let i = 0; i < messages.length; i += 2) {
|
||||
if (messages[i] && messages[i + 1]) {
|
||||
pairs.push({
|
||||
userMessage: messages[i],
|
||||
botMessage: messages[i + 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}, [messages]);
|
||||
|
||||
const toggleThread = (runId: string) => {
|
||||
setThreadMessages((prev) => ({
|
||||
...prev,
|
||||
[runId]: {
|
||||
...prev[runId],
|
||||
isExpanded: !prev[runId].isExpanded,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={messageListRef}
|
||||
className="flex flex-col space-y-4 p-4 overflow-y-hidden overflow-x-hidden"
|
||||
>
|
||||
{messagePairs.map(({ userMessage, botMessage }, idx) => {
|
||||
const thread = threadMessages[botMessage.run_id];
|
||||
const isStreaming = thread?.status === "streaming";
|
||||
const isError = thread?.status === "error";
|
||||
|
||||
return (
|
||||
<div key={idx} className="space-y-2 text-primary">
|
||||
{/* User Message */}
|
||||
<div className="flex items-start gap-2 ]">
|
||||
<div className="p-1.5 rounded bg-light text-accent">
|
||||
<User size={24} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="p-2 bg-accent rounded text-white">
|
||||
{userMessage.config.content}
|
||||
</div>
|
||||
{userMessage.config.models_usage && (
|
||||
<div className="text-xs text-secondary mt-1">
|
||||
Tokens:{" "}
|
||||
{userMessage.config.models_usage.prompt_tokens +
|
||||
userMessage.config.models_usage.completion_tokens}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bot Response */}
|
||||
<div className="flex items-start gap-2 ml-auto ">
|
||||
<div className="flex-1">
|
||||
<div className="p-2 bg-secondary rounded text-primary">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 text-sm">
|
||||
{isStreaming ? (
|
||||
<>Processing...</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
{thread?.finalResult?.content}
|
||||
<div className="mt-2 mb-2 text-sm text-secondary">
|
||||
{thread?.reason}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{isError && (
|
||||
<button
|
||||
onClick={() => onRetry(userMessage.config.content)}
|
||||
className="p-1 text-secondary hover:text-primary transition-colors"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
)}
|
||||
{thread && thread.messages?.length > 0 && (
|
||||
<button
|
||||
onClick={() => toggleThread(botMessage.run_id)}
|
||||
className="p-1 text-secondary hover:text-primary transition-colors"
|
||||
>
|
||||
{thread.isExpanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{botMessage.config.models_usage && (
|
||||
<div className="text-sm text-secondary -mt-4">
|
||||
{botMessage.config.models_usage.prompt_tokens +
|
||||
botMessage.config.models_usage.completion_tokens}{" "}
|
||||
tokens | {thread.messages.length} messages
|
||||
</div>
|
||||
)}
|
||||
{/* Thread View */}
|
||||
{thread && thread.isExpanded && (
|
||||
<ThreadView
|
||||
messages={thread.messages}
|
||||
status={thread.status}
|
||||
onCancel={onCancel}
|
||||
runId={botMessage.run_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-1.5 rounded bg-light text-primary">
|
||||
<Bot size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
User,
|
||||
Bot,
|
||||
StopCircle,
|
||||
} from "lucide-react";
|
||||
import { Tooltip } from "antd";
|
||||
import { MessageConfig } from "../../../types/datamodel";
|
||||
import { ThreadState } from "./types";
|
||||
|
||||
interface ThreadViewProps {
|
||||
messages: MessageConfig[];
|
||||
status: ThreadState["status"];
|
||||
reason?: string; // Add reason
|
||||
onCancel: (runId: string) => void;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
interface StatusBannerProps {
|
||||
status: ThreadState["status"];
|
||||
reason?: string; // Add reason prop
|
||||
onCancel: (runId: string) => void;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
const StatusBanner = ({
|
||||
status,
|
||||
reason,
|
||||
onCancel,
|
||||
runId,
|
||||
}: StatusBannerProps) => {
|
||||
// Define variants FIRST
|
||||
const variants = {
|
||||
streaming: {
|
||||
wrapper: "bg-secondary border border-secondary",
|
||||
text: "text-primary",
|
||||
icon: "text-accent",
|
||||
},
|
||||
complete: {
|
||||
wrapper: "bg-secondary p-2",
|
||||
text: "text-accent",
|
||||
icon: " ",
|
||||
},
|
||||
error: {
|
||||
wrapper: "bg-secondary border border-secondary",
|
||||
text: "text-primary",
|
||||
icon: "text-red-500",
|
||||
},
|
||||
cancelled: {
|
||||
wrapper: "bg-secondary border border-secondary",
|
||||
text: "text-red-500",
|
||||
icon: "text-red-500",
|
||||
},
|
||||
} as const;
|
||||
|
||||
const content = {
|
||||
streaming: {
|
||||
icon: <Loader2 className="animate-spin" size={16} />,
|
||||
text: "Processing",
|
||||
showStop: true,
|
||||
},
|
||||
complete: {
|
||||
icon: <CheckCircle size={16} />,
|
||||
text: reason || "Completed",
|
||||
showStop: false,
|
||||
},
|
||||
error: {
|
||||
icon: <AlertCircle size={16} />,
|
||||
text: reason || "Error occurred",
|
||||
showStop: false,
|
||||
},
|
||||
cancelled: {
|
||||
icon: <StopCircle size={16} />,
|
||||
text: reason || "Cancelled",
|
||||
showStop: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// THEN check valid status and use variants
|
||||
const validStatus = status && status in variants ? status : "error";
|
||||
const currentVariant = variants[validStatus];
|
||||
const currentContent = content[validStatus];
|
||||
|
||||
// Rest of component remains the same...
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center mt-2 justify-between p-2 rounded transition-all duration-200 ${currentVariant.wrapper}`}
|
||||
>
|
||||
<div className={`flex items-center gap-2 ${currentVariant.text}`}>
|
||||
<span className={currentVariant.icon}>{currentContent.icon}</span>
|
||||
<span className="text-sm font-medium">{currentContent.text}</span>
|
||||
{currentContent.showStop && (
|
||||
<Tooltip title="Stop processing" placement="right">
|
||||
<button
|
||||
onClick={() => onCancel(runId)}
|
||||
className="ml-2 flex items-center gap-1 px-2 py-1 rounded bg-red-500 hover:bg-red-600 text-white text-xs font-medium transition-colors duration-200"
|
||||
>
|
||||
<StopCircle size={12} />
|
||||
<span>Stop</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Message = ({ msg, isLast }: { msg: MessageConfig; isLast: boolean }) => {
|
||||
const isUser = msg.source === "user";
|
||||
|
||||
return (
|
||||
<div className={`relative group ${!isLast ? "mb-2" : ""}`}>
|
||||
<div
|
||||
className={`
|
||||
flex items-start gap-2 p-2 rounded
|
||||
${isUser ? "bg-secondary" : "bg-tertiary"}
|
||||
border border-secondary
|
||||
transition-all duration-200
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
p-1.5 rounded bg-light
|
||||
${isUser ? " text-accent" : "text-primary"}
|
||||
`}
|
||||
>
|
||||
{isUser ? <User size={14} /> : <Bot size={14} />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-primary">
|
||||
{msg.source}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-secondary whitespace-pre-wrap">
|
||||
{msg.content || ""}
|
||||
</div>
|
||||
|
||||
{msg.models_usage && (
|
||||
<div className="text-xs text-secondary mt-1">
|
||||
Tokens:{" "}
|
||||
{(msg.models_usage.prompt_tokens || 0) +
|
||||
(msg.models_usage.completion_tokens || 0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreadView: React.FC<ThreadViewProps> = ({
|
||||
messages = [],
|
||||
status,
|
||||
reason,
|
||||
onCancel,
|
||||
runId,
|
||||
}) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current && status === "streaming") {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages, status]);
|
||||
|
||||
// const displayMessages = messages.filter(
|
||||
// (msg): msg is MessageConfig =>
|
||||
// msg && typeof msg.content === "string" && msg.content !== "TERMINATE"
|
||||
// );
|
||||
const displayMessages = messages;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<StatusBanner
|
||||
status={status}
|
||||
reason={reason}
|
||||
onCancel={onCancel}
|
||||
runId={runId}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
style={{ maxHeight: "300px" }}
|
||||
className="overflow-y-scroll scroll p-2 space-y-2 bg-primary rounded border border-secondary"
|
||||
>
|
||||
{displayMessages.map((msg, idx) => (
|
||||
<Message
|
||||
key={idx}
|
||||
msg={msg}
|
||||
isLast={idx === displayMessages.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{displayMessages.length === 0 && status !== "streaming" && (
|
||||
<div className="text-sm text-secondary text-center p-2">
|
||||
No messages to display
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadView;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Message, TaskResult } from "../../../types/datamodel";
|
||||
|
||||
export type ThreadStatus = "streaming" | "complete" | "error" | "cancelled";
|
||||
|
||||
export interface ThreadState {
|
||||
messages: any[];
|
||||
finalResult?: any;
|
||||
status: ThreadStatus;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadState {
|
||||
messages: any[];
|
||||
finalResult?: any;
|
||||
status: "streaming" | "complete" | "error" | "cancelled";
|
||||
isExpanded: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface MessageListProps {
|
||||
messages: Message[];
|
||||
threadMessages: Record<string, ThreadState>;
|
||||
setThreadMessages: React.Dispatch<
|
||||
React.SetStateAction<Record<string, ThreadState>>
|
||||
>; // Add this
|
||||
onRetry: (query: string) => void;
|
||||
onCancel: (runId: string) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface ModelUsage {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
}
|
||||
|
||||
export interface SocketMessage {
|
||||
type: "message" | "result" | "completion";
|
||||
data?: {
|
||||
source?: string;
|
||||
models_usage?: ModelUsage | null;
|
||||
content?: string;
|
||||
task_result?: TaskResult;
|
||||
};
|
||||
status?: ThreadStatus;
|
||||
timestamp?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -1,824 +0,0 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
Cog6ToothIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
PaperAirplaneIcon,
|
||||
SignalSlashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
Tabs,
|
||||
message as ToastMessage,
|
||||
Tooltip,
|
||||
message,
|
||||
} from "antd";
|
||||
import * as React from "react";
|
||||
import {
|
||||
IChatMessage,
|
||||
IChatSession,
|
||||
IMessage,
|
||||
IStatus,
|
||||
IWorkflow,
|
||||
} from "../../types";
|
||||
import { examplePrompts, fetchJSON, getServerUrl, guid } from "../../utils";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import MetaDataView from "./metadata";
|
||||
import {
|
||||
AgentRow,
|
||||
BounceLoader,
|
||||
CollapseBox,
|
||||
LoadingBar,
|
||||
MarkdownView,
|
||||
} from "../../atoms";
|
||||
import { useConfigStore } from "../../../hooks/store";
|
||||
import ProfilerView from "./utils/profiler";
|
||||
|
||||
let socketMsgs: any[] = [];
|
||||
|
||||
const ChatBox = ({
|
||||
initMessages,
|
||||
session,
|
||||
editable = true,
|
||||
heightOffset = 160,
|
||||
}: {
|
||||
initMessages: IMessage[] | null;
|
||||
session: IChatSession | null;
|
||||
editable?: boolean;
|
||||
heightOffset?: number;
|
||||
}) => {
|
||||
// const session: IChatSession | null = useConfigStore((state) => state.session);
|
||||
const textAreaInputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
const messageBoxInputRef = React.useRef<HTMLDivElement>(null);
|
||||
const { user } = React.useContext(appContext);
|
||||
const wsClient = React.useRef<WebSocket | null>(null);
|
||||
const wsMessages = React.useRef<IChatMessage[]>([]);
|
||||
const [wsConnectionStatus, setWsConnectionStatus] =
|
||||
React.useState<string>("disconnected");
|
||||
const [workflow, setWorkflow] = React.useState<IWorkflow | null>(null);
|
||||
|
||||
const [socketMessages, setSocketMessages] = React.useState<any[]>([]);
|
||||
const [awaitingUserInput, setAwaitingUserInput] = React.useState(false); // New state for tracking user input
|
||||
const setAreSessionButtonsDisabled = useConfigStore(
|
||||
(state) => state.setAreSessionButtonsDisabled
|
||||
);
|
||||
|
||||
const MAX_RETRIES = 10;
|
||||
const RETRY_INTERVAL = 2000;
|
||||
|
||||
const [retries, setRetries] = React.useState(0);
|
||||
|
||||
const serverUrl = getServerUrl();
|
||||
|
||||
let websocketUrl = serverUrl.replace("http", "ws") + "/ws/";
|
||||
|
||||
// check if there is a protocol in the serverUrl e.g. /api. if use the page url
|
||||
if (!serverUrl.includes("http")) {
|
||||
const pageUrl = window.location.href;
|
||||
const url = new URL(pageUrl);
|
||||
const protocol = url.protocol;
|
||||
const host = url.host;
|
||||
const baseUrl = protocol + "//" + host + serverUrl;
|
||||
websocketUrl = baseUrl.replace("http", "ws") + "/ws/";
|
||||
} else {
|
||||
websocketUrl = serverUrl.replace("http", "ws") + "/ws/";
|
||||
}
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [text, setText] = React.useState("");
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const socketDivRef = React.useRef<HTMLDivElement>(null);
|
||||
const connectionId = useConfigStore((state) => state.connectionId);
|
||||
|
||||
const messages = useConfigStore((state) => state.messages);
|
||||
const setMessages = useConfigStore((state) => state.setMessages);
|
||||
|
||||
const parseMessage = (message: IMessage) => {
|
||||
let meta;
|
||||
try {
|
||||
meta = JSON.parse(message.meta);
|
||||
} catch (e) {
|
||||
meta = message?.meta;
|
||||
}
|
||||
const msg: IChatMessage = {
|
||||
text: message.content,
|
||||
sender: message.role === "user" ? "user" : "bot",
|
||||
meta: meta,
|
||||
id: message.id,
|
||||
};
|
||||
return msg;
|
||||
};
|
||||
|
||||
const parseMessages = (messages: any) => {
|
||||
return messages?.map(parseMessage);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// console.log("initMessages changed", initMessages);
|
||||
const initMsgs: IChatMessage[] = parseMessages(initMessages);
|
||||
setMessages(initMsgs);
|
||||
wsMessages.current = initMsgs;
|
||||
}, [initMessages]);
|
||||
|
||||
const promptButtons = examplePrompts.map((prompt, i) => {
|
||||
return (
|
||||
<Button
|
||||
key={"prompt" + i}
|
||||
type="primary"
|
||||
className=""
|
||||
onClick={() => {
|
||||
runWorkflow(prompt.prompt);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{prompt.title}{" "}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
const messageListView = messages && messages?.map((message: IChatMessage, i: number) => {
|
||||
const isUser = message.sender === "user";
|
||||
const css = isUser ? "bg-accent text-white " : "bg-light";
|
||||
// console.log("message", message);
|
||||
let hasMeta = false;
|
||||
if (message.meta) {
|
||||
hasMeta =
|
||||
message.meta.code !== null ||
|
||||
message.meta.images?.length > 0 ||
|
||||
message.meta.files?.length > 0 ||
|
||||
message.meta.scripts?.length > 0;
|
||||
}
|
||||
|
||||
let items: MenuProps["items"] = [];
|
||||
|
||||
if (isUser) {
|
||||
items.push({
|
||||
label: (
|
||||
<div
|
||||
onClick={() => {
|
||||
console.log("retrying");
|
||||
runWorkflow(message.text);
|
||||
}}
|
||||
>
|
||||
<ArrowPathIcon
|
||||
role={"button"}
|
||||
title={"Retry"}
|
||||
className="h-4 w-4 mr-1 inline-block"
|
||||
/>
|
||||
Retry
|
||||
</div>
|
||||
),
|
||||
key: "retrymessage",
|
||||
});
|
||||
items.push({
|
||||
label: (
|
||||
<div
|
||||
onClick={() => {
|
||||
// copy to clipboard
|
||||
navigator.clipboard.writeText(message.text);
|
||||
ToastMessage.success("Message copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicateIcon
|
||||
role={"button"}
|
||||
title={"Copy"}
|
||||
className="h-4 w-4 mr-1 inline-block"
|
||||
/>
|
||||
Copy
|
||||
</div>
|
||||
),
|
||||
key: "copymessage",
|
||||
});
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Dropdown menu={{ items }} trigger={["click"]} placement="bottomRight">
|
||||
<div
|
||||
role="button"
|
||||
className="float-right ml-2 duration-100 hover:bg-secondary font-semibold px-2 pb-1 rounded"
|
||||
>
|
||||
<span className="block -mt-2 text-primary "> ...</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={"message" + i} className={`align-right ${isUser ? "text-righpt" : ""} mb-2 border-b`}
|
||||
key={"message" + i}
|
||||
>
|
||||
{" "}
|
||||
<div className={` ${isUser ? "" : " w-full"} inline-flex gap-2`}>
|
||||
<div className=""></div>
|
||||
<div className="font-semibold text-secondary text-sm w-16">{`${
|
||||
isUser ? "USER" : "AGENTS"
|
||||
}`}</div>
|
||||
<div
|
||||
className={`inline-block group relative w-full p-2 rounded ${css}`}
|
||||
>
|
||||
{" "}
|
||||
{items.length > 0 && editable && (
|
||||
<div className=" group-hover:opacity-100 opacity-0 ">{menu}</div>
|
||||
)}
|
||||
{isUser && (
|
||||
<>
|
||||
<div className="inline-block">{message.text}</div>
|
||||
</>
|
||||
)}
|
||||
{!isUser && (
|
||||
<div
|
||||
className={` w-full chatbox prose dark:prose-invert text-primary rounded `}
|
||||
>
|
||||
<MarkdownView
|
||||
className="text-sm"
|
||||
data={message.text}
|
||||
showCode={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{message.meta && !isUser && (
|
||||
<>
|
||||
{" "}
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
{" "}
|
||||
<ChatBubbleLeftRightIcon className="h-4 w-4 inline-block mr-1" />
|
||||
Agent Messages
|
||||
</>
|
||||
),
|
||||
key: "1",
|
||||
children: (
|
||||
<div className="text-primary">
|
||||
<MetaDataView metadata={message.meta} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div>
|
||||
{" "}
|
||||
<SignalSlashIcon className="h-4 w-4 inline-block mr-1" />{" "}
|
||||
Profiler
|
||||
</div>
|
||||
),
|
||||
key: "2",
|
||||
children: (
|
||||
<div className="text-primary">
|
||||
<ProfilerView agentMessage={message} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
// console.log("messages updated, scrolling");
|
||||
setTimeout(() => {
|
||||
scrollChatBox(messageBoxInputRef);
|
||||
}, 500);
|
||||
}, [messages]);
|
||||
|
||||
const textAreaDefaultHeight = "64px";
|
||||
// clear text box if loading has just changed to false and there is no error
|
||||
React.useEffect(() => {
|
||||
if ((awaitingUserInput || loading === false) && textAreaInputRef.current) {
|
||||
if (textAreaInputRef.current) {
|
||||
if (error === null || (error && error.status === false)) {
|
||||
textAreaInputRef.current.value = "";
|
||||
textAreaInputRef.current.style.height = textAreaDefaultHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (textAreaInputRef.current) {
|
||||
textAreaInputRef.current.style.height = textAreaDefaultHeight; // Reset height to shrink if text is deleted
|
||||
const scrollHeight = textAreaInputRef.current.scrollHeight;
|
||||
textAreaInputRef.current.style.height = `${scrollHeight}px`;
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
const [waitingToReconnect, setWaitingToReconnect] = React.useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (waitingToReconnect) {
|
||||
return;
|
||||
}
|
||||
// Only set up the websocket once
|
||||
const socketUrl = websocketUrl + connectionId;
|
||||
console.log("socketUrl", socketUrl);
|
||||
if (!wsClient.current) {
|
||||
const client = new WebSocket(socketUrl);
|
||||
wsClient.current = client;
|
||||
client.onerror = (e) => {
|
||||
console.log("ws error", e);
|
||||
};
|
||||
|
||||
client.onopen = () => {
|
||||
setWsConnectionStatus("connected");
|
||||
console.log("ws opened");
|
||||
};
|
||||
|
||||
client.onclose = () => {
|
||||
if (wsClient.current) {
|
||||
// Connection failed
|
||||
console.log("ws closed by server");
|
||||
} else {
|
||||
// Cleanup initiated from app side, can return here, to not attempt a reconnect
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingToReconnect) {
|
||||
return;
|
||||
}
|
||||
setWsConnectionStatus("disconnected");
|
||||
setWaitingToReconnect(true);
|
||||
setWsConnectionStatus("reconnecting");
|
||||
setTimeout(() => {
|
||||
setWaitingToReconnect(null);
|
||||
}, RETRY_INTERVAL);
|
||||
};
|
||||
|
||||
client.onmessage = (message) => {
|
||||
const data = JSON.parse(message.data);
|
||||
console.log("received message", data);
|
||||
if (data && data.type === "agent_message") {
|
||||
// indicates an intermediate agent message update
|
||||
const newsocketMessages = Object.assign([], socketMessages);
|
||||
newsocketMessages.push(data.data);
|
||||
setSocketMessages(newsocketMessages);
|
||||
socketMsgs.push(data.data);
|
||||
setTimeout(() => {
|
||||
scrollChatBox(socketDivRef);
|
||||
scrollChatBox(messageBoxInputRef);
|
||||
}, 200);
|
||||
// console.log("received message", data, socketMsgs.length);
|
||||
} else if (data && data.type === "user_input_request") {
|
||||
setAwaitingUserInput(true); // Set awaiting input state
|
||||
textAreaInputRef.current.value = ""
|
||||
textAreaInputRef.current.placeholder = data.data.message.content
|
||||
const newsocketMessages = Object.assign([], socketMessages);
|
||||
newsocketMessages.push(data.data);
|
||||
setSocketMessages(newsocketMessages);
|
||||
socketMsgs.push(data.data);
|
||||
setTimeout(() => {
|
||||
scrollChatBox(socketDivRef);
|
||||
scrollChatBox(messageBoxInputRef);
|
||||
}, 200);
|
||||
ToastMessage.info(data.data.message)
|
||||
} else if (data && data.type === "agent_status") {
|
||||
// indicates a status message update
|
||||
const agentStatusSpan = document.getElementById("agentstatusspan");
|
||||
if (agentStatusSpan) {
|
||||
agentStatusSpan.innerHTML = data.data.message;
|
||||
}
|
||||
} else if (data && data.type === "agent_response") {
|
||||
// indicates a final agent response
|
||||
setAwaitingUserInput(false); // Set awaiting input state
|
||||
setAreSessionButtonsDisabled(false);
|
||||
processAgentResponse(data.data);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.log("Cleanup");
|
||||
// Dereference, so it will set up next time
|
||||
wsClient.current = null;
|
||||
client.close();
|
||||
};
|
||||
}
|
||||
}, [waitingToReconnect]);
|
||||
|
||||
const scrollChatBox = (element: any) => {
|
||||
element.current?.scroll({
|
||||
top: element.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
const mainDivRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const processAgentResponse = (data: any) => {
|
||||
if (data && data.status) {
|
||||
const msg = parseMessage(data.data);
|
||||
wsMessages.current.push(msg);
|
||||
setMessages(wsMessages.current);
|
||||
setLoading(false);
|
||||
setAwaitingUserInput(false);
|
||||
} else {
|
||||
console.log("error", data);
|
||||
// setError(data);
|
||||
ToastMessage.error(data.message);
|
||||
setLoading(false);
|
||||
setAwaitingUserInput(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchWorkFlow = (workflowId: number) => {
|
||||
const fetchUrl = `${serverUrl}/workflows/${workflowId}?user_id=${user?.email}`;
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
if (data.data && data.data.length > 0) {
|
||||
setWorkflow(data.data[0]);
|
||||
}
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
};
|
||||
fetchJSON(fetchUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const runWorkflow = (query: string) => {
|
||||
setError(null);
|
||||
setAreSessionButtonsDisabled(true);
|
||||
socketMsgs = [];
|
||||
let messageHolder = Object.assign([], messages);
|
||||
|
||||
const userMessage: IChatMessage = {
|
||||
text: query,
|
||||
sender: "user",
|
||||
};
|
||||
messageHolder.push(userMessage);
|
||||
setMessages(messageHolder);
|
||||
wsMessages.current.push(userMessage);
|
||||
|
||||
const messagePayload: IMessage = {
|
||||
role: "user",
|
||||
content: query,
|
||||
user_id: user?.email || "",
|
||||
session_id: session?.id,
|
||||
workflow_id: session?.workflow_id,
|
||||
connection_id: connectionId,
|
||||
};
|
||||
|
||||
const runWorkflowUrl = `${serverUrl}/sessions/${session?.id}/workflow/${session?.workflow_id}/run`;
|
||||
|
||||
const postData = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(messagePayload),
|
||||
};
|
||||
setLoading(true);
|
||||
|
||||
// check if socket connected, send on socket
|
||||
// else send on fetch
|
||||
if (wsClient.current && wsClient.current.readyState === 1) {
|
||||
wsClient.current.send(
|
||||
JSON.stringify({
|
||||
connection_id: connectionId,
|
||||
data: messagePayload,
|
||||
type: "user_message",
|
||||
session_id: session?.id,
|
||||
workflow_id: session?.workflow_id,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
fetch(runWorkflowUrl, postData)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
res.json().then((data) => {
|
||||
processAgentResponse(data);
|
||||
});
|
||||
} else {
|
||||
res.json().then((data) => {
|
||||
console.log("error", data);
|
||||
ToastMessage.error(data.message);
|
||||
setLoading(false);
|
||||
});
|
||||
ToastMessage.error(
|
||||
"Connection error. Ensure server is up and running."
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
|
||||
ToastMessage.error(
|
||||
"Connection error. Ensure server is up and running."
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
scrollChatBox(messageBoxInputRef);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sendUserResponse = (userResponse: string) => {
|
||||
setAwaitingUserInput(false);
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
textAreaInputRef.current.placeholder = "Write message here..."
|
||||
|
||||
const userMessage: IChatMessage = {
|
||||
text: userResponse,
|
||||
sender: "system",
|
||||
};
|
||||
|
||||
const messagePayload: IMessage = {
|
||||
role: "user",
|
||||
content: userResponse,
|
||||
user_id: user?.email || "",
|
||||
session_id: session?.id,
|
||||
workflow_id: session?.workflow_id,
|
||||
connection_id: connectionId,
|
||||
};
|
||||
|
||||
// check if socket connected,
|
||||
if (wsClient.current && wsClient.current.readyState === 1) {
|
||||
wsClient.current.send(
|
||||
JSON.stringify({
|
||||
connection_id: connectionId,
|
||||
data: messagePayload,
|
||||
type: "user_message",
|
||||
session_id: session?.id,
|
||||
workflow_id: session?.workflow_id,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.err("websocket client error")
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextChange = (
|
||||
event: React.ChangeEvent<HTMLTextAreaElement>
|
||||
): void => {
|
||||
setText(event.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||
): void => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
if (textAreaInputRef.current &&(awaitingUserInput || !loading)) {
|
||||
event.preventDefault();
|
||||
if (awaitingUserInput) {
|
||||
sendUserResponse(textAreaInputRef.current.value); // New function call for sending user input
|
||||
textAreaInputRef.current.value = "";
|
||||
} else {
|
||||
runWorkflow(textAreaInputRef.current.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectionColor = (status: string) => {
|
||||
if (status === "connected") {
|
||||
return "bg-green-500";
|
||||
} else if (status === "reconnecting") {
|
||||
return "bg-orange-500";
|
||||
} else if (status === "disconnected") {
|
||||
return "bg-red-500";
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (session && session.workflow_id) {
|
||||
fetchWorkFlow(session.workflow_id);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const WorkflowView = ({ workflow }: { workflow: IWorkflow }) => {
|
||||
return (
|
||||
<div id="workflow-view" className="text-xs cursor-pointer inline-block">
|
||||
{" "}
|
||||
{workflow.name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="chatbox-main"
|
||||
style={{ height: "calc(100vh - " + heightOffset + "px)" }}
|
||||
className="text-primary relative rounded "
|
||||
ref={mainDivRef}
|
||||
>
|
||||
<div
|
||||
id="workflow-name"
|
||||
style={{ zIndex: 100 }}
|
||||
className=" absolute right-3 bg-primary rounded text-secondary -top-6 p-2"
|
||||
>
|
||||
{" "}
|
||||
{workflow && <div className="text-xs"> {workflow.name}</div>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="message-box"
|
||||
ref={messageBoxInputRef}
|
||||
className="flex h-full flex-col rounded scroll pr-2 overflow-auto "
|
||||
style={{ minHeight: "30px", height: "calc(100vh - 310px)" }}
|
||||
>
|
||||
<div id="scroll-gradient" className="scroll-gradient h-10">
|
||||
{" "}
|
||||
<span className=" inline-block h-6"></span>{" "}
|
||||
</div>
|
||||
<div className="flex-1 boder mt-4"></div>
|
||||
{!messages && messages !== null && (
|
||||
<div id="loading-messages" className="w-full text-center boder mt-4">
|
||||
<div>
|
||||
{" "}
|
||||
<BounceLoader />
|
||||
</div>
|
||||
loading messages
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages && messages?.length === 0 && (
|
||||
<div id="no-messages" className="ml-2 text-sm text-secondary ">
|
||||
<InformationCircleIcon className="inline-block h-6 mr-2" />
|
||||
No messages in the current session. Start a conversation to begin.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div id="message-list" className="ml-2"> {messageListView}</div>
|
||||
{(loading || awaitingUserInput) && (
|
||||
<div id="loading-bar" className={` inline-flex gap-2 duration-300 `}>
|
||||
<div className=""></div>
|
||||
<div className="font-semibold text-secondary text-sm w-16">
|
||||
AGENTS
|
||||
</div>
|
||||
<div className="relative w-full ">
|
||||
<div className="mb-2 ">
|
||||
<LoadingBar>
|
||||
<div className="mb-1 inline-block ml-2 text-xs text-secondary">
|
||||
<span className="innline-block text-sm ml-2">
|
||||
{" "}
|
||||
<span id="agentstatusspan">
|
||||
{" "}
|
||||
agents working on task ..
|
||||
</span>
|
||||
</span>{" "}
|
||||
{socketMsgs.length > 0 && (
|
||||
<span className="border-l inline-block text-right ml-2 pl-2">
|
||||
{socketMsgs.length} agent message
|
||||
{socketMsgs.length > 1 && "s"} sent/received.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</LoadingBar>
|
||||
</div>
|
||||
|
||||
{socketMsgs.length > 0 && (
|
||||
<div
|
||||
id="agent-messages"
|
||||
ref={socketDivRef}
|
||||
style={{
|
||||
minHeight: "300px",
|
||||
maxHeight: "400px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
className={`inline-block scroll group relative p-2 rounded w-full bg-light `}
|
||||
>
|
||||
<CollapseBox
|
||||
open={true}
|
||||
title={`Agent Messages (${socketMsgs.length} message${
|
||||
socketMsgs.length > 1 ? "s" : ""
|
||||
}) `}
|
||||
>
|
||||
{socketMsgs?.map((message: any, i: number) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<AgentRow message={message} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CollapseBox>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{editable && (
|
||||
<div id="input-area" className="mt-2 p-2 absolute bg-primary bottom-0 w-full">
|
||||
<div
|
||||
id="input-form"
|
||||
className={`rounded p-2 shadow-lg flex mb-1 gap-2 ${
|
||||
loading && !awaitingUserInput ? " opacity-50 pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
{/* <input className="flex-1 p-2 ring-2" /> */}
|
||||
<form
|
||||
autoComplete="on"
|
||||
className="flex-1 relative"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
id="queryInput"
|
||||
name="queryInput"
|
||||
autoComplete="on"
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleTextChange}
|
||||
placeholder="Write message here..."
|
||||
ref={textAreaInputRef}
|
||||
className="flex items-center w-full resize-none text-gray-600 bg-white p-2 ring-2 rounded-sm pl-5 pr-16 h-64"
|
||||
style={{
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
minHeight: "50px",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
id="send-button"
|
||||
role={"button"}
|
||||
style={{ width: "45px", height: "35px" }}
|
||||
title="Send message"
|
||||
onClick={() => {
|
||||
if (textAreaInputRef.current && (awaitingUserInput || !loading)) {
|
||||
if (awaitingUserInput) {
|
||||
sendUserResponse(textAreaInputRef.current.value); // Use the new function for user input
|
||||
} else {
|
||||
runWorkflow(textAreaInputRef.current.value);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="absolute right-3 bottom-2 bg-accent hover:brightness-75 transition duration-300 rounded cursor-pointer flex justify-center items-center"
|
||||
>
|
||||
{" "}
|
||||
{(awaitingUserInput || !loading) && (
|
||||
<div className="inline-block ">
|
||||
<PaperAirplaneIcon className="h-6 w-6 text-white " />{" "}
|
||||
</div>
|
||||
)}
|
||||
{loading && !awaitingUserInput && (
|
||||
<div className="inline-block ">
|
||||
<Cog6ToothIcon className="text-white animate-spin rounded-full h-6 w-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<div className="mt-2 text-xs text-secondary">
|
||||
<Tooltip title={`Socket ${wsConnectionStatus}`}>
|
||||
<div
|
||||
className={`w-1 h-3 rounded inline-block mr-1 ${getConnectionColor(
|
||||
wsConnectionStatus
|
||||
)}`}
|
||||
></div>{" "}
|
||||
</Tooltip>
|
||||
Blank slate? Try one of the example prompts below{" "}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="prompt-buttons"
|
||||
className={`mt-2 inline-flex gap-2 flex-wrap ${
|
||||
(loading && !awaitingUserInput) ? "brightness-75 pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
{promptButtons}
|
||||
</div>
|
||||
</div>
|
||||
{error && !error.status && (
|
||||
<div id="error-message" className="p-2 rounded mt-4 text-orange-500 text-sm">
|
||||
{" "}
|
||||
<ExclamationTriangleIcon className="h-5 text-orange-500 inline-block mr-2" />{" "}
|
||||
{error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ChatBox;
|
||||
@@ -1,242 +0,0 @@
|
||||
import {
|
||||
DocumentTextIcon,
|
||||
PhotoIcon,
|
||||
VideoCameraIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import * as React from "react";
|
||||
import {
|
||||
CodeBlock,
|
||||
CodeLoader,
|
||||
CsvLoader,
|
||||
CollapseBox,
|
||||
ExpandView,
|
||||
GroupView,
|
||||
ImageLoader,
|
||||
MarkdownView,
|
||||
PdfViewer,
|
||||
AgentRow,
|
||||
} from "../../atoms";
|
||||
import { formatDuration, getServerUrl } from "../../utils";
|
||||
import { IMetadataFile } from "../../types";
|
||||
import Icon from "../../icons";
|
||||
|
||||
const MetaDataView = ({ metadata }: { metadata: any | null }) => {
|
||||
const serverUrl = getServerUrl();
|
||||
const renderFileContent = (file: IMetadataFile, i: number) => {
|
||||
const file_type = file.extension;
|
||||
const is_image = ["image"].includes(file.type);
|
||||
const is_code = ["code"].includes(file.type);
|
||||
const is_csv = ["csv"].includes(file.type);
|
||||
const is_pdf = ["pdf"].includes(file.type);
|
||||
const is_video = ["video"].includes(file.type);
|
||||
const file_name = file.name || "unknown";
|
||||
const file_path = file.path || "unknown";
|
||||
|
||||
let fileView = null;
|
||||
let fileTitle = (
|
||||
<div>
|
||||
{file.type === "image" ? (
|
||||
<PhotoIcon className="h-4 mr-1 inline-block" />
|
||||
) : (
|
||||
<DocumentTextIcon className="h-4 mr-1 inline-block" />
|
||||
)}{" "}
|
||||
<span className="break-all ">{file_name}</span>{" "}
|
||||
</div>
|
||||
);
|
||||
let icon = (
|
||||
<div className="p-2 bg-secondary rounded flex items-center justify-center h-full">
|
||||
<div className="">
|
||||
<div style={{ fontSize: "2em" }} className=" text-center mb-2">
|
||||
{file.extension}
|
||||
</div>
|
||||
<div>{fileTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (is_image) {
|
||||
fileView = (
|
||||
<div>
|
||||
<div className="mb-2">{fileTitle}</div>
|
||||
<ImageLoader
|
||||
src={`${serverUrl}/${file_path}`}
|
||||
className="w-full rounded"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
icon = fileView;
|
||||
} else if (is_video) {
|
||||
fileView = (
|
||||
<div className="mb-2">
|
||||
<a href={`${serverUrl}/${file_path}`}>{fileTitle}</a>
|
||||
<video controls className="w-full rounded">
|
||||
<source
|
||||
src={`${serverUrl}/${file_path}`}
|
||||
type={`video/${file_type}`}
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Update icon to show a video-related icon
|
||||
icon = (
|
||||
<div className=" relative rounded h-full">
|
||||
<div className="absolute rounded p-2 bg-secondary top-0 ">
|
||||
{fileTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{ minHeight: "150px" }}
|
||||
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary "
|
||||
>
|
||||
<VideoCameraIcon className="h-14 w-14" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (is_csv) {
|
||||
fileView = (
|
||||
<div className="h">
|
||||
<a href={`${serverUrl}/${file_path}`}>
|
||||
<div className="mb-4">{fileTitle}</div>
|
||||
</a>
|
||||
<CsvLoader
|
||||
csvUrl={`${serverUrl}/${file_path}`}
|
||||
className="w-full rounded"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
icon = (
|
||||
<div className=" relative rounded h-full">
|
||||
<div className="absolute rounded p-2 bg-secondary top-0 ">
|
||||
{fileTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{ minHeight: "150px" }}
|
||||
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary"
|
||||
>
|
||||
<Icon icon="csv" size={14} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (is_code) {
|
||||
fileView = (
|
||||
<div className="h">
|
||||
<a
|
||||
href={`${serverUrl}/${file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-4">{fileTitle}</div>
|
||||
</a>
|
||||
<CodeLoader
|
||||
url={`${serverUrl}/${file_path}`}
|
||||
className="w-full rounded"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
icon = (
|
||||
<div className=" relative rounded h-full">
|
||||
<div className="absolute rounded p-2 bg-secondary top-0 ">
|
||||
{fileTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{ minHeight: "150px" }}
|
||||
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary"
|
||||
>
|
||||
<Icon icon="python" size={14} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (is_pdf) {
|
||||
fileView = (
|
||||
<div className="h-full">
|
||||
<div className="mb-4">
|
||||
<a
|
||||
href={`${serverUrl}/${file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{fileTitle}
|
||||
</a>
|
||||
</div>
|
||||
<PdfViewer url={`${serverUrl}/${file_path}`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
icon = (
|
||||
<div className=" relative rounded h-full">
|
||||
<div className="absolute rounded p-2 bg-secondary top-0 ">
|
||||
{fileTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{ minHeight: "150px" }}
|
||||
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary "
|
||||
>
|
||||
<Icon icon="pdf" size={14} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
fileView = <span>Unsupported file type.</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className=" h-full rounded">
|
||||
<ExpandView className="mb-1" icon={icon} title={file_name}>
|
||||
{fileView}
|
||||
</ExpandView>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFile = (file: IMetadataFile, i: number) => (
|
||||
<div key={"metafilesrow" + i} className="text-primary ">
|
||||
{renderFileContent(file, i)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const files = (metadata.files || []).map(renderFile);
|
||||
|
||||
const messages = (metadata.messages || []).map((message: any, i: number) => {
|
||||
return (
|
||||
<div className=" mb-2 border-dashed" key={"messagerow" + i}>
|
||||
<AgentRow message={message} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const hasContent = files.length > 0;
|
||||
const hasMessages = messages.length > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hasMessages && (
|
||||
<div className="rounded bg-primary ">
|
||||
<CollapseBox
|
||||
open={false}
|
||||
title={`Agent Messages (${messages.length} message${
|
||||
messages.length > 1 ? "s" : ""
|
||||
}) | ${formatDuration(metadata?.time)}`}
|
||||
>
|
||||
{messages}
|
||||
</CollapseBox>
|
||||
</div>
|
||||
)}
|
||||
{hasContent && (
|
||||
<div className="rounded mt-2">
|
||||
<CollapseBox
|
||||
open={true}
|
||||
title={`Results (${files.length} file${
|
||||
files.length > 1 ? "s" : ""
|
||||
})`}
|
||||
>
|
||||
<div className="mt-2 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{files}
|
||||
</div>
|
||||
</CollapseBox>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetaDataView;
|
||||
@@ -1,94 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { IChatSession, IMessage, IStatus } from "../../types";
|
||||
import { fetchJSON, getServerUrl, setLocalStorage } from "../../utils";
|
||||
import ChatBox from "./chatbox";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import { message } from "antd";
|
||||
import SideBarView from "./sidebar";
|
||||
import { useConfigStore } from "../../../hooks/store";
|
||||
import SessionsView from "./sessions";
|
||||
|
||||
const RAView = () => {
|
||||
const session: IChatSession | null = useConfigStore((state) => state.session);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [messages, setMessages] = React.useState<IMessage[] | null>(null);
|
||||
|
||||
const [config, setConfig] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalStorage("ara_config", config);
|
||||
}, [config]);
|
||||
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const fetchMessagesUrl = `${serverUrl}/sessions/${session?.id}/messages?user_id=${user?.email}`;
|
||||
|
||||
const fetchMessages = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
setMessages(null);
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
setMessages(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(fetchMessagesUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user && session) {
|
||||
fetchMessages();
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<div className="h-full ">
|
||||
<div className="flex h-full ">
|
||||
<div className=" mr-2 rounded">
|
||||
<SideBarView />
|
||||
</div>
|
||||
<div className=" flex-1 ">
|
||||
{!session && (
|
||||
<div className=" w-full h-full flex items-center justify-center">
|
||||
<div className="w-2/3" id="middle">
|
||||
<div className="w-full text-center">
|
||||
{" "}
|
||||
<img
|
||||
src="/images/svgs/welcome.svg"
|
||||
alt="welcome"
|
||||
className="text-accent inline-block object-cover w-56"
|
||||
/>
|
||||
</div>
|
||||
<SessionsView />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{session !== null && (
|
||||
<ChatBox initMessages={messages} session={session} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default RAView;
|
||||
@@ -1,460 +0,0 @@
|
||||
import {
|
||||
ChatBubbleLeftRightIcon,
|
||||
ExclamationTriangleIcon,
|
||||
GlobeAltIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
Square3Stack3DIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Button, Dropdown, Input, MenuProps, Modal, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { IChatSession, IWorkflow, IStatus } from "../../types";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import { fetchJSON, getServerUrl, timeAgo } from "../../utils";
|
||||
import { LaunchButton, LoadingOverlay } from "../../atoms";
|
||||
import { useConfigStore } from "../../../hooks/store";
|
||||
import WorkflowSelector from "./utils/selectors";
|
||||
|
||||
const SessionsView = ({}: any) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const listSessionUrl = `${serverUrl}/sessions?user_id=${user?.email}`;
|
||||
const createSessionUrl = `${serverUrl}/sessions`;
|
||||
const publishSessionUrl = `${serverUrl}/sessions/publish`;
|
||||
|
||||
const sessions = useConfigStore((state) => state.sessions);
|
||||
|
||||
const setSessions = useConfigStore((state) => state.setSessions);
|
||||
const sampleSession: IChatSession = {
|
||||
user_id: user?.email || "",
|
||||
name:
|
||||
"New Session " +
|
||||
new Date().toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
}),
|
||||
};
|
||||
const [selectedSession, setSelectedSession] =
|
||||
React.useState<IChatSession | null>(sampleSession);
|
||||
// const [session, setSession] =
|
||||
// React.useState<IChatSession | null>(null);
|
||||
const session = useConfigStore((state) => state.session);
|
||||
const setSession = useConfigStore((state) => state.setSession);
|
||||
|
||||
const isSessionButtonsDisabled = useConfigStore(
|
||||
(state) => state.areSessionButtonsDisabled
|
||||
);
|
||||
|
||||
const deleteSession = (session: IChatSession) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const deleteSessionUrl = `${serverUrl}/sessions/delete?user_id=${user?.email}&session_id=${session.id}`;
|
||||
const payLoad = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: user?.email,
|
||||
session: session,
|
||||
}),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
fetchSessions();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(deleteSessionUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const [newSessionModalVisible, setNewSessionModalVisible] =
|
||||
React.useState(false);
|
||||
|
||||
const fetchSessions = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
// message.success(data.message);
|
||||
// console.log("sessions", data);
|
||||
setSessions(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(listSessionUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
const publishSession = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
const body = {
|
||||
user_id: user?.email,
|
||||
session: session,
|
||||
tags: ["published"],
|
||||
};
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
// setSessions(data.data);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(publishSessionUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (sessions && sessions.length > 0) {
|
||||
const firstSession = sessions[0];
|
||||
setSession(firstSession);
|
||||
} else {
|
||||
setSession(null);
|
||||
}
|
||||
}, [sessions]);
|
||||
|
||||
const createSession = (session: IChatSession) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
// const fetch;
|
||||
const payLoad = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(session),
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
message.success(data.message);
|
||||
fetchSessions();
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(createSessionUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
fetchSessions();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sessionRows = sessions.map((data: IChatSession, index: number) => {
|
||||
const isSelected = session?.id === data.id;
|
||||
const rowClass = isSelected
|
||||
? "bg-accent text-white"
|
||||
: "bg-secondary text-primary";
|
||||
|
||||
let items: MenuProps["items"] = [
|
||||
{
|
||||
label: (
|
||||
<div
|
||||
onClick={() => {
|
||||
console.log("deleting session");
|
||||
deleteSession(data);
|
||||
}}
|
||||
>
|
||||
<TrashIcon
|
||||
role={"button"}
|
||||
title={"Delete"}
|
||||
className="h-4 w-4 mr-1 inline-block"
|
||||
/>
|
||||
Delete
|
||||
</div>
|
||||
),
|
||||
key: "delete",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div
|
||||
onClick={() => {
|
||||
// get current clicked session
|
||||
setSelectedSession(data);
|
||||
setNewSessionModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<PencilIcon
|
||||
role={"button"}
|
||||
title={"Edit"}
|
||||
className="h-4 w-4 mr-1 inline-block"
|
||||
/>
|
||||
Edit
|
||||
</div>
|
||||
),
|
||||
key: "edit",
|
||||
},
|
||||
// {
|
||||
// label: (
|
||||
// <div
|
||||
// onClick={() => {
|
||||
// console.log("publishing session");
|
||||
// publishSession();
|
||||
// }}
|
||||
// >
|
||||
// <GlobeAltIcon
|
||||
// role={"button"}
|
||||
// title={"Publish"}
|
||||
// className="h-4 w-4 mr-1 inline-block"
|
||||
// />
|
||||
// Publish
|
||||
// </div>
|
||||
// ),
|
||||
// key: "publish",
|
||||
// },
|
||||
];
|
||||
|
||||
items.push();
|
||||
const menu = (
|
||||
<Dropdown
|
||||
menu={{ items: items }}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
className={`float-right ml-2 duration-100 hover:bg-secondary font-semibold px-2 pb-1 rounded ${
|
||||
isSelected ? "hover:text-accent" : ""
|
||||
}`}
|
||||
>
|
||||
<span className={`block -mt-2 ${isSelected ? "text-white" : ""}`}>
|
||||
{" "}
|
||||
...
|
||||
</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={"sessionsrow" + index}
|
||||
className={`group relative mb-2 pb-1 border-b border-dashed ${
|
||||
isSessionButtonsDisabled ? "opacity-50 pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
{items.length > 0 && (
|
||||
<div className=" absolute right-2 top-2 group-hover:opacity-100 opacity-0 ">
|
||||
{menu}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`rounded p-2 cursor-pointer ${rowClass}`}
|
||||
role="button"
|
||||
onClick={() => {
|
||||
// setWorkflowConfig(data.flow_config);
|
||||
if (!isSessionButtonsDisabled) {
|
||||
setSession(data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="text-xs mt-1">
|
||||
<Square3Stack3DIcon className="h-4 w-4 inline-block mr-1" />
|
||||
{data.name}
|
||||
</div>
|
||||
<div className="text-xs text-right ">
|
||||
{timeAgo(data.created_at || "")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
let windowHeight, skillsMaxHeight;
|
||||
if (typeof window !== "undefined") {
|
||||
windowHeight = window.innerHeight;
|
||||
skillsMaxHeight = windowHeight - 400 + "px";
|
||||
}
|
||||
|
||||
const NewSessionModal = ({ session }: { session: IChatSession | null }) => {
|
||||
const [workflow, setWorkflow] = React.useState<IWorkflow | null>(null);
|
||||
const [localSession, setLocalSession] = React.useState<IChatSession | null>(
|
||||
session
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (workflow && workflow.id && localSession) {
|
||||
setLocalSession({ ...localSession, workflow_id: workflow.id });
|
||||
}
|
||||
}, [workflow]);
|
||||
|
||||
const sessionExists =
|
||||
localSession !== null && localSession.id !== undefined;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onCancel={() => {
|
||||
setNewSessionModalVisible(false);
|
||||
}}
|
||||
title={
|
||||
<div className="font-semibold mb-2 pb-1 border-b">
|
||||
<Square3Stack3DIcon className="h-5 w-5 inline-block mr-1" />
|
||||
New Session{" "}
|
||||
</div>
|
||||
}
|
||||
open={newSessionModalVisible}
|
||||
footer={[
|
||||
<Button
|
||||
key="back"
|
||||
onClick={() => {
|
||||
setNewSessionModalVisible(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
disabled={!workflow}
|
||||
onClick={() => {
|
||||
setNewSessionModalVisible(false);
|
||||
if (localSession) {
|
||||
createSession(localSession);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<WorkflowSelector
|
||||
workflow={workflow}
|
||||
setWorkflow={setWorkflow}
|
||||
workflow_id={selectedSession?.workflow_id}
|
||||
disabled={sessionExists}
|
||||
/>
|
||||
<div className="my-2 text-xs"> Session Name </div>
|
||||
<Input
|
||||
placeholder="Session Name"
|
||||
value={localSession?.name || ""}
|
||||
onChange={(event) => {
|
||||
if (localSession) {
|
||||
setLocalSession({ ...localSession, name: event.target.value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs mt-4">
|
||||
{" "}
|
||||
{timeAgo(localSession?.created_at || "", true)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" ">
|
||||
<NewSessionModal session={selectedSession || sampleSession} />
|
||||
<div className="mb-2 relative">
|
||||
<div className="">
|
||||
<div className="font-semibold mb-2 pb-1 border-b">
|
||||
<ChatBubbleLeftRightIcon className="h-5 w-5 inline-block mr-1" />
|
||||
Sessions{" "}
|
||||
</div>
|
||||
{sessions && sessions.length > 0 && (
|
||||
<div className="text-xs hidden mb-2 pb-1 ">
|
||||
{" "}
|
||||
Create a new session or select an existing session to view chat.
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
maxHeight: skillsMaxHeight,
|
||||
}}
|
||||
className="mb-4 overflow-y-auto scroll rounded relative "
|
||||
>
|
||||
{sessionRows}
|
||||
<LoadingOverlay loading={loading} />
|
||||
</div>
|
||||
{(!sessions || sessions.length == 0) && !loading && (
|
||||
<div className="text-xs text-gray-500">
|
||||
No sessions found. Create a new session to get started.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-x-2">
|
||||
<div className="flex-1"></div>
|
||||
<LaunchButton
|
||||
className={`text-sm p-2 px-3 ${isSessionButtonsDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedSession(sampleSession);
|
||||
setNewSessionModalVisible(true);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<PlusIcon className="w-5 h-5 inline-block mr-1" />
|
||||
New
|
||||
</LaunchButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && !error.status && (
|
||||
<div className="p-2 border border-orange-500 text-secondary rounded mt-4 text-sm">
|
||||
{" "}
|
||||
<ExclamationTriangleIcon className="h-5 text-orange-500 inline-block mr-2" />{" "}
|
||||
{error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionsView;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
import * as React from "react";
|
||||
import SessionsView from "./sessions";
|
||||
|
||||
const SideBarView = () => {
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
const minWidth = isOpen ? "270px" : "50px";
|
||||
|
||||
let windowHeight, sidebarMaxHeight;
|
||||
if (typeof window !== "undefined") {
|
||||
windowHeight = window.innerHeight;
|
||||
sidebarMaxHeight = windowHeight - 180 + "px";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minWidth: minWidth,
|
||||
maxWidth: minWidth,
|
||||
height: "calc(100vh - 190px)",
|
||||
}}
|
||||
className=" "
|
||||
>
|
||||
<div className=" transition overflow-hidden duration-300 flex flex-col h-full p-2 overflow-y-scroll scroll rounded ">
|
||||
<div className={`${isOpen ? "" : "hidden"} `}>
|
||||
{/* <AgentsView /> */}
|
||||
{<SessionsView />}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
role="button"
|
||||
className=" hover:text-accent duration-150 "
|
||||
>
|
||||
{isOpen ? (
|
||||
<div className="mt-4 ">
|
||||
{" "}
|
||||
<ChevronLeftIcon className="w-6 h-6 inline-block rounded" />{" "}
|
||||
<span className="text-xs "> close sidebar</span>
|
||||
</div>
|
||||
) : (
|
||||
<ChevronRightIcon className="w-6 h-6 inline-block font-bold rounded " />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideBarView;
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Bar, Line } from "@ant-design/plots";
|
||||
import * as React from "react";
|
||||
import { IStatus } from "../../../../types";
|
||||
|
||||
const BarChartViewer = ({ data }: { data: any | null }) => {
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const config = {
|
||||
data: data.bar,
|
||||
xField: "agent",
|
||||
yField: "message",
|
||||
colorField: "tool_call",
|
||||
stack: true,
|
||||
axis: {
|
||||
y: { labelFormatter: "" },
|
||||
x: {
|
||||
labelSpacing: 4,
|
||||
},
|
||||
},
|
||||
style: {
|
||||
radiusTopLeft: 10,
|
||||
radiusTopRight: 10,
|
||||
},
|
||||
height: 60 * data.agents.length,
|
||||
};
|
||||
|
||||
const config_code_exec = Object.assign({}, config);
|
||||
config_code_exec.colorField = "code_execution";
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded relative">
|
||||
<div>
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<div className=" text-gray-700 border-b border-dashed p-4">
|
||||
{" "}
|
||||
Tool Call
|
||||
</div>
|
||||
<Bar {...config} />
|
||||
</div>
|
||||
<div className=" ">
|
||||
<div className=" text-gray-700 border-b border-dashed p-4">
|
||||
{" "}
|
||||
Code Execution Status
|
||||
</div>
|
||||
<Bar {...config_code_exec} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default BarChartViewer;
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Tooltip, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { IStatus, IChatMessage } from "../../../types";
|
||||
import { fetchJSON, getServerUrl } from "../../../utils";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
const BarChartViewer = React.lazy(() => import("./charts/bar"));
|
||||
|
||||
const ProfilerView = ({
|
||||
agentMessage,
|
||||
}: {
|
||||
agentMessage: IChatMessage | null;
|
||||
}) => {
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [profile, setProfile] = React.useState<any | null>(null);
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
|
||||
const fetchProfile = (messageId: number) => {
|
||||
const profilerUrl = `${serverUrl}/profiler/${messageId}?user_id=${user?.email}`;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
console.log(data);
|
||||
if (data && data.status) {
|
||||
setProfile(data.data);
|
||||
setTimeout(() => {
|
||||
// scroll parent to bottom
|
||||
const parent = document.getElementById("chatbox");
|
||||
if (parent) {
|
||||
parent.scrollTop = parent.scrollHeight;
|
||||
}
|
||||
}, 4000);
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(profilerUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user && agentMessage && agentMessage.id) {
|
||||
fetchProfile(agentMessage.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const UsageViewer = ({ usage }: { usage: any }) => {
|
||||
const usageRows = usage.map((usage: any, index: number) => (
|
||||
<div key={index} className=" borpder rounded">
|
||||
{(usage.total_cost != 0 || usage.total_tokens != 0) && (
|
||||
<>
|
||||
<div className="bg-secondary p-2 text-xs rounded-t">
|
||||
{usage.agent}
|
||||
</div>
|
||||
<div className="bg-tertiary p-3 rounded-b inline-flex gap-2 w-full">
|
||||
{usage.total_tokens && usage.total_tokens != 0 && (
|
||||
<div className="flex flex-col text-center w-full">
|
||||
<div className="w-full px-2 text-2xl ">
|
||||
{usage.total_tokens}
|
||||
</div>
|
||||
<div className="w-full text-xs">tokens</div>
|
||||
</div>
|
||||
)}
|
||||
{usage.total_cost && usage.total_cost != 0 && (
|
||||
<div className="flex flex-col text-center w-full">
|
||||
<div className="w-full px-2 text-2xl ">
|
||||
{usage.total_cost?.toFixed(3)}
|
||||
</div>
|
||||
<div className="w-full text-xs">USD</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className="inline-flex gap-3 flex-wrap">{usage && usageRows}</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" relative">
|
||||
<div className="text-sm ">
|
||||
{/* {profile && <RadarMetrics profileData={profile} />} */}
|
||||
{profile && <BarChartViewer data={profile} />}
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mt-4 mb-4 txt">
|
||||
LLM Costs
|
||||
<Tooltip
|
||||
title={
|
||||
"LLM tokens below based on data returned by the model. Support for exact costs may vary."
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="ml-1 text-gray-400 inline-block w-4 h-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{profile && profile.usage && <UsageViewer usage={profile.usage} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ProfilerView;
|
||||
@@ -1,122 +0,0 @@
|
||||
import { Select, message } from "antd";
|
||||
import * as React from "react";
|
||||
import { LoadingOverlay } from "../../../atoms";
|
||||
import { IWorkflow, IStatus } from "../../../types";
|
||||
import { fetchJSON, getServerUrl } from "../../../utils";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { Link } from "gatsby";
|
||||
|
||||
const WorkflowSelector = ({
|
||||
workflow,
|
||||
setWorkflow,
|
||||
workflow_id,
|
||||
disabled,
|
||||
}: {
|
||||
workflow: IWorkflow | null;
|
||||
setWorkflow: (workflow: IWorkflow) => void;
|
||||
workflow_id: number | undefined;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const [error, setError] = React.useState<IStatus | null>({
|
||||
status: true,
|
||||
message: "All good",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [workflows, setWorkflows] = React.useState<IWorkflow[]>([]);
|
||||
const [selectedWorkflow, setSelectedWorkflow] = React.useState<number>(0);
|
||||
|
||||
const { user } = React.useContext(appContext);
|
||||
const serverUrl = getServerUrl();
|
||||
const listWorkflowsUrl = `${serverUrl}/workflows?user_id=${user?.email}`;
|
||||
const fetchWorkFlow = () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
const payLoad = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const onSuccess = (data: any) => {
|
||||
if (data && data.status) {
|
||||
// message.success(data.message);
|
||||
setWorkflows(data.data);
|
||||
if (data.data.length > 0) {
|
||||
if (!disabled) {
|
||||
setWorkflow(data.data[0]);
|
||||
} else {
|
||||
const index = data.data.findIndex((item:IWorkflow) => item.id === workflow_id);
|
||||
if (index !== -1) {
|
||||
setSelectedWorkflow(index);
|
||||
setWorkflow(data.data[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(data.message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
setError(err);
|
||||
message.error(err.message);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchJSON(listWorkflowsUrl, payLoad, onSuccess, onError);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
fetchWorkFlow();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" mb-4 relative">
|
||||
<div className="text-sm mt-2 mb-2 pb-1 ">
|
||||
{" "}
|
||||
Please select an agent workflow to begin.{" "}
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2 ">
|
||||
<LoadingOverlay loading={loading} />
|
||||
|
||||
{workflows && workflows.length > 0 && (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
value={workflows[selectedWorkflow].name}
|
||||
onChange={(value: any) => {
|
||||
setSelectedWorkflow(value);
|
||||
setWorkflow(workflows[value]);
|
||||
}}
|
||||
options={
|
||||
workflows.map((config, index) => {
|
||||
return { label: config.name, value: index };
|
||||
}) as any
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-2 text-xs hidden">
|
||||
{" "}
|
||||
<div className="my-2 text-xs"> {workflow?.name}</div>
|
||||
View all workflows{" "}
|
||||
<span className="text-accent">
|
||||
{" "}
|
||||
<Link to="/build">here</Link>
|
||||
</span>{" "}
|
||||
</div>
|
||||
</div>
|
||||
{!workflows ||
|
||||
(workflows && workflows.length === 0 && (
|
||||
<div className="p-1 border rounded text-xs px-2 text-secondary">
|
||||
{" "}
|
||||
No agent workflows found.
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default WorkflowSelector;
|
||||
@@ -0,0 +1,20 @@
|
||||
import Markdown from "react-markdown";
|
||||
import React from "react";
|
||||
|
||||
interface MarkdownViewProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MarkdownView: React.FC<MarkdownViewProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`text-sm w-full prose dark:prose-invert text-primary rounded p-2 ${className}`}
|
||||
>
|
||||
<Markdown>{children}</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
|
||||
export const MonacoEditor = ({
|
||||
value,
|
||||
editorRef,
|
||||
language,
|
||||
onChange,
|
||||
minimap = true,
|
||||
}: {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
editorRef: any;
|
||||
language: string;
|
||||
minimap?: boolean;
|
||||
}) => {
|
||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||
const onEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor;
|
||||
setIsEditorReady(true);
|
||||
};
|
||||
return (
|
||||
<div id="monaco-editor" className="h-full rounded">
|
||||
<Editor
|
||||
height="100%"
|
||||
className="h-full rounded"
|
||||
defaultLanguage={language}
|
||||
defaultValue={value}
|
||||
value={value}
|
||||
onChange={(value: string | undefined) => {
|
||||
if (onChange && value) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
onMount={onEditorDidMount}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
wordWrap: "on",
|
||||
wrappingIndent: "indent",
|
||||
wrappingStrategy: "advanced",
|
||||
minimap: {
|
||||
enabled: minimap,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Session } from "../../../types/datamodel";
|
||||
import { getServerUrl } from "../../../utils";
|
||||
|
||||
export class SessionAPI {
|
||||
private getBaseUrl(): string {
|
||||
return getServerUrl();
|
||||
}
|
||||
|
||||
private getHeaders(): HeadersInit {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
async listSessions(userId: string): Promise<Session[]> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/sessions?user_id=${userId}`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to fetch sessions");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async getSession(sessionId: number, userId: string): Promise<Session> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/sessions/${sessionId}?user_id=${userId}`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to fetch session");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async createSession(
|
||||
sessionData: Partial<Session>,
|
||||
userId: string
|
||||
): Promise<Session> {
|
||||
const session = {
|
||||
...sessionData,
|
||||
user_id: userId, // Ensure user_id is included
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.getBaseUrl()}/sessions`, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(session),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to create session");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async updateSession(
|
||||
sessionId: number,
|
||||
sessionData: Partial<Session>,
|
||||
userId: string
|
||||
): Promise<Session> {
|
||||
const session = {
|
||||
...sessionData,
|
||||
id: sessionId,
|
||||
user_id: userId, // Ensure user_id is included
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/sessions/${sessionId}?user_id=${userId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(session),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to update session");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async deleteSession(sessionId: number, userId: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/sessions/${sessionId}?user_id=${userId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to delete session");
|
||||
}
|
||||
|
||||
// Adding messages endpoint
|
||||
async listSessionMessages(sessionId: number, userId: string): Promise<any[]> {
|
||||
// Replace 'any' with proper message type
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/sessions/${sessionId}/messages?user_id=${userId}`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to fetch messages");
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionAPI = new SessionAPI();
|
||||
@@ -0,0 +1,157 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Modal, Form, Input, message, Button, Select, Spin } from "antd";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import type { FormProps } from "antd";
|
||||
import { SessionEditorProps } from "./types";
|
||||
import { Team } from "../../../types/datamodel";
|
||||
import { teamAPI } from "../team/api";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
|
||||
type FieldType = {
|
||||
name: string;
|
||||
team_id?: string;
|
||||
};
|
||||
|
||||
export const SessionEditor: React.FC<SessionEditorProps> = ({
|
||||
session,
|
||||
onSave,
|
||||
onCancel,
|
||||
isOpen,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { user } = useContext(appContext);
|
||||
|
||||
// Fetch teams when modal opens
|
||||
useEffect(() => {
|
||||
const fetchTeams = async () => {
|
||||
if (isOpen) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const userId = user?.email || "";
|
||||
const teamsData = await teamAPI.listTeams(userId);
|
||||
setTeams(teamsData);
|
||||
} catch (error) {
|
||||
message.error("Failed to load teams");
|
||||
console.error("Error loading teams:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchTeams();
|
||||
}, [isOpen, user?.email]);
|
||||
|
||||
// Set form values when modal opens or session changes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
form.setFieldsValue({
|
||||
name: session?.name || "",
|
||||
team_id: session?.team_id || undefined,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [form, session, isOpen]);
|
||||
|
||||
const onFinish: FormProps<FieldType>["onFinish"] = async (values) => {
|
||||
try {
|
||||
await onSave({
|
||||
...values,
|
||||
id: session?.id,
|
||||
});
|
||||
message.success(
|
||||
`Session ${session ? "updated" : "created"} successfully`
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
|
||||
errorInfo
|
||||
) => {
|
||||
message.error("Please check the form for errors");
|
||||
console.error("Form validation failed:", errorInfo);
|
||||
};
|
||||
|
||||
const hasNoTeams = !loading && teams.length === 0;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={session ? "Edit Session" : "Create Session"}
|
||||
open={isOpen}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
className="text-primary"
|
||||
forceRender
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
name="session-form"
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="Session Name"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter a session name" },
|
||||
{ max: 100, message: "Session name cannot exceed 100 characters" },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Form.Item<FieldType>
|
||||
label="Team"
|
||||
name="team_id"
|
||||
rules={[{ required: true, message: "Please select a team" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select a team"
|
||||
loading={loading}
|
||||
disabled={loading || hasNoTeams}
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "")
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
options={teams.map((team) => ({
|
||||
value: team.id,
|
||||
label: `${team.config.name} (${team.config.team_type})`,
|
||||
}))}
|
||||
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{hasNoTeams && (
|
||||
<div className="flex border p-1 rounded -mt-2 mb-4 items-center gap-1.5 text-sm text-yellow-600">
|
||||
<TriangleAlertIcon className="h-4 w-4" />
|
||||
<span>No teams found. Please create a team first.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item className="flex justify-end mb-0">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit" disabled={hasNoTeams}>
|
||||
{session ? "Update" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionEditor;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { Select, Button, Popconfirm } from "antd";
|
||||
import { Edit, Trash2 } from "lucide-react";
|
||||
import type { SessionListProps } from "./types";
|
||||
import type { SelectProps } from "antd";
|
||||
|
||||
export const SessionList: React.FC<SessionListProps> = ({
|
||||
sessions,
|
||||
currentSession,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onDelete,
|
||||
isLoading,
|
||||
}) => {
|
||||
const options: SelectProps["options"] = [
|
||||
{
|
||||
label: "Sessions",
|
||||
options: sessions.map((session) => ({
|
||||
label: (
|
||||
<div className="flex items-center justify-between w-full pr-2">
|
||||
<span className="flex-1 truncate">{session.name}</span>
|
||||
<div className="flex gap-2 ml-2">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="p-0 min-w-[24px] h-6 text-primary"
|
||||
icon={<Edit className="w-4 h-4 text-primary" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(session);
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="Delete Session"
|
||||
description="Are you sure you want to delete this session?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
if (session.id) onDelete(session.id);
|
||||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="p-0 min-w-[24px] h-6"
|
||||
danger
|
||||
icon={<Trash2 className="w-4 h-4 text-primary" />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
value: session.id,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="w-64"
|
||||
placeholder={isLoading ? "Loading sessions..." : "Select a session"}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
value={currentSession?.id}
|
||||
onChange={(value) => {
|
||||
const session = sessions.find((s) => s.id === value);
|
||||
if (session) onSelect(session);
|
||||
}}
|
||||
options={options}
|
||||
notFoundContent={sessions.length === 0 ? "No sessions found" : undefined}
|
||||
dropdownStyle={{ minWidth: "256px" }}
|
||||
listHeight={256}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, { useCallback, useEffect, useState, useContext } from "react";
|
||||
import { Button, message, Collapse, Badge, CollapseProps } from "antd";
|
||||
import { Plus, ChevronDown } from "lucide-react";
|
||||
import { useConfigStore } from "../../../../hooks/store";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { sessionAPI } from "./api";
|
||||
import { SessionList } from "./list";
|
||||
import { SessionEditor } from "./editor";
|
||||
import type { Session } from "../../../types/datamodel";
|
||||
|
||||
export const SessionManager: React.FC = () => {
|
||||
// UI State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||
const [editingSession, setEditingSession] = useState<Session | undefined>();
|
||||
|
||||
// Global context and store
|
||||
const { user } = useContext(appContext);
|
||||
const { session, setSession, sessions, setSessions } = useConfigStore();
|
||||
|
||||
// Fetch all sessions
|
||||
const fetchSessions = useCallback(async () => {
|
||||
if (!user?.email) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await sessionAPI.listSessions(user.email);
|
||||
setSessions(data);
|
||||
if (!session && data.length > 0) {
|
||||
setSession(data[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching sessions:", error);
|
||||
message.error("Error loading sessions");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.email, setSessions]);
|
||||
|
||||
// Handle session operations
|
||||
const handleSaveSession = async (sessionData: Partial<Session>) => {
|
||||
if (!user?.email) return;
|
||||
|
||||
try {
|
||||
if (sessionData.id) {
|
||||
const updated = await sessionAPI.updateSession(
|
||||
sessionData.id,
|
||||
sessionData,
|
||||
user.email
|
||||
);
|
||||
setSessions(
|
||||
sessions.map((s) => (s.id && s.id === updated.id ? updated : s))
|
||||
);
|
||||
if (session?.id === updated.id) {
|
||||
setSession(updated);
|
||||
}
|
||||
} else {
|
||||
const created = await sessionAPI.createSession(sessionData, user.email);
|
||||
setSession(created);
|
||||
setSessions([...sessions, created]);
|
||||
}
|
||||
setIsEditorOpen(false);
|
||||
setEditingSession(undefined);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSession = async (sessionId: number) => {
|
||||
if (!user?.email) return;
|
||||
|
||||
try {
|
||||
await sessionAPI.deleteSession(sessionId, user.email);
|
||||
setSessions(sessions.filter((s) => s.id !== sessionId));
|
||||
if (sessions.length > 0) {
|
||||
setSession(sessions[0]);
|
||||
}
|
||||
if (session?.id === sessionId) {
|
||||
setSession(null);
|
||||
}
|
||||
message.success("Session deleted");
|
||||
} catch (error) {
|
||||
console.error("Error deleting session:", error);
|
||||
message.error("Error deleting session");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectSession = async (selectedSession: Session) => {
|
||||
if (!user?.email || !selectedSession.id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await sessionAPI.getSession(selectedSession.id, user.email);
|
||||
setSession(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading session:", error);
|
||||
message.error("Error loading session");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load sessions on mount
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
}, [fetchSessions]);
|
||||
|
||||
// Custom header with session count
|
||||
const CollapsibleHeader = () => (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">Sessions</span>
|
||||
<Badge
|
||||
count={sessions.length}
|
||||
showZero
|
||||
className="site-badge-count-4"
|
||||
style={{ backgroundColor: "#52525b" }}
|
||||
/>
|
||||
</div>
|
||||
{session && (
|
||||
<span className="text-sm text-gray-500">Current: {session.name}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Session management content
|
||||
const SessionContent = () => (
|
||||
<div className="flex gap-2 items-center">
|
||||
{sessions && sessions.length > 0 && (
|
||||
<SessionList
|
||||
sessions={sessions}
|
||||
currentSession={session}
|
||||
onSelect={handleSelectSession}
|
||||
onEdit={(session) => {
|
||||
setEditingSession(session);
|
||||
setIsEditorOpen(true);
|
||||
}}
|
||||
onDelete={handleDeleteSession}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setEditingSession(undefined);
|
||||
setIsEditorOpen(true);
|
||||
}}
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
>
|
||||
New Session{" "}
|
||||
{sessions.length === 0 && (
|
||||
<span className="relative flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-secondary"></span>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const items: CollapseProps["items"] = [
|
||||
{
|
||||
key: "1",
|
||||
label: <CollapsibleHeader />,
|
||||
children: <SessionContent />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-secondary rounded p-2">
|
||||
<div className="text-xs pb-2">
|
||||
Sessions <span className="px-1 text-accent">{sessions.length} </span>
|
||||
</div>
|
||||
<SessionContent />
|
||||
</div>
|
||||
<SessionEditor
|
||||
session={editingSession}
|
||||
isOpen={isEditorOpen}
|
||||
onSave={handleSaveSession}
|
||||
onCancel={() => {
|
||||
setIsEditorOpen(false);
|
||||
setEditingSession(undefined);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Session } from "../../../types/datamodel";
|
||||
|
||||
export interface SessionEditorProps {
|
||||
session?: Session;
|
||||
onSave: (session: Partial<Session>) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export interface SessionListProps {
|
||||
sessions: Session[];
|
||||
currentSession?: Session | null;
|
||||
onSelect: (session: Session) => void;
|
||||
onEdit: (session: Session) => void;
|
||||
onDelete: (sessionId: number) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionFormState {
|
||||
name: string;
|
||||
team_id: string;
|
||||
isSubmitting: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Team, AgentConfig } from "../../../types/datamodel";
|
||||
import { getServerUrl } from "../../../utils";
|
||||
|
||||
export class TeamAPI {
|
||||
private getBaseUrl(): string {
|
||||
return getServerUrl();
|
||||
}
|
||||
|
||||
private getHeaders(): HeadersInit {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
async listTeams(userId: string): Promise<Team[]> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams?user_id=${userId}`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status) throw new Error(data.message || "Failed to fetch teams");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async getTeam(teamId: number, userId: string): Promise<Team> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}?user_id=${userId}`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status) throw new Error(data.message || "Failed to fetch team");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async createTeam(teamData: Partial<Team>, userId: string): Promise<Team> {
|
||||
const team = {
|
||||
...teamData,
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.getBaseUrl()}/teams`, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(team),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!data.status) throw new Error(data.message || "Failed to create team");
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async deleteTeam(teamId: number, userId: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}?user_id=${userId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status) throw new Error(data.message || "Failed to delete team");
|
||||
}
|
||||
|
||||
// Team-Agent Link Management
|
||||
async linkAgent(teamId: number, agentId: number): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to link agent to team");
|
||||
}
|
||||
|
||||
async linkAgentWithSequence(
|
||||
teamId: number,
|
||||
agentId: number,
|
||||
sequenceId: number
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}/${sequenceId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(
|
||||
data.message || "Failed to link agent to team with sequence"
|
||||
);
|
||||
}
|
||||
|
||||
async unlinkAgent(teamId: number, agentId: number): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to unlink agent from team");
|
||||
}
|
||||
|
||||
async getTeamAgents(teamId: number): Promise<AgentConfig[]> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}/agents`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to fetch team agents");
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const teamAPI = new TeamAPI();
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Modal, Form, message, Button } from "antd";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import { TeamEditorProps } from "./types";
|
||||
import type { FormProps } from "antd";
|
||||
import type { Team, TeamConfig } from "../../../types/datamodel";
|
||||
import { MonacoEditor } from "../monaco";
|
||||
|
||||
const defaultTeamConfig: TeamConfig = {
|
||||
name: "",
|
||||
participants: [],
|
||||
team_type: "RoundRobinGroupChat",
|
||||
};
|
||||
|
||||
type FieldType = {
|
||||
config: string;
|
||||
};
|
||||
|
||||
export const TeamEditor: React.FC<TeamEditorProps> = ({
|
||||
team,
|
||||
onSave,
|
||||
onCancel,
|
||||
isOpen,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const editorRef = useRef(null);
|
||||
const [editorValue, setEditorValue] = useState<string>("");
|
||||
|
||||
// Set form values when modal opens or team changes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const configStr = team
|
||||
? JSON.stringify(team.config, null, 2)
|
||||
: JSON.stringify(defaultTeamConfig, null, 2);
|
||||
setEditorValue(configStr);
|
||||
form.setFieldsValue({
|
||||
config: configStr,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
setJsonError(null);
|
||||
}
|
||||
}, [form, team, isOpen]);
|
||||
|
||||
const validateJSON = (jsonString: string): TeamConfig | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
// Basic validation of required fields
|
||||
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
|
||||
throw new Error("Team name is required");
|
||||
}
|
||||
if (!Array.isArray(parsed.participants)) {
|
||||
throw new Error("Participants must be an array");
|
||||
}
|
||||
if (
|
||||
!["RoundRobinGroupChat", "SelectorGroupChat"].includes(parsed.team_type)
|
||||
) {
|
||||
throw new Error("Invalid team_type");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setJsonError(error.message);
|
||||
} else {
|
||||
setJsonError("Invalid JSON format");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onFinish: FormProps<FieldType>["onFinish"] = async () => {
|
||||
const config = validateJSON(editorValue);
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// When updating, keep the existing id and dates
|
||||
const teamData: Partial<Team> = team
|
||||
? {
|
||||
...team,
|
||||
config,
|
||||
// Remove date fields to let the backend handle them
|
||||
created_at: undefined,
|
||||
updated_at: undefined,
|
||||
}
|
||||
: { config };
|
||||
|
||||
await onSave(teamData);
|
||||
message.success(`Team ${team ? "updated" : "created"} successfully`);
|
||||
setJsonError(null);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorChange = (value: string) => {
|
||||
setEditorValue(value);
|
||||
form.setFieldsValue({ config: value });
|
||||
|
||||
// Clear error if JSON becomes valid
|
||||
try {
|
||||
JSON.parse(value);
|
||||
setJsonError(null);
|
||||
} catch (e) {
|
||||
// Don't set error while typing - Monaco will show syntax errors
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={team ? "Edit Team" : "Create Team"}
|
||||
open={isOpen}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
className="text-primary"
|
||||
width={800}
|
||||
forceRender
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
name="team-form"
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="mb-2 text-xs text-gray-500">
|
||||
Required fields: name (string), team_type ("RoundRobinGroupChat" |
|
||||
"SelectorGroupChat"), participants (array)
|
||||
</div>
|
||||
|
||||
<div className="h-[500px] mb-4">
|
||||
<MonacoEditor
|
||||
value={editorValue}
|
||||
onChange={handleEditorChange}
|
||||
editorRef={editorRef}
|
||||
language="json"
|
||||
minimap={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{jsonError && (
|
||||
<div className="flex items-center gap-1.5 text-sm text-red-500 mb-4">
|
||||
<TriangleAlertIcon className="h-4 w-4" />
|
||||
<span>{jsonError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item className="flex justify-end mb-0">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => form.submit()}
|
||||
disabled={!!jsonError}
|
||||
>
|
||||
{team ? "Update" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamEditor;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { Select, Button, Popconfirm } from "antd";
|
||||
import { Edit, Trash2 } from "lucide-react";
|
||||
import type { TeamListProps } from "./types";
|
||||
import type { SelectProps } from "antd";
|
||||
|
||||
export const TeamList: React.FC<TeamListProps> = ({
|
||||
teams,
|
||||
currentTeam,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onDelete,
|
||||
isLoading,
|
||||
}) => {
|
||||
const options: SelectProps["options"] = [
|
||||
{
|
||||
label: "Teams",
|
||||
options: teams.map((team) => ({
|
||||
label: (
|
||||
<div className="flex items-center justify-between w-full pr-2">
|
||||
<span className="flex-1 truncate">{team.config.name}</span>
|
||||
<div className="flex gap-2 ml-2">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="p-0 min-w-[24px] h-6 text-primary"
|
||||
icon={<Edit className="w-4 h-4 text-primary" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(team);
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="Delete Team"
|
||||
description="Are you sure you want to delete this team?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
if (team.id) onDelete(team.id);
|
||||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="p-0 min-w-[24px] h-6"
|
||||
danger
|
||||
icon={<Trash2 className="w-4 h-4 text-primary" />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
value: team.id,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="w-64"
|
||||
placeholder={isLoading ? "Loading teams..." : "Select a team"}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
value={currentTeam?.id}
|
||||
onChange={(value) => {
|
||||
const team = teams.find((t) => t.id === value);
|
||||
if (team) onSelect(team);
|
||||
}}
|
||||
options={options}
|
||||
notFoundContent={teams.length === 0 ? "No teams found" : undefined}
|
||||
dropdownStyle={{ minWidth: "256px" }}
|
||||
listHeight={256}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useCallback, useEffect, useState, useContext } from "react";
|
||||
import { Button, message, Badge } from "antd";
|
||||
import { Plus } from "lucide-react";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { teamAPI } from "./api";
|
||||
import { TeamList } from "./list";
|
||||
import { TeamEditor } from "./editor";
|
||||
import type { Team } from "../../../types/datamodel";
|
||||
|
||||
export const TeamManager: React.FC = () => {
|
||||
// UI State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||
const [editingTeam, setEditingTeam] = useState<Team | undefined>();
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [currentTeam, setCurrentTeam] = useState<Team | null>(null);
|
||||
|
||||
// Global context
|
||||
const { user } = useContext(appContext);
|
||||
|
||||
// Fetch all teams
|
||||
const fetchTeams = useCallback(async () => {
|
||||
if (!user?.email) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await teamAPI.listTeams(user.email);
|
||||
setTeams(data);
|
||||
if (!currentTeam && data.length > 0) {
|
||||
setCurrentTeam(data[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching teams:", error);
|
||||
message.error("Error loading teams");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.email, currentTeam]);
|
||||
|
||||
// Handle team operations
|
||||
const handleSaveTeam = async (teamData: Partial<Team>) => {
|
||||
if (!user?.email) return;
|
||||
|
||||
try {
|
||||
console.log("teamData", teamData);
|
||||
const savedTeam = await teamAPI.createTeam(teamData, user.email);
|
||||
|
||||
// Update teams list
|
||||
if (teamData.id) {
|
||||
setTeams(teams.map((t) => (t.id === savedTeam.id ? savedTeam : t)));
|
||||
if (currentTeam?.id === savedTeam.id) {
|
||||
setCurrentTeam(savedTeam);
|
||||
}
|
||||
} else {
|
||||
setTeams([...teams, savedTeam]);
|
||||
}
|
||||
|
||||
setIsEditorOpen(false);
|
||||
setEditingTeam(undefined);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTeam = async (teamId: number) => {
|
||||
if (!user?.email) return;
|
||||
|
||||
try {
|
||||
await teamAPI.deleteTeam(teamId, user.email);
|
||||
setTeams(teams.filter((t) => t.id !== teamId));
|
||||
if (currentTeam?.id === teamId) {
|
||||
setCurrentTeam(null);
|
||||
}
|
||||
message.success("Team deleted");
|
||||
} catch (error) {
|
||||
console.error("Error deleting team:", error);
|
||||
message.error("Error deleting team");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTeam = async (selectedTeam: Team) => {
|
||||
if (!user?.email || !selectedTeam.id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await teamAPI.getTeam(selectedTeam.id, user.email);
|
||||
setCurrentTeam(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading team:", error);
|
||||
message.error("Error loading team");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load teams on mount
|
||||
useEffect(() => {
|
||||
fetchTeams();
|
||||
}, [fetchTeams]);
|
||||
|
||||
// Content component
|
||||
const TeamContent = () => (
|
||||
<div className="flex gap-2 items-center">
|
||||
{teams && teams.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<TeamList
|
||||
teams={teams}
|
||||
currentTeam={currentTeam}
|
||||
onSelect={handleSelectTeam}
|
||||
onEdit={(team) => {
|
||||
setEditingTeam(team);
|
||||
setIsEditorOpen(true);
|
||||
}}
|
||||
onDelete={handleDeleteTeam}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setEditingTeam(undefined);
|
||||
setIsEditorOpen(true);
|
||||
}}
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
>
|
||||
New Team
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-secondary rounded p-2">
|
||||
<div className="text-xs pb-2">
|
||||
Teams <span className="px-1 text-accent">{teams.length} </span>
|
||||
</div>
|
||||
<TeamContent />
|
||||
</div>
|
||||
<TeamEditor
|
||||
team={editingTeam}
|
||||
isOpen={isEditorOpen}
|
||||
onSave={handleSaveTeam}
|
||||
onCancel={() => {
|
||||
setIsEditorOpen(false);
|
||||
setEditingTeam(undefined);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamManager;
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Team } from "../../../types/datamodel";
|
||||
|
||||
export interface TeamEditorProps {
|
||||
team?: Team;
|
||||
onSave: (team: Partial<Team>) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export interface TeamListProps {
|
||||
teams: Team[];
|
||||
currentTeam?: Team | null;
|
||||
onSelect: (team: Team) => void;
|
||||
onEdit: (team: Team) => void;
|
||||
onDelete: (teamId: number) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
@@ -1,25 +1,33 @@
|
||||
import { create } from "zustand";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Message, Session } from "../components/types/datamodel";
|
||||
|
||||
import { IChatMessage, IChatSession } from "../components/types";
|
||||
|
||||
interface ConfigState {
|
||||
messages: IChatMessage[] | null;
|
||||
setMessages: (messages: IChatMessage[]) => void;
|
||||
session: IChatSession | null;
|
||||
setSession: (session: IChatSession | null) => void;
|
||||
sessions: IChatSession[];
|
||||
setSessions: (sessions: IChatSession[]) => void;
|
||||
version: string | null;
|
||||
setVersion: (version: string) => void;
|
||||
connectionId: string;
|
||||
setConnectionId: (connectionId: string) => void;
|
||||
areSessionButtonsDisabled: boolean;
|
||||
setAreSessionButtonsDisabled: (disabled: boolean) => void;
|
||||
interface ISidebarState {
|
||||
isExpanded: boolean;
|
||||
isPinned: boolean;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigState>()((set) => ({
|
||||
messages: null,
|
||||
export interface IConfigState {
|
||||
messages: Message[];
|
||||
setMessages: (messages: Message[]) => void;
|
||||
session: Session | null;
|
||||
setSession: (session: Session | null) => void;
|
||||
sessions: Session[];
|
||||
setSessions: (sessions: Session[]) => void;
|
||||
version: string | null;
|
||||
setVersion: (version: string | null) => void;
|
||||
|
||||
// Sidebar state
|
||||
sidebar: ISidebarState;
|
||||
setSidebarState: (state: Partial<ISidebarState>) => void;
|
||||
collapseSidebar: () => void;
|
||||
expandSidebar: () => void;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<IConfigState>((set) => ({
|
||||
// Existing state
|
||||
messages: [],
|
||||
setMessages: (messages) => set({ messages }),
|
||||
session: null,
|
||||
setSession: (session) => set({ session }),
|
||||
@@ -28,7 +36,30 @@ export const useConfigStore = create<ConfigState>()((set) => ({
|
||||
version: null,
|
||||
setVersion: (version) => set({ version }),
|
||||
connectionId: uuidv4(),
|
||||
setConnectionId: (connectionId) => set({ connectionId }),
|
||||
areSessionButtonsDisabled: false,
|
||||
setAreSessionButtonsDisabled: (disabled) => set({ areSessionButtonsDisabled: disabled }),
|
||||
|
||||
// Sidebar state and actions
|
||||
sidebar: {
|
||||
isExpanded: true,
|
||||
isPinned: false,
|
||||
},
|
||||
|
||||
setSidebarState: (newState) =>
|
||||
set((state) => ({
|
||||
sidebar: { ...state.sidebar, ...newState },
|
||||
})),
|
||||
|
||||
collapseSidebar: () =>
|
||||
set((state) => ({
|
||||
sidebar: { ...state.sidebar, isExpanded: false },
|
||||
})),
|
||||
|
||||
expandSidebar: () =>
|
||||
set((state) => ({
|
||||
sidebar: { ...state.sidebar, isExpanded: true },
|
||||
})),
|
||||
|
||||
toggleSidebar: () =>
|
||||
set((state) => ({
|
||||
sidebar: { ...state.sidebar, isExpanded: !state.sidebar.isExpanded },
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4f7307728cfd62bc0d07d8cc3ab3809e36d8859a5ad314d020b21648642cf574
|
||||
size 12710
|
||||
oid sha256:7ec19e710d8b38d99b5f1b170db18eb192cd3a50fe80f1c103f88cce68e057b4
|
||||
size 33034
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/><circle cx="190.15351" cy="24.95465" r="20" fill="#6c63ff"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/><circle cx="433.63626" cy="105.17383" r="20" fill="#6c63ff"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
3
python/packages/autogen-studio/frontend/src/index.d.ts
vendored
Normal file
3
python/packages/autogen-studio/frontend/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module "*.jpg";
|
||||
declare module "*.png";
|
||||
declare module "*.svg";
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from "react"
|
||||
import { Link } from "gatsby"
|
||||
import { Link, HeadFC, PageProps } from "gatsby"
|
||||
|
||||
// styles
|
||||
const pageStyles = {
|
||||
color: "#232129",
|
||||
padding: "96px",
|
||||
@@ -24,18 +23,12 @@ const codeStyles = {
|
||||
borderRadius: 4,
|
||||
}
|
||||
|
||||
// markup
|
||||
const NotFoundPage = () => {
|
||||
const NotFoundPage: React.FC<PageProps> = () => {
|
||||
return (
|
||||
<main style={pageStyles}>
|
||||
<title>Not found</title>
|
||||
<h1 style={headingStyles}>Page not found</h1>
|
||||
<p style={paragraphStyles}>
|
||||
Sorry{" "}
|
||||
<span role="img" aria-label="Pensive emoji">
|
||||
😔
|
||||
</span>{" "}
|
||||
we couldn’t find what you were looking for.
|
||||
Sorry 😔, we couldn’t find what you were looking for.
|
||||
<br />
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<>
|
||||
@@ -52,3 +45,5 @@ const NotFoundPage = () => {
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
||||
|
||||
export const Head: HeadFC = () => <title>Not found</title>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import * as React from "react";
|
||||
import Layout from "../components/layout";
|
||||
import { graphql } from "gatsby";
|
||||
import BuildView from "../components/views/builder/build";
|
||||
|
||||
// markup
|
||||
const IndexPage = ({ data }: any) => {
|
||||
return (
|
||||
<Layout meta={data.site.siteMetadata} title="Home" link={"/build"}>
|
||||
<main style={{ height: "100%" }} className=" h-full ">
|
||||
<BuildView />
|
||||
Build
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { graphql } from "gatsby";
|
||||
import Layout from "../../components/layout";
|
||||
import GalleryView from "../../components/views/gallery/gallery";
|
||||
|
||||
// markup
|
||||
const GalleryPage = ({ location, data }: any) => {
|
||||
return (
|
||||
<Layout meta={data.site.siteMetadata} title="Gallery" link={"/gallery"}>
|
||||
<main style={{ height: "100%" }} className=" h-full ">
|
||||
<GalleryView location={location} />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const query = graphql`
|
||||
query HomePageQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
description
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default GalleryPage;
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as React from "react";
|
||||
import Layout from "../components/layout";
|
||||
import { graphql } from "gatsby";
|
||||
import RAView from "../components/views/playground/ra";
|
||||
import ChatView from "../components/views/playground/chat/chat";
|
||||
|
||||
// markup
|
||||
const IndexPage = ({ data }: any) => {
|
||||
return (
|
||||
<Layout meta={data.site.siteMetadata} title="Home" link={"/"}>
|
||||
<main style={{ height: "100%" }} className=" h-full ">
|
||||
<RAView />
|
||||
<ChatView initMessages={[]} />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
import Layout from "../components/layout";
|
||||
import { graphql } from "gatsby";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
|
||||
// markup
|
||||
const SettingsPage = ({ data }: any) => {
|
||||
return (
|
||||
<Layout meta={data.site.siteMetadata} title="Home" link={"/build"}>
|
||||
<main style={{ height: "100%" }} className=" h-full ">
|
||||
<div className="mb-2"> Settings</div>
|
||||
<div className="p-2 mt-4 bg-tertiary rounded ">
|
||||
<TriangleAlertIcon
|
||||
className="w-5 h-5 text-primary inline-block -mt-1"
|
||||
strokeWidth={1.5}
|
||||
/>{" "}
|
||||
Work in progress ..
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const query = graphql`
|
||||
query HomePageQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
description
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -3,27 +3,27 @@
|
||||
--color-bg-secondary: #1e293b;
|
||||
--color-bg-light: #27354c;
|
||||
--color-bg-tertiary: #374151;
|
||||
--color-bg-accent: #22c55e;
|
||||
--color-bg-accent: #464feb;
|
||||
--color-text-primary: #f7fafc;
|
||||
--color-text-secondary: #e2e8f0;
|
||||
--color-text-accent: #22c55e;
|
||||
--color-text-accent: #464feb;
|
||||
--color-border-primary: #f7fafc;
|
||||
--color-border-secondary: #e2e8f045;
|
||||
--color-border-accent: #22c55e;
|
||||
--color-border-accent: #464feb;
|
||||
}
|
||||
|
||||
.light {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #edf2f7;
|
||||
--color-bg-light: #f9fafb;
|
||||
--color-bg-tertiary: #ffffff;
|
||||
--color-bg-accent: #16a34a;
|
||||
--color-bg-tertiary: #f3f6f9;
|
||||
--color-bg-accent: #464feb;
|
||||
--color-text-primary: #334155;
|
||||
--color-text-secondary: #64748b;
|
||||
--color-text-accent: #16a34a;
|
||||
--color-text-accent: #464feb;
|
||||
--color-border-primary: #2d3748c1;
|
||||
--color-border-secondary: #edf2f7;
|
||||
--color-border-accent: #16a34a;
|
||||
--color-border-accent: #464feb;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@@ -31,28 +31,28 @@
|
||||
@tailwind utilities;
|
||||
|
||||
/* @layer base {
|
||||
body {
|
||||
@apply dark:bg-slate-800 dark:text-white;
|
||||
}
|
||||
} */
|
||||
body {
|
||||
@apply dark:bg-slate-800 dark:text-white;
|
||||
}
|
||||
} */
|
||||
|
||||
html {
|
||||
display: table;
|
||||
/* display: table; */
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
/* padding: 0px 64px 0px 64px; */
|
||||
@apply px-4 md:px-8 lg:px-16 !important;
|
||||
/* margin: 0px auto; */
|
||||
padding: 0px 64px 0px 64px;
|
||||
@apply px-4 md:px-8 lg:px-16 w-full !important;
|
||||
margin: 0px auto;
|
||||
min-width: 300px;
|
||||
max-width: 1400px;
|
||||
max-width: 1900px;
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
/* border: 2px solid green; */
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
/* border: 2px solid green; */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* @import "antd/dist/antd.css"; */
|
||||
|
||||
@@ -123,9 +123,9 @@ a:hover {
|
||||
@apply text-accent;
|
||||
}
|
||||
/* .iiz__img,
|
||||
iiz__zoom-img {
|
||||
@apply w-full;
|
||||
} */
|
||||
iiz__zoom-img {
|
||||
@apply w-full;
|
||||
} */
|
||||
|
||||
.ant-radio-checked .ant-radio-inner {
|
||||
@apply border-accent !important;
|
||||
@@ -259,6 +259,11 @@ iiz__zoom-img {
|
||||
}
|
||||
.ant-upload-drag-container,
|
||||
.ant-upload-text,
|
||||
.ant-btn,
|
||||
.ant-select,
|
||||
.ant-select > span,
|
||||
.ant-select-selector,
|
||||
.ant-form-item-required,
|
||||
.ant-upload-hint {
|
||||
@apply text-primary !important;
|
||||
}
|
||||
@@ -267,7 +272,9 @@ iiz__zoom-img {
|
||||
.ant-upload-list-item-info:hover {
|
||||
@apply bg-secondary text-accent !important;
|
||||
}
|
||||
.ant-pagination .ant-pagination-item {
|
||||
.ant-pagination .ant-pagination-item,
|
||||
.ant-select-selector,
|
||||
.ant-btn-variant-outlined {
|
||||
@apply bg-primary !important;
|
||||
}
|
||||
.ant-pagination .ant-pagination-item-active a {
|
||||
@@ -290,9 +297,11 @@ iiz__zoom-img {
|
||||
@apply border-secondary !important;
|
||||
}
|
||||
.ant-btn,
|
||||
.ant-select-arrow,
|
||||
.ant-btn:hover {
|
||||
@apply text-primary !important;
|
||||
}
|
||||
|
||||
:where(.ant-btn).ant-btn-compact-item.ant-btn-primary:not([disabled])
|
||||
+ .ant-btn-compact-item.ant-btn-primary:not([disabled]):before {
|
||||
@apply bg-secondary !important;
|
||||
@@ -341,8 +350,8 @@ iiz__zoom-img {
|
||||
}
|
||||
|
||||
/* .ant-radio-input::before {
|
||||
@apply bg-primary !important;
|
||||
} */
|
||||
@apply bg-primary !important;
|
||||
} */
|
||||
|
||||
.prose > pre {
|
||||
padding: 0px !important;
|
||||
@@ -356,16 +365,16 @@ iiz__zoom-img {
|
||||
}
|
||||
|
||||
/* div.chatbox > ul {
|
||||
list-style: disc !important;
|
||||
}
|
||||
div.chatbox > ul,
|
||||
div.chatbox > ol {
|
||||
padding-left: 20px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
div.chatbox > ol {
|
||||
list-style: decimal !important;
|
||||
} */
|
||||
list-style: disc !important;
|
||||
}
|
||||
div.chatbox > ul,
|
||||
div.chatbox > ol {
|
||||
padding-left: 20px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
div.chatbox > ol {
|
||||
list-style: decimal !important;
|
||||
} */
|
||||
|
||||
div#___gatsby,
|
||||
div#gatsby-focus-wrapper {
|
||||
|
||||
Reference in New Issue
Block a user