Merge branch 'main' into pr-feat-i18n
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 162 KiB |
|
|
@ -231,6 +231,7 @@ const ToolSelect = forwardRef<
|
|||
|
||||
// select management
|
||||
const addOption = (item: McpItem, isLocal?: boolean) => {
|
||||
setKeyword("");
|
||||
const currentSelected = initialSelectedTools || [];
|
||||
console.log(currentSelected.find((i) => i.id === item.id));
|
||||
if (isLocal) {
|
||||
|
|
@ -245,6 +246,7 @@ const ToolSelect = forwardRef<
|
|||
const newSelected = [...currentSelected, { ...item, isLocal }];
|
||||
onSelectedToolsChange?.(newSelected);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const removeOption = (item: McpItem) => {
|
||||
|
|
@ -472,7 +474,7 @@ const ToolSelect = forwardRef<
|
|||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
ref={inputRef}
|
||||
className="bg-transparent border-none !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 w-10 !h-[20px] p-0"
|
||||
className="bg-transparent border-none !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 w-auto !h-[20px] p-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
CircleSlash,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState, useRef, useEffect } from "react";
|
||||
import { TaskState } from "../TaskState";
|
||||
import { TaskState, TaskStateType } from "../TaskState";
|
||||
|
||||
interface TaskCardProps {
|
||||
taskInfo: any[];
|
||||
|
|
@ -34,6 +34,9 @@ interface TaskCardProps {
|
|||
onAddTask: () => void;
|
||||
onUpdateTask: (taskIndex: number, content: string) => void;
|
||||
onDeleteTask: (taskIndex: number) => void;
|
||||
selectedStates?: TaskStateType[];
|
||||
onStateChange?: (selectedStates: TaskStateType[]) => void;
|
||||
clickable?: boolean;
|
||||
}
|
||||
|
||||
export function TaskCard({
|
||||
|
|
@ -45,6 +48,9 @@ export function TaskCard({
|
|||
onAddTask,
|
||||
onUpdateTask,
|
||||
onDeleteTask,
|
||||
selectedStates = [],
|
||||
onStateChange,
|
||||
clickable = true,
|
||||
}: TaskCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -141,11 +147,33 @@ export function TaskCard({
|
|||
<div className="flex items-center gap-2 ">
|
||||
{taskType === 1 && (
|
||||
<TaskState
|
||||
done={0}
|
||||
progress={
|
||||
taskInfo.filter((task) => task.content !== "").length || 0
|
||||
done={
|
||||
taskInfo.filter(
|
||||
(task) =>
|
||||
task.status === "completed" || task.status === "failed"
|
||||
).length || 0
|
||||
}
|
||||
skipped={0}
|
||||
progress={
|
||||
taskInfo.filter(
|
||||
(task) =>
|
||||
task.status !== "completed" &&
|
||||
task.status !== "failed" &&
|
||||
task.status !== "skipped" &&
|
||||
task.status !== "waiting" &&
|
||||
task.status !== ""
|
||||
).length || 0
|
||||
}
|
||||
skipped={
|
||||
taskInfo.filter(
|
||||
(task) =>
|
||||
task.status === "skipped" ||
|
||||
task.status === "waiting" ||
|
||||
task.status === ""
|
||||
).length || 0
|
||||
}
|
||||
selectedStates={selectedStates}
|
||||
onStateChange={onStateChange}
|
||||
clickable={clickable}
|
||||
/>
|
||||
)}
|
||||
{taskType !== 1 && (
|
||||
|
|
@ -162,13 +190,19 @@ export function TaskCard({
|
|||
task.status !== "completed" &&
|
||||
task.status !== "failed" &&
|
||||
task.status !== "skipped" &&
|
||||
task.content !== ""
|
||||
task.status !== "waiting" &&
|
||||
task.status !== ""
|
||||
).length || 0
|
||||
}
|
||||
skipped={
|
||||
taskRunning?.filter((task) => task.status === "skipped")
|
||||
.length || 0
|
||||
taskRunning?.filter(
|
||||
(task) =>
|
||||
task.status === "skipped" || task.status === "waiting" || task.status === ""
|
||||
).length || 0
|
||||
}
|
||||
selectedStates={selectedStates}
|
||||
onStateChange={onStateChange}
|
||||
clickable={clickable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -279,7 +313,7 @@ export function TaskCard({
|
|||
{task.status === "running" && (
|
||||
<LoaderCircle
|
||||
size={16}
|
||||
className={`text-icon-success ${
|
||||
className={`text-icon-information ${
|
||||
chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
].status === "running" && "animate-spin"
|
||||
|
|
@ -316,7 +350,7 @@ export function TaskCard({
|
|||
</div>
|
||||
<div className="flex-1 flex flex-col items-start justify-center">
|
||||
<div
|
||||
className={` w-full ${
|
||||
className={` w-full break-words [overflow-wrap:anywhere] whitespace-pre-line ${
|
||||
task.status === "failed"
|
||||
? "text-text-cuation-default"
|
||||
: task.status === "blocked"
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ import { proxyFetchGet } from "@/api/http";
|
|||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { NoticeCard } from "./NoticeCard";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { PrivacyDialog } from "../Dialog/Privacy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TaskStateType } from "../TaskState";
|
||||
|
||||
export default function ChatBox(): JSX.Element {
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [selectedStates, setSelectedStates] = useState<TaskStateType[]>([]);
|
||||
const chatStore = useChatStore();
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
|
@ -464,6 +465,9 @@ export default function ChatBox(): JSX.Element {
|
|||
);
|
||||
chatStore.deleteTaskInfo(taskIndex);
|
||||
}}
|
||||
selectedStates={selectedStates}
|
||||
onStateChange={setSelectedStates}
|
||||
clickable={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
39
src/components/Dialog/CloseNotice.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
export default function CloseNoticeDialog({open, onOpenChange, trigger}: Props) {
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
window.electronAPI.closeWindow(true)
|
||||
}, [])
|
||||
|
||||
return <Dialog open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||
<DialogContent className="sm:max-w-[600px] p-0 !bg-popup-surface gap-0 !rounded-xl border border-zinc-300 shadow-sm">
|
||||
<DialogHeader className="!bg-popup-surface !rounded-t-xl p-md">
|
||||
<DialogTitle className="m-0">
|
||||
Close notice
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-md bg-popup-bg p-md">
|
||||
A task is currently running. Exiting will terminate it. Are you sure you want to exit?
|
||||
</div>
|
||||
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost" size="md">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button size="md" onClick={onSubmit} variant="primary">
|
||||
Yes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
|
|
@ -337,21 +337,21 @@ export default function HistorySidebar() {
|
|||
className={`transition-all duration-300 flex justify-start items-center gap-1 px-sm py-xs bg-menutabs-bg-default hover:bg-white-100% rounded-lg border border-solid border-white-100% shadow-history-item ${
|
||||
agentMap[
|
||||
taskAssigning.type as keyof typeof agentMap
|
||||
].borderColor
|
||||
]?.borderColor
|
||||
}`}
|
||||
>
|
||||
<Bot
|
||||
className={`w-3 h-3 ${
|
||||
agentMap[
|
||||
taskAssigning.type as keyof typeof agentMap
|
||||
].textColor
|
||||
]?.textColor
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`${
|
||||
agentMap[
|
||||
taskAssigning.type as keyof typeof agentMap
|
||||
].textColor
|
||||
]?.textColor
|
||||
} text-xs leading-17 font-medium`}
|
||||
>
|
||||
{taskAssigning.name}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,32 @@ import { useAuthStore } from "@/store/authStore";
|
|||
import { useEffect, useState } from "react";
|
||||
import { AnimationJson } from "@/components/AnimationJson";
|
||||
import animationData from "@/assets/animation/onboarding_success.json";
|
||||
import CloseNoticeDialog from "../Dialog/CloseNotice";
|
||||
import { useChatStore } from "@/store/chatStore";
|
||||
const Layout = () => {
|
||||
const { initState, setInitState, isFirstLaunch, setIsFirstLaunch } =
|
||||
useAuthStore();
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const chatStore = useChatStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeClose = () => {
|
||||
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
|
||||
if(["pending", "running", "pause"].includes(currentStatus)) {
|
||||
setNoticeOpen(true);
|
||||
} else {
|
||||
window.electronAPI.closeWindow(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.ipcRenderer.on("before-close", handleBeforeClose);
|
||||
|
||||
return () => {
|
||||
window.ipcRenderer.removeAllListeners("before-close");
|
||||
};
|
||||
}, [chatStore.tasks, chatStore.activeTaskId]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkToolInstalled = async () => {
|
||||
// in render process
|
||||
|
|
@ -25,6 +47,7 @@ const Layout = () => {
|
|||
};
|
||||
checkToolInstalled();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
|
||||
|
|
@ -46,6 +69,10 @@ const Layout = () => {
|
|||
)}
|
||||
<Outlet />
|
||||
<HistorySidebar />
|
||||
<CloseNoticeDialog
|
||||
onOpenChange={setNoticeOpen}
|
||||
open={noticeOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ export default function Home() {
|
|||
{agentMap[activeAgent?.type as keyof typeof agentMap]?.name}
|
||||
</div>
|
||||
<TaskState
|
||||
all={activeAgent?.tasks?.length || 0}
|
||||
reAssignTo={activeAgent?.tasks?.filter((task) => task.reAssignTo).length || 0}
|
||||
done={
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) =>
|
||||
|
|
@ -213,11 +215,12 @@ export default function Home() {
|
|||
(task) =>
|
||||
task.status !== "failed" &&
|
||||
task.status !== "completed" &&
|
||||
task.status !== "skipped"
|
||||
task.status !== "skipped"&&
|
||||
task.status !== "waiting"
|
||||
).length || 0
|
||||
}
|
||||
skipped={
|
||||
activeAgent?.tasks?.filter((task) => task.status === "skipped")
|
||||
activeAgent?.tasks?.filter((task) => task.status === "skipped"||task.status==="waiting")
|
||||
.length || 0
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,186 @@
|
|||
import { CircleCheckBig, LoaderCircle } from "lucide-react";
|
||||
import { CircleCheckBig, CircleSlash2, LoaderCircle } from "lucide-react";
|
||||
import { useChatStore } from "@/store/chatStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export const TaskState = ({
|
||||
done,
|
||||
progress,
|
||||
skipped,
|
||||
}: {
|
||||
|
||||
export type TaskStateType =
|
||||
| "all"
|
||||
| "done"
|
||||
| "reassigned"
|
||||
| "ongoing"
|
||||
| "pending";
|
||||
|
||||
export interface TaskStateProps {
|
||||
all?: number;
|
||||
done: number;
|
||||
progress: number;
|
||||
skipped: number;
|
||||
}) => {
|
||||
reAssignTo?: number;
|
||||
selectedStates?: TaskStateType[];
|
||||
onStateChange?: (selectedStates: TaskStateType[]) => void;
|
||||
clickable?: boolean;
|
||||
}
|
||||
|
||||
export const TaskState = ({
|
||||
all,
|
||||
done,
|
||||
reAssignTo,
|
||||
progress,
|
||||
skipped,
|
||||
selectedStates = [],
|
||||
onStateChange,
|
||||
clickable = true,
|
||||
}: TaskStateProps) => {
|
||||
const chatStore = useChatStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleStateClick = (state: TaskStateType) => {
|
||||
if (!clickable || !onStateChange) return;
|
||||
|
||||
let newSelectedStates: TaskStateType[];
|
||||
|
||||
if (state === "all") {
|
||||
newSelectedStates = selectedStates.includes("all") ? [] : ["all"];
|
||||
} else {
|
||||
const otherStates = selectedStates.filter((s) => s !== "all");
|
||||
if (otherStates.includes(state)) {
|
||||
newSelectedStates = otherStates.filter((s) => s !== state);
|
||||
} else {
|
||||
newSelectedStates = [...otherStates, state];
|
||||
}
|
||||
}
|
||||
|
||||
onStateChange(newSelectedStates);
|
||||
};
|
||||
|
||||
const isSelected = (state: TaskStateType) => {
|
||||
return selectedStates.includes(state);
|
||||
};
|
||||
|
||||
const fadeWidthClass = (selected: boolean) =>
|
||||
`inline-block overflow-hidden align-bottom transition-all duration-300 ease-in-out
|
||||
${selected ? "max-w-[40px] opacity-100" : "max-w-0 opacity-0"}
|
||||
group-hover:max-w-[40px] group-hover:opacity-100`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="w-auto bg-transparent flex items-center gap-1">
|
||||
<div className="flex gap-1 items-center py-0.5">
|
||||
<CircleCheckBig className="w-4 h-4 text-icon-primary" />
|
||||
<span className="text-text-body text-xs leading-tight font-normal">
|
||||
{done} {t("chat.done")}
|
||||
<div className="w-auto bg-transparent flex items-center gap-1 flex-wrap">
|
||||
{/* All */}
|
||||
{all && (
|
||||
<div
|
||||
className={`group hover:bg-tag-surface flex gap-xs items-center py-0.5 px-2 transition-all duration-200 ${
|
||||
isSelected("all") ? "bg-tag-surface" : "bg-transparent"
|
||||
} ${clickable ? "cursor-pointer" : ""}`}
|
||||
onClick={() => handleStateClick("all")}
|
||||
>
|
||||
<span className="text-xs font-normal text-text-body">
|
||||
All{" "}
|
||||
<span className={fadeWidthClass(isSelected("all"))}>{all}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Done */}
|
||||
<div
|
||||
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 transition-all duration-200 ${
|
||||
isSelected("done") && "bg-tag-surface"
|
||||
} ${
|
||||
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
|
||||
}`}
|
||||
onClick={() => handleStateClick("done")}
|
||||
>
|
||||
<CircleCheckBig
|
||||
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-icon-success ${
|
||||
isSelected("done") && "text-icon-success"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`transition-all duration-200 text-xs leading-tight font-normal text-text-label group-hover:text-text-success ${
|
||||
isSelected("done") && "text-text-success"
|
||||
}`}
|
||||
>
|
||||
Done{" "}
|
||||
<span className={fadeWidthClass(isSelected("done"))}>{done}</span>
|
||||
</span>
|
||||
</div>
|
||||
{progress !== 0 && (
|
||||
<div className="flex gap-1 items-center py-0.5">
|
||||
<LoaderCircle
|
||||
className={`w-4 h-4 text-icon-success ${
|
||||
chatStore.tasks[chatStore.activeTaskId as string].status ===
|
||||
"running" && "animate-spin"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-text-success text-xs leading-tight font-normal">
|
||||
{progress} {t("chat.in-progress")}
|
||||
|
||||
{/* Reassigned */}
|
||||
{reAssignTo && <div
|
||||
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 transition-all duration-200 ${
|
||||
isSelected("reassigned") && "bg-tag-surface"
|
||||
} ${
|
||||
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
|
||||
}`}
|
||||
onClick={() => handleStateClick("reassigned")}
|
||||
>
|
||||
<CircleSlash2
|
||||
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-icon-warning ${
|
||||
isSelected("reassigned") && "text-icon-warning"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`transition-all duration-200 text-xs leading-tight font-normal text-text-label group-hover:text-text-warning ${
|
||||
isSelected("reassigned") && "text-text-warning"
|
||||
}`}
|
||||
>
|
||||
Reassigned{" "}
|
||||
<span className={fadeWidthClass(isSelected("reassigned"))}>
|
||||
{reAssignTo}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{skipped !== 0 && (
|
||||
<div className="flex gap-1 items-center py-0.5">
|
||||
<LoaderCircle
|
||||
className={`w-4 h-4 text-icon-secondary`}
|
||||
/>
|
||||
<span className="text-text-label text-xs leading-tight font-normal">
|
||||
{skipped} {t("chat.unfinished")}
|
||||
</span>
|
||||
</div>}
|
||||
|
||||
{/* Ongoing */}
|
||||
<div
|
||||
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 ${
|
||||
isSelected("ongoing") && "bg-tag-surface"
|
||||
} ${
|
||||
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
|
||||
}`}
|
||||
onClick={() => handleStateClick("ongoing")}
|
||||
>
|
||||
<LoaderCircle
|
||||
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-icon-information ${
|
||||
isSelected("ongoing") && "!text-icon-information"
|
||||
} ${
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.status ===
|
||||
"running" && "animate-spin"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`transition-all duration-200 text-xs leading-tight font-normal text-text-label group-hover:text-text-information ${
|
||||
isSelected("ongoing") && "!text-text-information"
|
||||
}`}
|
||||
>
|
||||
Ongoing{" "}
|
||||
<span className={fadeWidthClass(isSelected("ongoing"))}>
|
||||
{progress}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Pending */}
|
||||
<div
|
||||
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 ${
|
||||
isSelected("pending") ? "bg-tag-surface" : "bg-transparent"
|
||||
} ${
|
||||
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
|
||||
}`}
|
||||
onClick={() => handleStateClick("pending")}
|
||||
>
|
||||
<LoaderCircle
|
||||
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-primary-foreground ${
|
||||
isSelected("pending") && "text-primary-foreground"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-xs leading-tight font-normal text-text-label group-hover:text-primary-foreground ${
|
||||
isSelected("pending") && "text-primary-foreground"
|
||||
}`}
|
||||
>
|
||||
Pending{" "}
|
||||
<span className={fadeWidthClass(isSelected("pending"))}>
|
||||
{skipped}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export default function Workflow({
|
|||
|
||||
return prev.map((node) => {
|
||||
// calculate node width and position based on expansion state
|
||||
const nodeWidth = node.data.isExpanded ? 560 : 280;
|
||||
const nodeWidth = node.data.isExpanded ? 684 : 342;
|
||||
const newPosition = { x: currentX, y: node.position.y };
|
||||
currentX += nodeWidth + 20; // 20 is the spacing between nodes
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ export default function Workflow({
|
|||
};
|
||||
|
||||
// calculate node width and position based on expansion state
|
||||
const nodeWidth = updatedNode.data.isExpanded ? 560 : 280;
|
||||
const nodeWidth = updatedNode.data.isExpanded ? 684 : 342;
|
||||
const newPosition = { x: currentX, y: node.position.y };
|
||||
currentX += nodeWidth + 20; // 20 is the spacing between nodes
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ export default function Workflow({
|
|||
},
|
||||
position: isEditMode
|
||||
? node.position
|
||||
: { x: index * 300 + 8, y: 16 },
|
||||
: { x: index * (342+20) + 8, y: 16 },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
|
@ -259,7 +259,7 @@ export default function Workflow({
|
|||
isEditMode: isEditMode,
|
||||
workerInfo: agent?.workerInfo,
|
||||
},
|
||||
position: { x: index * 300 + 8, y: 16 },
|
||||
position: { x: index * (342+20) + 8, y: 16 },
|
||||
type: "node",
|
||||
};
|
||||
}
|
||||
|
|
@ -303,7 +303,7 @@ export default function Workflow({
|
|||
<div className="text-text-body font-bold text-lg leading-relaxed">
|
||||
{t("workforce.your-ai-workforce")}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-sm ">
|
||||
<div className="flex items-center justify-center gap-sm">
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Trash2,
|
||||
Edit,
|
||||
SquareChevronLeft,
|
||||
CircleSlash2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Folder from "../Folder";
|
||||
|
|
@ -27,7 +28,7 @@ import ShinyText from "../ui/ShinyText/ShinyText";
|
|||
import { MarkDown } from "./MarkDown";
|
||||
import { Tooltip, TooltipTrigger } from "../ui/tooltip";
|
||||
import { TooltipContent } from "@radix-ui/react-tooltip";
|
||||
import { TaskState } from "../TaskState";
|
||||
import { TaskState, TaskStateType } from "../TaskState";
|
||||
import {
|
||||
Popover,
|
||||
PopoverClose,
|
||||
|
|
@ -58,6 +59,46 @@ interface NodeProps {
|
|||
export function Node({ id, data }: NodeProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(data.isExpanded);
|
||||
const [selectedTask, setSelectedTask] = useState<any>(null);
|
||||
const [selectedStates, setSelectedStates] = useState<TaskStateType[]>(['all']);
|
||||
|
||||
const [filterTasks, setFilterTasks] = useState<any[]>([]);
|
||||
useEffect(() => {
|
||||
const tasks = data.agent?.tasks || [];
|
||||
|
||||
if (selectedStates.includes("all") || selectedStates.length === 0) {
|
||||
setFilterTasks(tasks);
|
||||
} else {
|
||||
const newFiltered = tasks.filter((task) => {
|
||||
return selectedStates.some((state) => {
|
||||
switch (state) {
|
||||
case "done":
|
||||
return (task.status === "completed" || task.status === "failed") && !task.reAssignTo;
|
||||
case "reassigned":
|
||||
return !!task.reAssignTo;
|
||||
case "ongoing":
|
||||
return (
|
||||
task.status !== "failed" &&
|
||||
task.status !== "completed" &&
|
||||
task.status !== "skipped" &&
|
||||
task.status !== "waiting" &&
|
||||
task.status !== "" &&
|
||||
!task.reAssignTo
|
||||
);
|
||||
case "pending":
|
||||
return (
|
||||
(task.status === "skipped" ||
|
||||
task.status === "waiting" ||
|
||||
task.status === "") &&
|
||||
!task.reAssignTo
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
setFilterTasks(newFiltered);
|
||||
}
|
||||
}, [selectedStates, data.agent?.tasks]);
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { setCenter, getNode, setViewport, setNodes } = useReactFlow();
|
||||
|
|
@ -72,7 +113,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
// manually control node size
|
||||
useEffect(() => {
|
||||
if (data.isEditMode) {
|
||||
const targetWidth = isExpanded ? 560 : 280;
|
||||
const targetWidth = isExpanded ? 684 : 342;
|
||||
const targetHeight = 600;
|
||||
|
||||
setNodes((nodes) =>
|
||||
|
|
@ -251,15 +292,15 @@ export function Node({ id, data }: NodeProps) {
|
|||
const list = taskId.split(".");
|
||||
let idStr = "";
|
||||
list.shift();
|
||||
list.map((i: string) => {
|
||||
idStr += Number(i) + ".";
|
||||
list.map((i: string, index: number) => {
|
||||
idStr += Number(i) + (index === list.length - 1 ? "" : ".");
|
||||
});
|
||||
return idStr;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<NodeResizer
|
||||
minWidth={isExpanded ? 560 : 280}
|
||||
minWidth={isExpanded ? 684 : 342}
|
||||
minHeight={300}
|
||||
isVisible={data.isEditMode}
|
||||
keepAspectRatio={false}
|
||||
|
|
@ -276,10 +317,10 @@ export function Node({ id, data }: NodeProps) {
|
|||
ref={nodeRef}
|
||||
className={`${
|
||||
data.isEditMode
|
||||
? `w-full ${isExpanded ? "min-w-[560px]" : "min-w-[280px]"}`
|
||||
? `w-full ${isExpanded ? "min-w-[560px]" : "min-w-[342px]"}`
|
||||
: isExpanded
|
||||
? "w-[560px]"
|
||||
: "w-[280px]"
|
||||
? "w-[684px]"
|
||||
: "w-[342px]"
|
||||
} ${
|
||||
data.isEditMode ? "h-full" : "max-h-[calc(100vh-200px)]"
|
||||
} border-worker-border-default flex border border-solid rounded-xl overflow-hidden bg-worker-surface-primary ${
|
||||
|
|
@ -292,7 +333,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
>
|
||||
<div
|
||||
className={`py-2 px-3 pr-0 flex flex-col ${
|
||||
data.isEditMode ? "flex-1 min-w-[280px]" : "w-[280px] "
|
||||
data.isEditMode ? "flex-1 min-w-[342px]" : "w-[342px] "
|
||||
}`}
|
||||
>
|
||||
<div className=" flex items-center justify-between gap-sm pr-3">
|
||||
|
|
@ -447,28 +488,43 @@ export function Node({ id, data }: NodeProps) {
|
|||
</div>
|
||||
{data.agent?.tasks && data.agent?.tasks.length > 0 && (
|
||||
<div className="flex flex-col items-start justify-between gap-1 pt-sm border-[0px] border-t border-solid border-task-border-default pr-3">
|
||||
<div className="font-bold leading-tight text-xs">Subtasks</div>
|
||||
{/* <div className="font-bold leading-tight text-xs">Subtasks</div> */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<TaskState
|
||||
all={data.agent.tasks?.length || 0}
|
||||
done={
|
||||
data.agent?.tasks?.filter(
|
||||
(task) =>
|
||||
task.status === "failed" || task.status === "completed"
|
||||
(task.status === "failed" || task.status === "completed") && !task.reAssignTo
|
||||
).length || 0
|
||||
}
|
||||
reAssignTo={
|
||||
data.agent.tasks?.filter((task) => task.reAssignTo)
|
||||
?.length || 0
|
||||
}
|
||||
progress={
|
||||
data.agent?.tasks?.filter(
|
||||
(task) =>
|
||||
task.status !== "failed" &&
|
||||
task.status !== "completed" &&
|
||||
task.status !== "skipped"
|
||||
task.status !== "skipped" &&
|
||||
task.status !== "waiting" &&
|
||||
task.status !== "" &&
|
||||
!task.reAssignTo
|
||||
).length || 0
|
||||
}
|
||||
skipped={
|
||||
data.agent?.tasks?.filter(
|
||||
(task) => task.status === "skipped"
|
||||
(task) =>
|
||||
(task.status === "skipped" ||
|
||||
task.status === "waiting" ||
|
||||
task.status === "") &&
|
||||
!task.reAssignTo
|
||||
).length || 0
|
||||
}
|
||||
selectedStates={selectedStates}
|
||||
onStateChange={setSelectedStates}
|
||||
clickable={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -489,7 +545,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
}}
|
||||
>
|
||||
{data.agent?.tasks &&
|
||||
data.agent?.tasks.map((task, index) => {
|
||||
filterTasks.map((task, index) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
|
|
@ -510,7 +566,9 @@ export function Node({ id, data }: NodeProps) {
|
|||
}}
|
||||
key={`taskList-${task.id}-${task.failure_count}`}
|
||||
className={`rounded-lg flex gap-2 py-sm px-sm transition-all duration-300 ease-in-out animate-in fade-in-0 slide-in-from-left-2 ${
|
||||
task.status === "completed"
|
||||
task.reAssignTo
|
||||
? "bg-task-fill-warning"
|
||||
: task.status === "completed"
|
||||
? "bg-green-50"
|
||||
: task.status === "failed"
|
||||
? "bg-task-fill-error"
|
||||
|
|
@ -543,53 +601,89 @@ export function Node({ id, data }: NodeProps) {
|
|||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="pt-0.5">
|
||||
{task.status === "running" && (
|
||||
<LoaderCircle
|
||||
size={16}
|
||||
className={`text-icon-success ${
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.status === "running" && "animate-spin"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{task.status === "skipped" && (
|
||||
<LoaderCircle
|
||||
size={16}
|
||||
className={`text-icon-secondary `}
|
||||
/>
|
||||
)}
|
||||
{task.status === "completed" && (
|
||||
<CircleCheckBig
|
||||
size={16}
|
||||
className="text-icon-success"
|
||||
/>
|
||||
)}
|
||||
{task.status === "failed" && (
|
||||
<CircleSlash size={16} className="text-icon-cuation" />
|
||||
)}
|
||||
{task.status === "blocked" && (
|
||||
<TriangleAlert
|
||||
size={16}
|
||||
className="text-icon-warning"
|
||||
/>
|
||||
)}
|
||||
{(task.status === "" || task.status === "waiting") && (
|
||||
<Circle size={16} className="text-slate-400" />
|
||||
<div className="">
|
||||
{task.reAssignTo ? (
|
||||
// reassign to other agent
|
||||
<CircleSlash2 size={16} className="text-icon-warning" />
|
||||
) : (
|
||||
// normal task
|
||||
<>
|
||||
{task.status === "running" && (
|
||||
<LoaderCircle
|
||||
size={16}
|
||||
className={`text-icon-information ${
|
||||
chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
].status === "running" && "animate-spin"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{task.status === "skipped" && (
|
||||
<LoaderCircle
|
||||
size={16}
|
||||
className={`text-icon-secondary `}
|
||||
/>
|
||||
)}
|
||||
{task.status === "completed" && (
|
||||
<CircleCheckBig
|
||||
size={16}
|
||||
className="text-icon-success"
|
||||
/>
|
||||
)}
|
||||
{task.status === "failed" && (
|
||||
<CircleSlash
|
||||
size={16}
|
||||
className="text-icon-cuation"
|
||||
/>
|
||||
)}
|
||||
{task.status === "blocked" && (
|
||||
<TriangleAlert
|
||||
size={16}
|
||||
className="text-icon-warning"
|
||||
/>
|
||||
)}
|
||||
{(task.status === "" ||
|
||||
task.status === "waiting") && (
|
||||
<Circle size={16} className="text-slate-400" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-start justify-center">
|
||||
<div
|
||||
className={` w-full flex-grow-0 ${
|
||||
className={`w-full flex-grow-0 ${
|
||||
task.status === "failed"
|
||||
? "text-text-cuation-default"
|
||||
: task.status === "blocked"
|
||||
? "text-text-body"
|
||||
: "text-text-primary"
|
||||
} text-sm font-medium leading-13 select-text pointer-events-auto break-all text-wrap whitespace-pre-line`}
|
||||
} text-xs font-medium leading-13 select-text pointer-events-auto break-all text-wrap whitespace-pre-line`}
|
||||
>
|
||||
{getTaskId(task.id)}
|
||||
{task.content}
|
||||
<div className="flex items-center gap-sm">
|
||||
<div className="text-text-body text-xs font-bold leading-13">
|
||||
No. {getTaskId(task.id)}
|
||||
</div>
|
||||
{task.reAssignTo ? (
|
||||
<div className="text-text-warning text-xs font-bold leading-none rounded-lg px-1 py-0.5 bg-tag-fill-document">
|
||||
Reassigned to {task.reAssignTo}
|
||||
</div>
|
||||
) : (
|
||||
(task.failure_count ?? 0) > 0 && (
|
||||
<div
|
||||
className={`${
|
||||
task.status === "failed"
|
||||
? "bg-red-100 text-text-cuation"
|
||||
: task.status === "completed"
|
||||
? "bg-tag-fill-developer text-text-success-default"
|
||||
: "bg-tag-surface-hover text-text-label"
|
||||
} text-xs font-bold leading-none rounded-lg px-1 py-0.5`}
|
||||
>
|
||||
Attempt {task.failure_count}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>{task.content}</div>
|
||||
</div>
|
||||
{task?.status === "running" && (
|
||||
<div className="flex items-center gap-2 mt-xs animate-in fade-in-0 slide-in-from-bottom-2 duration-400">
|
||||
|
|
@ -597,7 +691,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
{task.toolkits &&
|
||||
task.toolkits.length > 0 &&
|
||||
task.toolkits
|
||||
.filter((tool) => tool.toolkitName !== "notice")
|
||||
.filter((tool: any) => tool.toolkitName !== "notice")
|
||||
.at(-1)?.toolkitStatus === "running" && (
|
||||
<div className="flex-1 min-w-0 flex justify-start items-center gap-sm animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
{agentMap[data.type]?.icon ?? (
|
||||
|
|
@ -621,11 +715,6 @@ export function Node({ id, data }: NodeProps) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{(task.failure_count ?? 0) > 0&& (
|
||||
<div className="text-text-cuation-default text-xs leading-17">
|
||||
retry {task.failure_count} times
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -636,7 +725,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
<div
|
||||
key={selectedTask?.id || "empty"}
|
||||
className={`${
|
||||
data.isEditMode ? "flex-1" : "w-[280px]"
|
||||
data.isEditMode ? "flex-1" : "w-[342px]"
|
||||
} flex flex-col gap-sm border-l bg-worker-surface-secondary rounded-r-xl px-sm pr-0 py-3 pt-sm animate-in fade-in-0 slide-in-from-right-2 duration-300 `}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -344,21 +344,21 @@ export default function Home() {
|
|||
className={`transition-all duration-300 flex justify-start items-center gap-1 px-sm py-xs bg-menutabs-bg-default rounded-lg border border-solid border-white-100% ${
|
||||
agentMap[
|
||||
taskAssigning.type as keyof typeof agentMap
|
||||
].borderColor
|
||||
]?.borderColor
|
||||
}`}
|
||||
>
|
||||
<Bot
|
||||
className={`w-3 h-3 ${
|
||||
agentMap[
|
||||
taskAssigning.type as keyof typeof agentMap
|
||||
].textColor
|
||||
]?.textColor
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`${
|
||||
agentMap[
|
||||
taskAssigning.type as keyof typeof agentMap
|
||||
].textColor
|
||||
]?.textColor
|
||||
} text-xs leading-17 font-medium`}
|
||||
>
|
||||
{taskAssigning.name}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useAuthStore } from "@/store/authStore";
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { ConfigFile } from "electron/main/utils/mcpConfig";
|
||||
|
||||
export default function SettingMCP() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -197,13 +198,28 @@ export default function SettingMCP() {
|
|||
setSaving(true);
|
||||
setErrorMsg(null);
|
||||
try {
|
||||
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, {
|
||||
const mcpData = {
|
||||
mcp_name: configForm.mcp_name,
|
||||
mcp_desc: configForm.mcp_desc,
|
||||
command: configForm.command,
|
||||
args: arrayToArgsJson(configForm.argsArr),
|
||||
env: configForm.env,
|
||||
});
|
||||
}
|
||||
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, mcpData);
|
||||
|
||||
if (window.ipcRenderer) {
|
||||
//Partial payload to empty env {}
|
||||
const payload: any = {
|
||||
description: configForm.mcp_desc,
|
||||
command: configForm.command,
|
||||
args: arrayToArgsJson(configForm.argsArr),
|
||||
};
|
||||
if (configForm.env && Object.keys(configForm.env).length > 0) {
|
||||
payload.env = configForm.env;
|
||||
}
|
||||
window.ipcRenderer.invoke("mcp-update", mcpData.mcp_name, payload);
|
||||
}
|
||||
|
||||
setShowConfig(null);
|
||||
fetchList();
|
||||
} catch (err: any) {
|
||||
|
|
@ -238,9 +254,27 @@ export default function SettingMCP() {
|
|||
setInstalling(true);
|
||||
try {
|
||||
if (addType === "local") {
|
||||
let data;
|
||||
let data:ConfigFile;
|
||||
try {
|
||||
data = JSON.parse(localJson);
|
||||
|
||||
// validate mcpServers structure
|
||||
if (!data.mcpServers || typeof data.mcpServers !== "object") {
|
||||
throw new Error("Invalid mcpServers");
|
||||
}
|
||||
|
||||
// check for name conflicts with existing items
|
||||
const serverNames = Object.keys(data.mcpServers);
|
||||
const conflict = serverNames.find((name) =>
|
||||
items.some((d) => d.mcp_name === name)
|
||||
);
|
||||
if (conflict) {
|
||||
toast.error(`MCP server "${conflict}" already exists`, {
|
||||
closeButton: true,
|
||||
});
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(t("setting.invalid-json"), { closeButton: true });
|
||||
setInstalling(false);
|
||||
|
|
@ -254,19 +288,14 @@ export default function SettingMCP() {
|
|||
}
|
||||
if (window.ipcRenderer) {
|
||||
const mcpServers = data["mcpServers"];
|
||||
Object.entries(mcpServers).forEach(async ([key, value]) => {
|
||||
for (const [key, value] of Object.entries(mcpServers)) {
|
||||
await window.ipcRenderer.invoke("mcp-install", key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowAdd(false);
|
||||
setLocalJson(`{
|
||||
"mcp_id": 0,
|
||||
"mcp_name": "",
|
||||
"mcp_desc": "",
|
||||
"command": "",
|
||||
"args": "",
|
||||
"env": {}
|
||||
"mcpServers": {}
|
||||
}`);
|
||||
setRemoteName("");
|
||||
setRemoteUrl("");
|
||||
|
|
@ -337,13 +366,13 @@ export default function SettingMCP() {
|
|||
{!isLoading && !error && items.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-400">{t("setting.no-mcp-servers")}</div>
|
||||
)}
|
||||
<MCPList
|
||||
{!isLoading && <MCPList
|
||||
items={items}
|
||||
onSetting={setShowConfig}
|
||||
onDelete={setDeleteTarget}
|
||||
onSwitch={handleSwitch}
|
||||
switchLoading={switchLoading}
|
||||
/>
|
||||
/>}
|
||||
<MCPConfigDialog
|
||||
open={!!showConfig}
|
||||
form={configForm}
|
||||
|
|
|
|||
|
|
@ -684,9 +684,6 @@ export default function SettingModels() {
|
|||
<SelectItem value="gpt-5">GPT-5</SelectItem>
|
||||
<SelectItem value="gpt-5-mini">GPT-5 mini</SelectItem>
|
||||
<SelectItem value="gpt-5-nano">GPT-5 nano</SelectItem>
|
||||
<SelectItem value="claude-opus-4-1-20250805">
|
||||
Claude Opus 4.1
|
||||
</SelectItem>
|
||||
<SelectItem value="claude-sonnet-4-20250514">
|
||||
Claude Sonnet 4
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function MCPConfigDialog({ open, form, mcp, onChange, onSave, onC
|
|||
<form onSubmit={onSave} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("setting.name")}</label>
|
||||
<input autoComplete="off" className="w-full border rounded px-3 py-2 text-sm" value={form.mcp_name} onChange={e => onChange({ ...form, mcp_name: e.target.value })} disabled={loading} />
|
||||
<input autoComplete="off" className="w-full border rounded px-3 py-2 text-sm" value={form.mcp_name} onChange={e => onChange({ ...form, mcp_name: e.target.value })} disabled readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("setting.description")}</label>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ interface MCPListProps {
|
|||
export default function MCPList({ items, onSetting, onDelete, onSwitch, switchLoading }: MCPListProps) {
|
||||
return (
|
||||
<div className='pt-4'>
|
||||
{items.map(item => (
|
||||
{items.map((item) => (
|
||||
<MCPListItem
|
||||
key={item.mcp_id}
|
||||
key={item.id}
|
||||
item={item}
|
||||
onSetting={onSetting}
|
||||
onDelete={onDelete}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,37 @@
|
|||
export function parseArgsToArray(args: string): string[] {
|
||||
try {
|
||||
// Try parsing as JSON array first
|
||||
const arr = JSON.parse(args);
|
||||
if (Array.isArray(arr)) return arr.map(String);
|
||||
} catch { }
|
||||
|
||||
// Handle malformed JSON by manually trimming { } and trying again
|
||||
if (args.trim().startsWith('{') && args.trim().endsWith('}')) {
|
||||
const trimmed = args.trim().slice(1, -1); // Remove { }
|
||||
try {
|
||||
// Try parsing the trimmed version as JSON array
|
||||
const arr = JSON.parse(`[${trimmed}]`);
|
||||
if (Array.isArray(arr)) return arr.map(String);
|
||||
} catch { }
|
||||
|
||||
// If still fails, treat as comma-separated
|
||||
if (trimmed.trim()) {
|
||||
return trimmed.split(',').map(arg => arg.trim()).filter(arg => arg !== '');
|
||||
}
|
||||
}
|
||||
|
||||
// If not JSON, treat as comma-separated string
|
||||
if (args.trim()) {
|
||||
return args.split(',').map(arg => arg.trim()).filter(arg => arg !== '');
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function arrayToArgsJson(arr: string[]): string {
|
||||
return JSON.stringify(arr.filter(v => v.trim() !== ''));
|
||||
const filtered = arr.filter(v => v.trim() !== '');
|
||||
if (filtered.length === 0) return '';
|
||||
|
||||
// Return as JSON stringified array
|
||||
return JSON.stringify(filtered);
|
||||
}
|
||||
|
|
@ -450,7 +450,7 @@ const chatStore = create<ChatStore>()(
|
|||
let taskRunning = [...tasks[taskId].taskRunning]
|
||||
let taskAssigning = [...tasks[taskId].taskAssigning]
|
||||
const targetTaskIndex = taskRunning.findIndex((task) => task.id === task_id)
|
||||
const targetTaskAssigningIndex = taskAssigning.findIndex((agent) => agent.tasks.find((task: TaskInfo) => task.id === task_id && (task.failure_count == 0 || !task.failure_count)))
|
||||
const targetTaskAssigningIndex = taskAssigning.findIndex((agent) => agent.tasks.find((task: TaskInfo) => task.id === task_id && !task.reAssignTo))
|
||||
if (targetTaskAssigningIndex !== -1) {
|
||||
const taskIndex = taskAssigning[targetTaskAssigningIndex].tasks.findIndex((task: TaskInfo) => task.id === task_id)
|
||||
taskAssigning[targetTaskAssigningIndex].tasks[taskIndex].status = state === "DONE" ? "completed" : "failed";
|
||||
|
|
@ -484,20 +484,19 @@ const chatStore = create<ChatStore>()(
|
|||
content: targetResult,
|
||||
step: "failed",
|
||||
})
|
||||
setStatus(taskId, 'pause')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (targetTaskIndex !== -1) {
|
||||
|
||||
console.log("targetTaskIndex", targetTaskIndex,state)
|
||||
taskRunning[targetTaskIndex].status = state === "DONE" ? "completed" : "failed";
|
||||
}
|
||||
setTaskRunning(taskId, taskRunning)
|
||||
setTaskAssigning(taskId, taskAssigning)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Activate agent
|
||||
if (agentMessages.step === "activate_agent" || agentMessages.step === "deactivate_agent") {
|
||||
let taskAssigning = [...tasks[taskId].taskAssigning]
|
||||
|
|
@ -540,7 +539,6 @@ const chatStore = create<ChatStore>()(
|
|||
setTaskAssigning(taskId, [...taskAssigning]);
|
||||
}
|
||||
if (agentMessages.step === "deactivate_agent") {
|
||||
taskAssigning[agentIndex].status = "completed";
|
||||
if (message) {
|
||||
const index = taskAssigning[agentIndex].log.findLastIndex((log) => log.data.method_name === agentMessages.data.method_name && log.data.toolkit_name === agentMessages.data.toolkit_name)
|
||||
if (index != -1) {
|
||||
|
|
@ -549,12 +547,11 @@ const chatStore = create<ChatStore>()(
|
|||
}
|
||||
|
||||
}
|
||||
// const taskIndex = taskRunning!.findLastIndex((task) => task.agent?.agent_id === agent_id && task.status !== 'completed' && task.status !== 'failed');
|
||||
const taskIndex = taskRunning.findIndex((task) => task.id === process_task_id);
|
||||
if (taskIndex !== -1) {
|
||||
taskRunning![taskIndex].agent!.status = "completed";
|
||||
taskRunning![taskIndex]!.status = "completed";
|
||||
}
|
||||
// const taskIndex = taskRunning.findIndex((task) => task.id === process_task_id);
|
||||
// if (taskIndex !== -1) {
|
||||
// taskRunning![taskIndex].agent!.status = "completed";
|
||||
// taskRunning![taskIndex]!.status = "completed";
|
||||
// }
|
||||
|
||||
|
||||
if (!type && historyId) {
|
||||
|
|
@ -580,11 +577,12 @@ const chatStore = create<ChatStore>()(
|
|||
if (agentMessages.step === "assign_task") {
|
||||
if (!agentMessages.data?.assignee_id || !agentMessages.data?.task_id) return;
|
||||
|
||||
const { assignee_id, task_id, content = "", state: taskState } = agentMessages.data as any;
|
||||
const { assignee_id, task_id, content = "", state: taskState, failure_count } = agentMessages.data as any;
|
||||
let taskAssigning = [...tasks[taskId].taskAssigning]
|
||||
let taskRunning = [...tasks[taskId].taskRunning]
|
||||
let taskInfo = [...tasks[taskId].taskInfo]
|
||||
|
||||
// Find the index of the agent corresponding to assignee_id
|
||||
const assigneeAgentIndex = taskAssigning!.findIndex((agent: Agent) => agent.agent_id === assignee_id);
|
||||
// Find task corresponding to task_id
|
||||
const task = taskInfo!.find((task: TaskInfo) => task.id === task_id);
|
||||
|
|
@ -594,6 +592,26 @@ const chatStore = create<ChatStore>()(
|
|||
if (assigneeAgentIndex === -1) return;
|
||||
const taskAgent = taskAssigning![assigneeAgentIndex];
|
||||
|
||||
// Find the agent to reassign the task to
|
||||
const target = taskAssigning
|
||||
.map((agent, agentIndex) => {
|
||||
if (agent.agent_id === assignee_id) return null
|
||||
|
||||
const taskIndex = agent.tasks.findIndex(
|
||||
(task: TaskInfo) => task.id === task_id && !task.reAssignTo
|
||||
)
|
||||
|
||||
return taskIndex !== -1 ? { agentIndex, taskIndex } : null
|
||||
})
|
||||
.find(Boolean)
|
||||
|
||||
if (target) {
|
||||
const { agentIndex, taskIndex } = target
|
||||
const agentName = taskAssigning.find((agent: Agent) => agent.agent_id === assignee_id)?.name
|
||||
taskAssigning[agentIndex].tasks[taskIndex].reAssignTo = agentName
|
||||
}
|
||||
|
||||
|
||||
// If the state is "waiting", only mark it in the agent's task list and do not add it to taskRunning
|
||||
if (taskState === "waiting") {
|
||||
if (!taskAssigning[assigneeAgentIndex].tasks.find(item => item.id === task_id)) {
|
||||
|
|
@ -607,10 +625,13 @@ const chatStore = create<ChatStore>()(
|
|||
if (taskAssigning && taskAssigning[assigneeAgentIndex]) {
|
||||
// Check if task already exists in the agent's task list
|
||||
const existingTaskIndex = taskAssigning[assigneeAgentIndex].tasks.findIndex(item => item.id === task_id);
|
||||
|
||||
|
||||
if (existingTaskIndex !== -1) {
|
||||
// Task already exists, update its status
|
||||
taskAssigning[assigneeAgentIndex].tasks[existingTaskIndex].status = "running";
|
||||
if (failure_count !== 0) {
|
||||
taskAssigning[assigneeAgentIndex].tasks[existingTaskIndex].failure_count = failure_count;
|
||||
}
|
||||
} else {
|
||||
// Task doesn't exist, add it
|
||||
let taskTemp = null
|
||||
|
|
@ -624,7 +645,7 @@ const chatStore = create<ChatStore>()(
|
|||
taskAssigning[assigneeAgentIndex].tasks.push(taskTemp ?? { id: task_id, content, status: "running", });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Only update or add to taskRunning, never duplicate
|
||||
if (taskRunningIndex === -1) {
|
||||
// Task not in taskRunning, add it
|
||||
|
|
@ -640,7 +661,6 @@ const chatStore = create<ChatStore>()(
|
|||
// Task already in taskRunning, update it
|
||||
taskRunning![taskRunningIndex] = {
|
||||
...taskRunning![taskRunningIndex],
|
||||
content,
|
||||
status: "",
|
||||
agent: JSON.parse(JSON.stringify(taskAgent)),
|
||||
};
|
||||
|
|
@ -1638,7 +1658,17 @@ const chatStore = create<ChatStore>()(
|
|||
clearTasks: () => {
|
||||
const { create } = get()
|
||||
console.log('clearTasks')
|
||||
fetchDelete('/task/stop-all')
|
||||
|
||||
window.ipcRenderer.invoke('restart-backend')
|
||||
.then((res) => {
|
||||
console.log('restart-backend', res)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error in clearTasks cleanup:', error)
|
||||
})
|
||||
|
||||
|
||||
// Immediately create new task to maintain UI responsiveness
|
||||
const newTaskId = create()
|
||||
set((state) => ({
|
||||
...state,
|
||||
|
|
|
|||
1
src/types/chatbox.d.ts
vendored
|
|
@ -16,6 +16,7 @@ declare global {
|
|||
toolkitStatus?: AgentStatus;
|
||||
}[];
|
||||
failure_count?: number;
|
||||
reAssignTo?:string;
|
||||
}
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
|
|
|
|||