refactor: remove compact/collapsible chat mode — ChatPanel is fullscreen only

This commit is contained in:
Markov 2026-02-24 13:22:46 +01:00
parent a01bfbfc40
commit 44884d937c
2 changed files with 55 additions and 129 deletions

View File

@ -13,84 +13,71 @@ const AUTHOR_ICON: Record<string, string> = {
interface Props {
chatId: string;
fullscreen?: boolean;
}
export default function ChatPanel({ chatId, fullscreen = false }: Props) {
export default function ChatPanel({ chatId }: Props) {
const [messages, setMessages] = useState<Message[]>([]);
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<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const initialLoad = useRef(true);
const bottomRef = useRef<HTMLDivElement>(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) {
const unsub = wsClient.on("message.new", (msg: Message) => {
if (msg.chat_id !== chatId) return;
setMessages((prev) => {
if (prev.some((m) => m.id === data.id)) return prev;
return [...prev, data as Message];
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,7 +134,6 @@ export default function ChatPanel({ chatId, fullscreen = false }: Props) {
);
};
if (fullscreen) {
return (
<div className="flex flex-col h-full">
<div
@ -188,61 +171,4 @@ export default function ChatPanel({ chatId, fullscreen = false }: Props) {
</div>
</div>
);
}
// Compact mode (collapsible)
return (
<div className={`border-b border-[var(--border)] transition-all ${open ? "h-64" : "h-10"}`}>
<button
onClick={() => setOpen(!open)}
className="w-full h-10 px-4 flex items-center text-sm text-[var(--muted)] hover:text-[var(--fg)]"
>
💬 Чат {open ? "▼" : "▲"}
</button>
{open && (
<div className="flex flex-col h-[calc(100%-2.5rem)]">
<div
ref={!fullscreen ? scrollRef : undefined}
onScroll={!fullscreen ? handleScroll : undefined}
className="flex-1 overflow-y-auto px-4 py-2 space-y-2"
>
{loadingOlder && (
<div className="text-center text-xs text-[var(--muted)] py-1">Загрузка...</div>
)}
{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 (
<div key={msg.id} className="text-sm" style={bgStyle}>
<span className="text-xs text-[var(--muted)]">
{AUTHOR_ICON[msg.author_type] || "👤"} {msg.author_slug}
</span>
<span className="ml-2">{msg.content}</span>
</div>
);
})}
{messages.length === 0 && (
<div className="text-sm text-[var(--muted)] italic py-4 text-center">Нет сообщений</div>
)}
<div ref={bottomRef} />
</div>
<div className="px-4 py-2 border-t border-[var(--border)] flex gap-2">
<input
value={input}
onChange={(e) => 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)]"
/>
<button onClick={handleSend} disabled={sending} className="text-sm text-[var(--accent)] px-2 disabled:opacity-50"></button>
</div>
</div>
)}
</div>
);
}

View File

@ -84,7 +84,7 @@ export default function ProjectPage() {
<KanbanBoard projectId={project.id} projectSlug={project.slug} />
)}
{activeTab === "chat" && project.chat_id && (
<ChatPanel chatId={project.chat_id} fullscreen />
<ChatPanel chatId={project.chat_id} />
)}
{activeTab === "chat" && !project.chat_id && (
<div className="flex items-center justify-center h-full text-[var(--muted)] text-sm">