fix(desktop): error response improvements — content, retry, model selector
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P1: onError callback now sets content to error message instead of empty string.
Previously API errors (404/429) produced empty assistant messages with only
a visual error badge — now the error text is persisted in message content.
P3: Retry button now re-sends the preceding user message via sendToGateway
instead of copying to input. Works for both virtualized and non-virtualized
message lists. Removed unused setInput prop from MessageBubble.
Also hides model selector in Tauri runtime (SaaS token pool routes models).
This commit is contained in:
@@ -231,6 +231,18 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
setPendingFiles([]);
|
setPendingFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createRetryHandler = (msgId: string) => () => {
|
||||||
|
if (isStreaming) return;
|
||||||
|
// Find the user message immediately before this error
|
||||||
|
const idx = messages.findIndex(m => m.id === msgId);
|
||||||
|
if (idx > 0) {
|
||||||
|
const prevMsg = messages[idx - 1];
|
||||||
|
if (prevMsg.role === 'user' && prevMsg.content) {
|
||||||
|
sendToGateway(prevMsg.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -433,6 +445,16 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
onHeightChange={setHeight}
|
onHeightChange={setHeight}
|
||||||
messageRefs={messageRefs}
|
messageRefs={messageRefs}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
|
retryForMessage={(msgId: string) => {
|
||||||
|
const idx = messages.findIndex(m => m.id === msgId);
|
||||||
|
if (idx > 0) {
|
||||||
|
const prevMsg = messages[idx - 1];
|
||||||
|
if (prevMsg.role === 'user' && prevMsg.content) {
|
||||||
|
return () => { if (!isStreaming) sendToGateway(prevMsg.content); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
messages.map((message) => (
|
messages.map((message) => (
|
||||||
@@ -445,7 +467,7 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
layout
|
layout
|
||||||
transition={defaultTransition}
|
transition={defaultTransition}
|
||||||
>
|
>
|
||||||
<MessageBubble message={message} setInput={setInput} />
|
<MessageBubble message={message} onRetry={createRetryHandler(message.id)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -575,7 +597,7 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageBubble({ message, setInput, onRetry }: { message: Message; setInput: (text: string) => void; onRetry?: () => void }) {
|
function MessageBubble({ message, onRetry }: { message: Message; setInput?: (text: string) => void; onRetry?: () => void }) {
|
||||||
if (message.role === 'tool') {
|
if (message.role === 'tool') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -679,17 +701,14 @@ function MessageBubble({ message, setInput, onRetry }: { message: Message; setIn
|
|||||||
{message.error && (
|
{message.error && (
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<p className="text-xs text-red-500">{message.error}</p>
|
<p className="text-xs text-red-500">{message.error}</p>
|
||||||
<button
|
{onRetry && (
|
||||||
onClick={() => {
|
<button
|
||||||
const text = typeof message.content === 'string' ? message.content : '';
|
onClick={onRetry}
|
||||||
if (text) {
|
className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors"
|
||||||
setInput(text);
|
>
|
||||||
}
|
重试
|
||||||
}}
|
</button>
|
||||||
className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors"
|
)}
|
||||||
>
|
|
||||||
重试
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Download button for AI messages - show on hover */}
|
{/* Download button for AI messages - show on hover */}
|
||||||
@@ -716,6 +735,7 @@ interface VirtualizedMessageRowProps {
|
|||||||
onHeightChange: (height: number) => void;
|
onHeightChange: (height: number) => void;
|
||||||
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
setInput: (text: string) => void;
|
setInput: (text: string) => void;
|
||||||
|
onRetry?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -726,7 +746,7 @@ function VirtualizedMessageRow({
|
|||||||
message,
|
message,
|
||||||
onHeightChange,
|
onHeightChange,
|
||||||
messageRefs,
|
messageRefs,
|
||||||
setInput,
|
onRetry,
|
||||||
style,
|
style,
|
||||||
ariaAttributes,
|
ariaAttributes,
|
||||||
}: VirtualizedMessageRowProps & {
|
}: VirtualizedMessageRowProps & {
|
||||||
@@ -761,7 +781,7 @@ function VirtualizedMessageRow({
|
|||||||
className="py-3"
|
className="py-3"
|
||||||
{...ariaAttributes}
|
{...ariaAttributes}
|
||||||
>
|
>
|
||||||
<MessageBubble message={message} setInput={setInput} />
|
<MessageBubble message={message} onRetry={onRetry} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -773,6 +793,7 @@ interface VirtualizedMessageListProps {
|
|||||||
onHeightChange: (id: string, height: number) => void;
|
onHeightChange: (id: string, height: number) => void;
|
||||||
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
setInput: (text: string) => void;
|
setInput: (text: string) => void;
|
||||||
|
retryForMessage: (msgId: string) => (() => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -786,6 +807,7 @@ function VirtualizedMessageList({
|
|||||||
onHeightChange,
|
onHeightChange,
|
||||||
messageRefs,
|
messageRefs,
|
||||||
setInput,
|
setInput,
|
||||||
|
retryForMessage,
|
||||||
}: VirtualizedMessageListProps) {
|
}: VirtualizedMessageListProps) {
|
||||||
// Row component for react-window v2
|
// Row component for react-window v2
|
||||||
const RowComponent = (props: {
|
const RowComponent = (props: {
|
||||||
@@ -802,6 +824,7 @@ function VirtualizedMessageList({
|
|||||||
onHeightChange={(h) => onHeightChange(messages[props.index].id, h)}
|
onHeightChange={(h) => onHeightChange(messages[props.index].id, h)}
|
||||||
messageRefs={messageRefs}
|
messageRefs={messageRefs}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
|
onRetry={retryForMessage(messages[props.index].id)}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
ariaAttributes={props.ariaAttributes}
|
ariaAttributes={props.ariaAttributes}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -502,10 +502,17 @@ export const useStreamStore = create<StreamState>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error: string) => {
|
onError: (error: string) => {
|
||||||
|
// Flush any remaining buffered deltas before erroring
|
||||||
|
if (flushTimer !== null) {
|
||||||
|
clearTimeout(flushTimer);
|
||||||
|
flushTimer = null;
|
||||||
|
}
|
||||||
|
flushBuffers();
|
||||||
|
|
||||||
_chat?.updateMessages(msgs =>
|
_chat?.updateMessages(msgs =>
|
||||||
msgs.map(m =>
|
msgs.map(m =>
|
||||||
m.id === assistantId
|
m.id === assistantId
|
||||||
? { ...m, content: '', streaming: false, error }
|
? { ...m, content: error, streaming: false, error }
|
||||||
: m.role === 'user' && m.optimistic && m.timestamp.getTime() >= streamStartTime
|
: m.role === 'user' && m.optimistic && m.timestamp.getTime() >= streamStartTime
|
||||||
? { ...m, optimistic: false }
|
? { ...m, optimistic: false }
|
||||||
: m
|
: m
|
||||||
|
|||||||
Reference in New Issue
Block a user