1916 lines
55 KiB
React
1916 lines
55 KiB
React
import React, { useState, useEffect, useContext, memo } from "react";
|
|
import { Context } from "../context/ContextApi.jsx";
|
|
import AuthenticationModal from "../components/AuthenticationModal.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/Workflows2.jsx";
|
|
import AppSearch from "../components/Appsearch.jsx";
|
|
import Markdown from 'react-markdown'
|
|
import remarkGfm from 'remark-gfm'
|
|
import { Paragraph, Blockquote, CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
|
|
|
import {
|
|
Box,
|
|
Button,
|
|
ButtonGroup,
|
|
Typography,
|
|
Chip,
|
|
CircularProgress,
|
|
Tooltip,
|
|
IconButton,
|
|
TextField,
|
|
Popover,
|
|
Divider,
|
|
|
|
AvatarGroup,
|
|
Avatar,
|
|
} from '@mui/material'
|
|
|
|
import {
|
|
CheckCircle as CheckCircleIcon,
|
|
Check as CheckIcon,
|
|
HourglassDisabled as HourglassDisabledIcon,
|
|
RestartAlt as RestartAltIcon,
|
|
ExpandMore as ExpandMoreIcon,
|
|
ExpandLess as ExpandLessIcon,
|
|
Send as SendIcon,
|
|
Error as ErrorIcon,
|
|
Close as CloseIcon,
|
|
OpenInNew as OpenInNewIcon,
|
|
Refresh as RefreshIcon,
|
|
Add as AddIcon,
|
|
Warning as WarningIcon,
|
|
Pause as PauseIcon,
|
|
Chat as ChatIcon,
|
|
} from '@mui/icons-material'
|
|
|
|
import {
|
|
green,
|
|
yellow,
|
|
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 [apps, setApps] = useState([])
|
|
const [appAuth, setAppAuth] = useState([])
|
|
|
|
const [showAgentStarter, setShowAgentStarter] = useState(false)
|
|
const [actionInput, setActionInput] = useState("")
|
|
const [questionAnswers, setQuestionAnswers] = useState({})
|
|
|
|
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
|
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
|
|
const [chosenApps, setChosenApps] = useState([])
|
|
const [planningEnabled, setPlanningEnabled] = useState([])
|
|
|
|
const activateApp = (appId) => {
|
|
if (appId === undefined || appId === null || appId === "") {
|
|
return
|
|
}
|
|
|
|
const url = `${globalUrl}/api/v1/apps/${appId}/activate`
|
|
fetch(url, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson.success !== false) {
|
|
getApps()
|
|
} else {
|
|
toast.warn("Failed to activate app: " + responseJson.reason)
|
|
}
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
|
|
return
|
|
}
|
|
|
|
setNewSelectedApp({})
|
|
setAppPickerAnchor(null)
|
|
if (chosenApps.findIndex((app) => app.id === newSelectedApp.objectID) !== -1) {
|
|
} else {
|
|
setChosenApps(chosenApps.concat([{
|
|
name: newSelectedApp.name,
|
|
id: newSelectedApp.objectID,
|
|
image: newSelectedApp.image_url,
|
|
}]))
|
|
|
|
var found = false
|
|
if (apps?.length > 0) {
|
|
for (var key in apps) {
|
|
const app = apps[key]
|
|
if (app?.id !== newSelectedApp?.objectID) {
|
|
continue
|
|
}
|
|
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
activateApp(newSelectedApp.objectID)
|
|
}
|
|
}
|
|
}, [newSelectedApp])
|
|
|
|
useEffect(() => {
|
|
if (chosenApps?.length > 0) {
|
|
return
|
|
}
|
|
|
|
if (apps?.length === 0) {
|
|
console.log("No apps loaded yet")
|
|
return
|
|
}
|
|
|
|
if (data === undefined || data === null) {
|
|
return
|
|
}
|
|
|
|
if (data?.allowed_actions === undefined || data?.allowed_actions === null) {
|
|
return
|
|
}
|
|
|
|
console.log("HERE!")
|
|
console.log("Data, apps: ", data, apps)
|
|
|
|
var newChosenApps = []
|
|
for (var key in data?.allowed_actions) {
|
|
const allowedAction = data?.allowed_actions[key]
|
|
if (!allowedAction?.startsWith("app:")) {
|
|
newChosenApps.push({
|
|
"name": allowedAction,
|
|
"large_image": "",
|
|
})
|
|
|
|
continue
|
|
}
|
|
|
|
const appId = allowedAction.split(":")[1]
|
|
for (var appKey in apps) {
|
|
const app = apps[appKey]
|
|
if (app.id !== appId) {
|
|
continue
|
|
}
|
|
|
|
newChosenApps.push(app)
|
|
break
|
|
}
|
|
}
|
|
|
|
if (newChosenApps.length > 0) {
|
|
setChosenApps(newChosenApps)
|
|
}
|
|
}, [data])
|
|
|
|
const {themeMode} = useContext(Context)
|
|
const theme = getTheme(themeMode)
|
|
const navigate = useNavigate();
|
|
|
|
if (document !== undefined && document !== null && !document?.title?.includes("Agent")) {
|
|
document.title = "Shuffle AI Agents"
|
|
}
|
|
|
|
const agentWrapperStyle = {
|
|
width: "100%",
|
|
minHeight: "100vh",
|
|
margin: "auto",
|
|
backgroundColor: theme.palette.backgroundColor,
|
|
|
|
paddingBottom: showAgentStarter ? 0 : 50,
|
|
|
|
}
|
|
|
|
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 Heading = (props) => {
|
|
const element = React.createElement(
|
|
`h${props.level}`,
|
|
{ style: { marginTop: 40 } },
|
|
props.children
|
|
);
|
|
|
|
return (
|
|
<Typography>
|
|
{props.level !== 1 ? (
|
|
<Divider
|
|
style={{
|
|
width: "90%",
|
|
marginTop: 40,
|
|
backgroundColor: theme.palette.inputColor,
|
|
}}
|
|
/>
|
|
) : null}
|
|
{element}
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
const markdownComponents = {
|
|
img: Img,
|
|
code: CodeHandler,
|
|
h1: Heading,
|
|
h2: Heading,
|
|
h3: Heading,
|
|
h4: Heading,
|
|
h5: Heading,
|
|
h6: Heading,
|
|
a: OuterLink,
|
|
p: Paragraph,
|
|
blockquote: Blockquote,
|
|
}
|
|
|
|
const findNodeData = (execution_data, node_id) => {
|
|
if (execution_data === undefined || execution_data === null) {
|
|
return
|
|
}
|
|
|
|
// If no node_id provided, look for the AI Agent node
|
|
if (node_id === undefined || node_id === null || node_id === "") {
|
|
for (var key in execution_data.results) {
|
|
const item = execution_data.results[key]
|
|
if (item?.action?.app_name === "AI Agent") {
|
|
node_id = item?.action?.id
|
|
break
|
|
}
|
|
}
|
|
|
|
if (node_id === undefined || node_id === null || node_id === "") {
|
|
// Fallback: if only one result, use it
|
|
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)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
var found = false
|
|
for (var key in execution_data.results) {
|
|
const item = execution_data.results[key]
|
|
|
|
if (item?.action?.app_name !== "AI Agent") {
|
|
continue
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
try {
|
|
if (JSON.stringify(responseJson) !== JSON.stringify(execution)) {
|
|
setExecution(responseJson)
|
|
}
|
|
} catch(e) {
|
|
console.log("Error comparing executions: ", e)
|
|
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)
|
|
setTimeout(() => {
|
|
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
|
}, 10000)
|
|
})
|
|
.catch((error) => {
|
|
toast.error("Error: " + error)
|
|
})
|
|
}
|
|
|
|
const getAppAuth = () => {
|
|
const url = `${globalUrl}/api/v1/apps/authentication`
|
|
fetch(url, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson.success !== false) {
|
|
setAppAuth(responseJson)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
toast.error("Error in auth load: " + error)
|
|
})
|
|
}
|
|
|
|
const getApps = () => {
|
|
const url = `${globalUrl}/api/v1/apps`
|
|
fetch(url, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson.success !== false) {
|
|
setApps(responseJson)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
toast.error("Error in app load: " + 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.")
|
|
}
|
|
|
|
getApps()
|
|
getAppAuth()
|
|
}, [])
|
|
|
|
const maxTimelineWidth = 275
|
|
|
|
const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
|
|
console.log("Submitting questions: ", decisionId, questionAnswers)
|
|
if (decisionId === undefined || decisionId === null || decisionId === "") {
|
|
toast.error("No decision ID provided. Cannot submit answers.")
|
|
return
|
|
}
|
|
|
|
if (Object.keys(questionAnswers).length === 0) {
|
|
toast.error("No answers provided. Cannot submit empty answers.")
|
|
return
|
|
}
|
|
|
|
var newArgument = {}
|
|
if (isContinuation === true) {
|
|
// Just a single answer
|
|
for (var key in questionAnswers) {
|
|
const answer = questionAnswers[key]
|
|
newArgument[key] = answer
|
|
}
|
|
|
|
if (Object.keys(newArgument).length === 0) {
|
|
toast.error("No answers details. Cannot submit the answer.")
|
|
return
|
|
}
|
|
|
|
} else {
|
|
for (var key in questionAnswers) {
|
|
// Special handler for continuation of normal actions
|
|
// Approves/denies continuation
|
|
if (key === "approve") {
|
|
newArgument[key] = questionAnswers[key]
|
|
break
|
|
}
|
|
|
|
const answer = questionAnswers[key]
|
|
newArgument["question_"+(answer.index)] = answer.value
|
|
}
|
|
}
|
|
|
|
setAgentRequestLoading(true)
|
|
const params = new URLSearchParams(window.location.search)
|
|
const executionId = params.get("execution_id")
|
|
const nodeId = params.get("node_id")
|
|
const authorization = params.get("authorization")
|
|
const workflowIdParam = params.get("workflow_id")
|
|
|
|
// workflow_id param > execution.workflow.id > executionId
|
|
const foundWorkflowId = workflowIdParam !== undefined && workflowIdParam !== null && workflowIdParam !== "" ? workflowIdParam : execution?.workflow?.id !== undefined && execution?.workflow?.id !== null && execution?.workflow?.id !== "" ? execution.workflow.id : executionId
|
|
|
|
const url = `${globalUrl}/api/v1/workflows/${foundWorkflowId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}&node_id=${nodeId}`
|
|
fetch(url, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
setAgentRequestLoading(false)
|
|
return response.json()
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson.success !== false) {
|
|
setTimeout(() => {
|
|
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
|
}, 500)
|
|
|
|
toast.success("Successfully submitted answers! The agent should continue shortly.")
|
|
} else {
|
|
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
setAgentRequestLoading(false)
|
|
toast.error("Problem with submitting: " + error)
|
|
})
|
|
}
|
|
|
|
var latestEndTime = 0
|
|
var originalStartTime = 0
|
|
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>
|
|
:
|
|
item.status === "IGNORED" || item.status === "IGNORE" ?
|
|
<Tooltip title={`${item.status}: Previous FAILURE before the agent was reran.`} placement="top">
|
|
<WarningIcon style={{color: yellow, marginRight: 10, }} />
|
|
</Tooltip>
|
|
:
|
|
<Tooltip title={`Not started yet: ${item.status}`} placement="top">
|
|
<HourglassDisabledIcon style={{marginRight: 10, }} />
|
|
</Tooltip>
|
|
|
|
const categoryStyle = {
|
|
width: 25,
|
|
height: 25,
|
|
marginRight: 10,
|
|
borderRadius: 5,
|
|
}
|
|
|
|
|
|
const validate = validateJson(item.details)
|
|
const itemStartTime = item.start_time
|
|
var itemEndTime = item.end_time
|
|
if (item.category === "agent" && itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
|
console.log("Rerender 1: ", itemStartTime, originalStartTime)
|
|
originalStartTime = itemStartTime
|
|
}
|
|
|
|
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
|
|
console.log("Rerender 2")
|
|
latestEndTime = itemEndTime
|
|
}
|
|
|
|
if (itemEndTime === undefined || itemEndTime === null) {
|
|
// Set it to now
|
|
itemEndTime = latestEndTime
|
|
}
|
|
|
|
if (item.category == "agent" && itemEndTime === 0) {
|
|
// Right now -> .toLocaleString() support
|
|
itemEndTime = Date.now() / 1000
|
|
|
|
//<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
|
|
|
|
if (itemEndTime > latestEndTime) {
|
|
latestEndTime = itemEndTime
|
|
}
|
|
}
|
|
|
|
const totalDuration = latestEndTime - originalStartTime
|
|
var currentDuration = itemStartTime - itemEndTime
|
|
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
|
//var timelineMarginLeft = 0
|
|
|
|
// Calculate how long the div should be
|
|
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
|
|
|
//console.log("CURRENT DURATION (1): ", currentDuration, itemStartTime, itemEndTime, originalStartTime, latestEndTime, totalDuration, timelineMarginLeft, timelineWidth)
|
|
if (totalDuration === currentDuration) {
|
|
timelineMarginLeft = 0
|
|
timelineWidth = maxTimelineWidth
|
|
}
|
|
|
|
// Just for simplicity's sake
|
|
if (currentDuration < -1000000 || currentDuration > 1000000) {
|
|
currentDuration = 0
|
|
}
|
|
|
|
if (currentDuration < 0) {
|
|
currentDuration = currentDuration * -1
|
|
}
|
|
|
|
const defaultTopPadding = 10
|
|
const open = openIndexes.includes(index)
|
|
|
|
if (item?.type === "agent" && item?.details?.original_input !== undefined) {
|
|
document.title = "Agent: " + item?.details?.original_input?.substring(0, 50)
|
|
}
|
|
|
|
var questions = []
|
|
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
|
item.type = "finalise"
|
|
item.category = "finalise"
|
|
item.label = item?.details?.reason || item.label
|
|
|
|
} else if (item?.category === "ask" || item?.details?.action === "ask") {
|
|
console.log("IN HERE?: ", item)
|
|
|
|
item.type = "question"
|
|
item.category = "ask"
|
|
item.label = item?.details?.reason || item.label
|
|
|
|
for (var fieldKey in item?.details?.fields) {
|
|
const field = item?.details?.fields[fieldKey]
|
|
if (field?.key !== "question") {
|
|
continue
|
|
}
|
|
|
|
questions.push({
|
|
"question": field?.value,
|
|
"index": questions.length + 1,
|
|
})
|
|
}
|
|
} else if (item?.details?.action === "api" && item?.details?.tool?.length > 0) {
|
|
item.label = item?.details?.reason || item.label
|
|
}
|
|
|
|
console.log("ITEM: ", item, questions)
|
|
|
|
var 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>
|
|
: item.category === "finalise" || item.category === "finish" || item.action === "finish" ?
|
|
<Tooltip title="The action finished successfully" placement="top">
|
|
<CheckIcon style={{color: green, marginRight: 10, }} />
|
|
</Tooltip>
|
|
:
|
|
<div style={categoryStyle} />
|
|
|
|
var showAuthentication = false
|
|
var selectedApp = {}
|
|
if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) {
|
|
|
|
// Find the app and inject the image
|
|
var toolName = item?.details?.tool?.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
|
if (toolName.includes("app:")) {
|
|
const toolNameSplit = toolName.split(":")
|
|
if (toolNameSplit.length >= 2) {
|
|
toolName = toolNameSplit[2]
|
|
}
|
|
}
|
|
|
|
for (var appKey in apps) {
|
|
const app = apps[appKey]
|
|
|
|
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
|
if (appname !== toolName) {
|
|
continue
|
|
}
|
|
|
|
if (app.large_image === undefined || app.large_image === null || app.large_image.length === 0) {
|
|
break
|
|
}
|
|
|
|
selectedApp = app
|
|
|
|
// Override the category
|
|
//item.category = app.name
|
|
//item.label = item?.details?.reason || item.label
|
|
parsedCategory =
|
|
<Tooltip title={app.name} placement="top">
|
|
<img src={app.large_image} style={categoryStyle} />
|
|
</Tooltip>
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!showAuthentication) {
|
|
if (item?.details?.run_details?.raw_response !== undefined && item?.details?.run_details?.raw_response !== null && item?.details?.run_details?.raw_response?.includes("app_authentication")) {
|
|
showAuthentication = true
|
|
}
|
|
}
|
|
|
|
var questionSubmitDisabled = questions.length === 0 ? true : false
|
|
for (var qKey in questions) {
|
|
const q = questions[qKey]
|
|
if (questionAnswers[q.question] === undefined || questionAnswers[q.question] === null || questionAnswers[q.question] === "") {
|
|
//console.log("EMPTY QUESTION: ", q)
|
|
questionSubmitDisabled = true
|
|
break
|
|
} else {
|
|
questionSubmitDisabled = false
|
|
}
|
|
}
|
|
|
|
const barColor = item.status === "IGNORED" ? yellow : item.status === "FINISHED" ? green :
|
|
item.status === "FAILURE" || item.status == "ABORTED" ? red :
|
|
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
|
|
red
|
|
|
|
const rerunAgentButton =
|
|
<Tooltip title="Rerun from the start with the same input" placement="right">
|
|
<span>
|
|
<IconButton
|
|
style={{marginLeft: 20, }}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
toast.info("Rerunning agent with the same input.")
|
|
setDisableButtons(true)
|
|
|
|
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
|
|
toast.error("No decision details found to rerun. Cannot proceed. Please go back to your workflow or /agents to start over.")
|
|
} else {
|
|
if (item?.details?.original_input !== undefined && item?.details?.original_input !== null && item?.details?.original_input !== "") {
|
|
setActionInput(item?.details?.original_input)
|
|
setDisableButtons(true)
|
|
submitInput(item?.details?.original_input)
|
|
return
|
|
}
|
|
|
|
//console.log("DETAILS: ", item?.details)
|
|
for (var messagekey in item?.details?.input?.messages) {
|
|
const message = item?.details?.input?.messages[messagekey]
|
|
if (message.role === "user" && !message?.role?.includes("USER CONTEXT")) {
|
|
setActionInput(message.content)
|
|
setDisableButtons(true)
|
|
|
|
submitInput(message.content)
|
|
//toast.info("Rerun started. Please wait a few seconds and this page should refresh automatically.")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<RestartAltIcon />
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
|
|
|
|
const rerunButton = item?.type === "processing" ? null :
|
|
<Tooltip title={item?.type === "decision" ? "Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." : ""} placement="right">
|
|
<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>
|
|
|
|
var itemLabel = item?.label?.replaceAll("_", " ") || ""
|
|
if (item?.details?.reason !== undefined && item?.details?.reason !== null && item?.details?.reason !== "") {
|
|
itemLabel = item?.details?.reason
|
|
}
|
|
|
|
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, }}>
|
|
{item?.start_time !== undefined && item?.start_time !== null && item?.start_time !== 0 ?
|
|
new Date(item.start_time * 1000).toLocaleString()
|
|
:
|
|
null
|
|
}
|
|
|
|
</div>
|
|
*/}
|
|
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
|
|
<Chip
|
|
label={item.type}
|
|
/>
|
|
</div>
|
|
<div style={{
|
|
minWidth: 400,
|
|
maxWidth: 400,
|
|
maxHeight: 150,
|
|
overflowX: "hidden",
|
|
overflowY: "auto",
|
|
}}>
|
|
<Markdown
|
|
components={markdownComponents}
|
|
id="markdown_wrapper"
|
|
className={"style.reactMarkdown"}
|
|
escapeHtml={false}
|
|
skipHtml={false}
|
|
remarkPlugins={[remarkGfm]}
|
|
style={{
|
|
minWidth: 400,
|
|
maxWidth: 450,
|
|
maxHeight: 150,
|
|
overflowX: "hidden",
|
|
overflowY: "auto",
|
|
|
|
paddingTop: defaultTopPadding,
|
|
paddingBottom: defaultTopPadding,
|
|
}}
|
|
>
|
|
{itemLabel}
|
|
</Markdown>
|
|
</div>
|
|
|
|
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right"
|
|
PopperProps={{
|
|
modifiers: [
|
|
{
|
|
name: 'offset',
|
|
options: {
|
|
offset: [0, 130], // [skid, distance]
|
|
},
|
|
},
|
|
],
|
|
}}
|
|
style={theme.palette.tooltip}
|
|
>
|
|
<div style={{
|
|
minWidth: maxTimelineWidth,
|
|
maxWidth: maxTimelineWidth,
|
|
paddingTop: defaultTopPadding*1.5,
|
|
marginLeft: 5,
|
|
}}>
|
|
{currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
|
|
<div style={{
|
|
backgroundColor: barColor,
|
|
marginLeft: timelineMarginLeft,
|
|
minWidth: timelineWidth,
|
|
maxWidth: timelineWidth,
|
|
minHeight: 10,
|
|
maxHeight: 10,
|
|
borderRadius: theme.palette.borderRadius,
|
|
}}>
|
|
</div>
|
|
:
|
|
<Typography variant="body2" color="textSecondary">
|
|
</Typography>
|
|
}
|
|
</div>
|
|
</Tooltip>
|
|
|
|
<div style={{
|
|
minWidth: 100,
|
|
maxWidth: 100,
|
|
display: "flex",
|
|
}}>
|
|
{item.category === "ask" ?
|
|
<span style={{display: "flex", }}>
|
|
{rerunButton}
|
|
|
|
<Tooltip title="Answer in the Form UI" placement="left">
|
|
<span>
|
|
<IconButton
|
|
style={{marginLeft: 0, }}
|
|
disabled={item?.details?.run_details?.status === "FINISHED"}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
//http://localhost:3002/forms/aadfe022-fe93-431c-8634-de42dd7440ac?authorization=9357f6a6-7d59-44be-ad66-be27657369ac&reference_execution=0726378d-b501-470f-b850-f7fb48cd8ca4&source_node=de446bcf-ad37-4337-9f72-e069c7425fac&backend_url=https://ec4245cd2941.ngrok-free.app
|
|
const newurl = `/forms/${execution?.workflow?.id}?authorization=${execution.authorization}&reference_execution=${execution.execution_id}&source_node=${agentActionResult?.action?.id}&decision_id=${item.details.run_details.id}&backend_url=${globalUrl}`
|
|
window.open(newurl, '_blank', 'noopener,noreferrer');
|
|
}}
|
|
>
|
|
<OpenInNewIcon />
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
</span>
|
|
:
|
|
item.category === "agent" ?
|
|
rerunAgentButton
|
|
:
|
|
item?.type === "decision" ?
|
|
item?.details?.run_details?.status === "WAITING" ?
|
|
agentRequestLoading ? null :
|
|
<div>
|
|
<Button
|
|
style={{
|
|
maxHeight: 30,
|
|
minHeight: 30,
|
|
minWidth: 75,
|
|
maxWidth: 75,
|
|
marginTop: 5,
|
|
}}
|
|
color="primary"
|
|
variant="contained"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
// FIXME: How do we approve it? Hmm
|
|
toast.info("Approving this step.")
|
|
|
|
// Keeping it as string to make it simple
|
|
const newAnswers = {
|
|
"approve": "true",
|
|
}
|
|
submitQuestions(item?.details?.run_details?.id, newAnswers)
|
|
}}
|
|
>
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
style={{
|
|
maxHeight: 30,
|
|
minHeight: 30,
|
|
minWidth: 75,
|
|
maxWidth: 75,
|
|
marginTop: 3,
|
|
}}
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
// Keeping it as string to make it simple
|
|
const newAnswers = {
|
|
"approve": "false",
|
|
}
|
|
submitQuestions(item?.details?.run_details?.id, newAnswers)
|
|
}}
|
|
>
|
|
Deny
|
|
</Button>
|
|
</div>
|
|
:
|
|
<div style={{display: "flex", }}>
|
|
{rerunButton}
|
|
<Tooltip title="Explore/debug app run" placement="left">
|
|
<span>
|
|
<IconButton
|
|
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
|
|
style={{marginLeft: 0, }}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
//http://localhost:3002/forms/aadfe022-fe93-431c-8634-de42dd7440ac?authorization=9357f6a6-7d59-44be-ad66-be27657369ac&reference_execution=0726378d-b501-470f-b850-f7fb48cd8ca4&source_node=de446bcf-ad37-4337-9f72-e069c7425fac&backend_url=https://ec4245cd2941.ngrok-free.app
|
|
window.open(item?.details?.run_details?.debug_url, '_blank', 'noopener,noreferrer');
|
|
}}
|
|
>
|
|
<OpenInNewIcon color={barColor === red ? "primary" : "secondary"} />
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
</div>
|
|
:
|
|
rerunButton
|
|
}
|
|
|
|
{item?.type === "processing" ? null :
|
|
<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>
|
|
|
|
{showAuthentication && selectedApp.id !== undefined ?
|
|
<div style={{minWidth: 300, maxWidth: 300, margin: "auto", marginTop: 0, marginBottom: 25, }}>
|
|
<AuthenticationModal
|
|
globalUrl={globalUrl}
|
|
userdata={userdata}
|
|
|
|
setAppAuthentication={setAppAuth}
|
|
selectedAppData={selectedApp}
|
|
/>
|
|
</div>
|
|
: null}
|
|
|
|
{item.category !== "agent" && questions?.length > 0 && (item?.status === "RUNNING" || item?.status === "WAITING") ?
|
|
<div>
|
|
{questions.map((q, questionIndex) => {
|
|
return (
|
|
<div style={{marginTop: 25, }}>
|
|
<div id="markdown_wrapper_outer" style={{cursor: "default", }}>
|
|
<Markdown
|
|
components={markdownComponents}
|
|
id="markdown_wrapper"
|
|
className={"style.reactMarkdown"}
|
|
escapeHtml={false}
|
|
skipHtml={false}
|
|
remarkPlugins={[remarkGfm]}
|
|
style={{
|
|
maxWidth: "100%", minWidth: "100%",
|
|
}}
|
|
>
|
|
{q.question}
|
|
</Markdown>
|
|
</div>
|
|
|
|
<TextField
|
|
label={`Question ${q.index}`}
|
|
placeholder="Your answer here"
|
|
variant="outlined"
|
|
style={{width: 800, marginTop: 20, }}
|
|
multiline
|
|
minRows={2}
|
|
defaultValue={questionAnswers[q.question]?.value || ""}
|
|
onBlur={(e) => {
|
|
console.log("Change: ", e.target.value)
|
|
try {
|
|
questionAnswers[q.question] = {
|
|
"index": questionIndex,
|
|
"value": e.target.value,
|
|
}
|
|
|
|
setQuestionAnswers({...questionAnswers, })
|
|
} catch (e) {
|
|
toast.warn("Something went wrong. Please contact support@shuffler.io. Details: " + e)
|
|
}
|
|
}}
|
|
|
|
/>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<Button
|
|
variant="contained"
|
|
style={{marginTop: 16, }}
|
|
disabled={questionSubmitDisabled || agentRequestLoading}
|
|
onClick={() => {
|
|
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
|
|
|
}}
|
|
>
|
|
{agentRequestLoading ?
|
|
<CircularProgress size={20} style={{marginRight: 3, }} />
|
|
:
|
|
"Submit"
|
|
}
|
|
</Button>
|
|
</div>
|
|
: null}
|
|
|
|
|
|
{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 [continuationText, setContinuationText] = useState("")
|
|
|
|
// Find the AI Agent result specifically, not just results[0]
|
|
var actionResult = null
|
|
if (execution?.results?.length > 0) {
|
|
for (var key in execution.results) {
|
|
const item = execution.results[key]
|
|
if (item?.action?.app_name === "AI Agent") {
|
|
actionResult = item
|
|
break
|
|
}
|
|
}
|
|
|
|
// Fallback to first result if no AI Agent found
|
|
if (actionResult === null) {
|
|
actionResult = execution.results[0]
|
|
}
|
|
} else {
|
|
actionResult = execution
|
|
}
|
|
|
|
const validate = validateJson(actionResult?.result)
|
|
if (validate.valid === true) {
|
|
actionResult.result = validate.result
|
|
}
|
|
|
|
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,
|
|
},
|
|
]
|
|
|
|
// Setting up the initial item
|
|
if (agent_data?.started_at === undefined && execution?.started_at !== undefined) {
|
|
timelineItems[0].start_time = execution?.started_at
|
|
}
|
|
|
|
if (agent_data?.completed_at === undefined && execution?.completed_at !== undefined) {
|
|
timelineItems[0].end_time = execution?.completed_at
|
|
}
|
|
|
|
// Always prioritise the execution status first
|
|
// agent (RUNNING) = workflow (EXECUTING)
|
|
if (execution?.status !== undefined) {
|
|
timelineItems[0].status = execution?.status
|
|
}
|
|
|
|
if (actionResult?.result?.status !== undefined && actionResult?.result?.status !== null && actionResult?.result?.status?.length > 0) {
|
|
if (timelineItems[0].status !== "FINISHED" && timelineItems[0].status !== "ABORTED" && timelineItems[0].status !== "FAILURE") {
|
|
timelineItems[0].status = actionResult?.result?.status
|
|
}
|
|
}
|
|
|
|
// 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 finishDecisionId = ""
|
|
var finishAnswer = ""
|
|
var sortedTimelineItems = []
|
|
for (var key in agent_data?.decisions) {
|
|
const item = agent_data.decisions[key]
|
|
|
|
if (item.run_details === undefined) {
|
|
console.log("Skipping item without run_details:", item)
|
|
continue
|
|
}
|
|
|
|
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)
|
|
|
|
if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
|
finishDecisionId = item?.run_details?.id
|
|
|
|
finishAnswer = item?.reason || ""
|
|
if (item?.fields !== undefined && item?.fields !== null) {
|
|
// Just choosing the first one
|
|
for (var fieldKey in item?.fields) {
|
|
const field = item?.fields[fieldKey]
|
|
finishAnswer = field?.value || finishAnswer
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fill blank space between items in timeline by adding "empty" decisions:
|
|
// 1. Sort
|
|
// 2. Fill blank space
|
|
// 3. Sort again
|
|
timelineItems.sort((a, b) => a.start_time - b.start_time)
|
|
for (var i = 0; i < timelineItems.length - 1; i++) {
|
|
if (timelineItems?.length <= 2) {
|
|
break
|
|
}
|
|
|
|
if (i === timelineItems.length - 1) {
|
|
break
|
|
}
|
|
|
|
const currentItem = timelineItems[i]
|
|
if (currentItem?.type === "processing") {
|
|
continue
|
|
}
|
|
|
|
const nextItem = timelineItems[i + 1]
|
|
if (nextItem?.type === "processing") {
|
|
continue
|
|
}
|
|
|
|
if (nextItem.start_time - currentItem.end_time > 1) {
|
|
/*
|
|
timelineItems?.push({
|
|
label: "",
|
|
type: "processing",
|
|
category: "processing",
|
|
status: "FINISHED",
|
|
start_time: currentItem.end_time,
|
|
end_time: nextItem.start_time,
|
|
})
|
|
*/
|
|
}
|
|
}
|
|
timelineItems.sort((a, b) => a.start_time - b.start_time)
|
|
|
|
const handleKeyDownCont = (e) => {
|
|
if (finishDecisionId === "") {
|
|
return
|
|
}
|
|
|
|
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
|
|
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
|
|
if (isCmdEnter || isCtrlEnter) {
|
|
e.preventDefault()
|
|
|
|
submitQuestions(finishDecisionId, {
|
|
"continue": continuationText,
|
|
}, true)
|
|
}
|
|
}
|
|
|
|
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}
|
|
/>
|
|
)
|
|
})}
|
|
|
|
{finishDecisionId !== "" ?
|
|
<Box
|
|
component="form"
|
|
style={{width: "100%", textAlign: "center",}}
|
|
onKeyDown={handleKeyDownCont}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
|
|
// Uses the submitQuestion and adds more details to
|
|
//setAgentRequestLoading ?
|
|
submitQuestions(finishDecisionId, {
|
|
"continue": continuationText,
|
|
}, true)
|
|
}}
|
|
>
|
|
{finishAnswer !== "" ?
|
|
<div style={{marginTop: 50, overflowX: "hidden", textAlign: "left", }}>
|
|
<Markdown
|
|
components={markdownComponents}
|
|
id="markdown_wrapper"
|
|
className={"style.reactMarkdown"}
|
|
escapeHtml={false}
|
|
skipHtml={false}
|
|
remarkPlugins={[remarkGfm]}
|
|
style={{
|
|
maxWidth: "100%",
|
|
minWidth: "100%",
|
|
}}
|
|
>
|
|
{finishAnswer}
|
|
</Markdown>
|
|
</div>
|
|
: null}
|
|
|
|
<div style={{display: "flex", maxWidth: 600, minWidth: 600, margin: "auto", marginTop: 50, }}>
|
|
<div>
|
|
<TextField
|
|
label="Add more details to continue the current task"
|
|
variant="outlined"
|
|
disabled={agentRequestLoading}
|
|
style={{width: 450, margin: "auto", }}
|
|
multiline
|
|
minRows={2}
|
|
value={continuationText}
|
|
onChange={(e) => {
|
|
console.log("Value: ", e.target.value)
|
|
//setActionInput(e.target.value)
|
|
//
|
|
setContinuationText(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"
|
|
disabled={continuationText === ""}
|
|
>
|
|
<SendIcon
|
|
color={continuationText === "" ? "disabled" : "primary"}
|
|
/>
|
|
</IconButton>
|
|
</Tooltip>
|
|
),
|
|
}}
|
|
/>
|
|
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
|
|
Any failed tasks will be set to ignored. <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
|
|
</Typography>
|
|
</div>
|
|
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
|
|
OR
|
|
</Typography>
|
|
<Button
|
|
variant={"contained"}
|
|
color="primary"
|
|
disabled={true}
|
|
style={{marginTop: 10, marginLeft: 20, minWidth: 150, maxWidth: 150, height: 56, }}
|
|
>
|
|
Create as Workflow
|
|
</Button>
|
|
</div>
|
|
</Box>
|
|
: null}
|
|
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const submitInput = (inputText) => {
|
|
//toast.info("Submitting AI Agent input: " + inputText);
|
|
|
|
setAgentRequestLoading(true)
|
|
//setShowAgentStarter(false);
|
|
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
|
|
//
|
|
setData({})
|
|
setExecution(null)
|
|
setAgentRequestLoading(true)
|
|
setShowAgentStarter(true)
|
|
setActionInput(inputText)
|
|
|
|
setAgentActionResult(null)
|
|
|
|
document.title = `Agent: ${inputText.substring(0, 30)}...`
|
|
if (inputText === undefined || inputText === null || inputText === "") {
|
|
toast.error("Please provide a valid input for the AI Agent.")
|
|
setAgentRequestLoading(false)
|
|
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()
|
|
var parsedAction = "API" // Default action for now
|
|
if (chosenApps.length > 0) {
|
|
parsedAction = ""
|
|
for (var appKey in chosenApps) {
|
|
const app = chosenApps[appKey]
|
|
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
|
parsedAction += `app:${app.id}:${appname.replaceAll(",", "").replaceAll(":", "")},`
|
|
}
|
|
|
|
parsedAction = parsedAction.slice(0, -1) // Remove last comma
|
|
}
|
|
|
|
/*
|
|
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": parsedAction,
|
|
}
|
|
],
|
|
"planning_mode": planningEnabled,
|
|
}
|
|
const url = `${globalUrl}/api/v1/apps/agent_starter/run`
|
|
*/
|
|
|
|
const data = {
|
|
"jsonrpc": "2.0",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"tool_name": parsedAction,
|
|
"input": {
|
|
"text": inputText,
|
|
},
|
|
}
|
|
}
|
|
const url = `${globalUrl}/api/v1/agent`
|
|
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)
|
|
})
|
|
|
|
}
|
|
|
|
const handleKeyDownRoot = (e) => {
|
|
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
|
|
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
|
|
if (isCmdEnter || isCtrlEnter) {
|
|
e.preventDefault()
|
|
submitInput(actionInput)
|
|
}
|
|
}
|
|
|
|
const chipStyle = {
|
|
margin: 4,
|
|
cursor: "pointer",
|
|
}
|
|
|
|
|
|
const typeButtonGroup =
|
|
<ButtonGroup style={{}}>
|
|
<Button
|
|
variant={showAgentStarter ? "contained" : "outlined"}
|
|
color="secondary"
|
|
onClick={() => {
|
|
if (actionInput === "" || actionInput === undefined || actionInput === null) {
|
|
setActionInput(data?.original_input || "")
|
|
}
|
|
|
|
//setChosenApps(chosenApps.concat([{
|
|
|
|
setTimeout(() => {
|
|
setShowAgentStarter(true)
|
|
}, 200)
|
|
}}
|
|
>
|
|
Restart
|
|
</Button>
|
|
<Button
|
|
variant={buttonState === "default" ? "contained" : "outlined"}
|
|
color="secondary"
|
|
disabled
|
|
onClick={() => {
|
|
setButtonState("default");
|
|
setShowAgentStarter(false)
|
|
}}
|
|
>
|
|
Default
|
|
</Button>
|
|
<Button
|
|
variant={buttonState === "timeline" ? "contained" : "outlined"}
|
|
color="secondary"
|
|
onClick={() => {
|
|
setButtonState("timeline");
|
|
setShowAgentStarter(false)
|
|
}}
|
|
>
|
|
Timeline
|
|
</Button>
|
|
</ButtonGroup>
|
|
|
|
return (
|
|
<div style={{paddingBottom: 500, }}>
|
|
<div style={{height: showAgentStarter ? "17vh" : "5vh", }}>
|
|
</div>
|
|
<div style={agentWrapperStyle}>
|
|
<div style={{
|
|
minWidth: 1000, maxWidth: 1000, margin: "auto",
|
|
}}>
|
|
<TextField
|
|
id="copy_element_shuffle"
|
|
style={{ display: "none" }}
|
|
/>
|
|
|
|
{showAgentStarter ?
|
|
<Box
|
|
component="form"
|
|
style={{textAlign: "center", }}
|
|
onKeyDown={handleKeyDownRoot}
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
submitInput(actionInput);
|
|
}}
|
|
>
|
|
{/*
|
|
<img src="/images/logos/agent.svg" style={{
|
|
width: 200,
|
|
height: 200,
|
|
borderRadius: theme.palette.borderRadius,
|
|
}} />
|
|
*/}
|
|
|
|
<div />
|
|
|
|
<img
|
|
src={theme.palette.singulBlackWhite}
|
|
style={{
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: theme.palette.borderRadius,
|
|
}}
|
|
/>
|
|
<Typography variant="h2" style={{marginTop: 30, }}>
|
|
What do you want to do?
|
|
</Typography>
|
|
<TextField
|
|
label="Get my emails for today and summarise them"
|
|
variant="outlined"
|
|
disabled={agentRequestLoading}
|
|
style={{width: 650, marginTop: 30, }}
|
|
multiline
|
|
minRows={1}
|
|
maxRows={20}
|
|
defaultValue={actionInput || ""}
|
|
onChange={(e) => {
|
|
setActionInput(e.target.value)
|
|
}}
|
|
InputProps={{
|
|
style: {
|
|
borderRadius: 30,
|
|
},
|
|
endAdornment: (
|
|
agentRequestLoading ?
|
|
<CircularProgress size={24} style={{marginRight: 10, }} />
|
|
:
|
|
<Tooltip title="Start the agent. Data can be anything, and it will run based on your selected tools">
|
|
<IconButton
|
|
type="submit"
|
|
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
|
|
disabled={actionInput === "" || actionInput === undefined || actionInput === null}
|
|
>
|
|
<SendIcon
|
|
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
|
|
/>
|
|
</IconButton>
|
|
</Tooltip>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<div style={{display: "flex", margin: "auto", marginTop: 20, minWidth: 500, maxWidth: 500, justifyContent: "center", overflowWrap: "wrap", }}>
|
|
<div style={{}}>
|
|
{/*
|
|
<Tooltip title="Enables/Disables planning mode. Agents will not DO anything - just plan what to do. Primarily used for usecase build helping." placement="top">
|
|
<span>
|
|
<Chip
|
|
id="planning_enabled"
|
|
icon={<ChatIcon />} label="Planning Mode"
|
|
style={chipStyle}
|
|
variant={planningEnabled ? "contained" : "outlined"}
|
|
onClick={() => {
|
|
setPlanningEnabled(!planningEnabled)
|
|
}}
|
|
disabled={true}
|
|
/>
|
|
</span>
|
|
</Tooltip>
|
|
*/}
|
|
<Chip
|
|
id="add_app_chip"
|
|
icon={<AddIcon />} label="Select Apps / MCPs"
|
|
style={chipStyle}
|
|
variant={"outlined"}
|
|
onClick={() => {
|
|
setAppPickerAnchor(document.getElementById("add_app_chip"))
|
|
}}
|
|
/>
|
|
<Popover
|
|
open={appPickerAnchor !== null}
|
|
anchorEl={appPickerAnchor}
|
|
onClose={() => {
|
|
setAppPickerAnchor(null)
|
|
}}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'left',
|
|
}}
|
|
>
|
|
<AppSearch
|
|
userdata={userdata}
|
|
defaultSearch={""}
|
|
newSelectedApp={newSelectedApp}
|
|
setNewSelectedApp={setNewSelectedApp}
|
|
inputHeight={250}
|
|
apps={apps}
|
|
/>
|
|
</Popover>
|
|
</div>
|
|
|
|
{chosenApps.map((app, index) => {
|
|
var chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
|
|
if (chosenName === "API") {
|
|
chosenName = "default"
|
|
}
|
|
|
|
const chosenImagePath = app?.image === undefined || app?.image === null || app?.image === "" ? app?.large_image === undefined || app?.large_image === null || app?.large_image === "" ? theme.palette.singulBlackWhite : app.large_image : app.image
|
|
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
|
|
|
|
return (
|
|
<Chip icon={chosenImage} label={chosenName} variant="outlined"
|
|
style={chipStyle}
|
|
onDelete={() => {
|
|
const newChosenApps = chosenApps.filter((a, i) => i !== index)
|
|
setChosenApps(newChosenApps)
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div style={{marginTop: 20, }}>
|
|
{execution === undefined || execution === null || execution?.execution_id === undefined || execution?.execution_id === null ? null : typeButtonGroup}
|
|
</div>
|
|
|
|
{/*
|
|
<div style={{
|
|
height: 300,
|
|
width: "100%",
|
|
|
|
background: showAgentStarter ? `linear-gradient(to bottom, ${theme.palette.backgroundColor}, #C9591D)` : "inherit",
|
|
backgroundSize: "100% 100%",
|
|
backgroundRepeat: "no-repeat",
|
|
backgroundPosition: "0px 100px",
|
|
}} />
|
|
*/}
|
|
</Box>
|
|
:
|
|
<div>
|
|
<div style={{display: "flex", }}>
|
|
<div style={{flex: 2, }}>
|
|
{typeButtonGroup}
|
|
|
|
<ButtonGroup style={{marginLeft: 25, }}>
|
|
<Tooltip title="Reload the agent data" placement="top">
|
|
<span>
|
|
<Button
|
|
disabled={execution === null || Object.keys(execution).length === 0}
|
|
style={{}}
|
|
variant={"outlined"}
|
|
color="secondary"
|
|
onClick={() => {
|
|
GetExecution(execution?.execution_id, agentActionResult?.action?.id, execution?.authorization)
|
|
}}
|
|
>
|
|
<RefreshIcon />
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
|
|
{execution?.status === "EXECUTING" ?
|
|
<Tooltip title="Stop the agent" placement="top">
|
|
<span>
|
|
<Button
|
|
disabled={execution === null || true}
|
|
style={{}}
|
|
variant={"outlined"}
|
|
color="secondary"
|
|
onClick={() => {
|
|
toast.warn("Not implemented. Should run abort.")
|
|
}}
|
|
>
|
|
<PauseIcon />
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
: null}
|
|
</ButtonGroup>
|
|
</div>
|
|
|
|
<div style={{flex: 1, }}>
|
|
<AvatarGroup>
|
|
{chosenApps?.map((app, index) => {
|
|
return(
|
|
<Tooltip title={`MCP / App: ${app?.name}`} key={index} placement="top">
|
|
<Avatar
|
|
key={index}
|
|
alt={app?.name}
|
|
src={app?.large_image ? app.large_image : app?.image ? app.image : theme.palette.singulBlackWhite}
|
|
onClick={() => {
|
|
if (app?.id !== undefined) {
|
|
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
|
|
}
|
|
}}
|
|
style={{
|
|
cursor: "pointer",
|
|
width: 30,
|
|
height: 30,
|
|
backgroundColor: theme.palette.backgroundColor,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)
|
|
})}
|
|
</AvatarGroup>
|
|
</div>
|
|
</div>
|
|
|
|
{buttonState === "timeline" ?
|
|
<TimelineRender agent_data={data} />
|
|
:
|
|
null
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AgentUI
|