feat: implement chat page with conversation list, message view, and polling
This commit is contained in:
47
src/components/ConversationList.tsx
Normal file
47
src/components/ConversationList.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
name: string | null;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
workspaceId: string;
|
||||
onSelect: (id: string) => void;
|
||||
selectedId: string | null;
|
||||
}
|
||||
|
||||
export default function ConversationList({ workspaceId, onSelect, selectedId }: Props) {
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
invoke<Conversation[]>("list_conversations", { workspaceId })
|
||||
.then(setConversations)
|
||||
.catch((e) => setError(String(e)));
|
||||
}, [workspaceId]);
|
||||
|
||||
return (
|
||||
<div style={{ width: 220, borderRight: "1px solid #ccc", overflowY: "auto" }}>
|
||||
<div style={{ padding: "12px 16px", fontWeight: "bold", borderBottom: "1px solid #ccc" }}>
|
||||
Conversations
|
||||
</div>
|
||||
{error && <p style={{ color: "red", padding: 8 }}>{error}</p>}
|
||||
{conversations.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
onClick={() => onSelect(c.id)}
|
||||
style={{
|
||||
padding: "10px 16px",
|
||||
cursor: "pointer",
|
||||
background: selectedId === c.id ? "#e8f0fe" : "transparent",
|
||||
}}
|
||||
>
|
||||
{c.name ?? `(${c.kind})`}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/components/MessageInput.tsx
Normal file
31
src/components/MessageInput.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export default function MessageInput({ conversationId }: { conversationId: string }) {
|
||||
const [content, setContent] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function handleSend() {
|
||||
if (!content.trim()) return;
|
||||
try {
|
||||
await invoke("send_message", { conversationId, content });
|
||||
setContent("");
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16, borderTop: "1px solid #ccc" }}>
|
||||
{error && <p style={{ color: "red", margin: "0 0 8px" }}>{error}</p>}
|
||||
<input
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||
placeholder="Type a message and press Enter…"
|
||||
style={{ width: "100%", padding: 10, boxSizing: "border-box", fontSize: 14 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/components/MessageList.tsx
Normal file
46
src/components/MessageList.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
user_id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function MessageList({ conversationId }: { conversationId: string }) {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const load = () =>
|
||||
invoke<Message[]>("list_messages", { conversationId })
|
||||
.then((msgs) => {
|
||||
setMessages(msgs);
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
})
|
||||
.catch((e) => 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 (
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: 16 }}>
|
||||
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||
{messages.map((m) => (
|
||||
<div key={m.id} style={{ marginBottom: 12 }}>
|
||||
<strong style={{ marginRight: 8 }}>{m.user_id}</strong>
|
||||
<span style={{ color: "#888", fontSize: 12, marginRight: 8 }}>
|
||||
{new Date(m.created_at).toLocaleTimeString()}
|
||||
</span>
|
||||
<div>{m.content}</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import ConversationList from "../components/ConversationList";
|
||||
import MessageList from "../components/MessageList";
|
||||
import MessageInput from "../components/MessageInput";
|
||||
|
||||
export default function ChatPage() {
|
||||
return <div>Chat</div>;
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100vh" }}>
|
||||
<ConversationList
|
||||
workspaceId={workspaceId!}
|
||||
onSelect={setConversationId}
|
||||
selectedId={conversationId}
|
||||
/>
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
{conversationId ? (
|
||||
<>
|
||||
<MessageList conversationId={conversationId} />
|
||||
<MessageInput conversationId={conversationId} />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#888" }}>
|
||||
Select a conversation
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user