feat: add retry/resend button on user messages

Hover over any user message to reveal a retry button (↻) that resends
the message text. Disabled while a response is generating.
Includes EN/FR i18n strings.
This commit is contained in:
Nicolas Varrot
2026-02-11 21:56:48 +00:00
parent 473d23c140
commit 5b2f3a340d
3 changed files with 16 additions and 3 deletions

View File

@@ -112,7 +112,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
</div> </div>
)} )}
{messages.filter(hasVisibleContent).map(msg => ( {messages.filter(hasVisibleContent).map(msg => (
<ChatMessageComponent key={msg.id} message={msg} /> <ChatMessageComponent key={msg.id} message={msg} onRetry={!isGenerating ? handleSend : undefined} />
))} ))}
{showTyping && <TypingIndicator />} {showTyping && <TypingIndicator />}
<div ref={bottomRef} /> <div ref={bottomRef} />

View File

@@ -9,7 +9,7 @@ import { ThinkingBlock } from './ThinkingBlock';
import { CodeBlock } from './CodeBlock'; import { CodeBlock } from './CodeBlock';
import { ToolCall } from './ToolCall'; import { ToolCall } from './ToolCall';
import { ImageBlock, buildImageSrc } from './ImageBlock'; import { ImageBlock, buildImageSrc } from './ImageBlock';
import { Bot, User, Wrench, Copy, Check } from 'lucide-react'; import { Bot, User, Wrench, Copy, Check, RefreshCw } from 'lucide-react';
import { t, getLocale } from '../lib/i18n'; import { t, getLocale } from '../lib/i18n';
import { useLocale } from '../hooks/useLocale'; import { useLocale } from '../hooks/useLocale';
// ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage // ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage
@@ -243,7 +243,7 @@ function getPlainText(message: ChatMessageType): string {
return message.content; return message.content;
} }
export function ChatMessageComponent({ message }: { message: ChatMessageType }) { export function ChatMessageComponent({ message, onRetry }: { message: ChatMessageType; onRetry?: (text: string) => void }) {
useLocale(); // re-render on locale change useLocale(); // re-render on locale change
const isUser = message.role === 'user'; const isUser = message.role === 'user';
@@ -278,6 +278,17 @@ export function ChatMessageComponent({ message }: { message: ChatMessageType })
{!isUser && !message.isStreaming && getPlainText(message).trim() && ( {!isUser && !message.isStreaming && getPlainText(message).trim() && (
<CopyButton text={getPlainText(message)} /> <CopyButton text={getPlainText(message)} />
)} )}
{/* Retry button (user messages only) */}
{isUser && onRetry && (
<button
onClick={() => onRetry(getPlainText(message))}
className="absolute top-2 right-2 h-7 w-7 rounded-lg border border-white/8 bg-zinc-800/80 backdrop-blur-sm flex items-center justify-center text-zinc-400 hover:text-cyan-300 hover:border-cyan-400/30 transition-all opacity-0 group-hover:opacity-100"
title={t('message.retry')}
aria-label={t('message.retry')}
>
<RefreshCw size={13} />
</button>
)}
{/* User-visible text */} {/* User-visible text */}
{message.blocks.length > 0 ? renderTextBlocks(message.blocks) : ( {message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
<div className="markdown-body"> <div className="markdown-body">

View File

@@ -59,6 +59,7 @@ const en = {
// Message actions // Message actions
'message.copy': 'Copy message', 'message.copy': 'Copy message',
'message.copied': 'Copied!', 'message.copied': 'Copied!',
'message.retry': 'Resend message',
// Timestamps // Timestamps
'time.yesterday': 'Yesterday', 'time.yesterday': 'Yesterday',
@@ -127,6 +128,7 @@ const fr: Record<keyof typeof en, string> = {
'message.copy': 'Copier le message', 'message.copy': 'Copier le message',
'message.copied': 'Copié !', 'message.copied': 'Copié !',
'message.retry': 'Renvoyer le message',
'time.yesterday': 'Hier', 'time.yesterday': 'Hier',