/
aaronair
/
Test_task_rt_solution
Обзор
Документация
Войти
/
aaronair
/
Test_task_rt_solution
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/SearchChatWidget.tsx
170 строк
6 KB
Ilikedrinkpivo
first_commit
28 май 2026, 14:51
28 май 2026, 14:51
b4498fc
Код
Авторство
О чём код?
import { useEffect, useId, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { getMockSearchReply } from "@/features/search/mockReplies"; import { cn } from "@/lib/cn"; interface Message { role: "user" | "assistant"; text: string; initiativeId?: string; initiativeTitle?: string; } export function SearchChatWidget() { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [loading, setLoading] = useState(false); const [messages, setMessages] = useState<Message[]>([ { role: "assistant", text: "Привет! Я демо-поиск по порталу. Спросите про инициативу, технологию или раздел портала.", }, ]); const panelId = useId(); const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { if (!open) return; inputRef.current?.focus(); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open]); const handleSearch = () => { const q = query.trim(); if (!q || loading) return; setMessages((m) => [...m, { role: "user", text: q }]); setQuery(""); setLoading(true); void getMockSearchReply(q).then((reply) => { setMessages((m) => [ ...m, { role: "assistant", text: reply.text, initiativeId: reply.initiativeId, initiativeTitle: reply.initiativeTitle, }, ]); setLoading(false); }); }; return ( <> <button type="button" onClick={() => setOpen(true)} className={cn( "fixed bottom-6 right-6 z-40 flex h-14 w-14 items-center justify-center rounded-full shadow-cardHover", "bg-gradient-to-br from-primary-600 to-accent-violet text-white", "hover:scale-105 transition-transform", open && "hidden", )} aria-label="Открыть поиск по порталу" > <svg viewBox="0 0 24 24" className="h-6 w-6" fill="none" stroke="currentColor" strokeWidth={2}> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" strokeLinecap="round" strokeLinejoin="round" /> </svg> </button> {open && ( <div id={panelId} role="dialog" aria-label="Поиск по порталу" className="fixed bottom-6 right-6 z-40 flex w-[380px] max-h-[min(520px,calc(100vh-3rem))] flex-col rounded-xl2 border border-ink-300/40 bg-surface-card shadow-cardHover overflow-hidden" > <header className="flex items-center justify-between border-b border-ink-300/40 px-4 py-3 bg-primary-50"> <div> <h2 className="text-sm font-semibold text-ink-900">Поиск по порталу</h2> <p className="text-xs text-ink-500">Демо-режим</p> </div> <button type="button" className="rounded-lg p-1.5 text-ink-500 hover:bg-surface-muted" onClick={() => setOpen(false)} aria-label="Закрыть" > ✕ </button> </header> <div className="flex-1 overflow-y-auto p-4 space-y-3 min-h-[200px]"> {messages.map((msg, i) => ( <div key={i} className={cn( "rounded-lg px-3 py-2 text-sm max-w-[90%]", msg.role === "user" ? "ml-auto bg-primary-600 text-white" : "bg-surface-muted text-ink-800", )} > {msg.text} {msg.initiativeId && ( <Link to={`/initiatives/${msg.initiativeId}`} className={cn( "mt-2 block text-xs font-medium underline", msg.role === "user" ? "text-white/90" : "text-primary-700", )} onClick={() => setOpen(false)} > {msg.initiativeTitle ?? "Открыть инициативу"} → </Link> )} </div> ))} {loading && ( <div className="text-sm text-ink-500 animate-pulse">Ищу…</div> )} </div> <footer className="border-t border-ink-300/40 p-3 flex items-center gap-2"> <input ref={inputRef} type="text" value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} placeholder="Спросите про инициативу…" className="flex-1 rounded-lg border border-ink-300/60 px-3 py-2 text-sm" /> <button type="button" className={cn( "flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors", query.trim() && !loading ? "bg-primary-600 text-white hover:bg-primary-700" : "bg-surface-muted text-ink-400 cursor-not-allowed", )} onClick={handleSearch} disabled={loading || !query.trim()} aria-label="Отправить" > <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden > <path d="M22 2 11 13" /> <path d="M22 2 15 22 11 13 2 9 22 2" /> </svg> </button> </footer> </div> )} </> ); }