evOo-local/apps/web/src/components/ai/CollaborationMarker.tsx
Cursor Agent c077ff4415
Production readiness phase 1: journeys doc, onboarding persistence, teardown, chat UX
- Add docs/PRODUCTION-READINESS.md with journey matrix and defect backlog
- Persist onboarding draft in session storage across reload
- Localize onboarding discovery main-bot role; German Chief of Staff string
- Destroy team sandboxes and home folders when deleting an account
- Keep composer/transcript visible during queued runs; extend send watchdog
- Add Playwright specs: reload persistence, DE intake, queued progress, console guard

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 07:42:02 +00:00

89 lines
2.6 KiB
TypeScript

import { BotAvatar, GroupAvatar, type GroupAvatarMember } from "@rakazo/ui-web";
import { useEffect, useMemo, useState } from "react";
import { isDoingStatus } from "../../lib/agent-activity";
import { personaWorkingLinesPlain, resolveBotPersona } from "../../lib/bot-persona";
import { Shimmer } from "./primitives";
/** Lightweight peer event shown without exposing the exchanged message body. */
export function CollaborationMarker({
ariaLabel,
color,
identity,
label,
onClick,
}: {
ariaLabel: string;
color: string;
identity: string;
label: string;
onClick: () => void;
}) {
return (
<div className="flex justify-start">
<button
type="button"
data-testid="peer-receipt-chip"
aria-label={ariaLabel}
onClick={onClick}
className="inline-flex max-w-full items-center gap-1.5 rounded-full px-2.5 py-1 text-ui-body-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground/75"
>
<BotAvatar color={color} identity={identity} className="size-icon-md" />
<span dir="auto" className="truncate">
{label}
</span>
</button>
</div>
);
}
export function ActiveBotGlyph({
bots,
label,
runStatus,
}: {
bots: GroupAvatarMember[];
label: string;
runStatus?: string;
}) {
const firstBot = bots[0];
const status = runStatus ?? firstBot?.status;
const persona = useMemo(
() =>
firstBot?.name
? resolveBotPersona({
name: firstBot.name,
title: "",
description: "",
})
: null,
[firstBot?.name],
);
const workingLines = useMemo(() => {
if (!firstBot?.name || !persona) return [label];
if (isDoingStatus(status) && status !== "running") return [label];
return [...personaWorkingLinesPlain(persona.archetype, firstBot.name)];
}, [firstBot?.name, label, persona, status]);
const [phraseIndex, setPhraseIndex] = useState(0);
useEffect(() => {
if (workingLines.length <= 1) return;
const interval = setInterval(() => {
setPhraseIndex((index) => (index + 1) % workingLines.length);
}, 3500);
return () => clearInterval(interval);
}, [workingLines.length]);
const displayLabel = workingLines[phraseIndex] ?? label;
return (
<div className="flex min-h-10 items-center px-1">
<span role="status" className="flex w-fit items-center gap-3">
<GroupAvatar members={bots} size={28} />
<span className="text-ui-body font-medium tracking-tight">
<Shimmer>{displayLabel}</Shimmer>
</span>
</span>
</div>
);
}