@@ -13,9 +13,9 @@ import { useLanguage } from '@/contexts/LanguageContext';
1313import { RepoInfo } from '@/types/repoinfo' ;
1414import Link from 'next/link' ;
1515import { useParams , useSearchParams } from 'next/navigation' ;
16- import React , { useCallback , useMemo , useRef , useState } from 'react' ;
16+ import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
1717import { motion } from 'framer-motion' ;
18- import { ArrowLeft , Book , BookOpen , MessageSquare , AlertTriangle , Home , Network , Search , RefreshCw , X } from 'lucide-react' ;
18+ import { ArrowLeft , Book , BookOpen , List , MessageSquare , AlertTriangle , Home , Network , Search , RefreshCw , X } from 'lucide-react' ;
1919import DependencyGraph from '@/components/DependencyGraph' ;
2020import WikiSidebarSkeleton from '@/components/skeletons/WikiSidebarSkeleton' ;
2121import WikiContentSkeleton from '@/components/skeletons/WikiContentSkeleton' ;
@@ -141,6 +141,9 @@ export default function RepoWikiPage() {
141141 const [ showGraph , setShowGraph ] = useState ( false ) ;
142142 const wikiContentRef = useRef < HTMLDivElement | null > ( null ) ;
143143
144+ // State for floating TOC (non-XL screens)
145+ const [ isFloatingTocOpen , setIsFloatingTocOpen ] = useState ( false ) ;
146+
144147 // State for Diagram Detail Panel (Click-to-Explain)
145148 const [ isDiagramPanelOpen , setIsDiagramPanelOpen ] = useState ( false ) ;
146149 const [ selectedDiagramNode , setSelectedDiagramNode ] = useState < {
@@ -322,9 +325,17 @@ export default function RepoWikiPage() {
322325 const handlePageSelect = ( pageId : string ) => {
323326 if ( currentPageId != pageId ) {
324327 setCurrentPageId ( pageId ) ;
328+ setIsFloatingTocOpen ( false ) ;
325329 }
326330 } ;
327331
332+ // Auto-select the first page when wiki loads without a selection
333+ useEffect ( ( ) => {
334+ if ( wikiStructure && ! currentPageId && wikiStructure . pages . length > 0 ) {
335+ setCurrentPageId ( wikiStructure . pages [ 0 ] . id ) ;
336+ }
337+ } , [ wikiStructure , currentPageId , setCurrentPageId ] ) ;
338+
328339 // Handler for diagram node clicks — opens the detail panel
329340 const handleDiagramNodeClick = useCallback ( ( nodeId : string , label : string , _rect : DOMRect , diagramData ?: import ( '@/types/diagramData' ) . DiagramData ) => {
330341 setSelectedDiagramNode ( { nodeId, label, diagramData : diagramData ?? null } ) ;
@@ -333,6 +344,39 @@ export default function RepoWikiPage() {
333344
334345 const [ isModelSelectionModalOpen , setIsModelSelectionModalOpen ] = useState ( false ) ;
335346
347+ // Onboarding tooltip state
348+ const [ onboardingStep , setOnboardingStep ] = useState < number | null > ( null ) ;
349+
350+ useEffect ( ( ) => {
351+ if ( ! wikiStructure || isLoading ) return ;
352+ try {
353+ if ( typeof window !== 'undefined' && ! localStorage . getItem ( 'bcw-onboarded' ) ) {
354+ const timer = setTimeout ( ( ) => setOnboardingStep ( 0 ) , 800 ) ;
355+ return ( ) => clearTimeout ( timer ) ;
356+ }
357+ } catch {
358+ // localStorage not available
359+ }
360+ } , [ wikiStructure , isLoading ] ) ;
361+
362+ useEffect ( ( ) => {
363+ if ( onboardingStep === null ) return ;
364+ const timer = setTimeout ( ( ) => {
365+ if ( onboardingStep < 2 ) {
366+ setOnboardingStep ( onboardingStep + 1 ) ;
367+ } else {
368+ setOnboardingStep ( null ) ;
369+ try { localStorage . setItem ( 'bcw-onboarded' , '1' ) ; } catch { /* noop */ }
370+ }
371+ } , 5000 ) ;
372+ return ( ) => clearTimeout ( timer ) ;
373+ } , [ onboardingStep ] ) ;
374+
375+ const dismissOnboarding = useCallback ( ( ) => {
376+ setOnboardingStep ( null ) ;
377+ try { localStorage . setItem ( 'bcw-onboarded' , '1' ) ; } catch { /* noop */ }
378+ } , [ ] ) ;
379+
336380 return (
337381 < div className = "flex-1 bg-background flex flex-col font-sans" >
338382 < style > { wikiStyles } </ style >
@@ -371,7 +415,7 @@ export default function RepoWikiPage() {
371415 < Search size = { 12 } className = "h-3 w-3" />
372416 < span className = "hidden sm:inline" > Search</ span >
373417 < kbd className = "hidden sm:inline-flex items-center gap-0.5 rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground ml-1" >
374- < span > ⌘ </ span > K
418+ { typeof navigator !== 'undefined' && / M a c | i P o d | i P h o n e | i P a d / . test ( navigator . platform ) ? '\u2318' : 'Ctrl+' } K
375419 </ kbd >
376420 </ button >
377421 { /* Graph button */ }
@@ -578,6 +622,25 @@ export default function RepoWikiPage() {
578622 </ motion . div >
579623 ) }
580624
625+ { /* Sidebar search trigger */ }
626+ < motion . div
627+ initial = { { opacity : 0 } }
628+ animate = { { opacity : 1 } }
629+ transition = { { duration : 0.3 , delay : 0.15 , ease : "easeOut" } }
630+ className = "px-4 pb-2"
631+ >
632+ < button
633+ onClick = { ( ) => setIsSearchOpen ( true ) }
634+ className = "flex items-center gap-2 w-full rounded-md border border-border bg-muted/40 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
635+ >
636+ < Search size = { 14 } className = "h-3.5 w-3.5 shrink-0" />
637+ < span className = "flex-1 text-left truncate" > Search pages...</ span >
638+ < kbd className = "hidden sm:inline-flex items-center gap-0.5 rounded border border-border bg-background px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shrink-0" >
639+ { typeof navigator !== 'undefined' && / M a c | i P o d | i P h o n e | i P a d / . test ( navigator . platform ) ? '\u2318' : 'Ctrl+' } K
640+ </ kbd >
641+ </ button >
642+ </ motion . div >
643+
581644 < motion . div
582645 initial = { { opacity : 0 } }
583646 animate = { { opacity : 1 } }
@@ -712,7 +775,7 @@ export default function RepoWikiPage() {
712775 ) }
713776 </ motion . article >
714777
715- { /* Floating Table of Contents */ }
778+ { /* Table of Contents -- full sidebar on XL screens */ }
716779 < aside className = "hidden xl:block w-56 shrink-0" >
717780 < div className = "sticky top-0 pt-2 max-h-[calc(100vh-12rem)] overflow-y-auto" >
718781 < TableOfContents
@@ -721,6 +784,46 @@ export default function RepoWikiPage() {
721784 />
722785 </ div >
723786 </ aside >
787+
788+ { /* Floating TOC button for non-XL screens */ }
789+ < div className = "xl:hidden fixed bottom-24 right-8 z-40" >
790+ < button
791+ onClick = { ( ) => setIsFloatingTocOpen ( prev => ! prev ) }
792+ className = { `h-10 w-10 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-105 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${
793+ isFloatingTocOpen
794+ ? 'bg-primary/90 text-primary-foreground ring-2 ring-primary ring-offset-2 ring-offset-background'
795+ : 'bg-card text-foreground border border-border hover:bg-accent'
796+ } `}
797+ aria-label = { isFloatingTocOpen ? 'Close table of contents' : 'Table of contents' }
798+ title = "Table of contents"
799+ >
800+ { isFloatingTocOpen ? < X size = { 16 } /> : < List size = { 16 } /> }
801+ </ button >
802+
803+ { /* TOC popover */ }
804+ { isFloatingTocOpen && (
805+ < motion . div
806+ initial = { { opacity : 0 , y : 8 , scale : 0.95 } }
807+ animate = { { opacity : 1 , y : 0 , scale : 1 } }
808+ exit = { { opacity : 0 , y : 8 , scale : 0.95 } }
809+ transition = { { duration : 0.15 } }
810+ className = "absolute bottom-12 right-0 w-64 max-h-80 overflow-y-auto rounded-xl border border-border bg-card shadow-lg p-4"
811+ >
812+ < TableOfContents
813+ content = { generatedPages [ currentPageId ] . content }
814+ scrollContainer = { wikiContentRef . current }
815+ />
816+ </ motion . div >
817+ ) }
818+ </ div >
819+ </ div >
820+ ) : currentPageId && pagesInProgress . has ( currentPageId ) ? (
821+ < div className = "flex flex-col items-center justify-center h-full text-muted-foreground" >
822+ < RefreshCw size = { 24 } className = "h-6 w-6 text-primary animate-spin mb-4" />
823+ < p className = "text-lg font-medium text-foreground" > Generating content...</ p >
824+ < p className = "text-sm" >
825+ { wikiStructure ?. pages . find ( p => p . id === currentPageId ) ?. title || 'This page' } is being generated
826+ </ p >
724827 </ div >
725828 ) : (
726829 < div className = "flex flex-col items-center justify-center h-full text-muted-foreground" >
@@ -879,6 +982,48 @@ export default function RepoWikiPage() {
879982 isOpen = { showGraph }
880983 onClose = { ( ) => setShowGraph ( false ) }
881984 />
985+
986+ { /* Onboarding tooltips */ }
987+ { onboardingStep !== null && (
988+ < div className = "fixed inset-0 z-[60] pointer-events-none" >
989+ { onboardingStep === 0 && (
990+ < motion . div
991+ initial = { { opacity : 0 , y : 8 } }
992+ animate = { { opacity : 1 , y : 0 } }
993+ className = "pointer-events-auto absolute top-32 left-4 lg:left-[calc(1rem)] max-w-[220px] bg-primary text-primary-foreground rounded-lg px-4 py-3 shadow-lg text-sm"
994+ >
995+ < p className = "font-medium mb-1" > Browse wiki pages</ p >
996+ < p className = "text-xs opacity-90" > Navigate sections and pages in the sidebar</ p >
997+ < button onClick = { dismissOnboarding } className = "mt-2 text-xs underline opacity-75 hover:opacity-100" > Dismiss</ button >
998+ < div className = "absolute -left-1.5 top-4 w-3 h-3 bg-primary rotate-45" />
999+ </ motion . div >
1000+ ) }
1001+ { onboardingStep === 1 && (
1002+ < motion . div
1003+ initial = { { opacity : 0 , y : 8 } }
1004+ animate = { { opacity : 1 , y : 0 } }
1005+ className = "pointer-events-auto absolute top-2 right-1/2 translate-x-1/2 max-w-[220px] bg-primary text-primary-foreground rounded-lg px-4 py-3 shadow-lg text-sm"
1006+ >
1007+ < p className = "font-medium mb-1" > Search pages</ p >
1008+ < p className = "text-xs opacity-90" > Press { typeof navigator !== 'undefined' && / M a c | i P o d | i P h o n e | i P a d / . test ( navigator . platform ) ? '\u2318' : 'Ctrl+' } K to quickly find any page</ p >
1009+ < button onClick = { dismissOnboarding } className = "mt-2 text-xs underline opacity-75 hover:opacity-100" > Dismiss</ button >
1010+ < div className = "absolute left-1/2 -translate-x-1/2 -bottom-1.5 w-3 h-3 bg-primary rotate-45" />
1011+ </ motion . div >
1012+ ) }
1013+ { onboardingStep === 2 && (
1014+ < motion . div
1015+ initial = { { opacity : 0 , y : 8 } }
1016+ animate = { { opacity : 1 , y : 0 } }
1017+ className = "pointer-events-auto absolute bottom-24 right-16 max-w-[220px] bg-primary text-primary-foreground rounded-lg px-4 py-3 shadow-lg text-sm"
1018+ >
1019+ < p className = "font-medium mb-1" > Ask AI anything</ p >
1020+ < p className = "text-xs opacity-90" > Click the chat button to ask questions about this codebase</ p >
1021+ < button onClick = { dismissOnboarding } className = "mt-2 text-xs underline opacity-75 hover:opacity-100" > Dismiss</ button >
1022+ < div className = "absolute -right-1.5 bottom-4 w-3 h-3 bg-primary rotate-45" />
1023+ </ motion . div >
1024+ ) }
1025+ </ div >
1026+ ) }
8821027 </ div >
8831028 ) ;
8841029}
0 commit comments