/
alexefan136
/
flowstack
Обзор
Документация
Войти
/
alexefan136
/
flowstack
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
ui/src/components/knowledge/KnowledgeHeader.tsx
258 строк
8 KB
Alexander Efanov
upd fix
04 авг 2026, 14:09
04 авг 2026, 14:09
f63ab86
Код
Авторство
О чём код?
// src/components/knowledge/KnowledgeHeader.tsx import { AlertTriangle, BookOpen, Clock, Search, Sparkles, type LucideIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent, } from "react"; import { cn } from "../../lib/utils"; // ============================================================================ // Types // ============================================================================ export type KnowledgeTab = "browse" | "search" | "gaps" | "freshness"; interface KnowledgeHeaderProps { activeTab: KnowledgeTab; onTabChange: (tab: KnowledgeTab) => void; } interface TabConfig { id: KnowledgeTab; label: string; icon: LucideIcon; description: string; } interface TabPosition { left: number; width: number; } // ============================================================================ // Constants // ============================================================================ const TABS: TabConfig[] = [ { id: "browse", label: "Документы", icon: BookOpen, description: "Просмотр загруженных документов", }, { id: "search", label: "Поиск", icon: Search, description: "Семантический поиск по базе знаний", }, { id: "gaps", label: "Пробелы", icon: AlertTriangle, description: "Выявление недостающей информации", }, { id: "freshness", label: "Свежесть", icon: Clock, description: "Проверка актуальности данных", }, ]; const TABS_MAP: Map<KnowledgeTab, TabConfig> = new Map( TABS.map((tab) => [tab.id, tab]), ); const DEFAULT_DESCRIPTION = "RAG база знаний для вашего workspace"; // ============================================================================ // Component // ============================================================================ export function KnowledgeHeader({ activeTab, onTabChange, }: KnowledgeHeaderProps) { const tabsContainerRef = useRef<HTMLDivElement>(null); const [indicatorPosition, setIndicatorPosition] = useState<TabPosition | null>(null); // ======================================================================== // Memoized Computations // ======================================================================== const activeTabConfig = useMemo(() => TABS_MAP.get(activeTab), [activeTab]); const description = activeTabConfig?.description ?? DEFAULT_DESCRIPTION; // ======================================================================== // Update indicator position when activeTab changes // ======================================================================== useEffect(() => { if (!tabsContainerRef.current) return; const activeButton = tabsContainerRef.current.querySelector<HTMLButtonElement>( `[data-tab-id="${activeTab}"]`, ); if (activeButton) { const containerRect = tabsContainerRef.current.getBoundingClientRect(); const buttonRect = activeButton.getBoundingClientRect(); setIndicatorPosition({ left: buttonRect.left - containerRect.left, width: buttonRect.width, }); } }, [activeTab]); // ======================================================================== // Event Handlers // ======================================================================== const handleKeyDown = useCallback( (event: KeyboardEvent<HTMLButtonElement>, currentTabId: KnowledgeTab) => { const currentIndex = TABS.findIndex((t) => t.id === currentTabId); if (currentIndex === -1) return; let nextIndex: number; switch (event.key) { case "ArrowRight": case "ArrowDown": nextIndex = (currentIndex + 1) % TABS.length; break; case "ArrowLeft": case "ArrowUp": nextIndex = (currentIndex - 1 + TABS.length) % TABS.length; break; case "Home": nextIndex = 0; break; case "End": nextIndex = TABS.length - 1; break; default: return; } event.preventDefault(); const nextTab = TABS[nextIndex]; onTabChange(nextTab.id); const nextButton = document.querySelector<HTMLButtonElement>( `[data-tab-id="${nextTab.id}"]`, ); nextButton?.focus(); }, [onTabChange], ); // ======================================================================== // Render // ======================================================================== return ( <div className="mb-6 space-y-4"> {/* Header */} <div className="flex items-start justify-between gap-4"> <div className="flex items-start gap-3"> <div className="w-12 h-12 rounded-xl gradient-primary flex items-center justify-center shrink-0 shadow-lime"> <Sparkles className="w-6 h-6 text-text-on-accent" aria-hidden="true" /> </div> <div> <h1 className="text-2xl font-bold text-text-primary mb-1"> База знаний </h1> <p className="text-sm text-text-muted leading-relaxed"> {description} </p> </div> </div> </div> {/* Tabs */} <nav aria-label="Разделы базы знаний"> <div ref={tabsContainerRef} className="relative flex gap-1 glass rounded-xl p-1 overflow-x-auto scrollbar-thin" role="tablist" aria-orientation="horizontal" > {/* Sliding indicator */} {indicatorPosition && ( <div className="absolute top-1 bottom-1 bg-accent-lime-soft rounded-lg transition-all duration-300 ease-out" style={{ left: `${indicatorPosition.left}px`, width: `${indicatorPosition.width}px`, }} aria-hidden="true" > {/* Glow effect */} <div className="absolute inset-0 rounded-lg opacity-50 blur-sm -z-10" style={{ background: "linear-gradient(135deg, var(--accent-lime-soft), transparent)", }} /> {/* Bottom line indicator */} <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-8 h-0.5 bg-accent-lime rounded-full" /> </div> )} {TABS.map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; return ( <button key={tab.id} type="button" role="tab" data-tab-id={tab.id} aria-selected={isActive} aria-controls={`tabpanel-${tab.id}`} id={`tab-${tab.id}`} tabIndex={isActive ? 0 : -1} onClick={() => onTabChange(tab.id)} onKeyDown={(e) => handleKeyDown(e, tab.id)} className={cn( "relative flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg", "transition-all duration-200 whitespace-nowrap z-10", "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-lime/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary", isActive ? "text-accent-lime" : "text-text-muted hover:text-text-primary hover:bg-(--glass-bg-strong)", )} title={tab.description} > <Icon className="w-4 h-4 shrink-0" aria-hidden="true" /> <span>{tab.label}</span> </button> ); })} </div> </nav> </div> ); }