feat(23-03): add messageType dispatch, ChatMessageList propagation, and chatApi handoff methods
- ChatMessage: add messageType/conversationId/onHandoff props; dispatch to ChatSpecCard, ChatHandoffIndicator, ChatTaskCreatedBadge, ChatStatusUpdateBadge based on messageType - ChatMessageList: propagate messageType and conversationId to ChatMessage; add onHandoff prop - chatApi: add handoffSpec() and postStatusUpdate() methods
This commit is contained in:
parent
9205ee82e3
commit
71839e0032
3 changed files with 67 additions and 0 deletions
|
|
@ -148,4 +148,22 @@ export const chatApi = {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handoffSpec(
|
||||||
|
conversationId: string,
|
||||||
|
spec: { what: string; why: string; constraints: string; success: string },
|
||||||
|
targetRole: string = "pm",
|
||||||
|
) {
|
||||||
|
return api.post<{ handoffMessageId: string; issues: Array<{ id: string; identifier: string; title: string }> }>(
|
||||||
|
`/conversations/${conversationId}/handoff`,
|
||||||
|
{ spec, targetRole },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
postStatusUpdate(
|
||||||
|
conversationId: string,
|
||||||
|
data: { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string },
|
||||||
|
) {
|
||||||
|
return api.post<{ id: string }>(`/conversations/${conversationId}/status-update`, data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { ChatMarkdownMessage } from "./ChatMarkdownMessage";
|
||||||
import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar";
|
import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar";
|
||||||
import { ChatStreamingCursor } from "./ChatStreamingCursor";
|
import { ChatStreamingCursor } from "./ChatStreamingCursor";
|
||||||
import { ChatMessageActions } from "./ChatMessageActions";
|
import { ChatMessageActions } from "./ChatMessageActions";
|
||||||
|
import { ChatSpecCard } from "./ChatSpecCard";
|
||||||
|
import { ChatHandoffIndicator } from "./ChatHandoffIndicator";
|
||||||
|
import { ChatTaskCreatedBadge } from "./ChatTaskCreatedBadge";
|
||||||
|
import { ChatStatusUpdateBadge } from "./ChatStatusUpdateBadge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import type { AgentRole } from "@paperclipai/shared";
|
import type { AgentRole } from "@paperclipai/shared";
|
||||||
|
|
@ -11,6 +15,8 @@ interface ChatMessageProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
role: "user" | "assistant" | "system";
|
role: "user" | "assistant" | "system";
|
||||||
content: string;
|
content: string;
|
||||||
|
messageType?: string | null;
|
||||||
|
conversationId?: string;
|
||||||
agentName?: string | null;
|
agentName?: string | null;
|
||||||
agentIcon?: string | null;
|
agentIcon?: string | null;
|
||||||
agentRole?: AgentRole | null;
|
agentRole?: AgentRole | null;
|
||||||
|
|
@ -19,12 +25,15 @@ interface ChatMessageProps {
|
||||||
isAnyStreaming?: boolean;
|
isAnyStreaming?: boolean;
|
||||||
onEdit?: (messageId: string, newContent: string) => void;
|
onEdit?: (messageId: string, newContent: string) => void;
|
||||||
onRetry?: (messageId: string) => void;
|
onRetry?: (messageId: string) => void;
|
||||||
|
onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessage({
|
export function ChatMessage({
|
||||||
id,
|
id,
|
||||||
role,
|
role,
|
||||||
content,
|
content,
|
||||||
|
messageType,
|
||||||
|
conversationId,
|
||||||
agentName,
|
agentName,
|
||||||
agentIcon,
|
agentIcon,
|
||||||
agentRole,
|
agentRole,
|
||||||
|
|
@ -33,10 +42,45 @@ export function ChatMessage({
|
||||||
isAnyStreaming,
|
isAnyStreaming,
|
||||||
onEdit,
|
onEdit,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
onHandoff,
|
||||||
}: ChatMessageProps) {
|
}: ChatMessageProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editValue, setEditValue] = useState(content);
|
const [editValue, setEditValue] = useState(content);
|
||||||
|
|
||||||
|
// Dispatch to specialized system message components (Phase 23)
|
||||||
|
if (role === "system" || messageType) {
|
||||||
|
if (messageType === "spec_card") {
|
||||||
|
return (
|
||||||
|
<ChatSpecCard
|
||||||
|
content={content}
|
||||||
|
messageId={id}
|
||||||
|
conversationId={conversationId}
|
||||||
|
onHandoff={onHandoff}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (messageType === "handoff") {
|
||||||
|
return <ChatHandoffIndicator content={content} />;
|
||||||
|
}
|
||||||
|
if (messageType === "task_created") {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(content) as { taskId?: string; taskTitle?: string; taskUrl?: string };
|
||||||
|
return <ChatTaskCreatedBadge taskId={data.taskId} taskTitle={data.taskTitle} taskUrl={data.taskUrl} />;
|
||||||
|
} catch {
|
||||||
|
return <ChatTaskCreatedBadge />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (messageType === "status_update") {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(content) as { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string };
|
||||||
|
return <ChatStatusUpdateBadge agentName={data.agentName} taskId={data.taskId} taskTitle={data.taskTitle} taskUrl={data.taskUrl} />;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through to default system message rendering (plain markdown)
|
||||||
|
}
|
||||||
|
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface ChatMessageListProps {
|
||||||
streamingAgentRole?: AgentRole | null;
|
streamingAgentRole?: AgentRole | null;
|
||||||
onEdit?: (messageId: string, newContent: string) => void;
|
onEdit?: (messageId: string, newContent: string) => void;
|
||||||
onRetry?: (messageId: string) => void;
|
onRetry?: (messageId: string) => void;
|
||||||
|
onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void;
|
||||||
agentMap?: Map<string, { name: string; icon: string | null; role: AgentRole | null }>;
|
agentMap?: Map<string, { name: string; icon: string | null; role: AgentRole | null }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ export function ChatMessageList({
|
||||||
streamingAgentRole,
|
streamingAgentRole,
|
||||||
onEdit,
|
onEdit,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
onHandoff,
|
||||||
agentMap,
|
agentMap,
|
||||||
}: ChatMessageListProps) {
|
}: ChatMessageListProps) {
|
||||||
const { messages, isLoading } = useChatMessages(conversationId);
|
const { messages, isLoading } = useChatMessages(conversationId);
|
||||||
|
|
@ -143,6 +145,8 @@ export function ChatMessageList({
|
||||||
id={msg.id}
|
id={msg.id}
|
||||||
role={msg.role as "user" | "assistant" | "system"}
|
role={msg.role as "user" | "assistant" | "system"}
|
||||||
content={msg.content}
|
content={msg.content}
|
||||||
|
messageType={msg.messageType}
|
||||||
|
conversationId={conversationId}
|
||||||
agentName={agent?.name ?? streamingAgentName}
|
agentName={agent?.name ?? streamingAgentName}
|
||||||
agentIcon={agent?.icon ?? streamingAgentIcon}
|
agentIcon={agent?.icon ?? streamingAgentIcon}
|
||||||
agentRole={agent?.role ?? streamingAgentRole}
|
agentRole={agent?.role ?? streamingAgentRole}
|
||||||
|
|
@ -151,6 +155,7 @@ export function ChatMessageList({
|
||||||
isAnyStreaming={isStreaming}
|
isAnyStreaming={isStreaming}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onRetry={onRetry}
|
onRetry={onRetry}
|
||||||
|
onHandoff={onHandoff}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue