返回 presentation-ai
HumanMessage.tsx
根目录 / src / components / presentation / agent / HumanMessage.tsx
1 "use client";
2
3 import { parseSlideXml } from "@/components/notebook/presentation/utils/parser";
4 import {
5 AlertDialog,
6 AlertDialogAction,
7 AlertDialogCancel,
8 AlertDialogContent,
9 AlertDialogDescription,
10 AlertDialogFooter,
11 AlertDialogHeader,
12 AlertDialogTitle,
13 AlertDialogTrigger,
14 } from "@/components/ui/alert-dialog";
15 import { TooltipButton } from "@/components/ui/button";
16 import { usePresentationState } from "@/states/presentation-state";
17 import { type UIMessage } from "ai";
18 import { Undo2 } from "lucide-react";
19 import * as React from "react";
20 import { toast } from "sonner";
21
22 function sanitizeHumanMessage(message: string) {
23 return message
24 .replace(/<slide-information>[\s\S]*?<\/slide-information>/gi, "")
25 .trim();
26 }
27
28 function getTextContent(message: UIMessage): string {
29 return message.parts
30 .filter((part): part is Extract<typeof part, { type: "text" }> => {
31 return part.type === "text";
32 })
33 .map((part) => part.text)
34 .join("\n");
35 }
36
37 function getImageUrls(message: UIMessage): string[] {
38 return message.parts.flatMap((part) => {
39 if (
40 part.type === "file" &&
41 part.mediaType.startsWith("image/") &&
42 typeof part.url === "string"
43 ) {
44 return [part.url];
45 }
46
47 return [];
48 });
49 }
50
51 export function extractSlideContent(message: string) {
52 const match = message.match(
53 /<slide-information>([\s\S]*?)<\/slide-information>/,
54 );
55
56 return match ? match[1]?.trim() : null;
57 }
58
59 export default function HumanMessageComponent({
60 message,
61 }: {
62 message: UIMessage;
63 }) {
64 const [open, setOpen] = React.useState(false);
65 const textContent = getTextContent(message);
66 const imageUrls = getImageUrls(message);
67
68 const handleRollback = React.useCallback(() => {
69 const slideContent = extractSlideContent(textContent);
70
71 if (!slideContent) {
72 toast.error("No slide information found");
73 return;
74 }
75
76 const parsedSlides = parseSlideXml(slideContent);
77
78 if (!parsedSlides.length) {
79 toast.error("Unable to restore this state");
80 return;
81 }
82
83 usePresentationState.getState().setSlides(parsedSlides);
84 toast.success("Presentation restored to this message");
85 }, [textContent]);
86
87 return (
88 <div className="grid w-full place-items-end gap-1.5">
89 {imageUrls.length > 0 ? (
90 <div className="flex flex-wrap gap-2">
91 {imageUrls.map((url, index) => (
92 <div
93 key={`${url}-${index}`}
94 className="relative h-16 w-20 overflow-hidden rounded-md border border-primary-foreground/20 bg-muted"
95 >
96 {/* biome-ignore lint/performance/noImgElement: Previewing uploaded slide images */}
97 <img
98 src={url}
99 alt={`Attached image ${index + 1}`}
100 className="h-full w-full object-cover"
101 />
102 </div>
103 ))}
104 </div>
105 ) : null}
106
107 <div className="w-full max-w-[80%] overflow-x-auto rounded-2xl bg-primary px-3 py-2 text-sm text-primary-foreground shadow-2xs">
108 {sanitizeHumanMessage(textContent)}
109 </div>
110
111 <div className="mt-1 mr-1 flex gap-2">
112 <AlertDialog open={open} onOpenChange={setOpen}>
113 <AlertDialogTrigger asChild>
114 <TooltipButton
115 className="flex size-6 items-center justify-center rounded-full bg-transparent hover:bg-accent"
116 aria-label="Rollback"
117 tooltipText="Rollback"
118 variant="ghost"
119 size="xs"
120 onClick={() => setOpen(true)}
121 >
122 <Undo2 className="size-5" />
123 </TooltipButton>
124 </AlertDialogTrigger>
125 <AlertDialogContent>
126 <AlertDialogHeader>
127 <AlertDialogTitle>Rollback Message?</AlertDialogTitle>
128 <AlertDialogDescription>
129 Restore the presentation state captured in this message.
130 </AlertDialogDescription>
131 </AlertDialogHeader>
132 <AlertDialogFooter>
133 <AlertDialogCancel onClick={() => setOpen(false)}>
134 Cancel
135 </AlertDialogCancel>
136 <AlertDialogAction onClick={handleRollback}>
137 Confirm Rollback
138 </AlertDialogAction>
139 </AlertDialogFooter>
140 </AlertDialogContent>
141 </AlertDialog>
142 </div>
143 </div>
144 );
145 }
146
146 lines Plain Text