mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 12:44:45 +00:00
Merge branch 'main' into edge-case-ui-fix
This commit is contained in:
commit
92bd3ac3bf
9 changed files with 498 additions and 201 deletions
|
|
@ -80,6 +80,7 @@ def agent_model(
|
|||
for attr in config_attrs:
|
||||
effective_config[attr] = getattr(options, attr)
|
||||
extra_params = options.extra_params or {}
|
||||
|
||||
init_param_keys = {
|
||||
"api_version",
|
||||
"azure_ad_token",
|
||||
|
|
@ -135,6 +136,12 @@ def agent_model(
|
|||
)
|
||||
model_platform_enum = None
|
||||
|
||||
if effective_config["model_platform"].lower() == "anthropic":
|
||||
if model_config.get("cache_control") is None:
|
||||
model_config["cache_control"] = "5m"
|
||||
if model_config.get("max_tokens") is None:
|
||||
model_config["max_tokens"] = 64000
|
||||
|
||||
model = ModelFactory.create(
|
||||
model_platform=effective_config["model_platform"],
|
||||
model_type=effective_config["model_type"],
|
||||
|
|
|
|||
|
|
@ -227,6 +227,10 @@ def create_agent(
|
|||
raise ValueError(f"Invalid model_type: {model_type}")
|
||||
if platform is None:
|
||||
raise ValueError(f"Invalid model_platform: {model_platform}")
|
||||
if str(platform).lower() == "anthropic":
|
||||
model_config_dict = dict(model_config_dict or {})
|
||||
if model_config_dict.get("max_tokens") is None:
|
||||
model_config_dict["max_tokens"] = 4096
|
||||
model = ModelFactory.create(
|
||||
model_platform=platform,
|
||||
model_type=mtype,
|
||||
|
|
@ -326,6 +330,10 @@ def validate_model_with_details(
|
|||
"Creating model",
|
||||
extra={"platform": model_platform, "model_type": model_type},
|
||||
)
|
||||
if str(model_platform).lower() == "anthropic":
|
||||
model_config_dict = dict(model_config_dict or {})
|
||||
if model_config_dict.get("max_tokens") is None:
|
||||
model_config_dict["max_tokens"] = 4096
|
||||
model = ModelFactory.create(
|
||||
model_platform=model_platform,
|
||||
model_type=model_type,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ logging.getLogger("camel").setLevel(logging.WARNING)
|
|||
logging.getLogger("camel.base_model").setLevel(logging.WARNING)
|
||||
logging.getLogger("camel.agents").setLevel(logging.WARNING)
|
||||
logging.getLogger("camel.societies").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
from app import api
|
||||
from app.component.environment import env
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ interface FileInfo {
|
|||
relativePath: string;
|
||||
task_id?: string;
|
||||
project_id?: string;
|
||||
source?: 'project_output' | 'camel_log';
|
||||
}
|
||||
|
||||
export class FileReader {
|
||||
|
|
@ -658,60 +659,13 @@ export class FileReader {
|
|||
}
|
||||
}
|
||||
|
||||
public getFileList(
|
||||
email: string,
|
||||
taskId: string,
|
||||
projectId?: string
|
||||
): FileInfo[] {
|
||||
const safeEmail = email
|
||||
.split('@')[0]
|
||||
.replace(/[\\/*?:"<>|\s]/g, '_')
|
||||
.replace(/^\.+|\.+$/g, '');
|
||||
const userHome = app.getPath('home');
|
||||
|
||||
let dirPath: string;
|
||||
|
||||
// Check if projectId is provided for new project-based structure
|
||||
if (projectId) {
|
||||
dirPath = path.join(
|
||||
userHome,
|
||||
'eigent',
|
||||
safeEmail,
|
||||
`project_${projectId}`,
|
||||
`task_${taskId}`
|
||||
);
|
||||
} else {
|
||||
// First try project-based structure (scan for existing projects)
|
||||
const userDir = path.join(userHome, 'eigent', safeEmail);
|
||||
const projectBasedPath = this.findTaskInProjects(userDir, taskId);
|
||||
|
||||
if (projectBasedPath) {
|
||||
dirPath = projectBasedPath;
|
||||
} else {
|
||||
// Fallback to legacy direct task structure
|
||||
dirPath = path.join(userHome, 'eigent', safeEmail, `task_${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.getFilesRecursive(dirPath, dirPath);
|
||||
} catch (err) {
|
||||
console.error('Load file failed:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public deleteTaskFiles(
|
||||
private resolveTaskPaths(
|
||||
email: string,
|
||||
taskId: string,
|
||||
projectId?: string
|
||||
): {
|
||||
success: boolean;
|
||||
path: { dirPath: string; logPath: string };
|
||||
dirPath: string;
|
||||
logPath: string;
|
||||
} {
|
||||
const safeEmail = email
|
||||
.split('@')[0]
|
||||
|
|
@ -722,7 +676,6 @@ export class FileReader {
|
|||
let dirPath: string;
|
||||
let logPath: string;
|
||||
|
||||
// Check if projectId is provided for new project-based structure
|
||||
if (projectId) {
|
||||
dirPath = path.join(
|
||||
userHome,
|
||||
|
|
@ -738,33 +691,85 @@ export class FileReader {
|
|||
`project_${projectId}`,
|
||||
`task_${taskId}`
|
||||
);
|
||||
} else {
|
||||
// First try project-based structure
|
||||
const userDir = path.join(userHome, 'eigent', safeEmail);
|
||||
const projectBasedPath = this.findTaskInProjects(userDir, taskId);
|
||||
return { dirPath, logPath };
|
||||
}
|
||||
|
||||
if (projectBasedPath) {
|
||||
dirPath = projectBasedPath;
|
||||
// Extract project from path to construct log path
|
||||
const projectMatch = projectBasedPath.match(/project_([^\\\/]+)/);
|
||||
if (projectMatch) {
|
||||
logPath = path.join(
|
||||
userHome,
|
||||
'.eigent',
|
||||
safeEmail,
|
||||
projectMatch[0],
|
||||
`task_${taskId}`
|
||||
);
|
||||
} else {
|
||||
logPath = path.join(userHome, '.eigent', safeEmail, `task_${taskId}`);
|
||||
}
|
||||
const userDir = path.join(userHome, 'eigent', safeEmail);
|
||||
const projectBasedPath = this.findTaskInProjects(userDir, taskId);
|
||||
|
||||
if (projectBasedPath) {
|
||||
dirPath = projectBasedPath;
|
||||
const projectMatch = projectBasedPath.match(/project_([^\\\/]+)/);
|
||||
if (projectMatch) {
|
||||
logPath = path.join(
|
||||
userHome,
|
||||
'.eigent',
|
||||
safeEmail,
|
||||
projectMatch[0],
|
||||
`task_${taskId}`
|
||||
);
|
||||
} else {
|
||||
// Fallback to legacy direct task structure
|
||||
dirPath = path.join(userHome, 'eigent', safeEmail, `task_${taskId}`);
|
||||
logPath = path.join(userHome, '.eigent', safeEmail, `task_${taskId}`);
|
||||
}
|
||||
return { dirPath, logPath };
|
||||
}
|
||||
|
||||
dirPath = path.join(userHome, 'eigent', safeEmail, `task_${taskId}`);
|
||||
logPath = path.join(userHome, '.eigent', safeEmail, `task_${taskId}`);
|
||||
return { dirPath, logPath };
|
||||
}
|
||||
|
||||
public getFileList(
|
||||
email: string,
|
||||
taskId: string,
|
||||
projectId?: string
|
||||
): FileInfo[] {
|
||||
const { dirPath, logPath } = this.resolveTaskPaths(
|
||||
email,
|
||||
taskId,
|
||||
projectId
|
||||
);
|
||||
const camelLogPath = path.join(logPath, 'camel_logs');
|
||||
|
||||
try {
|
||||
const projectFiles = fs.existsSync(dirPath)
|
||||
? this.getFilesRecursive(dirPath, dirPath).map((file) => ({
|
||||
...file,
|
||||
source: 'project_output' as const,
|
||||
}))
|
||||
: [];
|
||||
const camelLogFiles = fs.existsSync(camelLogPath)
|
||||
? this.getFilesRecursive(camelLogPath, camelLogPath).map((file) => ({
|
||||
...file,
|
||||
source: 'camel_log' as const,
|
||||
}))
|
||||
: [];
|
||||
|
||||
if (projectFiles.length === 0 && camelLogFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...projectFiles, ...camelLogFiles];
|
||||
} catch (err) {
|
||||
console.error('Load file failed:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public deleteTaskFiles(
|
||||
email: string,
|
||||
taskId: string,
|
||||
projectId?: string
|
||||
): {
|
||||
success: boolean;
|
||||
path: { dirPath: string; logPath: string };
|
||||
} {
|
||||
const { dirPath, logPath } = this.resolveTaskPaths(
|
||||
email,
|
||||
taskId,
|
||||
projectId
|
||||
);
|
||||
|
||||
try {
|
||||
let success = false;
|
||||
if (fs.existsSync(dirPath)) {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ interface CdpBrowser {
|
|||
addedAt: number;
|
||||
}
|
||||
let cdp_browser_pool: CdpBrowser[] = [];
|
||||
let cdpLastAssignedPort = 9223; // tracks the highest port ever assigned, never decreases
|
||||
let cdpHealthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const CDP_POOL_FILE = path.join(os.homedir(), '.eigent', 'cdp-browsers.json');
|
||||
|
|
@ -121,8 +122,12 @@ function loadCdpPool(): void {
|
|||
...b,
|
||||
isExternal: true,
|
||||
}));
|
||||
cdpLastAssignedPort = cdp_browser_pool.reduce(
|
||||
(max, b) => Math.max(max, b.port),
|
||||
cdpLastAssignedPort
|
||||
);
|
||||
log.info(
|
||||
`[CDP POOL] Loaded ${cdp_browser_pool.length} browser(s) from disk`
|
||||
`[CDP POOL] Loaded ${cdp_browser_pool.length} browser(s) from disk, lastAssignedPort=${cdpLastAssignedPort}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -759,18 +764,27 @@ function registerIpcHandlers() {
|
|||
// Launch CDP browser with automatic port assignment
|
||||
ipcMain.handle('launch-cdp-browser', async () => {
|
||||
try {
|
||||
// 1. Find available port (9224–9300) by checking no CDP browser is listening
|
||||
// 1. Always increment port from the last assigned port
|
||||
// Port 9223 is reserved for the login browser
|
||||
let port: number | null = null;
|
||||
for (let p = 9224; p < 9300; p++) {
|
||||
if (
|
||||
!cdp_browser_pool.some((b) => b.port === p) &&
|
||||
!(await isCdpPortAlive(p))
|
||||
) {
|
||||
for (let p = cdpLastAssignedPort + 1; p < 9300; p++) {
|
||||
if (!(await isCdpPortAlive(p))) {
|
||||
port = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wrap around if we hit the ceiling
|
||||
if (port === null) {
|
||||
for (let p = 9224; p <= cdpLastAssignedPort && p < 9300; p++) {
|
||||
if (
|
||||
!cdp_browser_pool.some((b) => b.port === p) &&
|
||||
!(await isCdpPortAlive(p))
|
||||
) {
|
||||
port = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (port === null) {
|
||||
return { success: false, error: 'No available port in 9224-9299' };
|
||||
}
|
||||
|
|
@ -905,6 +919,7 @@ function registerIpcHandlers() {
|
|||
addedAt: Date.now(),
|
||||
};
|
||||
cdp_browser_pool.push(newBrowser);
|
||||
cdpLastAssignedPort = port;
|
||||
saveCdpPool();
|
||||
notifyCdpPoolChanged();
|
||||
|
||||
|
|
@ -1815,7 +1830,6 @@ function registerIpcHandlers() {
|
|||
|
||||
// Read file content
|
||||
const fileContent = await fsp.readFile(filePath);
|
||||
log.info('File read successfully:', filePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 260 KiB |
|
|
@ -280,18 +280,30 @@ export function Node({ id, data }: NodeProps) {
|
|||
|
||||
const toolkits = selectedTask?.toolkits;
|
||||
const lastToolkit = toolkits?.[toolkits.length - 1];
|
||||
const toolkitChangeKey = `${selectedTask?.id ?? ''}:${toolkits?.length ?? 0}:${lastToolkit?.toolkitId ?? ''}:${lastToolkit?.toolkitStatus ?? ''}`;
|
||||
const toolkitChangeKey = `${selectedTask?.id ?? ''}:${toolkits?.length ?? 0}:${lastToolkit?.toolkitId ?? ''}:${lastToolkit?.toolkitStatus ?? ''}:${lastToolkit?.message?.slice(-30) ?? ''}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded || !toolkits?.length) return;
|
||||
scrollLogToBottom();
|
||||
}, [isExpanded, toolkits?.length, toolkitChangeKey, scrollLogToBottom]);
|
||||
|
||||
// Reset scroll-to-bottom flag when switching tasks so new task always starts at bottom
|
||||
// Scroll to bottom when a report appears
|
||||
useEffect(() => {
|
||||
if (!isExpanded || !selectedTask?.report) return;
|
||||
scrollLogToBottom();
|
||||
}, [isExpanded, selectedTask?.report, scrollLogToBottom]);
|
||||
|
||||
// Reset scroll-to-bottom flag when switching tasks or when panel opens
|
||||
useEffect(() => {
|
||||
wasAtBottomRef.current = true;
|
||||
}, [selectedTask?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
wasAtBottomRef.current = true;
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
// Track whether user has scrolled up so we don't override manual reading
|
||||
useEffect(() => {
|
||||
const el = logRef.current;
|
||||
|
|
@ -413,17 +425,17 @@ export function Node({ id, data }: NodeProps) {
|
|||
: 'w-[342px]'
|
||||
} ${
|
||||
data.isEditMode ? 'h-full' : 'max-h-[calc(100vh-200px)]'
|
||||
} flex overflow-hidden rounded-xl border border-solid border-worker-border-default bg-worker-surface-primary ${
|
||||
} rounded-xl border-worker-border-default bg-worker-surface-primary flex overflow-hidden border border-solid ${
|
||||
getCurrentTask()?.activeAgent === id
|
||||
? `${agentMap[data.type]?.borderColor} z-50`
|
||||
: 'z-10 border-worker-border-default'
|
||||
} transition-all duration-300 ease-in-out ${
|
||||
: 'border-worker-border-default z-10'
|
||||
} ease-in-out transition-all duration-300 ${
|
||||
(data.agent?.tasks?.length ?? 0) === 0 && 'opacity-30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex w-[342px] shrink-0 flex-col border-y-0 border-l-0 border-r-[0.5px] border-solid border-border-secondary">
|
||||
<div className="flex items-center justify-between gap-sm px-3 pb-1 pt-2">
|
||||
<div className="flex items-center justify-between gap-md">
|
||||
<div className="border-border-secondary flex w-[342px] shrink-0 flex-col border-y-0 border-r-[0.5px] border-l-0 border-solid">
|
||||
<div className="gap-sm px-3 pb-1 pt-2 flex items-center justify-between">
|
||||
<div className="gap-md flex items-center justify-between">
|
||||
<div
|
||||
className={`text-base font-bold leading-relaxed ${
|
||||
agentMap[data.type]?.textColor
|
||||
|
|
@ -432,7 +444,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
{agentMap[data.type]?.name || data.agent?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-xs">
|
||||
<div className="gap-xs flex items-center">
|
||||
<Button onClick={handleShowLog} variant="ghost" size="icon">
|
||||
{isExpanded ? <SquareChevronLeft /> : <SquareCode />}
|
||||
</Button>
|
||||
|
|
@ -448,7 +460,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
<Ellipsis />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[98px] rounded-[12px] border border-solid border-dropdown-border bg-dropdown-bg p-sm">
|
||||
<PopoverContent className="border-dropdown-border bg-dropdown-bg p-sm w-[98px] rounded-[12px] border border-solid">
|
||||
<div className="space-y-1">
|
||||
<PopoverClose asChild>
|
||||
<AddWorker
|
||||
|
|
@ -460,7 +472,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2"
|
||||
className="gap-2 w-full justify-start"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newWorkerList = workerList.filter(
|
||||
|
|
@ -484,7 +496,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
</div>
|
||||
<div
|
||||
ref={toolsRef}
|
||||
className="mb-sm flex min-h-4 flex-shrink-0 flex-wrap px-3 text-xs font-normal leading-tight text-text-label"
|
||||
className="mb-sm min-h-4 px-3 text-xs font-normal leading-tight text-text-label flex flex-shrink-0 flex-wrap"
|
||||
>
|
||||
{/* {JSON.stringify(data.agent)} */}
|
||||
{toolkitLabels.map((toolkit, index) => (
|
||||
|
|
@ -494,7 +506,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
))}
|
||||
</div>
|
||||
<div
|
||||
className="mb-2 max-h-[180px] px-3"
|
||||
className="mb-2 px-3 max-h-[180px]"
|
||||
onClick={() => {
|
||||
chatStore.setActiveWorkspace(
|
||||
chatStore.activeTaskId as string,
|
||||
|
|
@ -506,15 +518,15 @@ export function Node({ id, data }: NodeProps) {
|
|||
>
|
||||
{browserImages.length > 0 && (
|
||||
<div
|
||||
className={`grid h-[180px] w-full gap-1 overflow-hidden ${browserImageGridClass}`}
|
||||
className={`gap-1 grid h-[180px] w-full overflow-hidden ${browserImageGridClass}`}
|
||||
>
|
||||
{browserImages.map((img, index) => (
|
||||
<div
|
||||
key={`${img.img}-${index}`}
|
||||
className="relative h-full w-full overflow-hidden rounded-lg"
|
||||
className="rounded-lg relative h-full w-full overflow-hidden"
|
||||
>
|
||||
<img
|
||||
className="absolute left-0 top-0 h-[250%] w-[250%] origin-top-left scale-[0.4] object-cover"
|
||||
className="left-0 top-0 absolute h-[250%] w-[250%] origin-top-left scale-[0.4] object-cover"
|
||||
src={img.img}
|
||||
alt={data.type}
|
||||
/>
|
||||
|
|
@ -524,7 +536,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
(_, index) => (
|
||||
<div
|
||||
key={`browser-placeholder-${index}`}
|
||||
className="h-full w-full rounded-sm bg-surface-primary"
|
||||
className="rounded-sm bg-surface-primary h-full w-full"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
@ -533,8 +545,8 @@ export function Node({ id, data }: NodeProps) {
|
|||
{data.type === 'document_agent' &&
|
||||
data?.agent?.tasks &&
|
||||
data.agent.tasks.length > 0 && (
|
||||
<div className="relative h-[180px] w-full overflow-hidden rounded-sm">
|
||||
<div className="absolute left-0 top-0 h-[500px] w-[900px] origin-top-left scale-[0.36]">
|
||||
<div className="rounded-sm relative h-[180px] w-full overflow-hidden">
|
||||
<div className="left-0 top-0 absolute h-[500px] w-[900px] origin-top-left scale-[0.36]">
|
||||
<Folder data={data.agent as Agent} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -542,14 +554,14 @@ export function Node({ id, data }: NodeProps) {
|
|||
|
||||
{data.type === 'developer_agent' && terminalTasks.length > 0 && (
|
||||
<div
|
||||
className={`grid h-[180px] w-full gap-1 overflow-hidden ${terminalGridClass}`}
|
||||
className={`gap-1 grid h-[180px] w-full overflow-hidden ${terminalGridClass}`}
|
||||
>
|
||||
{terminalTasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="relative h-full w-full overflow-hidden rounded-lg object-cover"
|
||||
className="rounded-lg relative h-full w-full overflow-hidden object-cover"
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-[250%] w-[250%] origin-top-left scale-[0.4]">
|
||||
<div className="left-0 top-0 absolute h-[250%] w-[250%] origin-top-left scale-[0.4]">
|
||||
<Terminal content={task.terminal} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -558,7 +570,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
(_, index) => (
|
||||
<div
|
||||
key={`terminal-placeholder-${index}`}
|
||||
className="h-full w-full rounded-lg bg-surface-primary"
|
||||
className="rounded-lg bg-surface-primary h-full w-full"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
@ -566,7 +578,7 @@ 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 border-[0px] border-t border-solid border-task-border-default px-3 py-sm">
|
||||
<div className="gap-1 border-task-border-default px-3 py-sm flex flex-col items-start justify-between border-[0px] border-t border-solid">
|
||||
{/* <div className="font-bold leading-tight text-xs">Subtasks</div> */}
|
||||
<div className="flex flex-1 justify-end">
|
||||
<TaskState
|
||||
|
|
@ -618,7 +630,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
onWheel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="scrollbar scrollbar-always-visible flex flex-col gap-2 overflow-y-auto px-3 pb-2 duration-500 ease-out animate-in fade-in-0 slide-in-from-bottom-4"
|
||||
className="scrollbar scrollbar-always-visible gap-2 px-3 pb-2 ease-out animate-in fade-in-0 slide-in-from-bottom-4 flex flex-col overflow-y-auto duration-500"
|
||||
style={{
|
||||
maxHeight:
|
||||
data.img && data.img.length > 0
|
||||
|
|
@ -650,7 +662,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
}
|
||||
}}
|
||||
key={`taskList-${task.id}-${task.failure_count}`}
|
||||
className={`flex gap-2 rounded-xl px-sm py-sm transition-all duration-300 ease-in-out animate-in fade-in-0 slide-in-from-left-2 ${
|
||||
className={`gap-2 rounded-xl px-sm py-sm ease-in-out animate-in fade-in-0 slide-in-from-left-2 flex transition-all duration-300 ${
|
||||
task.reAssignTo
|
||||
? 'bg-task-fill-warning'
|
||||
: task.status === TaskStatus.COMPLETED
|
||||
|
|
@ -743,14 +755,14 @@ export function Node({ id, data }: NodeProps) {
|
|||
: task.status === TaskStatus.BLOCKED
|
||||
? 'text-text-body'
|
||||
: 'text-text-primary'
|
||||
} pointer-events-auto select-text whitespace-pre-line text-wrap break-all text-xs font-medium leading-13`}
|
||||
} text-xs font-medium leading-13 pointer-events-auto text-wrap break-all whitespace-pre-line select-text`}
|
||||
>
|
||||
<div className="flex items-center gap-sm">
|
||||
<div className="gap-sm flex items-center">
|
||||
<div className="text-xs font-bold leading-13 text-text-body">
|
||||
No. {getTaskId(task.id)}
|
||||
</div>
|
||||
{task.reAssignTo ? (
|
||||
<div className="rounded-lg bg-tag-fill-document px-1 py-0.5 text-xs font-bold leading-none text-text-warning">
|
||||
<div className="rounded-lg bg-tag-fill-document px-1 py-0.5 text-xs font-bold text-text-warning leading-none">
|
||||
Reassigned to {task.reAssignTo}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -772,11 +784,11 @@ export function Node({ id, data }: NodeProps) {
|
|||
<div>{task.content}</div>
|
||||
</div>
|
||||
{task?.status === TaskStatus.RUNNING && (
|
||||
<div className="duration-400 mt-xs flex items-center gap-2 animate-in fade-in-0 slide-in-from-bottom-2">
|
||||
<div className="mt-xs gap-2 animate-in fade-in-0 slide-in-from-bottom-2 flex items-center duration-400">
|
||||
{/* active toolkit */}
|
||||
{lastActiveToolkit?.toolkitStatus ===
|
||||
AgentStatusValue.RUNNING && (
|
||||
<div className="flex min-w-0 flex-1 items-center justify-start gap-sm duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<div className="min-w-0 gap-sm animate-in fade-in-0 slide-in-from-right-2 flex flex-1 items-center justify-start duration-300">
|
||||
{getToolkitIcon(
|
||||
lastActiveToolkit.toolkitName ?? ''
|
||||
)}
|
||||
|
|
@ -787,11 +799,11 @@ export function Node({ id, data }: NodeProps) {
|
|||
].activeWorkspace
|
||||
? '!w-[100px]'
|
||||
: '!w-[500px]'
|
||||
} min-w-0 flex-shrink-0 flex-grow-0 overflow-hidden text-ellipsis whitespace-nowrap pt-1 text-xs leading-17 text-text-primary`}
|
||||
} min-w-0 pt-1 text-xs leading-17 text-text-primary flex-shrink-0 flex-grow-0 overflow-hidden text-ellipsis whitespace-nowrap`}
|
||||
>
|
||||
<ShinyText
|
||||
text={task.toolkits?.[0].toolkitName}
|
||||
className="pointer-events-auto w-full select-text overflow-hidden text-ellipsis whitespace-nowrap text-xs font-bold leading-17 text-text-primary"
|
||||
className="text-xs font-bold leading-17 text-text-primary pointer-events-auto w-full overflow-hidden text-ellipsis whitespace-nowrap select-text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -812,14 +824,14 @@ export function Node({ id, data }: NodeProps) {
|
|||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 24 }}
|
||||
transition={{ duration: 0.3, ease: 'easeIn' }}
|
||||
className="flex w-[342px] shrink-0 flex-col gap-sm overflow-hidden rounded-r-xl bg-worker-surface-secondary py-2 pl-sm"
|
||||
className="gap-sm rounded-r-xl bg-worker-surface-secondary py-2 pl-sm flex w-[342px] shrink-0 flex-col overflow-hidden"
|
||||
>
|
||||
<div
|
||||
ref={logRef}
|
||||
onWheel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="scrollbar scrollbar-always-visible max-h-[calc(100vh-200px)] overflow-y-scroll pr-sm"
|
||||
className="scrollbar scrollbar-always-visible pr-sm max-h-[calc(100vh-200px)] overflow-y-scroll"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{selectedTask && (
|
||||
|
|
@ -829,7 +841,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -16 }}
|
||||
transition={{ duration: 0.25, ease: 'easeIn' }}
|
||||
className="flex w-full flex-col gap-sm"
|
||||
className="gap-sm flex w-full flex-col"
|
||||
>
|
||||
{selectedTask.toolkits &&
|
||||
selectedTask.toolkits.length > 0 &&
|
||||
|
|
@ -839,7 +851,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
{toolkit.toolkitName === 'notice' ? (
|
||||
<div
|
||||
key={`notice-${index}`}
|
||||
className="flex w-full flex-col gap-sm px-2 py-1"
|
||||
className="gap-sm px-2 py-1 flex w-full flex-col"
|
||||
>
|
||||
<MarkDown
|
||||
content={toolkit?.message}
|
||||
|
|
@ -877,10 +889,10 @@ export function Node({ id, data }: NodeProps) {
|
|||
toolkit.message;
|
||||
}
|
||||
}}
|
||||
className="flex flex-col items-start justify-center gap-1 rounded-lg bg-log-default p-1 px-2 transition-all duration-300 hover:opacity-50"
|
||||
className="gap-1 rounded-lg bg-log-default p-1 px-2 flex flex-col items-start justify-center transition-all duration-300 hover:opacity-50"
|
||||
>
|
||||
{/* first row: icon + toolkit name */}
|
||||
<div className="flex w-full items-center justify-start gap-sm">
|
||||
<div className="gap-sm flex w-full items-center justify-start">
|
||||
{toolkit.toolkitStatus ===
|
||||
AgentStatusValue.RUNNING ? (
|
||||
<LoaderCircle
|
||||
|
|
@ -896,13 +908,13 @@ export function Node({ id, data }: NodeProps) {
|
|||
) : (
|
||||
getToolkitIcon(toolkit.toolkitName)
|
||||
)}
|
||||
<span className="flex items-center gap-sm text-nowrap text-label-xs font-bold text-text-primary">
|
||||
<span className="gap-sm text-label-xs font-bold text-text-primary flex items-center text-nowrap">
|
||||
{toolkit.toolkitName}
|
||||
</span>
|
||||
</div>
|
||||
{/* second row: method + message */}
|
||||
<div className="pointer-events-auto flex w-full select-text items-start justify-center gap-sm overflow-hidden pl-6">
|
||||
<div className="text-nowrap text-label-xs font-bold text-text-primary">
|
||||
<div className="gap-sm pl-6 pointer-events-auto flex w-full items-start justify-center overflow-hidden select-text">
|
||||
<div className="text-label-xs font-bold text-text-primary text-nowrap">
|
||||
{toolkit.toolkitMethods
|
||||
? toolkit.toolkitMethods
|
||||
.charAt(0)
|
||||
|
|
@ -911,10 +923,10 @@ export function Node({ id, data }: NodeProps) {
|
|||
: ''}
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-full flex-1 truncate text-label-xs font-normal text-text-primary ${
|
||||
className={`text-label-xs font-normal text-text-primary max-w-full flex-1 truncate ${
|
||||
data.isEditMode
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-hidden truncate'
|
||||
: 'truncate overflow-hidden'
|
||||
}`}
|
||||
>
|
||||
{toolkit.message}
|
||||
|
|
@ -925,7 +937,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
{toolkit.message && (
|
||||
<TooltipContent
|
||||
align="start"
|
||||
className="scrollbar pointer-events-auto !fixed left-6 z-[9999] max-h-[200px] w-max max-w-[296px] select-text overflow-y-auto text-wrap break-words rounded-lg border border-solid border-task-border-default bg-surface-tertiary p-2 text-label-xs"
|
||||
className="scrollbar left-6 rounded-lg border-task-border-default bg-surface-tertiary p-2 text-label-xs pointer-events-auto !fixed z-[9999] max-h-[200px] w-max max-w-[296px] overflow-y-auto border border-solid text-wrap break-words select-text"
|
||||
side="bottom"
|
||||
sideOffset={4}
|
||||
>
|
||||
|
|
@ -948,9 +960,9 @@ export function Node({ id, data }: NodeProps) {
|
|||
onWheel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="group relative my-2 flex w-full flex-col rounded-lg bg-surface-primary"
|
||||
className="group my-2 rounded-lg bg-surface-primary relative flex w-full flex-col"
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between rounded-lg bg-surface-primary py-2 pl-2 pr-2">
|
||||
<div className="top-0 rounded-lg bg-surface-primary py-2 pl-2 pr-2 sticky z-10 flex items-center justify-between">
|
||||
<div className="text-label-sm font-bold text-text-primary">
|
||||
Completion Report
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,165 @@ interface Task {
|
|||
nextExecutionId?: string;
|
||||
}
|
||||
|
||||
type UploadFileSource = 'project_output' | 'camel_log' | 'user_attachment';
|
||||
|
||||
interface UploadCandidate {
|
||||
path: string;
|
||||
name: string;
|
||||
uploadName: string;
|
||||
source: UploadFileSource;
|
||||
}
|
||||
|
||||
interface GeneratedUploadFile {
|
||||
path?: string;
|
||||
name?: string;
|
||||
isFolder?: boolean;
|
||||
relativePath?: string;
|
||||
source?: Exclude<UploadFileSource, 'user_attachment'>;
|
||||
}
|
||||
|
||||
interface UploadOutcome {
|
||||
success: boolean;
|
||||
fileName: string;
|
||||
source: UploadFileSource;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
function getFileNameFromPath(filePath: string): string {
|
||||
const segments = filePath.split(/[\\/]/).filter(Boolean);
|
||||
return segments.at(-1) || 'file';
|
||||
}
|
||||
|
||||
function isReadableLocalPath(filePath?: string): filePath is string {
|
||||
if (!filePath) return false;
|
||||
return !/^(https?:|file:|blob:|data:)/i.test(filePath);
|
||||
}
|
||||
|
||||
function buildUploadName(
|
||||
fileName: string,
|
||||
source: UploadFileSource,
|
||||
taskId: string,
|
||||
attachmentIndex: number,
|
||||
relativePath?: string
|
||||
): string {
|
||||
if (source === 'camel_log') {
|
||||
if (relativePath) {
|
||||
return `camel_log/${relativePath}/${fileName}`;
|
||||
}
|
||||
return `camel_log/${fileName}`;
|
||||
}
|
||||
|
||||
if (source === 'user_attachment') {
|
||||
return `user_attachment/${fileName}`;
|
||||
}
|
||||
|
||||
return `project_output/${fileName}`;
|
||||
}
|
||||
|
||||
export function collectTaskUploadFiles(
|
||||
generatedFiles: GeneratedUploadFile[],
|
||||
messages: Message[],
|
||||
pendingAttaches: File[] = [],
|
||||
taskId = 'unknown_task'
|
||||
): UploadCandidate[] {
|
||||
const uploadCandidates: Array<
|
||||
Omit<UploadCandidate, 'uploadName'> & { relativePath?: string }
|
||||
> = [];
|
||||
|
||||
for (const file of generatedFiles) {
|
||||
if (!file?.path || !file?.name || file.isFolder) continue;
|
||||
uploadCandidates.push({
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
relativePath: file.relativePath,
|
||||
source: file.source === 'camel_log' ? 'camel_log' : 'project_output',
|
||||
});
|
||||
}
|
||||
|
||||
const attachmentFiles = [
|
||||
...messages.flatMap((message) => message.attaches || []),
|
||||
...pendingAttaches,
|
||||
];
|
||||
|
||||
for (const attachment of attachmentFiles) {
|
||||
if (!isReadableLocalPath(attachment?.filePath)) continue;
|
||||
uploadCandidates.push({
|
||||
path: attachment.filePath,
|
||||
name:
|
||||
attachment.fileName?.trim() || getFileNameFromPath(attachment.filePath),
|
||||
source: 'user_attachment',
|
||||
});
|
||||
}
|
||||
|
||||
const uniqueCandidates = new Map<string, UploadCandidate>();
|
||||
let attachmentIndex = 1;
|
||||
for (const file of uploadCandidates) {
|
||||
if (!uniqueCandidates.has(file.path)) {
|
||||
const { relativePath, ...rest } = file;
|
||||
uniqueCandidates.set(file.path, {
|
||||
...rest,
|
||||
uploadName: buildUploadName(
|
||||
file.name,
|
||||
file.source,
|
||||
taskId,
|
||||
file.source === 'user_attachment' ? attachmentIndex++ : 0,
|
||||
relativePath
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueCandidates.values());
|
||||
}
|
||||
|
||||
async function uploadTaskFiles(
|
||||
files: UploadCandidate[],
|
||||
uploadTargetId: string
|
||||
): Promise<UploadOutcome[]> {
|
||||
const results: UploadOutcome[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const result = await window.ipcRenderer.invoke('read-file', file.path);
|
||||
if (!result.success || !result.data) {
|
||||
results.push({
|
||||
success: false,
|
||||
fileName: file.name,
|
||||
source: file.source,
|
||||
error: result.error || 'Failed to read file',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([result.data], {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
formData.append('file', blob, file.uploadName);
|
||||
// TODO(file): rename endpoint to use project_id
|
||||
formData.append('task_id', uploadTargetId);
|
||||
|
||||
await uploadFile('/api/v1/chat/files/upload', formData);
|
||||
console.log('File uploaded successfully:', file.uploadName, file.source);
|
||||
results.push({
|
||||
success: true,
|
||||
fileName: file.uploadName,
|
||||
source: file.source,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('File upload failed:', file.uploadName, file.source, error);
|
||||
results.push({
|
||||
success: false,
|
||||
fileName: file.uploadName,
|
||||
source: file.source,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export interface ChatStore {
|
||||
updateCount: number;
|
||||
activeTaskId: string | null;
|
||||
|
|
@ -2229,83 +2388,59 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
)
|
||||
);
|
||||
|
||||
// Async file upload
|
||||
let res = await window.ipcRenderer.invoke(
|
||||
'get-file-list',
|
||||
email,
|
||||
currentTaskId,
|
||||
(project_id || projectStore.activeProjectId) as string
|
||||
);
|
||||
if (
|
||||
!type &&
|
||||
import.meta.env.VITE_USE_LOCAL_PROXY !== 'true' &&
|
||||
res.length > 0
|
||||
) {
|
||||
// Upload files sequentially to avoid overwhelming the server
|
||||
const uploadResults = await Promise.allSettled(
|
||||
res
|
||||
.filter((file: any) => !file.isFolder)
|
||||
.map(async (file: any) => {
|
||||
try {
|
||||
// Read file content using Electron API
|
||||
const result = await window.ipcRenderer.invoke(
|
||||
'read-file',
|
||||
file.path
|
||||
);
|
||||
if (result.success && result.data) {
|
||||
// Create FormData for file upload
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([result.data], {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
formData.append('file', blob, file.name);
|
||||
//TODO(file): rename endpoint to use project_id
|
||||
formData.append(
|
||||
'task_id',
|
||||
(project_id || projectStore.activeProjectId) as string
|
||||
);
|
||||
const uploadTargetId = (project_id ||
|
||||
projectStore.activeProjectId) as string | undefined;
|
||||
if (!type && import.meta.env.VITE_USE_LOCAL_PROXY !== 'true') {
|
||||
if (!uploadTargetId) {
|
||||
console.warn(
|
||||
'Skip file upload because no active project ID was found'
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const generatedFiles =
|
||||
((await window.ipcRenderer.invoke(
|
||||
'get-file-list',
|
||||
email,
|
||||
currentTaskId,
|
||||
uploadTargetId
|
||||
)) as GeneratedUploadFile[]) || [];
|
||||
const filesToUpload = collectTaskUploadFiles(
|
||||
generatedFiles,
|
||||
tasks[currentTaskId].messages,
|
||||
tasks[currentTaskId].attaches,
|
||||
currentTaskId
|
||||
);
|
||||
|
||||
// Upload file
|
||||
await uploadFile('/api/v1/chat/files/upload', formData);
|
||||
console.log('File uploaded successfully:', file.name);
|
||||
return { success: true, fileName: file.name };
|
||||
} else {
|
||||
console.error('Failed to read file:', result.error);
|
||||
return {
|
||||
success: false,
|
||||
fileName: file.name,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('File upload failed:', error);
|
||||
return { success: false, fileName: file.name, error };
|
||||
if (filesToUpload.length > 0) {
|
||||
const uploadResults = await uploadTaskFiles(
|
||||
filesToUpload,
|
||||
uploadTargetId
|
||||
);
|
||||
const failedUploads = uploadResults.filter(
|
||||
(result) => !result.success
|
||||
);
|
||||
if (failedUploads.length > 0) {
|
||||
console.error('Failed to upload files:', failedUploads);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Count successful uploads
|
||||
const successCount = uploadResults.filter(
|
||||
(result) =>
|
||||
result.status === 'fulfilled' && result.value.success
|
||||
).length;
|
||||
const generatedSuccessCount = uploadResults.filter(
|
||||
(result) =>
|
||||
result.success && result.source === 'project_output'
|
||||
).length;
|
||||
|
||||
// Log failures
|
||||
const failures = uploadResults.filter(
|
||||
(result) =>
|
||||
result.status === 'rejected' ||
|
||||
(result.status === 'fulfilled' && !result.value.success)
|
||||
);
|
||||
if (failures.length > 0) {
|
||||
console.error('Failed to upload files:', failures);
|
||||
}
|
||||
|
||||
// add remote file count for successful uploads only
|
||||
if (successCount > 0) {
|
||||
proxyFetchPost(`/api/v1/user/stat`, {
|
||||
action: 'file_generate_count',
|
||||
value: successCount,
|
||||
});
|
||||
if (generatedSuccessCount > 0) {
|
||||
proxyFetchPost(`/api/v1/user/stat`, {
|
||||
action: 'file_generate_count',
|
||||
value: generatedSuccessCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to prepare task files for upload:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,10 @@ vi.mock('../../../src/store/projectStore', () => ({
|
|||
import { proxyFetchGet } from '@/api/http';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { generateUniqueId } from '../../../src/lib';
|
||||
import { useChatStore } from '../../../src/store/chatStore';
|
||||
import {
|
||||
collectTaskUploadFiles,
|
||||
useChatStore,
|
||||
} from '../../../src/store/chatStore';
|
||||
import { useProjectStore } from '../../../src/store/projectStore';
|
||||
import { ChatTaskStatus } from '../../../src/types/constants';
|
||||
|
||||
|
|
@ -114,6 +117,117 @@ describe('ChatStore - Core Functionality', () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Task Upload Files', () => {
|
||||
it('collects project outputs, camel logs, and unique user attachments', () => {
|
||||
const uploadFiles = collectTaskUploadFiles(
|
||||
[
|
||||
{
|
||||
path: '/tmp/project/report.md',
|
||||
name: 'report.md',
|
||||
source: 'project_output',
|
||||
},
|
||||
{
|
||||
path: '/tmp/logs/ba4462e1/agent.log',
|
||||
name: 'agent.log',
|
||||
relativePath: 'ba4462e1',
|
||||
source: 'camel_log',
|
||||
},
|
||||
{
|
||||
path: '/tmp/project',
|
||||
name: 'project',
|
||||
isFolder: true,
|
||||
source: 'project_output',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
content: 'question',
|
||||
attaches: [
|
||||
{
|
||||
fileName: 'brief.pdf',
|
||||
filePath: '/Users/test/Documents/brief.pdf',
|
||||
},
|
||||
{
|
||||
fileName: 'report.md',
|
||||
filePath: '/tmp/project/report.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any,
|
||||
[
|
||||
{
|
||||
fileName: 'followup.csv',
|
||||
filePath: '/Users/test/Documents/followup.csv',
|
||||
},
|
||||
],
|
||||
'task-123'
|
||||
);
|
||||
|
||||
expect(uploadFiles).toEqual([
|
||||
{
|
||||
path: '/tmp/project/report.md',
|
||||
name: 'report.md',
|
||||
uploadName: 'project_output/report.md',
|
||||
source: 'project_output',
|
||||
},
|
||||
{
|
||||
path: '/tmp/logs/ba4462e1/agent.log',
|
||||
name: 'agent.log',
|
||||
uploadName: 'camel_log/ba4462e1/agent.log',
|
||||
source: 'camel_log',
|
||||
},
|
||||
{
|
||||
path: '/Users/test/Documents/brief.pdf',
|
||||
name: 'brief.pdf',
|
||||
uploadName: 'user_attachment/brief.pdf',
|
||||
source: 'user_attachment',
|
||||
},
|
||||
{
|
||||
path: '/Users/test/Documents/followup.csv',
|
||||
name: 'followup.csv',
|
||||
uploadName: 'user_attachment/followup.csv',
|
||||
source: 'user_attachment',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips remote attachment URLs and falls back to filename from path', () => {
|
||||
const uploadFiles = collectTaskUploadFiles(
|
||||
[],
|
||||
[
|
||||
{
|
||||
id: 'msg-2',
|
||||
role: 'user',
|
||||
content: 'question',
|
||||
attaches: [
|
||||
{
|
||||
fileName: '',
|
||||
filePath: 'C:\\Users\\test\\Desktop\\notes.txt',
|
||||
},
|
||||
{
|
||||
fileName: 'remote.pdf',
|
||||
filePath: 'https://example.com/remote.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any,
|
||||
[],
|
||||
'task-456'
|
||||
);
|
||||
|
||||
expect(uploadFiles).toEqual([
|
||||
{
|
||||
path: 'C:\\Users\\test\\Desktop\\notes.txt',
|
||||
name: 'notes.txt',
|
||||
uploadName: 'user_attachment/notes.txt',
|
||||
source: 'user_attachment',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Creation', () => {
|
||||
it('should create a task with unique ID', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue