mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-16 19:50:50 +00:00
Merge branch 'main' into 621-bug-react-warning-confirmbuttondisabled-prop-passed-to-dom-element
This commit is contained in:
commit
d47bb9f165
30 changed files with 653 additions and 333 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
|
|
@ -8,7 +9,7 @@ from fastapi.responses import StreamingResponse
|
|||
from utils import traceroot_wrapper as traceroot
|
||||
from app.component import code
|
||||
from app.exception.exception import UserException
|
||||
from app.model.chat import Chat, HumanReply, McpServers, Status, SupplementChat, AddTaskRequest
|
||||
from app.model.chat import Chat, HumanReply, McpServers, Status, SupplementChat, AddTaskRequest, sse_json
|
||||
from app.service.chat_service import step_solve
|
||||
from app.service.task import (
|
||||
Action,
|
||||
|
|
@ -30,13 +31,51 @@ from camel.tasks.task import Task
|
|||
router = APIRouter()
|
||||
|
||||
# Create traceroot logger for chat controller
|
||||
chat_logger = traceroot.get_logger('chat_controller')
|
||||
chat_logger = traceroot.get_logger("chat_controller")
|
||||
|
||||
# SSE timeout configuration (10 minutes in seconds)
|
||||
SSE_TIMEOUT_SECONDS = 10 * 60
|
||||
|
||||
|
||||
async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS):
|
||||
"""
|
||||
Wraps a stream generator with timeout handling.
|
||||
|
||||
Closes the SSE connection if no data is received within the timeout period.
|
||||
"""
|
||||
last_data_time = time.time()
|
||||
generator = stream_generator.__aiter__()
|
||||
|
||||
try:
|
||||
while True:
|
||||
elapsed = time.time() - last_data_time
|
||||
remaining_timeout = timeout_seconds - elapsed
|
||||
|
||||
try:
|
||||
data = await asyncio.wait_for(generator.__anext__(), timeout=remaining_timeout)
|
||||
last_data_time = time.time()
|
||||
yield data
|
||||
except asyncio.TimeoutError:
|
||||
chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection")
|
||||
yield sse_json("error", {"message": "Connection timeout: No data received for 10 minutes"})
|
||||
break
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
except asyncio.CancelledError:
|
||||
chat_logger.info("Stream cancelled")
|
||||
raise
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error in stream wrapper: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/chat", name="start chat")
|
||||
@traceroot.trace()
|
||||
async def post(data: Chat, request: Request):
|
||||
chat_logger.info("Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email})
|
||||
chat_logger.info(
|
||||
"Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
|
||||
)
|
||||
task_lock = get_or_create_task_lock(data.project_id)
|
||||
|
||||
# Set user-specific environment path for this thread
|
||||
|
|
@ -57,7 +96,14 @@ async def post(data: Chat, request: Request):
|
|||
chat_logger.info(f"Set search config: {key}", extra={"project_id": data.project_id})
|
||||
|
||||
email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", data.email.split("@")[0]).strip(".")
|
||||
camel_log = Path.home() / ".eigent" / email_sanitized / ("project_" + data.project_id) / ("task_" + data.task_id) / "camel_logs"
|
||||
camel_log = (
|
||||
Path.home()
|
||||
/ ".eigent"
|
||||
/ email_sanitized
|
||||
/ ("project_" + data.project_id)
|
||||
/ ("task_" + data.task_id)
|
||||
/ "camel_logs"
|
||||
)
|
||||
camel_log.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
os.environ["CAMEL_LOG_DIR"] = str(camel_log)
|
||||
|
|
@ -68,8 +114,13 @@ async def post(data: Chat, request: Request):
|
|||
# Put initial action in queue to start processing
|
||||
await task_lock.put_queue(ActionImproveData(data=data.question))
|
||||
|
||||
chat_logger.info("Chat session initialized, starting streaming response", extra={"project_id": data.project_id, "task_id": data.task_id, "log_dir": str(camel_log)})
|
||||
return StreamingResponse(step_solve(data, request, task_lock), media_type="text/event-stream")
|
||||
chat_logger.info(
|
||||
"Chat session initialized, starting streaming response",
|
||||
extra={"project_id": data.project_id, "task_id": data.task_id, "log_dir": str(camel_log)},
|
||||
)
|
||||
return StreamingResponse(
|
||||
timeout_stream_wrapper(step_solve(data, request, task_lock)), media_type="text/event-stream"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/chat/{id}", name="improve chat")
|
||||
|
|
@ -84,14 +135,14 @@ def improve(id: str, data: SupplementChat):
|
|||
# Reset status to allow processing new messages
|
||||
task_lock.status = Status.confirming
|
||||
# Clear any existing background tasks since workforce was stopped
|
||||
if hasattr(task_lock, 'background_tasks'):
|
||||
if hasattr(task_lock, "background_tasks"):
|
||||
task_lock.background_tasks.clear()
|
||||
# Note: conversation_history and last_task_result are preserved
|
||||
|
||||
# Log context preservation
|
||||
if hasattr(task_lock, 'conversation_history'):
|
||||
if hasattr(task_lock, "conversation_history"):
|
||||
chat_logger.info(f"[CONTEXT] Preserved {len(task_lock.conversation_history)} conversation entries")
|
||||
if hasattr(task_lock, 'last_task_result'):
|
||||
if hasattr(task_lock, "last_task_result"):
|
||||
chat_logger.info(f"[CONTEXT] Preserved task result: {len(task_lock.last_task_result)} chars")
|
||||
|
||||
# Update file save path if task_id is provided
|
||||
|
|
@ -100,7 +151,7 @@ def improve(id: str, data: SupplementChat):
|
|||
try:
|
||||
# Get current environment values needed to construct new path
|
||||
current_email = None
|
||||
|
||||
|
||||
# Extract email from current file_save_path if available
|
||||
current_file_save_path = os.environ.get("file_save_path", "")
|
||||
if current_file_save_path:
|
||||
|
|
@ -109,7 +160,7 @@ def improve(id: str, data: SupplementChat):
|
|||
eigent_index = path_parts.index("eigent")
|
||||
if eigent_index + 1 < len(path_parts):
|
||||
current_email = path_parts[eigent_index + 1]
|
||||
|
||||
|
||||
# If we have the necessary information, update the file_save_path
|
||||
if current_email and id:
|
||||
# Create new path using the existing pattern: email/project_{project_id}/task_{task_id}
|
||||
|
|
@ -117,12 +168,12 @@ def improve(id: str, data: SupplementChat):
|
|||
new_folder_path.mkdir(parents=True, exist_ok=True)
|
||||
os.environ["file_save_path"] = str(new_folder_path)
|
||||
chat_logger.info(f"Updated file_save_path to: {new_folder_path}")
|
||||
|
||||
|
||||
# Store the new folder path in task_lock for potential cleanup and persistence
|
||||
task_lock.new_folder_path = new_folder_path
|
||||
else:
|
||||
chat_logger.warning(f"Could not update file_save_path - email: {current_email}, project_id: {id}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error updating file path for project_id: {id}, task_id: {data.task_id}: {e}")
|
||||
|
||||
|
|
@ -167,7 +218,7 @@ def human_reply(id: str, data: HumanReply):
|
|||
@router.post("/chat/{id}/install-mcp")
|
||||
@traceroot.trace()
|
||||
def install_mcp(id: str, data: McpServers):
|
||||
chat_logger.info("Installing MCP servers", extra={"task_id": id, "servers_count": len(data.get('mcpServers', {}))})
|
||||
chat_logger.info("Installing MCP servers", extra={"task_id": id, "servers_count": len(data.get("mcpServers", {}))})
|
||||
task_lock = get_task_lock(id)
|
||||
asyncio.run(task_lock.put_queue(ActionInstallMcpData(action=Action.install_mcp, data=data)))
|
||||
chat_logger.info("MCP installation queued", extra={"task_id": id})
|
||||
|
|
@ -180,7 +231,7 @@ def add_task(id: str, data: AddTaskRequest):
|
|||
"""Add a new task to the workforce"""
|
||||
chat_logger.info(f"Adding task to workforce for task_id: {id}, content: {data.content[:100]}...")
|
||||
task_lock = get_task_lock(id)
|
||||
|
||||
|
||||
try:
|
||||
# Queue the add task action
|
||||
add_task_action = ActionAddTaskData(
|
||||
|
|
@ -188,11 +239,11 @@ def add_task(id: str, data: AddTaskRequest):
|
|||
project_id=data.project_id,
|
||||
task_id=data.task_id,
|
||||
additional_info=data.additional_info,
|
||||
insert_position=data.insert_position
|
||||
insert_position=data.insert_position,
|
||||
)
|
||||
asyncio.run(task_lock.put_queue(add_task_action))
|
||||
return Response(status_code=201)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error adding task for task_id: {id}: {e}")
|
||||
raise UserException(code.error, f"Failed to add task: {str(e)}")
|
||||
|
|
@ -204,7 +255,7 @@ def remove_task(project_id: str, task_id: str):
|
|||
"""Remove a task from the workforce"""
|
||||
chat_logger.info(f"Removing task {task_id} from workforce for project_id: {project_id}")
|
||||
task_lock = get_task_lock(project_id)
|
||||
|
||||
|
||||
try:
|
||||
# Queue the remove task action
|
||||
remove_task_action = ActionRemoveTaskData(task_id=task_id, project_id=project_id)
|
||||
|
|
@ -212,7 +263,7 @@ def remove_task(project_id: str, task_id: str):
|
|||
|
||||
chat_logger.info(f"Task removal request queued for project_id: {project_id}, removing task: {task_id}")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error removing task {task_id} for project_id: {project_id}: {e}")
|
||||
raise UserException(code.error, f"Failed to remove task: {str(e)}")
|
||||
|
|
@ -224,7 +275,7 @@ def skip_task(project_id: str):
|
|||
"""Skip a task in the workforce"""
|
||||
chat_logger.info(f"Skipping task in workforce for project_id: {project_id}")
|
||||
task_lock = get_task_lock(project_id)
|
||||
|
||||
|
||||
try:
|
||||
# Queue the skip task action
|
||||
skip_task_action = ActionSkipTaskData(project_id=project_id)
|
||||
|
|
@ -232,7 +283,7 @@ def skip_task(project_id: str):
|
|||
|
||||
chat_logger.info(f"Task skip request queued for project_id: {project_id}")
|
||||
return Response(status_code=201)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error skipping task for project_id: {project_id}: {e}")
|
||||
raise UserException(code.error, f"Failed to skip task: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -1061,7 +1061,7 @@ async function createWindow() {
|
|||
transparent: true,
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
backgroundColor: '#00000000',
|
||||
backgroundColor: '#f5f5f580',
|
||||
titleBarStyle: isMac ? 'hidden' : undefined,
|
||||
trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined,
|
||||
icon: path.join(VITE_PUBLIC, 'favicon.ico'),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useRef, useEffect } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||
import { Paperclip, ArrowRight, X, Image, FileText, UploadCloud, Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -108,18 +109,26 @@ export const Inputbox = ({
|
|||
const dragCounter = useRef(0);
|
||||
const [hoveredFilePath, setHoveredFilePath] = useState<string | null>(null);
|
||||
const [isRemainingOpen, setIsRemainingOpen] = useState(false);
|
||||
const remainingRef = useRef<HTMLDivElement | null>(null);
|
||||
const hoverCloseTimerRef = useRef<number | null>(null);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
if (!remainingRef.current) return;
|
||||
if (!remainingRef.current.contains(e.target as Node)) {
|
||||
setIsRemainingOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDocClick);
|
||||
return () => document.removeEventListener("mousedown", onDocClick);
|
||||
}, []);
|
||||
const openRemainingPopover = () => {
|
||||
if (hoverCloseTimerRef.current) {
|
||||
window.clearTimeout(hoverCloseTimerRef.current);
|
||||
hoverCloseTimerRef.current = null;
|
||||
}
|
||||
setIsRemainingOpen(true);
|
||||
};
|
||||
|
||||
const scheduleCloseRemainingPopover = () => {
|
||||
if (hoverCloseTimerRef.current) {
|
||||
window.clearTimeout(hoverCloseTimerRef.current);
|
||||
}
|
||||
hoverCloseTimerRef.current = window.setTimeout(() => {
|
||||
setIsRemainingOpen(false);
|
||||
hoverCloseTimerRef.current = null;
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// Auto-resize textarea on value changes (hug content up to max height)
|
||||
useEffect(() => {
|
||||
|
|
@ -148,7 +157,7 @@ export const Inputbox = ({
|
|||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !disabled) {
|
||||
if (e.key === "Enter" && !e.shiftKey && !disabled && !isComposing) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
|
|
@ -251,34 +260,36 @@ export const Inputbox = ({
|
|||
{/* Text Input Area */}
|
||||
<div className="box-border flex gap-2.5 items-center justify-center pb-2 pt-2.5 px-0 relative w-full">
|
||||
<div className="flex-1 box-border flex gap-2.5 items-center justify-center min-h-px min-w-px mx-2 py-0 relative">
|
||||
<Textarea
|
||||
variant="none"
|
||||
size="default"
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
disabled={disabled}
|
||||
placeholder= {t("chat.ask-placeholder")}
|
||||
className={cn(
|
||||
"flex-1 resize-none",
|
||||
"border-none shadow-none focus-visible:ring-0 focus-visible:outline-none",
|
||||
"px-0 py-0 min-h-[40px] max-h-[200px]",
|
||||
"scrollbar overflow-auto",
|
||||
isActive ? "text-input-text-focus" : "text-input-text-default"
|
||||
)}
|
||||
style={{
|
||||
fontFamily: "Inter",
|
||||
}}
|
||||
rows={1}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
|
||||
}}
|
||||
/>
|
||||
<Textarea
|
||||
variant="none"
|
||||
size="default"
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
disabled={disabled}
|
||||
placeholder= {t("chat.ask-placeholder")}
|
||||
className={cn(
|
||||
"flex-1 resize-none",
|
||||
"border-none shadow-none focus-visible:ring-0 focus-visible:outline-none",
|
||||
"px-0 py-0 min-h-[40px] max-h-[200px]",
|
||||
"scrollbar overflow-auto",
|
||||
isActive ? "text-input-text-focus" : "text-input-text-default"
|
||||
)}
|
||||
style={{
|
||||
fontFamily: "Inter",
|
||||
}}
|
||||
rows={1}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -330,57 +341,64 @@ export const Inputbox = ({
|
|||
})}
|
||||
{/* Show remaining count if more than 5 files */}
|
||||
{remainingCount > 0 && (
|
||||
<div ref={remainingRef} className="relative">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="bg-tag-surface box-border flex items-center relative rounded-lg h-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsRemainingOpen((v) => !v);
|
||||
}}
|
||||
<Popover open={isRemainingOpen} onOpenChange={setIsRemainingOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="bg-tag-surface box-border flex items-center relative rounded-lg h-auto"
|
||||
onMouseEnter={openRemainingPopover}
|
||||
onMouseLeave={scheduleCloseRemainingPopover}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<p className="font-['Inter'] font-bold leading-tight text-text-body text-xs whitespace-nowrap my-0">
|
||||
{remainingCount}+
|
||||
</p>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
className="!w-auto max-w-40 p-1 rounded-md border border-dropdown-border bg-dropdown-bg shadow-perfect"
|
||||
onMouseEnter={openRemainingPopover}
|
||||
onMouseLeave={scheduleCloseRemainingPopover}
|
||||
>
|
||||
<p className="font-['Inter'] font-bold leading-tight text-text-body text-xs whitespace-nowrap my-0">
|
||||
{remainingCount}+
|
||||
</p>
|
||||
</Button>
|
||||
{isRemainingOpen && (
|
||||
<div className="absolute left-0 mt-1 z-30 max-w-40 p-1 rounded-md border border-dropdown-border bg-dropdown-bg shadow-perfect">
|
||||
<div className="max-h-64 overflow-auto gap-1 flex flex-col">
|
||||
{files.slice(maxVisibleFiles, maxVisibleFiles + 5).map((file) => {
|
||||
const isHovered = hoveredFilePath === file.filePath;
|
||||
return (
|
||||
<div
|
||||
key={file.filePath}
|
||||
className="flex items-center gap-1 px-1 py-0.5 bg-tag-surface rounded-md"
|
||||
onMouseEnter={() => setHoveredFilePath(file.filePath)}
|
||||
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
|
||||
<div className="max-h-[176px] overflow-auto scrollbar-hide gap-1 flex flex-col">
|
||||
{files.slice(maxVisibleFiles).map((file) => {
|
||||
const isHovered = hoveredFilePath === file.filePath;
|
||||
return (
|
||||
<div
|
||||
key={file.filePath}
|
||||
className="flex items-center gap-1 px-1 py-0.5 bg-tag-surface hover:bg-tag-surface-hover transition-colors duration-300 cursor-pointer rounded-lg"
|
||||
onMouseEnter={() => setHoveredFilePath(file.filePath)}
|
||||
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
className={cn(
|
||||
"rounded-md cursor-pointer flex items-center justify-center w-6 h-6"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFile(file.filePath);
|
||||
setIsRemainingOpen(false);
|
||||
}}
|
||||
title={isHovered ? "Remove file" : file.fileName}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
className={cn(
|
||||
"rounded-md cursor-pointer flex items-center justify-center w-6 h-6"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFile(file.filePath);
|
||||
setIsRemainingOpen(false);
|
||||
}}
|
||||
title={isHovered ? "Remove file" : file.fileName}
|
||||
>
|
||||
{isHovered ? <X className="text-icon-secondary size-4" /> : getFileIcon(file.fileName)}
|
||||
</a>
|
||||
<p className="flex-1 font-['Inter'] font-bold leading-tight text-text-body text-xs whitespace-nowrap my-0 overflow-hidden text-ellipsis">
|
||||
{file.fileName}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{isHovered ? <X className="text-icon-secondary size-4" /> : getFileIcon(file.fileName)}
|
||||
</a>
|
||||
<p className="flex-1 font-['Inter'] font-bold leading-tight text-text-body text-xs whitespace-nowrap my-0 overflow-hidden text-ellipsis">
|
||||
{file.fileName}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default function BottomBox({
|
|||
else if (state === "confirm") backgroundClass = "bg-input-bg-confirm";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full overflow-hidden relative z-50">
|
||||
<div className="flex flex-col w-full relative z-50">
|
||||
{/* QueuedBox overlay (should not affect BoxMain layout) */}
|
||||
{queuedMessages.length > 0 && (
|
||||
<div className="px-2 z-50 pointer-events-auto">
|
||||
|
|
@ -82,7 +82,7 @@ export default function BottomBox({
|
|||
</div>
|
||||
)}
|
||||
{/* BoxMain */}
|
||||
<div className={`flex flex-col gap-2 w-full p-2 rounded-lg ${backgroundClass} overflow-hidden`}>
|
||||
<div className={`flex flex-col gap-2 w-full p-2 rounded-t-lg ${backgroundClass}`}>
|
||||
{/* BoxHeader variants */}
|
||||
{state === "splitting" && (
|
||||
<BoxHeaderSplitting />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const FloatingAction = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-2 bottom-2 left-0 right-0 flex w-full justify-center items-center z-20 pointer-events-none",
|
||||
"sticky top-2 bottom-2 mt-4 left-0 right-0 flex w-full justify-center items-center z-20 pointer-events-none",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Copy, FileText } from "lucide-react";
|
||||
import { MarkDown } from "./MarkDown";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Button } from "../../ui/button";
|
||||
|
||||
interface MessageCardProps {
|
||||
interface AgentMessageCardProps {
|
||||
id: string;
|
||||
content: string;
|
||||
role: "user" | "agent";
|
||||
className?: string;
|
||||
typewriter?: boolean;
|
||||
attaches?: File[];
|
||||
|
|
@ -16,15 +15,14 @@ interface MessageCardProps {
|
|||
// global Map to track completed typewriter effect content hash
|
||||
const completedTypewriterHashes = new Map<string, boolean>();
|
||||
|
||||
export function MessageCard({
|
||||
export function AgentMessageCard({
|
||||
id,
|
||||
content,
|
||||
role,
|
||||
typewriter = true,
|
||||
onTyping,
|
||||
className,
|
||||
attaches,
|
||||
}: MessageCardProps) {
|
||||
}: AgentMessageCardProps) {
|
||||
// use content hash to track if typewriter effect is completed
|
||||
const contentHash = useMemo(() => {
|
||||
return `${id}-${content}`;
|
||||
|
|
@ -34,11 +32,11 @@ export function MessageCard({
|
|||
const isCompleted = completedTypewriterHashes.has(contentHash);
|
||||
|
||||
// if completed, disable typewriter effect
|
||||
const enableTypewriter = role === "agent" && !isCompleted;
|
||||
const enableTypewriter = !isCompleted;
|
||||
|
||||
// when typewriter effect is completed, record to global Map
|
||||
const handleTypingComplete = () => {
|
||||
if (role === "agent" && !isCompleted) {
|
||||
if (!isCompleted) {
|
||||
completedTypewriterHashes.set(contentHash, true);
|
||||
}
|
||||
if (onTyping) {
|
||||
|
|
@ -53,17 +51,13 @@ export function MessageCard({
|
|||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`relative ${
|
||||
role === "agent" ? "bg-white-0% py-0" : "bg-white-80%"
|
||||
} w-full rounded-xl border px-4 py-3 ${className || ""} group`}
|
||||
className={`relative bg-white-0% w-full rounded-xl border px-sm py-3 ${className || ""} group overflow-hidden`}
|
||||
>
|
||||
{role === "user" && (
|
||||
<div className="absolute bottom-[0px] right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<Button onClick={handleCopy} variant="ghost" size="icon">
|
||||
<Copy />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-[0px] right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<Button onClick={handleCopy} variant="ghost" size="icon">
|
||||
<Copy />
|
||||
</Button>
|
||||
</div>
|
||||
<MarkDown
|
||||
content={content}
|
||||
onTyping={handleTypingComplete}
|
||||
|
|
@ -79,11 +73,11 @@ export function MessageCard({
|
|||
window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
|
||||
}}
|
||||
key={"attache-" + file.fileName}
|
||||
className="cursor-pointer flex items-center gap-2 bg-message-fill-default border border-solid border-task-border-default rounded-[12px] px-2 py-1 w-[140px] "
|
||||
className="cursor-pointer flex w-full items-center gap-2 bg-message-fill-default border border-solid border-task-border-default rounded-2xl pl-2 py-1 "
|
||||
>
|
||||
<FileText size={24} className="flex-shrink-0" />
|
||||
<div className="flex flex-col">
|
||||
<div className="max-w-[100px] font-bold text-sm text-body text-text-body overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div className="max-w-48 font-bold text-sm text-body text-text-body overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{file?.fileName?.split(".")[0]}
|
||||
</div>
|
||||
<div className="font-medium leading-29 text-xs text-text-body">
|
||||
|
|
@ -98,3 +92,4 @@ export function MessageCard({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
73
src/components/ChatBox/MessageItem/FeedbackCard.tsx
Normal file
73
src/components/ChatBox/MessageItem/FeedbackCard.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface FeedbackCardProps {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
onConfirm?: () => void;
|
||||
onSkip?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FeedbackCard({
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
onConfirm,
|
||||
onSkip,
|
||||
className,
|
||||
}: FeedbackCardProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(content);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`bg-message-fill-secondary w-full rounded-xl border px-4 py-3 flex flex-col gap-4 items-center justify-center relative group overflow-hidden ${className || ""}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Copy button - appears on hover */}
|
||||
<div className="absolute bottom-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<Button onClick={handleCopy} variant="ghost" size="icon">
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<p className="font-inter font-bold leading-normal text-sm text-text-body w-full">
|
||||
{title}
|
||||
</p>
|
||||
|
||||
{/* Content */}
|
||||
<p className="font-inter font-medium leading-normal text-sm text-text-body w-full">
|
||||
{content}
|
||||
</p>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-1 items-center w-full">
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
className="flex-1"
|
||||
>
|
||||
Answer Agent
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSkip}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="flex-1"
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,11 +5,11 @@ import remarkGfm from "remark-gfm";
|
|||
export const MarkDown = memo(
|
||||
({
|
||||
content,
|
||||
speed = 15,
|
||||
speed = 10,
|
||||
onTyping,
|
||||
enableTypewriter = true, // Whether to enable typewriter effect
|
||||
pTextSize = "text-[13px]",
|
||||
olPadding = "",
|
||||
pTextSize = "text-body-sm",
|
||||
olPadding = "pl-3",
|
||||
}: {
|
||||
content: string;
|
||||
speed?: number;
|
||||
|
|
@ -23,6 +23,9 @@ export const MarkDown = memo(
|
|||
useEffect(() => {
|
||||
if (!enableTypewriter) {
|
||||
setDisplayedContent(content);
|
||||
if (onTyping) {
|
||||
onTyping();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -33,9 +36,6 @@ export const MarkDown = memo(
|
|||
if (index < content.length) {
|
||||
setDisplayedContent(content.slice(0, index + 1));
|
||||
index++;
|
||||
if (onTyping) {
|
||||
onTyping();
|
||||
}
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
// when typewriter effect is completed, call callback
|
||||
|
|
@ -49,7 +49,7 @@ export const MarkDown = memo(
|
|||
}, [content, speed, enableTypewriter, onTyping]);
|
||||
|
||||
return (
|
||||
<div className="max-w-none">
|
||||
<div className="max-w-none markdown-container overflow-hidden">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
|
|
@ -70,36 +70,35 @@ export const MarkDown = memo(
|
|||
),
|
||||
p: ({ children }) => (
|
||||
<p
|
||||
className={`${pTextSize} font-medium text-primary leading-10 font-inter whitespace-pre-wrap break-words text-wrap`}
|
||||
style={{ margin: 0 }}
|
||||
className={`${pTextSize} font-medium text-text-body leading-10 font-inter whitespace-pre-wrap break-all`}
|
||||
style={{ margin: 0, wordBreak: 'break-all' }}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul
|
||||
className={`list-disc list-inside text-[13px] text-primary mb-2 ${olPadding}`}
|
||||
className={`list-disc list-outside text-body-sm text-text-body ml-3 mb-2 ${olPadding}`}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol
|
||||
className={`list-decimal list-inside text-[13px] text-primary mb-2 ${olPadding}`}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 list-inside">{children}</li>
|
||||
<li className="my-sm text-body-sm text-text-body">{children}</li>
|
||||
),
|
||||
code: ({ children }) => (
|
||||
<code className="bg-zinc-100 px-1 py-0.5 rounded text-xs font-mono">
|
||||
<code
|
||||
className="bg-zinc-100 px-1 py-0.5 rounded text-body-sm text-text-body font-mono whitespace-pre-wrap break-all"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-zinc-100 p-2 rounded text-xs overflow-x-auto">
|
||||
<pre
|
||||
className="bg-zinc-100 p-2 rounded text-xs overflow-x-auto whitespace-pre-wrap break-all"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
|
|
@ -117,7 +116,8 @@ export const MarkDown = memo(
|
|||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
className=" hover:text-blue-800 underline break-all"
|
||||
className="hover:text-blue-800 underline break-all"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { TaskType } from "./TaskType";
|
||||
import { TaskItem } from "./TaskItem";
|
||||
import { TaskType } from "../TaskBox/TaskType";
|
||||
import { TaskItem } from "../TaskBox/TaskItem";
|
||||
import ShinyText from "@/components/ui/ShinyText/ShinyText";
|
||||
|
||||
|
||||
import { ChevronDown, SquareCode } from "lucide-react";
|
||||
import { useMemo, useState, useRef, useEffect } from "react";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
172
src/components/ChatBox/MessageItem/UserMessageCard.tsx
Normal file
172
src/components/ChatBox/MessageItem/UserMessageCard.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { Copy, FileText, X, Image } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "../../ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
interface UserMessageCardProps {
|
||||
id: string;
|
||||
content: string;
|
||||
className?: string;
|
||||
attaches?: File[];
|
||||
}
|
||||
|
||||
export function UserMessageCard({
|
||||
id,
|
||||
content,
|
||||
className,
|
||||
attaches,
|
||||
}: UserMessageCardProps) {
|
||||
const [hoveredFilePath, setHoveredFilePath] = useState<string | null>(null);
|
||||
const [isRemainingOpen, setIsRemainingOpen] = useState(false);
|
||||
const hoverCloseTimerRef = useRef<number | null>(null);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(content);
|
||||
};
|
||||
|
||||
// Popover handles outside clicks; no manual listener needed
|
||||
const openRemainingPopover = () => {
|
||||
if (hoverCloseTimerRef.current) {
|
||||
window.clearTimeout(hoverCloseTimerRef.current);
|
||||
hoverCloseTimerRef.current = null;
|
||||
}
|
||||
setIsRemainingOpen(true);
|
||||
};
|
||||
|
||||
const scheduleCloseRemainingPopover = () => {
|
||||
if (hoverCloseTimerRef.current) {
|
||||
window.clearTimeout(hoverCloseTimerRef.current);
|
||||
}
|
||||
hoverCloseTimerRef.current = window.setTimeout(() => {
|
||||
setIsRemainingOpen(false);
|
||||
hoverCloseTimerRef.current = null;
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const getFileIcon = (fileName: string) => {
|
||||
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
||||
if (["jpg", "jpeg", "png", "gif", "webp"].includes(ext)) {
|
||||
return <Image className="w-4 h-4 text-icon-primary" />;
|
||||
}
|
||||
return <FileText className="w-4 h-4 text-icon-primary" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`relative bg-white-80% w-full rounded-xl border px-sm py-2 ${className || ""} group overflow-visible`}
|
||||
>
|
||||
<div className="absolute bottom-[0px] right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<Button onClick={handleCopy} variant="ghost" size="icon">
|
||||
<Copy />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-text-body text-body-sm whitespace-pre-wrap break-words">
|
||||
{content}
|
||||
</div>
|
||||
{attaches && attaches.length > 0 && (
|
||||
<div className="box-border flex flex-wrap gap-1 items-start relative w-full mt-2">
|
||||
{(() => {
|
||||
// Show max 4 files + count indicator
|
||||
const maxVisibleFiles = 4;
|
||||
const visibleFiles = attaches.slice(0, maxVisibleFiles);
|
||||
const remainingCount = attaches.length > maxVisibleFiles ? attaches.length - maxVisibleFiles : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{visibleFiles.map((file) => {
|
||||
const isHovered = hoveredFilePath === file.filePath;
|
||||
return (
|
||||
<div
|
||||
key={"attache-" + file.fileName}
|
||||
className={cn(
|
||||
"bg-tag-surface box-border flex gap-0.5 items-center relative rounded-lg max-w-32 h-auto cursor-pointer hover:bg-tag-surface-hover transition-colors duration-300"
|
||||
)}
|
||||
onMouseEnter={() => setHoveredFilePath(file.filePath)}
|
||||
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
|
||||
}}
|
||||
>
|
||||
{/* File icon */}
|
||||
<div className="rounded-md flex items-center justify-center w-6 h-6">
|
||||
{getFileIcon(file.fileName)}
|
||||
</div>
|
||||
|
||||
{/* File Name */}
|
||||
<p
|
||||
className={cn(
|
||||
"flex-1 font-['Inter'] font-bold leading-tight min-h-px min-w-px overflow-ellipsis overflow-hidden relative text-text-body text-xs whitespace-nowrap my-0"
|
||||
)}
|
||||
title={file.fileName}
|
||||
>
|
||||
{file.fileName}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Show remaining count if more than 4 files */}
|
||||
{remainingCount > 0 && (
|
||||
<Popover open={isRemainingOpen} onOpenChange={setIsRemainingOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="bg-tag-surface box-border flex items-center relative rounded-lg h-auto"
|
||||
onMouseEnter={openRemainingPopover}
|
||||
onMouseLeave={scheduleCloseRemainingPopover}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<p className="font-['Inter'] font-bold leading-tight text-text-body text-xs whitespace-nowrap my-0">
|
||||
{remainingCount}+
|
||||
</p>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
className="!w-auto max-w-40 p-1 rounded-md border border-dropdown-border bg-dropdown-bg shadow-perfect"
|
||||
onMouseEnter={openRemainingPopover}
|
||||
onMouseLeave={scheduleCloseRemainingPopover}
|
||||
>
|
||||
<div className="max-h-[176px] overflow-auto scrollbar-hide gap-1 flex flex-col">
|
||||
{attaches.slice(maxVisibleFiles).map((file) => {
|
||||
const isHovered = hoveredFilePath === file.filePath;
|
||||
return (
|
||||
<div
|
||||
key={file.filePath}
|
||||
className="flex items-center gap-1 py-0.5 bg-tag-surface hover:bg-tag-surface-hover transition-colors duration-300 cursor-pointer rounded-lg"
|
||||
onMouseLeave={() => setHoveredFilePath((prev) => (prev === file.filePath ? null : prev))}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.ipcRenderer.invoke("reveal-in-folder", file.filePath);
|
||||
setIsRemainingOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="rounded-md flex items-center justify-center w-6 h-6">
|
||||
{getFileIcon(file.fileName)}
|
||||
</div>
|
||||
<p className="flex-1 font-['Inter'] font-bold leading-tight text-text-body text-xs whitespace-nowrap my-0 overflow-hidden text-ellipsis">
|
||||
{file.fileName}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
|
|||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`flex-1 relative z-10 flex flex-col overflow-y-auto scrollbar gap-2 ${className}`}
|
||||
className={`flex-1 relative z-10 flex flex-col mt-sm overflow-y-auto scrollbar ${className}`}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{chatStores.map(({ chatId, chatStore }) => {
|
||||
|
|
@ -149,9 +149,12 @@ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
|
|||
}
|
||||
|
||||
const task = chatState.tasks[activeTaskId];
|
||||
const hasMessages = task.messages.length > 0 || task.hasMessages;
|
||||
|
||||
if (!hasMessages) {
|
||||
const messages = task.messages || [];
|
||||
|
||||
// Only render if there are actual user messages (not just empty or system messages)
|
||||
const hasUserMessages = messages.some((msg: any) => msg.role === 'user' && msg.content);
|
||||
|
||||
if (!hasUserMessages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const ProjectSection = React.forwardRef<HTMLDivElement, ProjectSectionPro
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative"
|
||||
className="relative mb-8"
|
||||
>
|
||||
{/* User Query Groups */}
|
||||
<div className="space-y-0">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
CircleSlash,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState, useRef, useEffect } from "react";
|
||||
import { TaskState, TaskStateType } from "../TaskState";
|
||||
import { TaskState, TaskStateType } from "@/components/TaskState";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
|
||||
interface TaskCardProps {
|
||||
|
|
@ -173,16 +173,16 @@ export function TaskCard({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full h-auto flex flex-col gap-2 transition-all duration-300">
|
||||
<div className="w-full h-auto bg-task-surface backdrop-blur-[5px] rounded-xl py-sm relative overflow-hidden">
|
||||
<div className="w-full h-auto flex flex-col pl-sm gap-2 transition-all duration-300">
|
||||
<div className="w-full h-auto bg-task-surface rounded-xl py-sm relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full bg-transparent">
|
||||
<Progress value={progressValue} className="h-[2px] w-full" />
|
||||
</div>
|
||||
<div className="text-sm font-bold leading-13 mb-2.5 px-sm">
|
||||
{summaryTask
|
||||
? summaryTask.split("|")[0].replace(/"/g, "")
|
||||
: "Thinking hard..."}
|
||||
</div>
|
||||
{summaryTask && (
|
||||
<div className="text-sm font-bold leading-13 mb-2.5 px-sm">
|
||||
{summaryTask.split("|")[0].replace(/"/g, "")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{summaryTask && (
|
||||
<div className={`flex items-center justify-between gap-2 px-sm`}>
|
||||
|
|
@ -422,7 +422,7 @@ export function TaskCard({
|
|||
</div>
|
||||
<div className="flex-1 flex flex-col items-start justify-center">
|
||||
<div
|
||||
className={` w-full break-words [overflow-wrap:anywhere] whitespace-pre-line ${
|
||||
className={` w-full break-words whitespace-pre-line ${
|
||||
task.status === "failed"
|
||||
? "text-text-cuation-default"
|
||||
: task.status === "blocked"
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
X,
|
||||
} from "lucide-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Button } from "../../ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TaskItemProps {
|
||||
|
|
@ -12,7 +12,7 @@ export const TypeCardSkeleton = ({
|
|||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full h-auto flex flex-col gap-2 px-2 py-sm transition-all duration-300 ">
|
||||
<div className="w-full h-auto flex flex-col gap-2 pl-2 py-sm transition-all duration-300 ">
|
||||
<div className="w-full h-auto bg-task-surface backdrop-blur-[5px] rounded-xl py-sm relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full bg-transparent">
|
||||
<Progress value={100} className="h-[2px] w-full" />
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
||||
import { MessageCard } from './MessageCard';
|
||||
import { NoticeCard } from './NoticeCard';
|
||||
import { TypeCardSkeleton } from './TypeCardSkeleton';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { UserMessageCard } from './MessageItem/UserMessageCard';
|
||||
import { AgentMessageCard } from './MessageItem/AgentMessageCard';
|
||||
import { NoticeCard } from './MessageItem/NoticeCard';
|
||||
import { TypeCardSkeleton } from './TaskBox/TypeCardSkeleton';
|
||||
import { TaskCard } from './TaskBox/TaskCard';
|
||||
import { VanillaChatStore } from '@/store/chatStore';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
interface QueryGroup {
|
||||
queryId: string;
|
||||
|
|
@ -54,7 +56,7 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
}
|
||||
return false;
|
||||
})());
|
||||
|
||||
|
||||
const isLastUserQuery = !queryGroup.taskMessage &&
|
||||
!isHumanReply &&
|
||||
activeTaskId &&
|
||||
|
|
@ -131,10 +133,13 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
}, [task]);
|
||||
|
||||
// Check if we're in skeleton phase
|
||||
const anyToSubTasksMessage = task?.messages.find((m: any) => m.step === "to_sub_tasks");
|
||||
const isSkeletonPhase = task && (
|
||||
(!task.messages.find((m: any) => m.step === "to_sub_tasks") &&
|
||||
!task.hasWaitComfirm && task.messages.length > 0) ||
|
||||
task.isTakeControl
|
||||
(task.status !== 'finished' &&
|
||||
!anyToSubTasksMessage &&
|
||||
!task.hasWaitComfirm &&
|
||||
task.messages.length > 0) ||
|
||||
(task.isTakeControl && !anyToSubTasksMessage)
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -155,20 +160,18 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="px-2 py-sm"
|
||||
className="pl-sm py-sm"
|
||||
>
|
||||
<MessageCard
|
||||
<UserMessageCard
|
||||
id={queryGroup.userMessage.id}
|
||||
role={queryGroup.userMessage.role}
|
||||
content={queryGroup.userMessage.content}
|
||||
onTyping={() => {}}
|
||||
attaches={queryGroup.userMessage.attaches}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Sticky Task Box - Show for each query group that has a task */}
|
||||
{task && (
|
||||
{/* Sticky Task Box - Show only when task exists and NOT in skeleton phase */}
|
||||
{task && !isSkeletonPhase && !isHumanReply && (
|
||||
<motion.div
|
||||
ref={taskBoxRef}
|
||||
className="sticky top-0 z-20"
|
||||
|
|
@ -182,19 +185,11 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
paddingTop: isTaskBoxSticky ? 0 : 8,
|
||||
paddingBottom: isTaskBoxSticky ? 0 : 8,
|
||||
paddingLeft: isTaskBoxSticky ? 0 : 8,
|
||||
paddingRight: isTaskBoxSticky ? 0 : 8
|
||||
y: 0
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: 0.1, // Slight delay for sequencing
|
||||
paddingTop: { duration: 0.3, ease: "easeInOut" },
|
||||
paddingBottom: { duration: 0.3, ease: "easeInOut" },
|
||||
paddingLeft: { duration: 0.3, ease: "easeInOut" },
|
||||
paddingRight: { duration: 0.3, ease: "easeInOut" }
|
||||
delay: 0.1 // Slight delay for sequencing
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -241,21 +236,20 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex flex-col gap-4"
|
||||
className="flex flex-col pl-3 gap-4"
|
||||
>
|
||||
<MessageCard
|
||||
<AgentMessageCard
|
||||
typewriter={
|
||||
task?.type !== "replay" ||
|
||||
(task?.type === "replay" && task?.delayTime !== 0)
|
||||
}
|
||||
id={message.id}
|
||||
role={message.role}
|
||||
content={message.content}
|
||||
onTyping={() => {}}
|
||||
/>
|
||||
{/* File List */}
|
||||
{message.fileList && (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<div className="flex pl-3 gap-2 flex-wrap">
|
||||
{message.fileList.map((file: any) => (
|
||||
<motion.div
|
||||
key={`file-${file.name}`}
|
||||
|
|
@ -284,28 +278,42 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
);
|
||||
} else if (message.content === "skip") {
|
||||
return (
|
||||
<MessageCard
|
||||
<motion.div
|
||||
key={`skip-${message.id}`}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex flex-col pl-3 gap-4"
|
||||
>
|
||||
<AgentMessageCard
|
||||
key={message.id}
|
||||
id={message.id}
|
||||
role={message.role}
|
||||
content="No reply received, task continues..."
|
||||
onTyping={() => {}}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MessageCard
|
||||
<motion.div
|
||||
key={`message-${message.id}`}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex flex-col pl-3 gap-4"
|
||||
>
|
||||
<AgentMessageCard
|
||||
key={message.id}
|
||||
typewriter={
|
||||
task?.type !== "replay" ||
|
||||
(task?.type === "replay" && task?.delayTime !== 0)
|
||||
}
|
||||
id={message.id}
|
||||
role={message.role}
|
||||
content={message.content}
|
||||
onTyping={() => {}}
|
||||
attaches={message.attaches}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
} else if (message.step === "end" && message.content === "") {
|
||||
|
|
@ -315,7 +323,7 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex flex-col gap-4"
|
||||
className="flex flex-col pl-3 gap-4"
|
||||
>
|
||||
{message.fileList && (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
|
|
@ -329,10 +337,11 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
chatState.setSelectedFile(activeTaskId as string, file);
|
||||
chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace");
|
||||
}}
|
||||
className="flex items-center gap-2 bg-message-fill-default rounded-sm px-2 py-1 w-[140px] cursor-pointer hover:bg-message-fill-hover transition-colors"
|
||||
>
|
||||
className="flex items-center gap-2 bg-message-fill-default rounded-2xl px-2 py-1 w-[120px] cursor-pointer hover:bg-message-fill-hover transition-colors"
|
||||
>
|
||||
<FileText size={16} className="text-icon-primary flex-shrink-0" />
|
||||
<div className="flex flex-col">
|
||||
<div className="max-w-[100px] font-bold text-sm text-body text-text-body overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div className="max-w-48 font-bold text-sm text-body text-text-body overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{file.name.split(".")[0]}
|
||||
</div>
|
||||
<div className="font-medium leading-29 text-xs text-text-body">
|
||||
|
|
|
|||
|
|
@ -484,9 +484,16 @@ export default function ChatBox(): JSX.Element {
|
|||
const messageIndex = chatStore.tasks[taskId].messages.findLastIndex(
|
||||
(item) => item.step === "to_sub_tasks"
|
||||
);
|
||||
const question = chatStore.tasks[taskId].messages[messageIndex - 2].content;
|
||||
const questionMessage = chatStore.tasks[taskId].messages[messageIndex - 2];
|
||||
const question = questionMessage.content;
|
||||
// Get the file attachments from the original user message (not from task.attaches which gets cleared after sending)
|
||||
const attachments = questionMessage.attaches || [];
|
||||
let id = chatStore.create();
|
||||
chatStore.setHasMessages(id, true);
|
||||
// Copy the file attachments to the new task
|
||||
if (attachments.length > 0) {
|
||||
chatStore.setAttaches(id, attachments);
|
||||
}
|
||||
chatStore.removeTask(taskId);
|
||||
proxyFetchDelete(`/api/chat/history/${taskId}`);
|
||||
setMessage(question);
|
||||
|
|
@ -645,10 +652,11 @@ export default function ChatBox(): JSX.Element {
|
|||
// Check if any chat store in the project has messages
|
||||
const hasAnyMessages = useMemo(() => {
|
||||
// First check current active chat store
|
||||
if (chatStore.activeTaskId &&
|
||||
(chatStore.tasks[chatStore.activeTaskId].messages.length > 0 ||
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages)) {
|
||||
return true;
|
||||
if (chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId]) {
|
||||
const activeTask = chatStore.tasks[chatStore.activeTaskId];
|
||||
if ((activeTask.messages && activeTask.messages.length > 0) || activeTask.hasMessages) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Then check all other chat stores in the project
|
||||
|
|
@ -662,11 +670,9 @@ export default function ChatBox(): JSX.Element {
|
|||
}, [chatStore, getAllChatStoresMemoized]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<div className="w-full h-full flex-none items-center justify-center">
|
||||
{hasAnyMessages ? (
|
||||
<div className="w-full h-[calc(100vh-54px)] flex flex-col rounded-xl border border-border-disabled border-solid relative shadow-blur-effect overflow-hidden">
|
||||
<div className="absolute inset-0 blur-bg bg-bg-surface-secondary pointer-events-none"></div>
|
||||
|
||||
<div className="w-full h-full flex-1 flex flex-col">
|
||||
{/* New Project Chat Container */}
|
||||
<ProjectChatContainer
|
||||
onPauseResume={handlePauseResume}
|
||||
|
|
@ -723,8 +729,8 @@ export default function ChatBox(): JSX.Element {
|
|||
</div>
|
||||
) : (
|
||||
// Init ChatBox
|
||||
<div className="w-full h-[calc(100vh-54px)] flex items-center rounded-xl border border-border-disabled py-2 border-solid relative overflow-hidden">
|
||||
<div className="absolute inset-0 blur-bg bg-bg-surface-secondary pointer-events-none"></div>
|
||||
<div className="w-full h-[calc(100vh-54px)] flex items-center py-2 relative overflow-hidden">
|
||||
<div className="absolute inset-0 pointer-events-none"></div>
|
||||
<div className=" w-full flex flex-col relative z-10">
|
||||
<div className="flex flex-col items-center gap-1 h-[210px] justify-end">
|
||||
<div className="text-body-lg text-text-heading text-center font-bold">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import FolderComponent from "./FolderComponent";
|
||||
|
||||
import { MarkDown } from "@/components/ChatBox/MarkDown";
|
||||
import { MarkDown } from "@/components/ChatBox/MessageItem/MarkDown";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { proxyFetchGet } from "@/api/http";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ export default function HistorySidebar() {
|
|||
/>
|
||||
</div>
|
||||
<div className="text-left text-[14px] text-text-primary font-bold leading-9 overflow-hidden text-ellipsis break-words line-clamp-3">
|
||||
{task?.messages[0]?.content || t("layout.new-project")}
|
||||
{task?.messages?.[0]?.content || t("layout.new-project")}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Progress
|
||||
|
|
@ -430,13 +430,13 @@ export default function HistorySidebar() {
|
|||
<TooltipSimple
|
||||
content={
|
||||
<p>
|
||||
{task?.messages[0]?.content || t("layout.new-project")}
|
||||
{task?.messages?.[0]?.content || t("layout.new-project")}
|
||||
</p>
|
||||
}
|
||||
className="w-[300px] bg-surface-tertiary p-2 text-wrap break-words text-label-xs select-text pointer-events-auto shadow-perfect"
|
||||
>
|
||||
<span>
|
||||
{task?.messages[0]?.content || t("dashboard.new-project")}
|
||||
{task?.messages?.[0]?.content || t("dashboard.new-project")}
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,17 @@ import {
|
|||
ChevronLeft,
|
||||
LayoutGrid,
|
||||
Share,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import "./index.css";
|
||||
import folderIcon from "@/assets/Folder.svg";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useSidebarStore } from "@/store/sidebarStore";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
|
|
@ -140,6 +147,8 @@ function HeaderWin() {
|
|||
|
||||
const handleEndProject = async () => {
|
||||
const taskId = chatStore.activeTaskId;
|
||||
const currentProjectId = projectStore.activeProjectId;
|
||||
|
||||
if (!taskId) {
|
||||
toast.error(t("layout.no-active-project-to-end"));
|
||||
return;
|
||||
|
|
@ -179,9 +188,9 @@ function HeaderWin() {
|
|||
// Remove from local store
|
||||
chatStore.removeTask(taskId);
|
||||
|
||||
// Create a new project
|
||||
const newTaskId = chatStore.create();
|
||||
chatStore.setActiveTaskId(newTaskId);
|
||||
// Create a completely new project instead of just a new task
|
||||
// This ensures we start fresh without any residual state
|
||||
projectStore.createProject("new project");
|
||||
|
||||
// Navigate to home with replace to force refresh
|
||||
navigate("/", { replace: true });
|
||||
|
|
@ -240,7 +249,6 @@ function HeaderWin() {
|
|||
onClick={() => navigate("/")}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
{t("layout.back")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -304,60 +312,6 @@ function HeaderWin() {
|
|||
platform === "darwin" && "pr-2"
|
||||
} flex h-full items-center z-50 relative no-drag gap-1`}
|
||||
>
|
||||
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
|
||||
<>
|
||||
<TooltipSimple content={t("layout.report-bug")} side="bottom" align="center">
|
||||
<Button
|
||||
onClick={exportLog}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="no-drag"
|
||||
>
|
||||
<FileDown className="w-4 h-4" />
|
||||
{t("layout.report-bug")}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</>
|
||||
)}
|
||||
<TooltipSimple content={t("layout.refer-friends")} side="bottom" align="center">
|
||||
<Button
|
||||
onClick={getReferFriendsLink}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="no-drag"
|
||||
>
|
||||
<img
|
||||
src={giftIcon}
|
||||
alt="gift-icon"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{t("layout.refer-friends")}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple content={t("layout.settings")} side="bottom" align="center">
|
||||
<Button
|
||||
onClick={() => navigate("/history?tab=settings")}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="no-drag"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
{t("layout.settings")}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
{chatStore.activeTaskId &&
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && (
|
||||
<TooltipSimple content={t("layout.share")} side="bottom" align="end">
|
||||
<Button
|
||||
onClick={() => handleShare(chatStore.activeTaskId as string)}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
className="no-drag !text-button-fill-information-foreground"
|
||||
>
|
||||
{t("layout.share")}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
{chatStore.activeTaskId &&
|
||||
chatStore.tasks[chatStore.activeTaskId as string] &&
|
||||
(
|
||||
|
|
@ -377,6 +331,50 @@ function HeaderWin() {
|
|||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
{chatStore.activeTaskId &&
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && (
|
||||
<TooltipSimple content={t("layout.share")} side="bottom" align="end">
|
||||
<Button
|
||||
onClick={() => handleShare(chatStore.activeTaskId as string)}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="no-drag !text-button-fill-information-foreground bg-button-fill-information"
|
||||
>
|
||||
{t("layout.share")}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="no-drag"
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-36">
|
||||
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
|
||||
<DropdownMenuItem onClick={exportLog} className="cursor-pointer">
|
||||
<FileDown className="w-4 h-4" />
|
||||
{t("layout.report-bug")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={getReferFriendsLink} className="cursor-pointer">
|
||||
<img
|
||||
src={giftIcon}
|
||||
alt="gift-icon"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{t("layout.refer-friends")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate("/history?tab=settings")} className="cursor-pointer">
|
||||
<Settings className="w-4 h-4" />
|
||||
{t("layout.settings")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{location.pathname === "/history" && (
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const MarkDown = ({
|
|||
return text.replace(/\\n/g, " \n "); // add two spaces before \n, so ReactMarkdown will recognize it as a line break
|
||||
};
|
||||
return (
|
||||
<div className="prose prose-sm w-full select-text pointer-events-auto overflow-x-auto">
|
||||
<div className="prose prose-sm w-full select-text pointer-events-auto overflow-x-auto markdown-container">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
|
|
@ -81,13 +81,13 @@ export const MarkDown = ({
|
|||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol
|
||||
className={`list-decimal list-inside text-xs text-primary mb-1 ${olPadding}`}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
// ol: ({ children }) => (
|
||||
// <ol
|
||||
// className={`list-decimal list-inside text-xs text-primary mb-1 ${olPadding}`}
|
||||
// >
|
||||
// {children}
|
||||
// </ol>
|
||||
// ),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 list-inside break-all">{children}</li>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -63,10 +63,15 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-solid border-zinc-200 bg-white-100% p-xs text-popover-foreground shadow-md",
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden p-xs text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
borderRadius: 'var(--borderRadius-rounded-xl, 0.75rem)',
|
||||
border: '1px solid var(--dropdown-border, #CCC)',
|
||||
background: 'var(--dropdown-bg, #FFF)'
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
|
|
@ -82,7 +87,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-none transition-colors bg-menubutton-fill-default data-[highlighted]:bg-menubutton-fill-hover focus:bg-menubutton-fill-active focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -393,11 +393,11 @@ export default function Project() {
|
|||
</div>
|
||||
<div
|
||||
className={`px-3 h-full flex gap-sm border-[0px] border-solid ${
|
||||
task.taskAssigning.length > 0 &&
|
||||
task.taskAssigning && task.taskAssigning.length > 0 &&
|
||||
"border-x border-white-100%"
|
||||
}`}
|
||||
>
|
||||
{task.taskAssigning.map((taskAssigning) => (
|
||||
{task.taskAssigning && task.taskAssigning.map((taskAssigning) => (
|
||||
<div
|
||||
key={taskAssigning.agent_id}
|
||||
aria-label="Toggle bold"
|
||||
|
|
|
|||
|
|
@ -77,7 +77,14 @@ export default function Home() {
|
|||
navigate("/");
|
||||
};
|
||||
|
||||
useEffect(() => {}, []);
|
||||
useEffect(() => {
|
||||
// Update active tab when URL parameter changes
|
||||
const tabFromUrl = searchParams.get('tab');
|
||||
const validTabs = ["projects", "workers", "trigger", "settings", "mcp_tools"];
|
||||
if (tabFromUrl && validTabs.includes(tabFromUrl)) {
|
||||
setActiveTab(tabFromUrl as typeof activeTab);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div ref={scrollContainerRef} className="h-full overflow-y-auto scrollbar-hide mx-auto">
|
||||
|
|
|
|||
|
|
@ -75,19 +75,18 @@ export default function Home() {
|
|||
|
||||
// capture webview
|
||||
const captureWebview = async () => {
|
||||
if (
|
||||
chatStore.tasks[chatStore.activeTaskId as string].status === "finished"
|
||||
) {
|
||||
const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
|
||||
if (!activeTask || activeTask.status === "finished") {
|
||||
return;
|
||||
}
|
||||
webviews.map((webview) => {
|
||||
window.ipcRenderer
|
||||
.invoke("capture-webview", webview.id)
|
||||
.then((base64: string) => {
|
||||
if (chatStore.tasks[chatStore.activeTaskId as string].type) return;
|
||||
const currentTask = chatStore.tasks[chatStore.activeTaskId as string];
|
||||
if (!currentTask || currentTask.type) return;
|
||||
let taskAssigning = [
|
||||
...chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.taskAssigning,
|
||||
...currentTask.taskAssigning,
|
||||
];
|
||||
const searchAgentIndex = taskAssigning.findIndex(
|
||||
(agent) => agent.agent_id === webview.agent_id
|
||||
|
|
@ -186,14 +185,12 @@ export default function Home() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 flex flex-row overflow-hidden pt-8">
|
||||
<div className="h-full min-h-0 flex flex-row overflow-hidden pt-10 px-2 pb-2">
|
||||
<ReactFlowProvider>
|
||||
<div className="flex-1 min-w-0 min-h-0 flex items-center justify-center gap-2 relative overflow-hidden">
|
||||
<div className="flex-1 min-w-0 min-h-0 flex items-center justify-center bg-surface-secondary border-solid border-border-tertiary rounded-2xl gap-2 relative overflow-hidden">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={30} minSize={20}>
|
||||
<div className="flex-1 h-full min-w-0 min-h-0 flex items-center justify-center py-2 pl-2 overflow-hidden">
|
||||
<ChatBox />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle={true} className="custom-resizable-handle" />
|
||||
<ResizablePanel>
|
||||
|
|
|
|||
|
|
@ -8,14 +8,7 @@ const Login = lazy(() => import("@/pages/Login"));
|
|||
const Signup = lazy(() => import("@/pages/SignUp"));
|
||||
const Home = lazy(() => import("@/pages/Home"));
|
||||
const History = lazy(() => import("@/pages/History"));
|
||||
const Setting = lazy(() => import("@/pages/Setting"));
|
||||
const NotFound = lazy(() => import("@/pages/NotFound"));
|
||||
const SettingGeneral = lazy(() => import("@/pages/Setting/General"));
|
||||
const SettingPrivacy = lazy(() => import("@/pages/Setting/Privacy"));
|
||||
const SettingModels = lazy(() => import("@/pages/Setting/Models"));
|
||||
const SettingAPI = lazy(() => import("@/pages/Setting/API"));
|
||||
const SettingMCP = lazy(() => import("@/pages/Setting/MCP"));
|
||||
const MCPMarket = lazy(() => import("@/pages/Setting/MCPMarket"));
|
||||
|
||||
// Route guard: Check if user is logged in
|
||||
const ProtectedRoute = () => {
|
||||
|
|
@ -49,16 +42,8 @@ const AppRoutes = () => (
|
|||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
<Route path="/setting" element={<Setting />}>
|
||||
{/* Setting sub-routes */}
|
||||
<Route index element={<Navigate to="general" replace />} />
|
||||
<Route path="general" element={<SettingGeneral />} />
|
||||
<Route path="privacy" element={<SettingPrivacy />} />
|
||||
<Route path="models" element={<SettingModels />} />
|
||||
<Route path="api" element={<SettingAPI />} />
|
||||
<Route path="mcp" element={<SettingMCP />} />
|
||||
<Route path="mcp_market" element={<MCPMarket />} />
|
||||
</Route>
|
||||
<Route path="/setting" element={<Navigate to="/history?tab=settings" replace />} />
|
||||
<Route path="/setting/*" element={<Navigate to="/history?tab=settings" replace />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ body {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -92,17 +91,16 @@ body {
|
|||
|
||||
/* Custom ResizableHandle Styles */
|
||||
.custom-resizable-handle {
|
||||
width: 4px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
background: rgba(200,200,200,0.3);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-resizable-handle:hover {
|
||||
background: var(--border-primary);
|
||||
width: 4px;
|
||||
border-radius: 20px;
|
||||
background: var(--border-information);
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
transform: none;
|
||||
}
|
||||
|
|
@ -260,15 +258,15 @@ code {
|
|||
}
|
||||
|
||||
.scrollbar.scrolling::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.8);
|
||||
background-color: rgba(156, 163, 175, 0.2);
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.8);
|
||||
background-color: rgba(156, 163, 175, 0.2);
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-always-visible {
|
||||
|
|
@ -416,4 +414,9 @@ code {
|
|||
.stack-login-btn,
|
||||
.stack-login-btn button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-container ol {
|
||||
padding-left: 1rem;
|
||||
font-size: 12px; /* text-sm */
|
||||
}
|
||||
|
|
@ -363,7 +363,7 @@ module.exports = {
|
|||
},
|
||||
menubutton: {
|
||||
"fill-default": "var(--menubutton-fill-default)",
|
||||
"fill-hover": "var(--menubutton-fill-active)",
|
||||
"fill-hover": "var(--menubutton-fill-hover)",
|
||||
"fill-active": "var(--menubutton-fill-active)",
|
||||
"border-active": "var(--menubutton-border-active)",
|
||||
"border-default": "var(--menubutton-border-default)",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue