({ message, isLoading, onRetry, onEdit })
| 248 | } |
| 249 | |
| 250 | export function PreviewMessage({ message, isLoading, onRetry, onEdit }) { |
| 251 | const isUser = message.role === 'user'; |
| 252 | const isSystem = message.role === 'system'; |
| 253 | const [copied, setCopied] = useState(false); |
| 254 | const [editing, setEditing] = useState(false); |
| 255 | const [editText, setEditText] = useState(''); |
| 256 | const textareaRef = useRef(null); |
| 257 | const [showWorking, setShowWorking] = useState(false); |
| 258 | |
| 259 | // Extract text from parts (AI SDK v5+) or fall back to content |
| 260 | const text = |
| 261 | message.parts |
| 262 | ?.filter((p) => p.type === 'text') |
| 263 | .map((p) => p.text) |
| 264 | .join('\n') || |
| 265 | message.content || |
| 266 | ''; |
| 267 | |
| 268 | const partsLength = message.parts?.length || 0; |
| 269 | const textLength = text.length; |
| 270 | let lastToolPart; |
| 271 | for (let i = (message.parts?.length || 0) - 1; i >= 0; i--) { |
| 272 | if (message.parts[i].type?.startsWith('tool-')) { lastToolPart = message.parts[i]; break; } |
| 273 | } |
| 274 | const hasRunningTool = (lastToolPart?.state === 'input-streaming' || lastToolPart?.state === 'input-available') || false; |
| 275 | |
| 276 | useEffect(() => { |
| 277 | if (!isLoading || hasRunningTool) { |
| 278 | setShowWorking(false); |
| 279 | return; |
| 280 | } |
| 281 | setShowWorking(false); |
| 282 | const timer = setTimeout(() => setShowWorking(true), 500); |
| 283 | return () => clearTimeout(timer); |
| 284 | }, [isLoading, partsLength, textLength, hasRunningTool]); |
| 285 | |
| 286 | // Extract file parts |
| 287 | const fileParts = message.parts?.filter((p) => p.type === 'file') || []; |
| 288 | const imageParts = fileParts.filter((p) => p.mediaType?.startsWith('image/')); |
| 289 | const otherFileParts = fileParts.filter((p) => !p.mediaType?.startsWith('image/')); |
| 290 | const hasToolParts = message.parts?.some((p) => p.type?.startsWith('tool-')) || false; |
| 291 | |
| 292 | // System messages render as a subtle info banner |
| 293 | if (isSystem) { |
| 294 | return ( |
| 295 | <div className="w-full px-4 py-2 rounded-md bg-muted/50 border border-border/50"> |
| 296 | <p className="text-xs text-muted-foreground whitespace-pre-wrap">{text}</p> |
| 297 | </div> |
| 298 | ); |
| 299 | } |
| 300 | |
| 301 | const handleCopy = async () => { |
| 302 | try { |
| 303 | await navigator.clipboard.writeText(text); |
| 304 | setCopied(true); |
| 305 | setTimeout(() => setCopied(false), 2000); |
| 306 | } catch {} |
| 307 | }; |
nothing calls this directly
no test coverage detected