mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
State management - Pending task / live timeline are now Query-cache single source; Zustand mirror removed (fixes duplicate assistant render caused by the invalidate→refetch race window) - WS subscriptions moved from ChatWindow to global useRealtimeSync so pending state survives minimize and refresh - New GET /chat/sessions/:id/pending-task to recover live state on mount - Drafts persisted per-session (was per-workspace) Unread tracking - Migration 040: chat_session.unread_since (event-driven; old chats stay clean — no mass backfill) - POST /chat/sessions/:id/read clears unread; broadcasts chat:session_read so other devices sync - New GET /chat/pending-tasks aggregate for the FAB - ChatFab: brand-color impulse animation while running, brand-dot badge of unread session count - ChatWindow auto-marks read when user is viewing the session Header redesign - Two independent dropdowns: agent (avatar + name + My/Others grouping) at the input bottom-left; session (title + agent avatar) in the header - ⊕ new-chat button replaces the old + and history buttons - Session dropdown lists all sessions across agents with avatars - Empty state: 3 clickable starter prompts that send immediately - Mention link renderer falls through to default span on null — fixes @member/@agent/@all silently disappearing app-wide - User messages render through Markdown - Enter submits in chat input only (with IME guard + codeBlock skip); bubble menu hidden in chat Misc - Partial index on agent_task_queue for fast pending-task lookup - 2 new storage keys added to clearWorkspaceStorage - useMarkChatSessionRead has onError rollback - chat.* namespace logs across store, mutations, components, realtime Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import { MessageCircle } from "lucide-react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
import { useChatStore } from "@multica/core/chat";
|
|
import { chatSessionsOptions, pendingChatTasksOptions } from "@multica/core/chat/queries";
|
|
import { useWorkspaceId } from "@multica/core/hooks";
|
|
import { createLogger } from "@multica/core/logger";
|
|
import {
|
|
Tooltip,
|
|
TooltipTrigger,
|
|
TooltipContent,
|
|
} from "@multica/ui/components/ui/tooltip";
|
|
|
|
const logger = createLogger("chat.ui");
|
|
|
|
export function ChatFab() {
|
|
const wsId = useWorkspaceId();
|
|
const isOpen = useChatStore((s) => s.isOpen);
|
|
const toggle = useChatStore((s) => s.toggle);
|
|
const { data: sessions = [] } = useQuery(chatSessionsOptions(wsId));
|
|
const { data: pending } = useQuery(pendingChatTasksOptions(wsId));
|
|
|
|
if (isOpen) return null;
|
|
|
|
const unreadSessionCount = sessions.filter((s) => s.has_unread).length;
|
|
const isRunning = (pending?.tasks ?? []).length > 0;
|
|
|
|
const handleClick = () => {
|
|
logger.info("fab.click (open chat)", { unreadSessionCount, isRunning });
|
|
toggle();
|
|
};
|
|
|
|
// Tooltip text communicates the state that isn't carried by the icon/badge.
|
|
const tooltip = isRunning
|
|
? "Multica is working..."
|
|
: unreadSessionCount > 0
|
|
? `${unreadSessionCount} unread ${unreadSessionCount === 1 ? "chat" : "chats"}`
|
|
: "Ask Multica";
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
onClick={handleClick}
|
|
className={cn(
|
|
"absolute bottom-2 right-2 z-50 flex size-10 cursor-pointer items-center justify-center rounded-full ring-1 ring-foreground/10 bg-card text-muted-foreground shadow-sm transition-transform hover:scale-110 hover:text-accent-foreground active:scale-95",
|
|
// Impulse the button itself while a chat task is running — no
|
|
// outer ring to keep things calm.
|
|
isRunning && "animate-chat-impulse",
|
|
)}
|
|
>
|
|
<MessageCircle className="size-5" />
|
|
{unreadSessionCount > 0 && (
|
|
<span className="pointer-events-none absolute -top-0.5 -right-0.5 flex min-w-4 h-4 items-center justify-center rounded-full bg-brand px-1 text-xs font-semibold leading-none text-background">
|
|
{unreadSessionCount > 9 ? "9+" : unreadSessionCount}
|
|
</span>
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" sideOffset={10}>{tooltip}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|