mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
added search bar
This commit is contained in:
parent
9da91bc6f3
commit
ac30af4d10
1 changed files with 211 additions and 38 deletions
|
|
@ -16,10 +16,12 @@ import {
|
|||
Pencil,
|
||||
Plug,
|
||||
LoaderIcon,
|
||||
Search,
|
||||
Settings,
|
||||
Square,
|
||||
SquarePen,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
|
|
@ -354,6 +356,36 @@ function SyncStatusBar() {
|
|||
)
|
||||
}
|
||||
|
||||
function filterTree(nodes: TreeNode[], query: string): TreeNode[] {
|
||||
const lowerQuery = query.toLowerCase()
|
||||
return nodes.reduce<TreeNode[]>((acc, node) => {
|
||||
if (node.kind === "dir") {
|
||||
const filteredChildren = filterTree(node.children || [], query)
|
||||
if (filteredChildren.length > 0) {
|
||||
acc.push({ ...node, children: filteredChildren })
|
||||
}
|
||||
} else if (node.name.toLowerCase().includes(lowerQuery)) {
|
||||
acc.push(node)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
function collectDirPaths(nodes: TreeNode[]): Set<string> {
|
||||
const paths = new Set<string>()
|
||||
for (const node of nodes) {
|
||||
if (node.kind === "dir") {
|
||||
paths.add(node.path)
|
||||
if (node.children) {
|
||||
for (const p of collectDirPaths(node.children)) {
|
||||
paths.add(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
export function SidebarContentPanel({
|
||||
tree,
|
||||
selectedPath,
|
||||
|
|
@ -370,6 +402,35 @@ export function SidebarContentPanel({
|
|||
...props
|
||||
}: SidebarContentPanelProps) {
|
||||
const { activeSection, setActiveSection } = useSidebarSection()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Auto-focus when search opens
|
||||
useEffect(() => {
|
||||
if (searchOpen) {
|
||||
// Small delay so the input is rendered before focusing
|
||||
requestAnimationFrame(() => searchInputRef.current?.focus())
|
||||
}
|
||||
}, [searchOpen])
|
||||
|
||||
// Clear query when search closes or section changes
|
||||
useEffect(() => {
|
||||
if (!searchOpen) setSearchQuery("")
|
||||
}, [searchOpen])
|
||||
useEffect(() => {
|
||||
setSearchQuery("")
|
||||
}, [activeSection])
|
||||
|
||||
// Compute filtered data
|
||||
const filteredRuns = searchQuery
|
||||
? runs.filter((r) => (r.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: runs
|
||||
const filteredBackgroundTasks = searchQuery
|
||||
? backgroundTasks.filter((t) => t.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: backgroundTasks
|
||||
const filteredTree = searchQuery ? filterTree(tree, searchQuery) : tree
|
||||
const searchExpandedPaths = searchQuery ? collectDirPaths(filteredTree) : expandedPaths
|
||||
|
||||
return (
|
||||
<Sidebar className="border-r-0" {...props}>
|
||||
|
|
@ -399,22 +460,32 @@ export function SidebarContentPanel({
|
|||
<SidebarContent>
|
||||
{activeSection === "knowledge" && (
|
||||
<KnowledgeSection
|
||||
tree={tree}
|
||||
tree={filteredTree}
|
||||
selectedPath={selectedPath}
|
||||
expandedPaths={expandedPaths}
|
||||
expandedPaths={searchExpandedPaths}
|
||||
onSelectFile={onSelectFile}
|
||||
actions={knowledgeActions}
|
||||
onVoiceNoteCreated={onVoiceNoteCreated}
|
||||
searchOpen={searchOpen}
|
||||
setSearchOpen={setSearchOpen}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
searchInputRef={searchInputRef}
|
||||
/>
|
||||
)}
|
||||
{activeSection === "tasks" && (
|
||||
<TasksSection
|
||||
runs={runs}
|
||||
runs={filteredRuns}
|
||||
currentRunId={currentRunId}
|
||||
processingRunIds={processingRunIds}
|
||||
actions={tasksActions}
|
||||
backgroundTasks={backgroundTasks}
|
||||
backgroundTasks={filteredBackgroundTasks}
|
||||
selectedBackgroundTask={selectedBackgroundTask}
|
||||
searchOpen={searchOpen}
|
||||
setSearchOpen={setSearchOpen}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
searchInputRef={searchInputRef}
|
||||
/>
|
||||
)}
|
||||
</SidebarContent>
|
||||
|
|
@ -691,6 +762,11 @@ function KnowledgeSection({
|
|||
onSelectFile,
|
||||
actions,
|
||||
onVoiceNoteCreated,
|
||||
searchOpen,
|
||||
setSearchOpen,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
searchInputRef,
|
||||
}: {
|
||||
tree: TreeNode[]
|
||||
selectedPath: string | null
|
||||
|
|
@ -698,6 +774,11 @@ function KnowledgeSection({
|
|||
onSelectFile: (path: string, kind: "file" | "dir") => void
|
||||
actions: KnowledgeActions
|
||||
onVoiceNoteCreated?: (path: string) => void
|
||||
searchOpen: boolean
|
||||
setSearchOpen: (open: boolean) => void
|
||||
searchQuery: string
|
||||
setSearchQuery: (query: string) => void
|
||||
searchInputRef: React.RefObject<HTMLInputElement | null>
|
||||
}) {
|
||||
const isExpanded = expandedPaths.size > 0
|
||||
|
||||
|
|
@ -711,38 +792,79 @@ function KnowledgeSection({
|
|||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-center gap-1 py-1 sticky top-0 z-10 bg-sidebar border-b border-sidebar-border">
|
||||
{quickActions.map((action) => (
|
||||
<Tooltip key={action.label}>
|
||||
<div className="sticky top-0 z-10 bg-sidebar border-b border-sidebar-border">
|
||||
<div className="flex items-center justify-center gap-1 py-1">
|
||||
{quickActions.map((action) => (
|
||||
<Tooltip key={action.label}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={action.action}
|
||||
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
||||
>
|
||||
<action.icon className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{action.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={action.action}
|
||||
onClick={isExpanded ? actions.collapseAll : actions.expandAll}
|
||||
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
||||
>
|
||||
<action.icon className="size-4" />
|
||||
{isExpanded ? (
|
||||
<ChevronsDownUp className="size-4" />
|
||||
) : (
|
||||
<ChevronsUpDown className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{action.label}</TooltipContent>
|
||||
<TooltipContent side="bottom">
|
||||
{isExpanded ? "Collapse All" : "Expand All"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={isExpanded ? actions.collapseAll : actions.expandAll}
|
||||
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronsDownUp className="size-4" />
|
||||
) : (
|
||||
<ChevronsUpDown className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{isExpanded ? "Collapse All" : "Expand All"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setSearchOpen(!searchOpen)}
|
||||
className={cn(
|
||||
"rounded p-1.5 transition-colors",
|
||||
searchOpen
|
||||
? "text-sidebar-foreground bg-sidebar-accent"
|
||||
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
)}
|
||||
>
|
||||
<Search className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Search</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{searchOpen && (
|
||||
<div className="flex items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-accent/30 mx-2 mb-1 px-2 py-1">
|
||||
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") setSearchOpen(false)
|
||||
}}
|
||||
placeholder="Search files..."
|
||||
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="rounded p-0.5 text-muted-foreground hover:text-sidebar-foreground transition-colors"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
||||
<SidebarMenu>
|
||||
|
|
@ -996,6 +1118,11 @@ function TasksSection({
|
|||
actions,
|
||||
backgroundTasks = [],
|
||||
selectedBackgroundTask,
|
||||
searchOpen,
|
||||
setSearchOpen,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
searchInputRef,
|
||||
}: {
|
||||
runs: RunListItem[]
|
||||
currentRunId?: string | null
|
||||
|
|
@ -1003,19 +1130,65 @@ function TasksSection({
|
|||
actions?: TasksActions
|
||||
backgroundTasks?: BackgroundTaskItem[]
|
||||
selectedBackgroundTask?: string | null
|
||||
searchOpen: boolean
|
||||
setSearchOpen: (open: boolean) => void
|
||||
searchQuery: string
|
||||
setSearchQuery: (query: string) => void
|
||||
searchInputRef: React.RefObject<HTMLInputElement | null>
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Sticky New Chat button - matches Knowledge section height */}
|
||||
{/* Sticky New Chat button + search */}
|
||||
<div className="sticky top-0 z-10 bg-sidebar border-b border-sidebar-border py-0.5">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={actions?.onNewChat} className="gap-2">
|
||||
<SquarePen className="size-4 shrink-0" />
|
||||
<span className="text-sm">New chat</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<div className="flex items-center gap-1 px-1">
|
||||
<SidebarMenu className="flex-1">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={actions?.onNewChat} className="gap-2">
|
||||
<SquarePen className="size-4 shrink-0" />
|
||||
<span className="text-sm">New chat</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setSearchOpen(!searchOpen)}
|
||||
className={cn(
|
||||
"rounded p-1.5 transition-colors shrink-0",
|
||||
searchOpen
|
||||
? "text-sidebar-foreground bg-sidebar-accent"
|
||||
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
)}
|
||||
>
|
||||
<Search className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Search</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{searchOpen && (
|
||||
<div className="flex items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-accent/30 mx-2 mb-1 mt-0.5 px-2 py-1">
|
||||
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") setSearchOpen(false)
|
||||
}}
|
||||
placeholder="Search chats..."
|
||||
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="rounded p-0.5 text-muted-foreground hover:text-sidebar-foreground transition-colors"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
||||
{/* Background Tasks Section */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue