fix: address final review findings

- Finding 1: add AppState::authed_get/authed_post helpers that attach
  Bearer token when present; all 4 commands now use them
- Finding 2: MessageInput accepts onSent callback, calls it after
  successful send; ChatPage passes refreshKey bump as onSent
- Finding 3: MessageList useEffect uses cancellation flag so stale
  in-flight responses cannot overwrite a newly selected conversation
- Finding 4: scrollIntoView moved to separate effect keyed on
  messages.length so scroll only fires when new messages arrive
This commit is contained in:
2026-08-12 17:58:02 +02:00
parent 91260c8e30
commit 455d304885
7 changed files with 56 additions and 17 deletions

View File

@@ -1,7 +1,12 @@
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
export default function MessageInput({ conversationId }: { conversationId: string }) {
interface Props {
conversationId: string;
onSent: () => void;
}
export default function MessageInput({ conversationId, onSent }: Props) {
const [content, setContent] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -11,6 +16,7 @@ export default function MessageInput({ conversationId }: { conversationId: strin
await invoke("send_message", { conversationId, content });
setContent("");
setError(null);
onSent();
} catch (e) {
setError(String(e));
}

View File

@@ -8,25 +8,41 @@ interface Message {
created_at: string;
}
export default function MessageList({ conversationId }: { conversationId: string }) {
interface Props {
conversationId: string;
refreshKey: number;
}
export default function MessageList({ conversationId, refreshKey }: Props) {
const [messages, setMessages] = useState<Message[]>([]);
const [error, setError] = useState<string | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const prevLengthRef = useRef(0);
// Fix 3: cancellation flag prevents stale responses from overwriting new conversation
useEffect(() => {
let cancelled = false;
const load = () =>
invoke<Message[]>("list_messages", { conversationId })
.then((msgs) => {
setMessages(msgs);
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
if (!cancelled) setMessages(msgs);
})
.catch((e) => setError(String(e)));
.catch((e) => { if (!cancelled) setError(String(e)); });
load();
// ponytail: polling every 3s — replace with WebSocket when server supports it
const id = setInterval(load, 3000);
return () => clearInterval(id);
}, [conversationId]);
return () => { cancelled = true; clearInterval(id); };
// Fix 2: refreshKey in deps triggers immediate reload after send
}, [conversationId, refreshKey]);
// Fix 4: scroll only when message count increases, not on every poll
useEffect(() => {
if (messages.length > prevLengthRef.current) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
prevLengthRef.current = messages.length;
}
}, [messages.length]);
return (
<div style={{ flex: 1, overflowY: "auto", padding: 16 }}>

View File

@@ -7,6 +7,7 @@ import MessageInput from "../components/MessageInput";
export default function ChatPage() {
const { workspaceId } = useParams<{ workspaceId: string }>();
const [conversationId, setConversationId] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
return (
<div style={{ display: "flex", height: "100vh" }}>
@@ -18,8 +19,11 @@ export default function ChatPage() {
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
{conversationId ? (
<>
<MessageList conversationId={conversationId} />
<MessageInput conversationId={conversationId} />
<MessageList conversationId={conversationId} refreshKey={refreshKey} />
<MessageInput
conversationId={conversationId}
onSent={() => setRefreshKey((k) => k + 1)}
/>
</>
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#888" }}>