fix: resolve "sorry something went wrong" errors and show images in chat

Three fixes from beta tester session feedback:

1. MCP error handling (backend/app/services/assistant_chat_service.py)
   - The MCP Microsoft Learn integration was catching only BadRequestError.
     Any other error type (APIStatusError, APIConnectionError, timeout) from
     the external MCP server propagated as a 502, causing the generic error.
   - Now catches all Exception types when MCP is active and retries without
     MCP using the stable client.messages.create endpoint.

2. Frontend error UX (frontend/src/pages/AssistantChatPage.tsx)
   - catch {} was silently swallowing all errors and inserting a generic
     assistant message. Now: differentiates 429 (rate limit) vs 502/503
     (AI unavailable), removes the optimistic user message on failure,
     restores the failed message to the input so users can retry without
     retyping, and logs errors to console for debugging.

3. Image attachments visible in chat (frontend/src/components/assistant/ChatMessage.tsx)
   - Uploaded images were sent to the AI correctly but never shown in the
     chat thread. Now captures preview URLs before clearing pendingUploads
     and renders thumbnails above the user bubble, clickable to full size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-07 13:09:16 +00:00
parent e8e12cc7e5
commit 290f2be2fd
3 changed files with 66 additions and 21 deletions

View File

@@ -303,6 +303,8 @@ async def _call_anthropic_cached(
} }
] ]
_mcp_active = mcp_servers is not anthropic.NOT_GIVEN
try: try:
response = await client.beta.messages.create( response = await client.beta.messages.create(
model=settings.AI_MODEL_ANTHROPIC, model=settings.AI_MODEL_ANTHROPIC,
@@ -313,12 +315,22 @@ async def _call_anthropic_cached(
tools=tools, tools=tools,
betas=["mcp-client-2025-11-20"], betas=["mcp-client-2025-11-20"],
) )
except anthropic.BadRequestError as e: except Exception as e:
# MCP server failures (rate limits, connection errors) should not # MCP server failures surface as many error types — BadRequestError,
# block the assistant entirely — retry without MCP tools. # APIStatusError, APIConnectionError, APITimeoutError. Always retry
if "MCP server" in str(e) and mcp_servers is not anthropic.NOT_GIVEN: # without MCP when MCP was active, so a flaky external server never
logger.warning("MCP server error, retrying without MCP: %s", e) # blocks the assistant entirely.
response = await client.beta.messages.create( _is_mcp_error = _mcp_active and (
"MCP server" in str(e)
or "mcp" in type(e).__name__.lower()
or isinstance(e, (anthropic.BadRequestError, anthropic.APIStatusError))
)
if _is_mcp_error:
logger.warning(
"MCP server error (%s), retrying without MCP: %s",
type(e).__name__, e,
)
response = await client.messages.create(
model=settings.AI_MODEL_ANTHROPIC, model=settings.AI_MODEL_ANTHROPIC,
max_tokens=max_tokens, max_tokens=max_tokens,
system=system_blocks, system=system_blocks,

View File

@@ -7,9 +7,10 @@ interface ChatMessageProps {
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: string content: string
suggestedFlows?: SuggestedFlow[] suggestedFlows?: SuggestedFlow[]
imageUrls?: string[]
} }
export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps) { export function ChatMessage({ role, content, suggestedFlows, imageUrls }: ChatMessageProps) {
return ( return (
<div className={`flex gap-3 ${role === 'user' ? 'flex-row-reverse' : ''}`}> <div className={`flex gap-3 ${role === 'user' ? 'flex-row-reverse' : ''}`}>
{/* Avatar */} {/* Avatar */}
@@ -25,6 +26,21 @@ export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps)
{/* Content */} {/* Content */}
<div className={`max-w-[80%] space-y-2 ${role === 'user' ? 'text-right' : ''}`}> <div className={`max-w-[80%] space-y-2 ${role === 'user' ? 'text-right' : ''}`}>
{/* Image attachments (user messages only) */}
{role === 'user' && imageUrls && imageUrls.length > 0 && (
<div className={`flex flex-wrap gap-2 ${role === 'user' ? 'justify-end' : ''}`}>
{imageUrls.map((url, i) => (
<a key={i} href={url} target="_blank" rel="noopener noreferrer">
<img
src={url}
alt={`Attachment ${i + 1}`}
className="h-24 w-auto max-w-[200px] rounded-xl object-cover border border-border cursor-pointer hover:opacity-90 transition-opacity"
/>
</a>
))}
</div>
)}
<div <div
className={`rounded-2xl px-4 py-3 text-[0.875rem] leading-relaxed ${ className={`rounded-2xl px-4 py-3 text-[0.875rem] leading-relaxed ${
role === 'user' role === 'user'

View File

@@ -25,6 +25,7 @@ interface MessageWithMeta {
fork?: ForkMetadata | null fork?: ForkMetadata | null
actions?: ActionItem[] | null actions?: ActionItem[] | null
questions?: QuestionItem[] | null questions?: QuestionItem[] | null
imageUrls?: string[]
} }
export default function AssistantChatPage() { export default function AssistantChatPage() {
@@ -322,12 +323,14 @@ export default function AssistantChatPage() {
if (!input.trim() || !activeChatId || loading) return if (!input.trim() || !activeChatId || loading) return
const userMessage = input.trim() const userMessage = input.trim()
const completedUploadIds = pendingUploads const completedUploads = pendingUploads.filter((u) => u.status === 'done' && u.result?.id)
.filter((u) => u.status === 'done' && u.result?.id) const completedUploadIds = completedUploads.map((u) => u.result!.id)
.map((u) => u.result!.id) const imageUrls = completedUploads
.filter((u) => u.preview)
.map((u) => u.preview)
setInput('') setInput('')
setPendingUploads([]) setPendingUploads([])
setMessages(prev => [...prev, { role: 'user', content: userMessage }]) setMessages(prev => [...prev, { role: 'user', content: userMessage, imageUrls: imageUrls.length > 0 ? imageUrls : undefined }])
setLoading(true) setLoading(true)
const sentForChatId = activeChatId const sentForChatId = activeChatId
@@ -363,11 +366,21 @@ export default function AssistantChatPage() {
setActiveActions(response.actions || []) setActiveActions(response.actions || [])
setShowTaskLane(true) setShowTaskLane(true)
} }
} catch { } catch (err: unknown) {
setMessages(prev => [ console.error('[AssistantChat] sendChatMessage failed:', err)
...prev, const status = (err as { response?: { status?: number } })?.response?.status
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' }, let errorMsg: string
]) if (status === 429) {
errorMsg = "You're sending messages too quickly. Wait a moment and try again."
} else if (status === 502 || status === 503) {
errorMsg = "The AI is temporarily unavailable. Please try again in a few seconds."
} else {
errorMsg = "Something went wrong sending your message. Please try again."
}
// Remove the optimistic user message and restore it to the input so they can retry
setMessages(prev => prev.slice(0, -1))
setInput(userMessage)
toast.error(errorMsg)
} finally { } finally {
setLoading(false) setLoading(false)
requestAnimationFrame(() => inputRef.current?.focus()) requestAnimationFrame(() => inputRef.current?.focus())
@@ -421,11 +434,14 @@ export default function AssistantChatPage() {
setActiveQuestions([]) setActiveQuestions([])
setActiveActions([]) setActiveActions([])
} }
} catch { } catch (err: unknown) {
setMessages(prev => [ console.error('[AssistantChat] handleTaskSubmit failed:', err)
...prev, const status = (err as { response?: { status?: number } })?.response?.status
{ role: 'assistant', content: 'Sorry, something went wrong processing your responses. Please try again.' }, const errorMsg = status === 429
]) ? "You're sending messages too quickly. Wait a moment and try again."
: "Something went wrong submitting your responses. Please try again."
setMessages(prev => prev.slice(0, -1))
toast.error(errorMsg)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -832,6 +848,7 @@ export default function AssistantChatPage() {
role={msg.role} role={msg.role}
content={msg.content} content={msg.content}
suggestedFlows={msg.suggestedFlows} suggestedFlows={msg.suggestedFlows}
imageUrls={msg.imageUrls}
/> />
))} ))}
{loading && ( {loading && (