优化聊天框输入聚焦逻辑,增强网络错误处理

This commit is contained in:
2026-03-24 16:25:09 +08:00
parent 045391d036
commit 825acbf29c
3 changed files with 48 additions and 13 deletions
+10
View File
@@ -113,6 +113,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const [conversationId, setConversationId] = useState<string | undefined>(undefined); const [conversationId, setConversationId] = useState<string | undefined>(undefined);
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
const bottomRef = useRef<HTMLDivElement>(null); const bottomRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const theme = useTheme(); const theme = useTheme();
const canSend = useMemo(() => input.trim().length > 0 && !isStreaming, [input, isStreaming]); const canSend = useMemo(() => input.trim().length > 0 && !isStreaming, [input, isStreaming]);
@@ -122,6 +123,14 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" }); bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isStreaming]); }, [messages, isStreaming]);
useEffect(() => {
if (!open) return;
const timer = window.setTimeout(() => {
inputRef.current?.focus();
}, 0);
return () => window.clearTimeout(timer);
}, [open]);
const handleSend = async () => { const handleSend = async () => {
const prompt = input.trim(); const prompt = input.trim();
if (!prompt || isStreaming) return; if (!prompt || isStreaming) return;
@@ -443,6 +452,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
}} }}
> >
<TextField <TextField
inputRef={inputRef}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
+14
View File
@@ -97,4 +97,18 @@ describe("streamCopilotChat", () => {
{ type: "error", message: "Login expired. Please sign in again.", detail: undefined }, { type: "error", message: "Login expired. Please sign in again.", detail: undefined },
]); ]);
}); });
it("emits network error when fetch throws", async () => {
apiFetch.mockRejectedValue(new TypeError("Failed to fetch"));
const events: Array<{ type: string; message?: string; detail?: string }> = [];
await streamCopilotChat({
message: "hi",
onEvent: (event) => events.push(event),
});
expect(events).toEqual([
{ type: "error", message: "network request failed", detail: "Failed to fetch" },
]);
});
}); });
+24 -13
View File
@@ -38,19 +38,30 @@ export const streamCopilotChat = async ({
signal, signal,
onEvent, onEvent,
}: StreamOptions) => { }: StreamOptions) => {
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, { let response: Response;
method: "POST", try {
signal, response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, {
headers: { method: "POST",
"Content-Type": "application/json", signal,
Accept: "text/event-stream", headers: {
}, "Content-Type": "application/json",
body: JSON.stringify({ Accept: "text/event-stream",
message, },
conversation_id: conversationId, body: JSON.stringify({
}), message,
skipAuthRedirect: true, conversation_id: conversationId,
}); }),
skipAuthRedirect: true,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
onEvent({
type: "error",
message: "network request failed",
detail,
});
return;
}
if (!response.ok || !response.body) { if (!response.ok || !response.body) {
const detail = await response.text(); const detail = await response.text();