feat: queen profile in message bubbles

This commit is contained in:
Richard Tang
2026-04-16 11:21:02 -07:00
parent 36ebf27e3e
commit d20b617008
3 changed files with 90 additions and 23 deletions
+32 -4
View File
@@ -27,6 +27,8 @@ export interface ContextUsageEntry {
import MarkdownContent from "@/components/MarkdownContent";
import QuestionWidget from "@/components/QuestionWidget";
import MultiQuestionWidget from "@/components/MultiQuestionWidget";
import { useColony } from "@/context/ColonyContext";
import { useQueenProfile } from "@/context/QueenProfileContext";
import ParallelSubagentBubble, {
type SubagentGroup,
} from "@/components/ParallelSubagentBubble";
@@ -338,6 +340,15 @@ function InlineAskUserBubble({
const color = getColor(msg.agent, msg.role);
const thread = msg.thread || activeThread;
const { queenProfiles } = useColony();
const { openQueenProfile } = useQueenProfile();
const queenProfileId = isQueen
? queenProfiles.find((q) => q.name === msg.agent)?.id ?? null
: null;
const handleQueenClick = queenProfileId
? () => openQueenProfile(queenProfileId)
: undefined;
const handleSingle = (answer: string) => {
setState("submitted");
onSend(answer, thread);
@@ -357,12 +368,14 @@ function InlineAskUserBubble({
return (
<div className="flex gap-3">
<div
className={`flex-shrink-0 ${isQueen ? "w-9 h-9" : "w-7 h-7"} rounded-xl flex items-center justify-center`}
className={`flex-shrink-0 ${isQueen ? "w-9 h-9" : "w-7 h-7"} rounded-xl flex items-center justify-center${handleQueenClick ? " cursor-pointer hover:opacity-80 transition-opacity" : ""}`}
style={{
backgroundColor: `${color}18`,
border: `1.5px solid ${color}35`,
boxShadow: isQueen ? `0 0 12px ${color}20` : undefined,
}}
onClick={handleQueenClick}
title={handleQueenClick ? `View ${msg.agent}'s profile` : undefined}
>
{isQueen ? (
<Crown className="w-4 h-4" style={{ color }} />
@@ -375,8 +388,9 @@ function InlineAskUserBubble({
>
<div className="flex items-center gap-2 mb-1">
<span
className={`font-medium ${isQueen ? "text-sm" : "text-xs"}`}
className={`font-medium ${isQueen ? "text-sm" : "text-xs"}${handleQueenClick ? " cursor-pointer hover:underline" : ""}`}
style={{ color }}
onClick={handleQueenClick}
>
{msg.agent}
</span>
@@ -437,6 +451,13 @@ const MessageBubble = memo(
const isQueen = msg.role === "queen";
const color = getColor(msg.agent, msg.role);
// Resolve queen profile ID so clicking avatar/name opens the profile panel
const { queenProfiles } = useColony();
const { openQueenProfile } = useQueenProfile();
const queenProfileId = isQueen
? queenProfiles.find((q) => q.name === msg.agent)?.id ?? null
: null;
if (msg.type === "run_divider") {
return (
<div className="flex items-center gap-3 py-2 my-1">
@@ -531,15 +552,21 @@ const MessageBubble = memo(
);
}
const handleQueenClick = queenProfileId
? () => openQueenProfile(queenProfileId)
: undefined;
return (
<div className="flex gap-3">
<div
className={`flex-shrink-0 ${isQueen ? "w-9 h-9" : "w-7 h-7"} rounded-xl flex items-center justify-center`}
className={`flex-shrink-0 ${isQueen ? "w-9 h-9" : "w-7 h-7"} rounded-xl flex items-center justify-center${handleQueenClick ? " cursor-pointer hover:opacity-80 transition-opacity" : ""}`}
style={{
backgroundColor: `${color}18`,
border: `1.5px solid ${color}35`,
boxShadow: isQueen ? `0 0 12px ${color}20` : undefined,
}}
onClick={handleQueenClick}
title={handleQueenClick ? `View ${msg.agent}'s profile` : undefined}
>
{isQueen ? (
<Crown className="w-4 h-4" style={{ color }} />
@@ -552,8 +579,9 @@ const MessageBubble = memo(
>
<div className="flex items-center gap-2 mb-1">
<span
className={`font-medium ${isQueen ? "text-sm" : "text-xs"}`}
className={`font-medium ${isQueen ? "text-sm" : "text-xs"}${handleQueenClick ? " cursor-pointer hover:underline" : ""}`}
style={{ color }}
onClick={handleQueenClick}
>
{msg.agent}
</span>
@@ -0,0 +1,31 @@
import { createContext, useContext, useCallback, type ReactNode } from "react";
interface QueenProfileContextValue {
openQueenProfile: (queenId: string) => void;
}
const QueenProfileContext = createContext<QueenProfileContextValue | null>(null);
export function QueenProfileProvider({
onOpen,
children,
}: {
onOpen: (queenId: string) => void;
children: ReactNode;
}) {
const openQueenProfile = useCallback(
(queenId: string) => onOpen(queenId),
[onOpen],
);
return (
<QueenProfileContext.Provider value={{ openQueenProfile }}>
{children}
</QueenProfileContext.Provider>
);
}
export function useQueenProfile() {
const ctx = useContext(QueenProfileContext);
if (!ctx) throw new Error("useQueenProfile must be used within QueenProfileProvider");
return ctx;
}
+27 -19
View File
@@ -1,10 +1,11 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { Outlet, useLocation } from "react-router-dom";
import Sidebar from "@/components/Sidebar";
import AppHeader from "@/components/AppHeader";
import QueenProfilePanel from "@/components/QueenProfilePanel";
import { ColonyProvider, useColony } from "@/context/ColonyContext";
import { HeaderActionsProvider } from "@/context/HeaderActionsContext";
import { QueenProfileProvider } from "@/context/QueenProfileContext";
export default function AppLayout() {
return (
@@ -27,26 +28,33 @@ function AppLayoutInner() {
setOpenQueenId(null);
}, [location.pathname]);
const handleOpenQueenProfile = useCallback(
(queenId: string) => setOpenQueenId((prev) => (prev === queenId ? null : queenId)),
[],
);
return (
<div className="flex h-screen bg-background overflow-hidden">
<Sidebar />
<div className="flex-1 min-w-0 flex flex-col">
<AppHeader onOpenQueenProfile={setOpenQueenId} />
<div className="flex-1 min-h-0 flex">
<main className="flex-1 min-w-0 flex flex-col">
<Outlet />
</main>
{openQueenId && (
<QueenProfilePanel
queenId={openQueenId}
colonies={colonies.filter(
(c) => c.queenProfileId === openQueenId,
)}
onClose={() => setOpenQueenId(null)}
/>
)}
<QueenProfileProvider onOpen={handleOpenQueenProfile}>
<div className="flex h-screen bg-background overflow-hidden">
<Sidebar />
<div className="flex-1 min-w-0 flex flex-col">
<AppHeader onOpenQueenProfile={handleOpenQueenProfile} />
<div className="flex-1 min-h-0 flex">
<main className="flex-1 min-w-0 flex flex-col">
<Outlet />
</main>
{openQueenId && (
<QueenProfilePanel
queenId={openQueenId}
colonies={colonies.filter(
(c) => c.queenProfileId === openQueenId,
)}
onClose={() => setOpenQueenId(null)}
/>
)}
</div>
</div>
</div>
</div>
</QueenProfileProvider>
);
}