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:
@@ -7,8 +7,7 @@ pub async fn list_conversations(
|
||||
workspace_id: String,
|
||||
) -> Result<Vec<Conversation>, String> {
|
||||
state
|
||||
.client
|
||||
.get(format!("{}/conversations?workspace_id={}", BASE_URL, workspace_id))
|
||||
.authed_get(&format!("{}/conversations?workspace_id={}", BASE_URL, workspace_id))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
|
||||
@@ -7,8 +7,7 @@ pub async fn list_messages(
|
||||
conversation_id: String,
|
||||
) -> Result<Vec<Message>, String> {
|
||||
state
|
||||
.client
|
||||
.get(format!("{}/conversations/{}/messages", BASE_URL, conversation_id))
|
||||
.authed_get(&format!("{}/conversations/{}/messages", BASE_URL, conversation_id))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
@@ -37,8 +36,7 @@ pub async fn send_message(
|
||||
});
|
||||
|
||||
state
|
||||
.client
|
||||
.post(format!("{}/messages", BASE_URL))
|
||||
.authed_post(&format!("{}/messages", BASE_URL))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::state::{AppState, BASE_URL};
|
||||
#[tauri::command]
|
||||
pub async fn list_workspaces(state: tauri::State<'_, AppState>) -> Result<Vec<Workspace>, String> {
|
||||
state
|
||||
.client
|
||||
.get(format!("{}/workspaces", BASE_URL))
|
||||
.authed_get(&format!("{}/workspaces", BASE_URL))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
|
||||
@@ -16,4 +16,21 @@ impl AppState {
|
||||
user_id: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authed_get(&self, url: &str) -> reqwest::RequestBuilder {
|
||||
let builder = self.client.get(url);
|
||||
// ponytail: omit header when no token — server may not require auth yet
|
||||
match self.token.lock().unwrap().clone() {
|
||||
Some(t) => builder.bearer_auth(t),
|
||||
None => builder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authed_post(&self, url: &str) -> reqwest::RequestBuilder {
|
||||
let builder = self.client.post(url);
|
||||
match self.token.lock().unwrap().clone() {
|
||||
Some(t) => builder.bearer_auth(t),
|
||||
None => builder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
Reference in New Issue
Block a user