feat: add enhanced MULTIX UI features
Added all missing MULTIX enhancements matching the original screenshot: 1. STREAMING indicator: - Animated purple badge with sparkles icon - Shows live token count during streaming - Pulsing animation effect 2. Status badges: - PENDING/RUNNING/DONE badges for tasks - Color-coded based on status 3. APEX/SHIELD renamed: - 'Auto' → 'APEX' with tooltip - 'Shield' → 'SHIELD' with tooltip 4. THINKING indicator: - Bouncing dots animation (3 dots) - Shows THINKING or SENDING status 5. STOP button: - Red stop button appears during agent work - Calls cancel endpoint to interrupt 6. Detailed token stats bar: - INPUT/OUTPUT tokens - REASONING tokens (amber) - CACHE READ (emerald) - CACHE WRITE (cyan) - COST (violet) - MODEL (indigo) 7. Message navigation sidebar: - YOU/ASST labels for each message - Click to scroll to message - Appears on right side when viewing task
This commit is contained in:
@@ -27,6 +27,12 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
Shield,
|
Shield,
|
||||||
Activity,
|
Activity,
|
||||||
|
Square,
|
||||||
|
Clock,
|
||||||
|
Sparkles,
|
||||||
|
StopCircle,
|
||||||
|
Bot,
|
||||||
|
User,
|
||||||
} from "lucide-solid";
|
} from "lucide-solid";
|
||||||
import type { InstanceMessageStore } from "@/stores/message-v2/instance-store";
|
import type { InstanceMessageStore } from "@/stores/message-v2/instance-store";
|
||||||
import type { Task } from "@/types/session";
|
import type { Task } from "@/types/session";
|
||||||
@@ -105,9 +111,21 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
|||||||
return {
|
return {
|
||||||
used: usage?.actualUsageTokens ?? 0,
|
used: usage?.actualUsageTokens ?? 0,
|
||||||
total: usage?.totalCost ?? 0,
|
total: usage?.totalCost ?? 0,
|
||||||
|
input: usage?.inputTokens ?? 0,
|
||||||
|
output: usage?.outputTokens ?? 0,
|
||||||
|
reasoning: usage?.reasoningTokens ?? 0,
|
||||||
|
cacheRead: usage?.cacheReadTokens ?? 0,
|
||||||
|
cacheWrite: usage?.cacheWriteTokens ?? 0,
|
||||||
|
cost: usage?.totalCost ?? 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get current model from instance
|
||||||
|
const currentModel = createMemo(() => {
|
||||||
|
const instance = instances().get(props.instanceId);
|
||||||
|
return instance?.modelId || "unknown";
|
||||||
|
});
|
||||||
|
|
||||||
const activeTaskSessionId = createMemo(() => {
|
const activeTaskSessionId = createMemo(() => {
|
||||||
const task = selectedTask();
|
const task = selectedTask();
|
||||||
return task?.taskSessionId || props.sessionId;
|
return task?.taskSessionId || props.sessionId;
|
||||||
@@ -153,7 +171,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const ids = filteredMessageIds();
|
const ids = filteredMessageIds();
|
||||||
const thinking = isAgentThinking();
|
const thinking = isAgentThinking();
|
||||||
|
|
||||||
// Scroll when message count changes or when thinking starts
|
// Scroll when message count changes or when thinking starts
|
||||||
if (ids.length > 0 || thinking) {
|
if (ids.length > 0 || thinking) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -242,6 +260,25 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Stop/cancel the current agent operation
|
||||||
|
const handleStopAgent = async () => {
|
||||||
|
const task = selectedTask();
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
log.info("Stopping agent for task:", task.id);
|
||||||
|
// Send interrupt signal via the session API
|
||||||
|
try {
|
||||||
|
const targetSessionId = task.taskSessionId || props.sessionId;
|
||||||
|
// Use the cancel endpoint or interrupt mechanism
|
||||||
|
await fetch(`/api/workspaces/${props.instanceId}/sessions/${targetSessionId}/cancel`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
log.info("Agent stopped successfully");
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to stop agent:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="h-full flex flex-col bg-[#0a0a0b] text-zinc-300 font-sans selection:bg-indigo-500/30">
|
<main class="h-full flex flex-col bg-[#0a0a0b] text-zinc-300 font-sans selection:bg-indigo-500/30">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -274,6 +311,27 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
|
{/* STREAMING indicator */}
|
||||||
|
<Show when={isAgentThinking()}>
|
||||||
|
<div class="flex items-center space-x-2 px-3 py-1.5 bg-violet-500/15 border border-violet-500/30 rounded-lg animate-pulse shadow-[0_0_20px_rgba(139,92,246,0.2)]">
|
||||||
|
<Sparkles size={12} class="text-violet-400 animate-spin" style={{ "animation-duration": "3s" }} />
|
||||||
|
<span class="text-[10px] font-black text-violet-400 uppercase tracking-tight">Streaming</span>
|
||||||
|
<span class="text-[10px] font-bold text-violet-300">{formatTokenTotal(tokenStats().used)}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Task status badge */}
|
||||||
|
<Show when={selectedTask()}>
|
||||||
|
<div class={`px-2 py-1 rounded text-[9px] font-black uppercase tracking-tight border ${selectedTask()?.status === "completed"
|
||||||
|
? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
|
||||||
|
: selectedTask()?.status === "in-progress"
|
||||||
|
? "bg-indigo-500/15 border-indigo-500/30 text-indigo-400"
|
||||||
|
: "bg-amber-500/15 border-amber-500/30 text-amber-400"
|
||||||
|
}`}>
|
||||||
|
{selectedTask()?.status === "completed" ? "DONE" : selectedTask()?.status === "in-progress" ? "RUNNING" : "PENDING"}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<button class="p-2 text-zinc-500 hover:text-white transition-all hover:bg-white/5 rounded-xl active:scale-90">
|
<button class="p-2 text-zinc-500 hover:text-white transition-all hover:bg-white/5 rounded-xl active:scale-90">
|
||||||
<Command size={18} strokeWidth={2} />
|
<Command size={18} strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
@@ -336,181 +394,275 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Main Content Area - min-h-0 is critical for flex containers with overflow */}
|
{/* Main Content Area - min-h-0 is critical for flex containers with overflow */}
|
||||||
<div class="flex-1 min-h-0 relative overflow-hidden flex flex-col">
|
<div class="flex-1 min-h-0 relative overflow-hidden flex">
|
||||||
<div
|
{/* Main chat area */}
|
||||||
ref={scrollContainer}
|
<div class="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden custom-scrollbar"
|
<div
|
||||||
>
|
ref={scrollContainer}
|
||||||
<Show when={!selectedTaskId()} fallback={
|
class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden custom-scrollbar"
|
||||||
<div class="p-3 pb-4 overflow-x-hidden">
|
>
|
||||||
<MessageBlockList
|
<Show when={!selectedTaskId()} fallback={
|
||||||
instanceId={props.instanceId}
|
<div class="p-3 pb-4 overflow-x-hidden">
|
||||||
sessionId={activeTaskSessionId()}
|
<MessageBlockList
|
||||||
store={messageStore}
|
instanceId={props.instanceId}
|
||||||
messageIds={filteredMessageIds}
|
sessionId={activeTaskSessionId()}
|
||||||
lastAssistantIndex={lastAssistantIndex}
|
store={messageStore}
|
||||||
showThinking={() => true}
|
messageIds={filteredMessageIds}
|
||||||
thinkingDefaultExpanded={() => true}
|
lastAssistantIndex={lastAssistantIndex}
|
||||||
showUsageMetrics={() => true}
|
showThinking={() => true}
|
||||||
scrollContainer={() => scrollContainer}
|
thinkingDefaultExpanded={() => true}
|
||||||
setBottomSentinel={setBottomSentinel}
|
showUsageMetrics={() => true}
|
||||||
/>
|
scrollContainer={() => scrollContainer}
|
||||||
</div>
|
setBottomSentinel={setBottomSentinel}
|
||||||
}>
|
/>
|
||||||
{/* Pipeline View */}
|
|
||||||
<div class="p-4 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<h2 class="text-2xl font-black text-white tracking-tight leading-none">Pipeline</h2>
|
|
||||||
<p class="text-xs font-medium text-zinc-500 uppercase tracking-[0.2em]">Agentic Orchestration</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
}>
|
||||||
<div class="space-y-4">
|
{/* Pipeline View */}
|
||||||
<div class="flex items-center justify-between">
|
<div class="p-4 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
<span class="text-[10px] font-bold text-zinc-600 uppercase tracking-widest">Active Threads</span>
|
<div class="space-y-2">
|
||||||
<div class="h-px flex-1 bg-white/5 mx-4" />
|
<h2 class="text-2xl font-black text-white tracking-tight leading-none">Pipeline</h2>
|
||||||
<span class="text-[10px] font-black text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20">
|
<p class="text-xs font-medium text-zinc-500 uppercase tracking-[0.2em]">Agentic Orchestration</p>
|
||||||
{tasks().length}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-3">
|
<div class="space-y-4">
|
||||||
<For each={tasks()} fallback={
|
<div class="flex items-center justify-between">
|
||||||
<div class="group relative p-8 rounded-3xl border border-dashed border-white/5 bg-zinc-900/20 flex flex-col items-center justify-center text-center space-y-4 transition-all hover:bg-zinc-900/40 hover:border-white/10">
|
<span class="text-[10px] font-bold text-zinc-600 uppercase tracking-widest">Active Threads</span>
|
||||||
<div class="w-12 h-12 rounded-2xl bg-white/5 flex items-center justify-center text-zinc-600 group-hover:text-indigo-400 group-hover:scale-110 transition-all duration-500">
|
<div class="h-px flex-1 bg-white/5 mx-4" />
|
||||||
<Plus size={24} strokeWidth={1.5} />
|
<span class="text-[10px] font-black text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20">
|
||||||
</div>
|
{tasks().length}
|
||||||
<div class="space-y-1">
|
</span>
|
||||||
<p class="text-sm font-bold text-zinc-400">No active tasks</p>
|
</div>
|
||||||
<p class="text-[11px] text-zinc-600">Send a message below to start a new thread</p>
|
|
||||||
</div>
|
<div class="grid gap-3">
|
||||||
</div>
|
<For each={tasks()} fallback={
|
||||||
}>
|
<div class="group relative p-8 rounded-3xl border border-dashed border-white/5 bg-zinc-900/20 flex flex-col items-center justify-center text-center space-y-4 transition-all hover:bg-zinc-900/40 hover:border-white/10">
|
||||||
{(task) => (
|
<div class="w-12 h-12 rounded-2xl bg-white/5 flex items-center justify-center text-zinc-600 group-hover:text-indigo-400 group-hover:scale-110 transition-all duration-500">
|
||||||
<button
|
<Plus size={24} strokeWidth={1.5} />
|
||||||
onClick={() => setSelectedTaskId(task.id)}
|
|
||||||
class="group relative p-4 rounded-2xl border border-white/5 bg-zinc-900/40 hover:bg-zinc-800/60 hover:border-indigo-500/30 transition-all duration-300 text-left flex items-start space-x-4 active:scale-[0.98]"
|
|
||||||
>
|
|
||||||
<div class={`mt-1 w-2 h-2 rounded-full shadow-[0_0_10px_rgba(var(--color),0.5)] ${task.status === "completed" ? "bg-emerald-500 shadow-emerald-500/40" :
|
|
||||||
task.status === "in-progress" ? "bg-indigo-500 shadow-indigo-500/40 animate-pulse" :
|
|
||||||
"bg-zinc-600 shadow-zinc-600/20"
|
|
||||||
}`} />
|
|
||||||
<div class="flex-1 min-w-0 space-y-1">
|
|
||||||
<p class="text-sm font-bold text-zinc-100 truncate group-hover:text-white transition-colors">
|
|
||||||
{task.title}
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center space-x-3 text-[10px] font-bold text-zinc-500 uppercase tracking-tight">
|
|
||||||
<span>{new Date(task.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
||||||
<span class="w-1 h-1 rounded-full bg-zinc-800" />
|
|
||||||
<span>{task.messageIds?.length || 0} messages</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight size={16} class="text-zinc-700 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" />
|
<div class="space-y-1">
|
||||||
</button>
|
<p class="text-sm font-bold text-zinc-400">No active tasks</p>
|
||||||
)}
|
<p class="text-[11px] text-zinc-600">Send a message below to start a new thread</p>
|
||||||
</For>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}>
|
||||||
</div>
|
{(task) => (
|
||||||
</Show>
|
<button
|
||||||
</div>
|
onClick={() => setSelectedTaskId(task.id)}
|
||||||
|
class="group relative p-4 rounded-2xl border border-white/5 bg-zinc-900/40 hover:bg-zinc-800/60 hover:border-indigo-500/30 transition-all duration-300 text-left flex items-start space-x-4 active:scale-[0.98]"
|
||||||
{/* Chat Input Area - Fixed at bottom */}
|
>
|
||||||
<div class="p-3 bg-[#0a0a0b] border-t border-white/5 shrink-0">
|
<div class={`mt-1 w-2 h-2 rounded-full shadow-[0_0_10px_rgba(var(--color),0.5)] ${task.status === "completed" ? "bg-emerald-500 shadow-emerald-500/40" :
|
||||||
<div class="w-full bg-zinc-900/80 border border-white/10 rounded-2xl shadow-lg p-3">
|
task.status === "in-progress" ? "bg-indigo-500 shadow-indigo-500/40 animate-pulse" :
|
||||||
{/* Header Row */}
|
"bg-zinc-600 shadow-zinc-600/20"
|
||||||
<div class="flex items-center justify-between mb-2">
|
}`} />
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex-1 min-w-0 space-y-1">
|
||||||
<div class="w-5 h-5 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center">
|
<p class="text-sm font-bold text-zinc-100 truncate group-hover:text-white transition-colors">
|
||||||
<AtSign size={10} class="text-white" strokeWidth={3} />
|
{task.title}
|
||||||
</div>
|
</p>
|
||||||
<div class="flex flex-col">
|
<div class="flex items-center space-x-3 text-[10px] font-bold text-zinc-500 uppercase tracking-tight">
|
||||||
<span class="text-[10px] font-bold text-zinc-100 uppercase tracking-wide">
|
<span>{new Date(task.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||||
{selectedTaskId() ? "Task Context" : "Global Pipeline"}
|
<span class="w-1 h-1 rounded-full bg-zinc-800" />
|
||||||
</span>
|
<span>{task.messageIds?.length || 0} messages</span>
|
||||||
<span class="text-[9px] text-zinc-500 uppercase">
|
</div>
|
||||||
{selectedTaskId() ? "MultiX Threaded" : "Auto-Task"}
|
</div>
|
||||||
</span>
|
<ChevronRight size={16} class="text-zinc-700 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
)}
|
||||||
|
</For>
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleAutonomous(props.instanceId)}
|
|
||||||
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().isAutonomous
|
|
||||||
? "bg-indigo-500/20 border-indigo-500/40 text-indigo-400"
|
|
||||||
: "bg-white/5 border-white/10 text-zinc-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Auto
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => toggleAutoApproval(props.instanceId)}
|
|
||||||
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().autoApproval
|
|
||||||
? "bg-emerald-500/20 border-emerald-500/40 text-emerald-400"
|
|
||||||
: "bg-white/5 border-white/10 text-zinc-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Shield
|
|
||||||
</button>
|
|
||||||
<Show when={tokenStats().used > 0}>
|
|
||||||
<div class="px-2 py-0.5 bg-emerald-500/10 rounded border border-emerald-500/20 text-[9px] font-bold text-emerald-400">
|
|
||||||
{formatTokenTotal(tokenStats().used)}
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
<Show when={isSending() || isAgentThinking()}>
|
|
||||||
<div class="flex items-center space-x-1 px-2 py-0.5 bg-indigo-500/10 rounded border border-indigo-500/20">
|
|
||||||
<Loader2 size={10} class="text-indigo-400 animate-spin" />
|
|
||||||
<span class="text-[9px] font-bold text-indigo-400">{isAgentThinking() ? "Thinking" : "Sending"}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Text Input */}
|
{/* Chat Input Area - Fixed at bottom */}
|
||||||
<textarea
|
<div class="p-3 bg-[#0a0a0b] border-t border-white/5 shrink-0">
|
||||||
value={chatInput()}
|
<div class="w-full bg-zinc-900/80 border border-white/10 rounded-2xl shadow-lg p-3">
|
||||||
onInput={(e) => setChatInput(e.currentTarget.value)}
|
{/* Header Row */}
|
||||||
placeholder={selectedTaskId() ? "Send instruction to this task..." : "Type to create a new task and begin..."}
|
<div class="flex items-center justify-between mb-2">
|
||||||
class="w-full bg-transparent border-none focus:ring-0 focus:outline-none text-[13px] text-zinc-100 placeholder-zinc-600 resize-none min-h-[40px] max-h-32 leading-relaxed disabled:opacity-50"
|
<div class="flex items-center space-x-2">
|
||||||
onKeyDown={handleKeyDown}
|
<div class="w-5 h-5 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center">
|
||||||
disabled={isSending()}
|
<AtSign size={10} class="text-white" strokeWidth={3} />
|
||||||
rows={1}
|
</div>
|
||||||
/>
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[10px] font-bold text-zinc-100 uppercase tracking-wide">
|
||||||
|
{selectedTaskId() ? "Task Context" : "Global Pipeline"}
|
||||||
|
</span>
|
||||||
|
<span class="text-[9px] text-zinc-500 uppercase">
|
||||||
|
{selectedTaskId() ? "MultiX Threaded" : "Auto-Task"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Footer Row */}
|
<div class="flex items-center space-x-2">
|
||||||
<div class="flex items-center justify-between pt-2 border-t border-white/5 mt-2">
|
<button
|
||||||
<div class="flex items-center space-x-3">
|
onClick={() => toggleAutonomous(props.instanceId)}
|
||||||
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1">
|
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().isAutonomous
|
||||||
<Hash size={14} />
|
? "bg-indigo-500/20 border-indigo-500/40 text-indigo-400"
|
||||||
</button>
|
: "bg-white/5 border-white/10 text-zinc-500"
|
||||||
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1">
|
}`}
|
||||||
<Mic size={14} />
|
title="APEX - Autonomous Programming EXecution mode"
|
||||||
</button>
|
>
|
||||||
<div class="w-px h-3 bg-zinc-800" />
|
APEX
|
||||||
<div class="flex items-center space-x-1 text-zinc-600">
|
</button>
|
||||||
<kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-[9px] font-bold border border-white/5">ENTER</kbd>
|
<button
|
||||||
<span class="text-[9px]">to send</span>
|
onClick={() => toggleAutoApproval(props.instanceId)}
|
||||||
|
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().autoApproval
|
||||||
|
? "bg-emerald-500/20 border-emerald-500/40 text-emerald-400"
|
||||||
|
: "bg-white/5 border-white/10 text-zinc-500"
|
||||||
|
}`}
|
||||||
|
title="SHIELD - Auto-approval mode"
|
||||||
|
>
|
||||||
|
SHIELD
|
||||||
|
</button>
|
||||||
|
<Show when={tokenStats().used > 0}>
|
||||||
|
<div class="px-2 py-0.5 bg-emerald-500/10 rounded border border-emerald-500/20 text-[9px] font-bold text-emerald-400">
|
||||||
|
{formatTokenTotal(tokenStats().used)}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={isSending() || isAgentThinking()}>
|
||||||
|
<div class="flex items-center space-x-1 px-2 py-0.5 bg-indigo-500/10 rounded border border-indigo-500/20">
|
||||||
|
<div class="flex space-x-0.5">
|
||||||
|
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "0ms" }} />
|
||||||
|
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "150ms" }} />
|
||||||
|
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "300ms" }} />
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-bold text-indigo-400">{isAgentThinking() ? "THINKING" : "SENDING"}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
{/* STOP button */}
|
||||||
|
<Show when={isAgentThinking()}>
|
||||||
|
<button
|
||||||
|
onClick={handleStopAgent}
|
||||||
|
class="flex items-center space-x-1 px-2 py-0.5 bg-rose-500/20 hover:bg-rose-500/30 rounded border border-rose-500/40 text-[9px] font-bold text-rose-400 transition-all"
|
||||||
|
title="Stop agent"
|
||||||
|
>
|
||||||
|
<StopCircle size={10} />
|
||||||
|
<span>STOP</span>
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{/* Text Input */}
|
||||||
onClick={handleSendMessage}
|
<textarea
|
||||||
disabled={!chatInput().trim() || isSending()}
|
value={chatInput()}
|
||||||
class="px-4 py-1.5 bg-indigo-500 hover:bg-indigo-400 text-white rounded-lg text-[11px] font-bold uppercase tracking-wide transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center space-x-1.5"
|
onInput={(e) => setChatInput(e.currentTarget.value)}
|
||||||
>
|
placeholder={selectedTaskId() ? "Send instruction to this task..." : "Type to create a new task and begin..."}
|
||||||
<Show when={isSending()} fallback={
|
class="w-full bg-transparent border-none focus:ring-0 focus:outline-none text-[13px] text-zinc-100 placeholder-zinc-600 resize-none min-h-[40px] max-h-32 leading-relaxed disabled:opacity-50"
|
||||||
<>
|
onKeyDown={handleKeyDown}
|
||||||
<span>{selectedTaskId() ? "Update Task" : "Launch Task"}</span>
|
disabled={isSending()}
|
||||||
<ArrowUp size={12} strokeWidth={3} />
|
rows={1}
|
||||||
</>
|
/>
|
||||||
}>
|
|
||||||
<Loader2 size={12} class="animate-spin" />
|
{/* Footer Row */}
|
||||||
</Show>
|
<div class="flex items-center justify-between pt-2 border-t border-white/5 mt-2">
|
||||||
</button>
|
<div class="flex items-center space-x-2 flex-wrap gap-y-1">
|
||||||
|
{/* Detailed token stats */}
|
||||||
|
<Show when={tokenStats().input > 0 || tokenStats().output > 0}>
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">INPUT</span>
|
||||||
|
<span class="text-[9px] font-bold text-zinc-400">{tokenStats().input.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">OUTPUT</span>
|
||||||
|
<span class="text-[9px] font-bold text-zinc-400">{tokenStats().output.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={tokenStats().reasoning > 0}>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">REASONING</span>
|
||||||
|
<span class="text-[9px] font-bold text-amber-400">{tokenStats().reasoning.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={tokenStats().cacheRead > 0}>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">CACHE READ</span>
|
||||||
|
<span class="text-[9px] font-bold text-emerald-400">{tokenStats().cacheRead.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={tokenStats().cacheWrite > 0}>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">CACHE WRITE</span>
|
||||||
|
<span class="text-[9px] font-bold text-cyan-400">{tokenStats().cacheWrite.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">COST</span>
|
||||||
|
<span class="text-[9px] font-bold text-violet-400">${tokenStats().cost.toFixed(4)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<span class="text-[8px] font-bold text-zinc-600 uppercase">MODEL</span>
|
||||||
|
<span class="text-[9px] font-bold text-indigo-400">{currentModel()}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={!(tokenStats().input > 0 || tokenStats().output > 0)}>
|
||||||
|
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1">
|
||||||
|
<Hash size={14} />
|
||||||
|
</button>
|
||||||
|
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1">
|
||||||
|
<Mic size={14} />
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-3 bg-zinc-800" />
|
||||||
|
<div class="flex items-center space-x-1 text-zinc-600">
|
||||||
|
<kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-[9px] font-bold border border-white/5">ENTER</kbd>
|
||||||
|
<span class="text-[9px]">to send</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={!chatInput().trim() || isSending()}
|
||||||
|
class="px-4 py-1.5 bg-indigo-500 hover:bg-indigo-400 text-white rounded-lg text-[11px] font-bold uppercase tracking-wide transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center space-x-1.5"
|
||||||
|
>
|
||||||
|
<Show when={isSending()} fallback={
|
||||||
|
<>
|
||||||
|
<span>{selectedTaskId() ? "Update Task" : "Launch Task"}</span>
|
||||||
|
<ArrowUp size={12} strokeWidth={3} />
|
||||||
|
</>
|
||||||
|
}>
|
||||||
|
<Loader2 size={12} class="animate-spin" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
{/* Message Navigation Sidebar - YOU/ASST labels */}
|
||||||
|
<Show when={selectedTaskId() && filteredMessageIds().length > 0}>
|
||||||
|
<div class="w-12 shrink-0 bg-zinc-900/40 border-l border-white/5 overflow-y-auto py-2 px-1 flex flex-col items-center gap-1">
|
||||||
|
<For each={filteredMessageIds()}>
|
||||||
|
{(messageId, index) => {
|
||||||
|
const msg = () => messageStore().getMessage(messageId);
|
||||||
|
const isUser = () => msg()?.role === "user";
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Scroll to message
|
||||||
|
const element = document.getElementById(`msg-${messageId}`);
|
||||||
|
element?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}}
|
||||||
|
class={`w-9 py-1 rounded text-[8px] font-black uppercase transition-all ${isUser()
|
||||||
|
? "bg-indigo-500/20 border border-indigo-500/40 text-indigo-400 hover:bg-indigo-500/30"
|
||||||
|
: "bg-emerald-500/20 border border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/30"
|
||||||
|
}`}
|
||||||
|
title={`${isUser() ? "User" : "Assistant"} message ${index() + 1}`}
|
||||||
|
>
|
||||||
|
{isUser() ? "YOU" : "ASST"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</main >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user