719 lines
20 KiB
React
719 lines
20 KiB
React
import React, { useState, useEffect, useContext, memo } from "react";
|
|
import { Context } from "../context/ContextApi.jsx";
|
|
import { useNavigate, Link, useLocation } from "react-router-dom";
|
|
import { getTheme } from "../theme.jsx";
|
|
import { toast } from "react-toastify"
|
|
import ReactJson from "react-json-view-ssr";
|
|
import { v4 as uuidv4} from "uuid";
|
|
import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows.jsx";
|
|
|
|
import {
|
|
Box,
|
|
Button,
|
|
ButtonGroup,
|
|
Typography,
|
|
Chip,
|
|
CircularProgress,
|
|
Tooltip,
|
|
IconButton,
|
|
TextField,
|
|
} from '@mui/material'
|
|
|
|
import {
|
|
CheckCircle as CheckCircleIcon,
|
|
HourglassDisabled as HourglassDisabledIcon,
|
|
RestartAlt as RestartAltIcon,
|
|
ExpandMore as ExpandMoreIcon,
|
|
ExpandLess as ExpandLessIcon,
|
|
Send as SendIcon,
|
|
Error as ErrorIcon,
|
|
} from '@mui/icons-material'
|
|
|
|
import {
|
|
green,
|
|
red,
|
|
} from '../views/AngularWorkflow.jsx'
|
|
|
|
const AgentUI = (props) => {
|
|
const { globalUrl, isLoggedIn, isLoaded, userdata, } = props
|
|
const [buttonState, setButtonState] = useState("timeline")
|
|
const [execution, setExecution] = useState(null)
|
|
const [agentActionResult, setAgentActionResult] = useState(null)
|
|
const [agentRequestLoading, setAgentRequestLoading] = useState(false)
|
|
const [data, setData] = useState({})
|
|
const [openIndexes, setOpenIndexes] = useState([])
|
|
const [disableButtons, setDisableButtons] = useState(false)
|
|
|
|
const [originalStartTime, setOriginalStartTime] = useState(0)
|
|
const [latestEndTime, setLatestEndTime] = useState(0)
|
|
const [showAgentStarter, setShowAgentStarter] = useState(false)
|
|
const [actionInput, setActionInput] = useState("")
|
|
|
|
const {themeMode} = useContext(Context)
|
|
const theme = getTheme(themeMode)
|
|
const navigate = useNavigate();
|
|
|
|
const agentWrapperStyle = {
|
|
width: 1000,
|
|
height: 1000,
|
|
margin: "auto",
|
|
paddingTop: 100,
|
|
}
|
|
|
|
if (data.input === undefined || data.input === null) {
|
|
data.input = ""
|
|
} else {
|
|
const verifiedInput = validateJson(data.input)
|
|
if (verifiedInput.valid === true) {
|
|
data.input = JSON.stringify(verifiedInput.result, null, 2)
|
|
}
|
|
}
|
|
|
|
const findNodeData = (execution_data, node_id) => {
|
|
if (execution_data === undefined || execution_data === null) {
|
|
return
|
|
}
|
|
|
|
if (node_id === undefined || node_id === null || node_id === "") {
|
|
return
|
|
}
|
|
|
|
var found = false
|
|
for (var key in execution_data.results) {
|
|
const item = execution_data.results[key]
|
|
if (item?.action?.id !== node_id) {
|
|
continue
|
|
}
|
|
|
|
setAgentActionResult(item)
|
|
|
|
found = true
|
|
const validate = validateJson(item.result)
|
|
if (validate.valid) {
|
|
setData(validate.result)
|
|
} else {
|
|
toast.warn("Action output result is not valid JSON!")
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if (found === false) {
|
|
toast.warn("Failed to find the relevant AI Agent result")
|
|
|
|
if (execution_data?.results?.length === 1) {
|
|
setAgentActionResult(execution_data.results[0])
|
|
const validatedData = validateJson(execution_data.results[0].result)
|
|
if (validatedData.valid) {
|
|
setData(validatedData.result)
|
|
} else {
|
|
toast.warn("Action output result is not valid JSON!")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const GetExecution = (execution_id, node_id, authorization) => {
|
|
if (execution_id === undefined || execution_id === null) {
|
|
toast.error("No execution ID provided. Please provide execution_id in the URL.")
|
|
return
|
|
}
|
|
|
|
//if (node_id === undefined || node_id === null || node_id === "") {
|
|
// toast.error("No node ID provided. Please provide node_id in the URL.")
|
|
// return
|
|
//}
|
|
|
|
if (authorization === undefined || authorization === null) {
|
|
toast.error("No authorization provided. Please provide authorization in the URL.")
|
|
return
|
|
}
|
|
|
|
const headers = {}
|
|
const executionRequest = {
|
|
"execution_id": execution_id,
|
|
"authorization": authorization,
|
|
}
|
|
|
|
const url = `${globalUrl}/api/v1/streams/results`
|
|
fetch(url, {
|
|
method: "POST",
|
|
headers: headers,
|
|
body: JSON.stringify(executionRequest),
|
|
credentials: "include",
|
|
cors: "no-cors",
|
|
})
|
|
.then((response) => {
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson.success !== false) {
|
|
if (responseJson.status === "EXECUTING") {
|
|
// Recursively looking for updates until it's not executing anymore
|
|
setTimeout(() => {
|
|
GetExecution(execution_id, node_id, authorization)
|
|
}, 3000)
|
|
} else {
|
|
setDisableButtons(false)
|
|
setDisableButtons(false)
|
|
}
|
|
|
|
setExecution(responseJson)
|
|
findNodeData(responseJson, node_id)
|
|
} else {
|
|
setDisableButtons(false)
|
|
if (responseJson.reason === undefined || responseJson.reason === null) {
|
|
toast.error("Failed to load the agent data. Please try again and contact support@shuffler.io if this persists")
|
|
} else {
|
|
toast.error("Error: " + responseJson.reason)
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
setDisableButtons(false)
|
|
toast.error("Error: " + error)
|
|
})
|
|
}
|
|
|
|
const RerunDecision = (decision) => {
|
|
if (execution.execution_id === undefined || execution.execution_id === null) {
|
|
toast.error("No workflow run loaded. Please try again and contact support@shuffler.io if this persists.")
|
|
return
|
|
}
|
|
|
|
if (agentActionResult === undefined || agentActionResult === null) {
|
|
toast.error("Failed to find the relevant agent action. Please try again, and contact support@shuffler.io if it persists.")
|
|
return
|
|
}
|
|
|
|
console.log("DECISION: ", decision)
|
|
|
|
const url = `${globalUrl}/api/v1/apps/agent/run?rerun=true&decision_id=${decision?.run_details?.id}`
|
|
var body = agentActionResult.action
|
|
console.log("BODY: ", body)
|
|
body.source_execution = execution.execution_id
|
|
body.source_workflow = execution.workflow.id
|
|
|
|
fetch(url, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
credentials: "include",
|
|
cors: "no-cors",
|
|
})
|
|
.then((response) => {
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
console.log("RESP: ", responseJson)
|
|
if (responseJson.success !== false) {
|
|
} else {
|
|
if (responseJson.reason === undefined || responseJson.reason === null) {
|
|
toast.warn("Failed to restart the agent decision. Please try again and contact support@shuffler.io if this persists")
|
|
} else {
|
|
toast.warn(responseJson.reason)
|
|
|
|
}
|
|
}
|
|
|
|
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
|
})
|
|
.catch((error) => {
|
|
toast.error("Error: " + error)
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search)
|
|
const executionId = params.get("execution_id")
|
|
const nodeId = params.get("node_id")
|
|
const authorization = params.get("authorization")
|
|
if (executionId !== undefined && executionId !== null && authorization !== undefined && authorization !== null) {
|
|
GetExecution(executionId, nodeId, authorization)
|
|
} else {
|
|
setShowAgentStarter(true)
|
|
//toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
|
|
}
|
|
}, [])
|
|
|
|
const maxTimelineWidth = 150
|
|
const TimelineItem = (props) => {
|
|
const { item, index } = props;
|
|
const [hovered, setHovered] = useState(false);
|
|
|
|
const parsedStatus = item.status === "RUNNING" || item.status === "WAITING" ?
|
|
<CircularProgress size={20} style={{marginRight: 10, }} />
|
|
:
|
|
item.status === "FINISHED" ?
|
|
<Tooltip title="Finished" placement="top">
|
|
<CheckCircleIcon style={{color: green, marginRight: 10, }} />
|
|
</Tooltip>
|
|
:
|
|
item.status === "ABORTED" || item.status === "FAILURE" ?
|
|
<Tooltip title={`${item.status}: Check the raw data`} placement="top">
|
|
<ErrorIcon style={{color: red, marginRight: 10, }} />
|
|
</Tooltip>
|
|
:
|
|
<Tooltip title={`Not started yet: ${item.status}`} placement="top">
|
|
<HourglassDisabledIcon style={{marginRight: 10, }} />
|
|
</Tooltip>
|
|
|
|
const categoryStyle = {
|
|
width: 20,
|
|
height: 20,
|
|
marginRight: 10,
|
|
}
|
|
|
|
const parsedCategory = item.category === "singul" ?
|
|
<Tooltip title="Singul" placement="top">
|
|
<img src="/images/logos/singul.svg" style={categoryStyle} />
|
|
</Tooltip>
|
|
: item.category === "ask" ?
|
|
<Tooltip title="Ask" placement="top">
|
|
<img src="/images/workflows/UserInput2.svg" style={categoryStyle} />
|
|
</Tooltip>
|
|
:
|
|
<div style={categoryStyle} />
|
|
|
|
const validate = validateJson(item.details)
|
|
const itemStartTime = item.start_time
|
|
var itemEndTime = item.end_time
|
|
if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
|
console.log("Rerender 1")
|
|
//setOriginalStartTime(itemStartTime)
|
|
}
|
|
|
|
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
|
|
console.log("Rerender 2")
|
|
setLatestEndTime(itemEndTime)
|
|
}
|
|
|
|
if (itemEndTime === undefined || itemEndTime === null) {
|
|
// Set it to now
|
|
itemEndTime = latestEndTime
|
|
}
|
|
|
|
const totalDuration = latestEndTime - originalStartTime
|
|
const currentDuration = itemStartTime - itemEndTime
|
|
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
|
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
|
|
|
if (totalDuration === currentDuration) {
|
|
timelineMarginLeft = 0
|
|
timelineWidth = maxTimelineWidth
|
|
}
|
|
|
|
const defaultTopPadding = 10
|
|
const open = openIndexes.includes(index)
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: 45,
|
|
cursor: hovered ? "pointer" : "default",
|
|
borderTop: "1px solid " + theme.palette.surfaceColor,
|
|
borderRadius: theme.palette.borderRadius,
|
|
}}
|
|
onMouseEnter={() => {
|
|
if (!hovered) {
|
|
console.log("HOVER")
|
|
setHovered(true)
|
|
}
|
|
}}
|
|
onMouseLeave={() => {
|
|
if (hovered) {
|
|
setHovered(false)
|
|
}
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
backgroundColor: hovered ? theme.palette.surfaceColor : "inherit",
|
|
}}
|
|
onClick={(e) => {
|
|
if (item.details === undefined || item.details === null || item.details === "") {
|
|
|
|
if (item?.category === "agent" && item?.type === "agent") {
|
|
// Show all the data
|
|
if (openIndexes.includes(index)) {
|
|
console.log("Rerender 3")
|
|
setOpenIndexes(openIndexes.filter((i) => i !== index))
|
|
} else {
|
|
console.log("Rerender 4")
|
|
setOpenIndexes([...openIndexes, index])
|
|
}
|
|
} else {
|
|
toast.warn("No details to open")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if (openIndexes.includes(index)) {
|
|
console.log("Rerender 5")
|
|
setOpenIndexes(openIndexes.filter((i) => i !== index))
|
|
} else {
|
|
console.log("Rerender 6")
|
|
setOpenIndexes([...openIndexes, index])
|
|
}
|
|
}}
|
|
>
|
|
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
|
|
{parsedStatus}
|
|
</div>
|
|
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
|
|
{parsedCategory}
|
|
</div>
|
|
<div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}>
|
|
{/* To ISO string from unix time */}
|
|
{new Date(item.start_time * 1000).toLocaleString()}
|
|
</div>
|
|
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
|
|
<Chip
|
|
label={item.type}
|
|
/>
|
|
</div>
|
|
<div style={{
|
|
minWidth: 200,
|
|
maxWidth: 200,
|
|
paddingTop: defaultTopPadding,
|
|
}}>
|
|
{item.label}
|
|
</div>
|
|
|
|
<Tooltip title={`Time taken: ${currentDuration*-1} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
|
|
<div style={{
|
|
minWidth: maxTimelineWidth,
|
|
maxWidth: maxTimelineWidth,
|
|
paddingTop: defaultTopPadding*1.3,
|
|
}}>
|
|
{currentDuration !== 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
|
|
<div style={{
|
|
backgroundColor: item.status === "FINISHED" ?
|
|
green : item.status === "RUNNING" || item.status === "" ?
|
|
theme.palette.main : theme.palette.surfaceColor,
|
|
|
|
marginLeft: timelineMarginLeft,
|
|
minWidth: timelineWidth,
|
|
maxWidth: timelineWidth,
|
|
height: 10,
|
|
}} />
|
|
: null}
|
|
</div>
|
|
</Tooltip>
|
|
|
|
<div style={{
|
|
minWidth: 100,
|
|
maxWidth: 100,
|
|
display: "flex",
|
|
}}>
|
|
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="left">
|
|
<span>
|
|
<IconButton
|
|
disabled={item.type !== "decision" || disableButtons}
|
|
style={{marginLeft: 20, }}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
toast.info("Attempting to rerun this decision by itself.")
|
|
setDisableButtons(true)
|
|
RerunDecision(item.details)
|
|
}}
|
|
>
|
|
<RestartAltIcon />
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip title="Explore results" placement="right">
|
|
<span>
|
|
<IconButton
|
|
disabled={item.details === undefined || item.details === null || item.details === ""}
|
|
style={{marginLeft: 10, }}
|
|
>
|
|
{open ?
|
|
<ExpandLessIcon />
|
|
:
|
|
<ExpandMoreIcon />
|
|
}
|
|
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
{open ?
|
|
<div style={{marginTop: 10, marginBottom: 10, }}>
|
|
{validate.valid === true ?
|
|
<ReactJson
|
|
src={validate.result}
|
|
theme={theme.palette.jsonTheme}
|
|
style={theme.palette.reactJsonStyle}
|
|
shouldCollapse={(jsonField) => {
|
|
return collapseField(jsonField)
|
|
}}
|
|
iconStyle={theme.palette.jsonIconStyle}
|
|
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
|
|
displayArrayKey={false}
|
|
enableClipboard={(copy) => {
|
|
handleReactJsonClipboard(copy);
|
|
}}
|
|
displayDataTypes={false}
|
|
onSelect={(select) => {
|
|
HandleJsonCopy(validate.result, select, "exec")
|
|
}}
|
|
name={false}
|
|
/>
|
|
:
|
|
<code>
|
|
{item.details}
|
|
</code>
|
|
}
|
|
</div>
|
|
: null }
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const TimelineRender = (props) => {
|
|
const { agent_data } = props;
|
|
|
|
const actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
|
var timelineItems = [
|
|
{
|
|
"label": "AI Agent 2",
|
|
"type": "agent",
|
|
"category": "agent",
|
|
"details": actionResult?.result,
|
|
|
|
"status": agent_data?.status,
|
|
"start_time": agent_data?.started_at,
|
|
"end_time": agent_data?.completed_at,
|
|
},
|
|
]
|
|
|
|
// Autofixer for result lol
|
|
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
|
|
const verifiedInput = validateJson(actionResult?.result)
|
|
if (verifiedInput.valid === true && verifiedInput.result?.decisions !== undefined && verifiedInput.result?.decisions !== null) {
|
|
agent_data.decisions = verifiedInput.result?.decisions
|
|
|
|
setAgentActionResult(actionResult)
|
|
}
|
|
}
|
|
|
|
var sortedTimelineItems = []
|
|
for (var key in agent_data?.decisions) {
|
|
const item = agent_data.decisions[key]
|
|
|
|
if (item.run_details.started_at === undefined || item.run_details.started_at === null) {
|
|
item.run_details.started_at = originalStartTime
|
|
}
|
|
|
|
if (item.run_details.completed_at === undefined || item.run_details.completed_at === null) {
|
|
item.run_details.completed_at = Math.floor(Date.now() / 1000)
|
|
}
|
|
|
|
var newTimelineItem = {
|
|
"label": item.action,
|
|
"type": "decision",
|
|
"category": item.category,
|
|
|
|
"status": item.run_details.status,
|
|
"start_time": item.run_details.started_at,
|
|
"end_time": item.run_details.completed_at,
|
|
}
|
|
|
|
newTimelineItem.details = item
|
|
timelineItems.push(newTimelineItem)
|
|
}
|
|
|
|
timelineItems.sort((a, b) => {
|
|
if (a.start_time < b.start_time) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
})
|
|
|
|
return (
|
|
<div style={{marginTop: 20, }}>
|
|
{/*
|
|
<div style={{display: "flex", }}>
|
|
{agent_data?.status === "RUNNING" || agent_data?.status === "WAITING" ?
|
|
<CircularProgress size={20} style={{marginRight: 10, }} />
|
|
:
|
|
agent_data?.status === "FINISHED" ?
|
|
<CheckCircleIcon style={{color: green, marginRight: 10, }} />
|
|
:
|
|
<HourglassDisabledIcon style={{marginRight: 10, }} />
|
|
}
|
|
|
|
<Typography variant="h6" style={{marginLeft: 20, }}>
|
|
{agent_data?.status}
|
|
</Typography>
|
|
</div>
|
|
*/}
|
|
|
|
<div style={{marginTop: 10, }} />
|
|
{timelineItems?.map((item, index) => {
|
|
return (
|
|
<TimelineItem
|
|
item={item}
|
|
index={index}
|
|
key={index}
|
|
/>
|
|
)
|
|
})}
|
|
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const submitInput = (inputText) => {
|
|
//toast.info("Submitting AI Agent input: " + inputText);
|
|
|
|
setAgentRequestLoading(true)
|
|
//setShowAgentStarter(false);
|
|
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
|
|
|
|
if (inputText === undefined || inputText === null || inputText === "") {
|
|
toast.error("Please provide a valid input for the AI Agent.")
|
|
return
|
|
}
|
|
|
|
// 1. Run the execution. Can this be a single-action run?
|
|
// 2. Get the execution ID and node ID from the response.
|
|
const uuid = uuidv4()
|
|
const data = {
|
|
"id": uuid,
|
|
"name":"agent",
|
|
//"app_name":"Shuffle AI",
|
|
"app_name":"AI Agent", // Failover for rerun
|
|
"app_id":"shuffle_agent",
|
|
"app_version":"1.0.0",
|
|
|
|
"environment":"cloud",
|
|
"parameters":[
|
|
{
|
|
"name":"app_name",
|
|
"value":"openai"
|
|
},
|
|
{
|
|
"name":"input",
|
|
"value": inputText
|
|
},
|
|
{
|
|
"name":"action",
|
|
"value":"list_tickets"
|
|
}
|
|
]}
|
|
|
|
const url = `${globalUrl}/api/v1/apps/agent_starter/run`
|
|
fetch(url, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
setAgentRequestLoading(false)
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
//toast.success("Got response!")
|
|
console.log("Agent run response: ", responseJson)
|
|
|
|
if (responseJson.success === true && responseJson.authorization !== undefined && responseJson.execution_id !== undefined) {
|
|
navigate("?execution_id=" + responseJson.execution_id + "&authorization=" + responseJson.authorization)
|
|
setShowAgentStarter(false)
|
|
GetExecution(responseJson.execution_id, "", responseJson.authorization)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
setAgentRequestLoading(false)
|
|
toast.error("Error: " + error)
|
|
})
|
|
|
|
}
|
|
|
|
return (
|
|
<div style={agentWrapperStyle}>
|
|
|
|
{showAgentStarter ?
|
|
<Box component="form" style={{textAlign: "center", }} onSubmit={(e) => {
|
|
e.preventDefault();
|
|
submitInput(actionInput);
|
|
}}>
|
|
<img src="/images/logos/agent.svg" style={{
|
|
width: 200,
|
|
height: 200,
|
|
}} />
|
|
<div />
|
|
|
|
<Typography variant="h5" style={{marginTop: 30, }}>
|
|
Shuffle AI Agents
|
|
</Typography>
|
|
<TextField
|
|
label="What do you want to do?"
|
|
variant="outlined"
|
|
disabled={agentRequestLoading}
|
|
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
|
multiline
|
|
minRows={2}
|
|
defaultValue={execution?.execution_id || ""}
|
|
onChange={(e) => {
|
|
setActionInput(e.target.value)
|
|
}}
|
|
InputProps={{
|
|
endAdornment: (
|
|
agentRequestLoading ?
|
|
<CircularProgress size={24} style={{marginRight: 10, }} />
|
|
:
|
|
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
|
|
<IconButton type="submit">
|
|
<SendIcon
|
|
color="primary"
|
|
/>
|
|
</IconButton>
|
|
</Tooltip>
|
|
),
|
|
}}
|
|
/>
|
|
</Box>
|
|
:
|
|
<div>
|
|
<ButtonGroup style={{marginTop: 50, }}>
|
|
<Button
|
|
variant={buttonState === "default" ? "contained" : "outlined"}
|
|
color="secondary"
|
|
onClick={() => {
|
|
setButtonState("default");
|
|
}}
|
|
>
|
|
Default
|
|
</Button>
|
|
<Button
|
|
variant={buttonState === "timeline" ? "contained" : "outlined"}
|
|
color="secondary"
|
|
onClick={() => {
|
|
setButtonState("timeline");
|
|
}}
|
|
>
|
|
Timeline
|
|
</Button>
|
|
</ButtonGroup>
|
|
|
|
{buttonState === "timeline" ?
|
|
<TimelineRender agent_data={data} />
|
|
:
|
|
null
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AgentUI
|