mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 10:59:06 +02:00
Replace the floating chat FAB/window with a first-class Chat tab under Inbox, laid out as an IM-style two-pane surface (thread list + conversation). Highlights: - New Chat page (packages/views/chat/chat-page.tsx) with URL-addressable session selection; web + desktop routing wired up. Removes the old chat-fab / chat-window / resize-handles / context-items paths. - IM thread list: agent avatar + last-message preview + IM timestamp, red unread *count* badge (read-cursor model), presence-gated typing vs waiting. Rename lives only in the conversation header ⋯ menu (not the list hover). - Per-session conversation header (rename / view agent / delete), agent-aware empty state (avatar + name + description + starter prompts), and a deterministic clean-title derivation from the first message. - Server: read-cursor unread model (migration 145) and per-user pinned agents (migration 146, dedicated chat_pinned_agent table + handler/queries). New-agent welcome chat auto-enqueues a real agent run (LLM intro, no static template). - Design: fade the global --border token; borderless list headers on Chat/Inbox, kept (faded) on the conversation header. Verified: pnpm typecheck (all packages), go build ./..., go vet, gofmt. Co-authored-by: multica-agent <github@multica.ai>
74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import type { Agent } from "@multica/core/types";
|
|
import { ActorAvatar } from "../../common/actor-avatar";
|
|
import { useT } from "../../i18n";
|
|
|
|
// Three starter prompts shown on the empty compose. Each is keyed into the
|
|
// chat namespace so labels translate per locale; the icon stays raw since
|
|
// emojis are locale-neutral.
|
|
const STARTER_KEYS: ("list_open" | "summarize_today" | "plan_next")[] = [
|
|
"list_open",
|
|
"summarize_today",
|
|
"plan_next",
|
|
];
|
|
const STARTER_ICONS: Record<(typeof STARTER_KEYS)[number], string> = {
|
|
list_open: "📋",
|
|
summarize_today: "📝",
|
|
plan_next: "💡",
|
|
};
|
|
|
|
/**
|
|
* Empty compose placeholder shown when a chat has no messages yet. Agent-aware:
|
|
* it leads with the chosen agent's avatar + name + description so the user knows
|
|
* exactly who they're about to talk to, then offers starter prompts.
|
|
*/
|
|
export function EmptyState({
|
|
agent,
|
|
onPickPrompt,
|
|
}: {
|
|
agent: Agent | null;
|
|
onPickPrompt: (text: string) => void;
|
|
}) {
|
|
const { t } = useT("chat");
|
|
const description = agent?.description?.trim();
|
|
return (
|
|
<div className="flex flex-1 flex-col items-center justify-center gap-4 px-6 py-8">
|
|
{agent && (
|
|
<ActorAvatar
|
|
actorType="agent"
|
|
actorId={agent.id}
|
|
size={56}
|
|
className="ring-1 ring-inset ring-border"
|
|
/>
|
|
)}
|
|
<div className="max-w-sm space-y-1 text-center">
|
|
<h3 className="text-base font-semibold">
|
|
{agent
|
|
? t(($) => $.empty_state.chat_with_named, { name: agent.name })
|
|
: t(($) => $.empty_state.first_time_title)}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{description || t(($) => $.empty_state.returning_subtitle)}
|
|
</p>
|
|
</div>
|
|
<div className="w-full max-w-xs space-y-2">
|
|
{STARTER_KEYS.map((key) => {
|
|
const text = t(($) => $.starter_prompts[key]);
|
|
return (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() => onPickPrompt(text)}
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-accent hover:border-brand/40"
|
|
>
|
|
<span className="mr-2">{STARTER_ICONS[key]}</span>
|
|
{text}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|