diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index ae7a6ae..cf97c58 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -13,84 +13,71 @@ const AUTHOR_ICON: Record = { interface Props { chatId: string; - fullscreen?: boolean; } -export default function ChatPanel({ chatId, fullscreen = false }: Props) { +export default function ChatPanel({ chatId }: Props) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); - const [open, setOpen] = useState(false); const [sending, setSending] = useState(false); const [loadingOlder, setLoadingOlder] = useState(false); const [hasMore, setHasMore] = useState(true); - const bottomRef = useRef(null); const scrollRef = useRef(null); - const initialLoad = useRef(true); + const bottomRef = useRef(null); + const isInitial = useRef(true); - // Initial load — last N messages + // Load initial messages useEffect(() => { if (!chatId) return; - initialLoad.current = true; + isInitial.current = true; setHasMore(true); getMessages({ chat_id: chatId, limit: PAGE_SIZE }) .then((msgs) => { setMessages(msgs); setHasMore(msgs.length >= PAGE_SIZE); - // Scroll to bottom on initial load setTimeout(() => bottomRef.current?.scrollIntoView(), 50); }) .catch(() => {}); }, [chatId]); - // WS real-time messages + // WS: new messages useEffect(() => { - const unsub = wsClient.on("message.new", (data: any) => { - if (data.chat_id === chatId) { - setMessages((prev) => { - if (prev.some((m) => m.id === data.id)) return prev; - return [...prev, data as Message]; - }); - } + const unsub = wsClient.on("message.new", (msg: Message) => { + if (msg.chat_id !== chatId) return; + setMessages((prev) => { + if (prev.some((m) => m.id === msg.id)) return prev; + return [...prev, msg]; + }); }); return () => { unsub?.(); }; }, [chatId]); - // Auto-scroll to bottom on new messages (only if already near bottom) + // Auto-scroll on new messages (only if near bottom) useEffect(() => { - if (initialLoad.current) { - initialLoad.current = false; + if (isInitial.current) { + isInitial.current = false; return; } const el = scrollRef.current; if (!el) return; - const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100; - if (nearBottom) { + if (el.scrollHeight - el.scrollTop - el.clientHeight < 100) { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [messages]); - // Load older messages on scroll to top + // Load older messages const loadOlder = useCallback(async () => { if (loadingOlder || !hasMore || !chatId || messages.length === 0) return; setLoadingOlder(true); const el = scrollRef.current; const prevHeight = el?.scrollHeight || 0; - try { - const older = await getMessages({ - chat_id: chatId, - limit: PAGE_SIZE, - offset: messages.length, - }); + const older = await getMessages({ chat_id: chatId, limit: PAGE_SIZE, offset: messages.length }); if (older.length < PAGE_SIZE) setHasMore(false); if (older.length > 0) { setMessages((prev) => { - // Dedup - const existingIds = new Set(prev.map((m) => m.id)); - const unique = older.filter((m) => !existingIds.has(m.id)); - return [...unique, ...prev]; + const ids = new Set(prev.map((m) => m.id)); + return [...older.filter((m) => !ids.has(m.id)), ...prev]; }); - // Maintain scroll position requestAnimationFrame(() => { if (el) el.scrollTop = el.scrollHeight - prevHeight; }); @@ -104,10 +91,7 @@ export default function ChatPanel({ chatId, fullscreen = false }: Props) { const handleScroll = useCallback(() => { const el = scrollRef.current; - if (!el) return; - if (el.scrollTop < 50 && hasMore && !loadingOlder) { - loadOlder(); - } + if (el && el.scrollTop < 50 && hasMore && !loadingOlder) loadOlder(); }, [loadOlder, hasMore, loadingOlder]); const handleSend = async () => { @@ -150,99 +134,41 @@ export default function ChatPanel({ chatId, fullscreen = false }: Props) { ); }; - if (fullscreen) { - return ( -
-
- {loadingOlder && ( -
Загрузка...
- )} - {!hasMore && messages.length > 0 && ( -
— начало истории —
- )} - {messages.map(renderMessage)} - {messages.length === 0 && ( -
Нет сообщений
- )} -
-
-
- setInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSend()} - placeholder="Сообщение..." - className="flex-1 bg-[var(--bg)] border border-[var(--border)] rounded-lg px-4 py-2 text-sm outline-none focus:border-[var(--accent)]" - /> - -
-
- ); - } - - // Compact mode (collapsible) return ( -
- - - {open && ( -
-
- {loadingOlder && ( -
Загрузка...
- )} - {messages.map((msg) => { - const bgStyle = - msg.author_type === "system" - ? { backgroundColor: "rgba(161, 120, 0, 0.2)", borderRadius: 6, padding: "4px 8px" } - : msg.author_type === "agent" - ? { backgroundColor: "rgba(0, 120, 60, 0.2)", borderRadius: 6, padding: "4px 8px" } - : undefined; - return ( -
- - {AUTHOR_ICON[msg.author_type] || "👤"} {msg.author_slug} - - {msg.content} -
- ); - })} - {messages.length === 0 && ( -
Нет сообщений
- )} -
-
-
- setInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSend()} - placeholder="Сообщение..." - className="flex-1 bg-[var(--bg)] border border-[var(--border)] rounded px-3 py-1.5 text-sm outline-none focus:border-[var(--accent)]" - /> - -
-
- )} + {loadingOlder && ( +
Загрузка...
+ )} + {!hasMore && messages.length > 0 && ( +
— начало истории —
+ )} + {messages.map(renderMessage)} + {messages.length === 0 && ( +
Нет сообщений
+ )} +
+
+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSend()} + placeholder="Сообщение..." + className="flex-1 bg-[var(--bg)] border border-[var(--border)] rounded-lg px-4 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> + +
); } diff --git a/src/pages/ProjectPage.tsx b/src/pages/ProjectPage.tsx index 037c7d7..b43497a 100644 --- a/src/pages/ProjectPage.tsx +++ b/src/pages/ProjectPage.tsx @@ -84,7 +84,7 @@ export default function ProjectPage() { )} {activeTab === "chat" && project.chat_id && ( - + )} {activeTab === "chat" && !project.chat_id && (