feat(24-03): add bookmark toggle to ChatMessage and ChatMessageActions

- Add onBookmark/isBookmarked props to ChatMessageActions
- Render ChatMessageBookmark as last action for user and assistant messages
- Add onBookmark/isBookmarked props to ChatMessage, thread to ChatMessageActions
- System messages do not receive bookmark actions
This commit is contained in:
Nexus Dev 2026-04-01 22:39:29 +00:00
parent 0dde83b566
commit ce037c235c
2 changed files with 51 additions and 18 deletions

View file

@ -26,6 +26,8 @@ interface ChatMessageProps {
onEdit?: (messageId: string, newContent: string) => void;
onRetry?: (messageId: string) => void;
onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void;
onBookmark?: (messageId: string) => void;
isBookmarked?: boolean;
}
export function ChatMessage({
@ -43,6 +45,8 @@ export function ChatMessage({
onEdit,
onRetry,
onHandoff,
onBookmark,
isBookmarked,
}: ChatMessageProps) {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(content);
@ -135,6 +139,8 @@ export function ChatMessage({
role="user"
isStreaming={isAnyStreaming}
onEdit={() => setIsEditing(true)}
onBookmark={id && onBookmark ? () => onBookmark(id) : undefined}
isBookmarked={isBookmarked}
/>
</div>
</div>
@ -159,6 +165,8 @@ export function ChatMessage({
role="assistant"
isStreaming={isAnyStreaming}
onRetry={id && onRetry ? () => onRetry(id) : undefined}
onBookmark={id && onBookmark ? () => onBookmark(id) : undefined}
isBookmarked={isBookmarked}
/>
</div>
);

View file

@ -1,20 +1,23 @@
import { Pencil, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ChatMessageBookmark } from "./ChatMessageBookmark";
interface ChatMessageActionsProps {
role: "user" | "assistant" | "system";
isStreaming?: boolean;
onEdit?: () => void;
onRetry?: () => void;
onBookmark?: () => void;
isBookmarked?: boolean;
}
export function ChatMessageActions({ role, isStreaming, onEdit, onRetry }: ChatMessageActionsProps) {
export function ChatMessageActions({ role, isStreaming, onEdit, onRetry, onBookmark, isBookmarked }: ChatMessageActionsProps) {
if (isStreaming) return null;
if (role === "user" && onEdit) {
return (
<div className="absolute top-1 right-1 hidden group-hover:flex">
<div className="absolute top-1 right-1 hidden group-hover:flex items-center gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
@ -29,27 +32,49 @@ export function ChatMessageActions({ role, isStreaming, onEdit, onRetry }: ChatM
</TooltipTrigger>
<TooltipContent>Edit message</TooltipContent>
</Tooltip>
{onBookmark && (
<ChatMessageBookmark
messageId=""
conversationId=""
isBookmarked={isBookmarked ?? false}
onToggle={onBookmark}
/>
)}
</div>
);
}
if (role === "assistant" && onRetry) {
if (role === "assistant") {
const hasActions = !!onRetry || !!onBookmark;
if (!hasActions) return null;
return (
<div className="flex justify-end mt-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hidden group-hover:inline-flex"
onClick={onRetry}
aria-label="Retry response"
>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Retry response</TooltipContent>
</Tooltip>
<div className="flex justify-end mt-1 gap-0.5">
{onRetry && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hidden group-hover:inline-flex"
onClick={onRetry}
aria-label="Retry response"
>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Retry response</TooltipContent>
</Tooltip>
)}
{onBookmark && (
<div className="hidden group-hover:inline-flex">
<ChatMessageBookmark
messageId=""
conversationId=""
isBookmarked={isBookmarked ?? false}
onToggle={onBookmark}
/>
</div>
)}
</div>
);
}