diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 618a642..0550365 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -587,6 +587,10 @@ export function App() { setAttachments((prev) => prev.filter((a) => a.id !== id)) } onPickFile={handlePickFile} + failedIndex={failed?.index ?? null} + retryCount={retryCount} + maxRetries={MAX_RETRIES} + onRetry={readOnly ? undefined : retryFailed} /> void }) { +function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFailed, retryCount, maxRetries, retryDisabled, onRetry }: { msg: ChatMessage; index: number; authorAvatar?: string; authorName: string; onReport?: (index: number) => void; isFailed?: boolean; retryCount?: number; maxRetries?: number; retryDisabled?: boolean; onRetry?: () => void }) { const { t } = useT(); const isUser = msg.role === "user"; const [copied, setCopied] = useState(false); @@ -43,17 +43,30 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport }: { msg )} - {!isUser && onReport && ( + {!isUser && (onReport || (isFailed && onRetry)) && (
- - {copied ? : } - - - - - onReport(index)}> - - + {onReport && ( + <> + + {copied ? : } + + + + + onReport(index)}> + + + + )} + {isFailed && onRetry && ( + + + + )}
)} @@ -183,6 +196,10 @@ export function Chat({ ocrAvailable, onRemoveAttachment, onPickFile, + failedIndex, + retryCount, + maxRetries, + onRetry, }: { user: User; conv: Conversation | null; @@ -205,6 +222,11 @@ export function Chat({ ocrAvailable: boolean; onRemoveAttachment: (id: string) => void; onPickFile: (kind: "ocr" | "text", file: File) => void; + /** Trailing model-error bubble index (retry arrow), if any. */ + failedIndex: number | null; + retryCount: number; + maxRetries: number; + onRetry?: () => void; }) { const [draft, setDraft] = useState(""); const [attachOpen, setAttachOpen] = useState(false); @@ -298,7 +320,7 @@ export function Chat({ )} {messages.filter((m) => m.role !== "system").map((m, i) => ( - + ))} {streaming !== "" && (
diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts index 1f7537e..071e185 100644 --- a/frontend/src/lib/i18n.ts +++ b/frontend/src/lib/i18n.ts @@ -425,6 +425,7 @@ const en = { "msg.copy": "Copy message", "msg.search": "Search the web", "msg.report": "Report response", + "msg.retry": "Retry", "report.title": "Report this response", "report.reason": "Reason", @@ -930,6 +931,7 @@ const es: Dict = { "msg.copy": "Copiar mensaje", "msg.search": "Buscar en la web", "msg.report": "Denunciar respuesta", + "msg.retry": "Reintentar", "report.title": "Denunciar esta respuesta", "report.reason": "Motivo", "report.reasonPh": "Selecciona un motivo…", @@ -1430,6 +1432,7 @@ const fr: Dict = { "msg.copy": "Copier le message", "msg.search": "Rechercher sur le web", "msg.report": "Signaler la réponse", + "msg.retry": "Réessayer", "report.title": "Signaler cette réponse", "report.reason": "Motif", "report.reasonPh": "Choisir un motif…", @@ -1918,6 +1921,7 @@ const ru: Dict = { "msg.copy": "Скопировать сообщение", "msg.search": "Искать в интернете", "msg.report": "Пожаловаться на ответ", + "msg.retry": "Повторить", "report.title": "Пожаловаться на этот ответ", "report.reason": "Причина", "report.reasonPh": "Выберите причину…", @@ -2411,6 +2415,7 @@ const it: Dict = { "msg.copy": "Copia messaggio", "msg.search": "Cerca nel web", "msg.report": "Segnala risposta", + "msg.retry": "Riprova", "report.title": "Segnala questa risposta", "report.reason": "Motivo", "report.reasonPh": "Seleziona un motivo…", @@ -2895,6 +2900,7 @@ const pt: Dict = { "msg.copy": "Copiar mensagem", "msg.search": "Pesquisar na web", "msg.report": "Denunciar resposta", + "msg.retry": "Tentar novamente", "report.title": "Denunciar esta resposta", "report.reason": "Motivo", "report.reasonPh": "Selecione um motivo…",