-
Notifications
You must be signed in to change notification settings - Fork 453
feat: [ENG-3022] Render query results as a list in LocalWebUI #754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: proj/byterover-tool-mode
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import {useNavigate} from 'react-router-dom' | ||
| import {toast} from 'sonner' | ||
|
|
||
| import {useGetContextNodes} from '../api/get-context-nodes' | ||
| import {stalePathMessage} from '../utils/topic-viewer-navigation' | ||
| import {findNodeByPath} from '../utils/tree-utils' | ||
|
|
||
| export function useNavigateToContextPath() { | ||
| const navigate = useNavigate() | ||
| const {data} = useGetContextNodes() | ||
| const nodes = data?.nodes ?? [] | ||
|
|
||
| return (path: string) => { | ||
| if (findNodeByPath(nodes, path)) { | ||
| navigate(`/contexts?path=${encodeURIComponent(path)}`) | ||
| } else { | ||
| toast.error(stalePathMessage(path)) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import {Badge} from '@campfirein/byterover-packages/components/badge' | ||
| import {Card} from '@campfirein/byterover-packages/components/card' | ||
| import {Tooltip, TooltipContent, TooltipTrigger} from '@campfirein/byterover-packages/components/tooltip' | ||
| import {cn} from '@campfirein/byterover-packages/lib/utils' | ||
| import {ChevronDown, ChevronUp, FileText} from 'lucide-react' | ||
| import {type ComponentRef, useLayoutEffect, useRef, useState} from 'react' | ||
|
|
||
| import type {QueryToolModeMatchedDoc} from '../utils/query-tool-mode-results' | ||
|
|
||
| import {useNavigateToContextPath} from '../../context/hooks/use-navigate-to-context-path' | ||
| import {MarkdownInline} from './markdown-inline' | ||
| import {SectionLabel, TerminalDot} from './task-detail-shared' | ||
|
|
||
| export function QueryResultsList({matchedDocs}: {matchedDocs: QueryToolModeMatchedDoc[]}) { | ||
| const label = `Result · ${matchedDocs.length} ${matchedDocs.length === 1 ? 'match' : 'matches'}` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick (non-blocking): <SectionLabel count={matchedDocs.length}>Result</SectionLabel>If the inline "match/matches" pluralization was intentional (for readability), feel free to ignore. |
||
| const openPath = useNavigateToContextPath() | ||
|
|
||
| return ( | ||
| <section className="relative pl-8"> | ||
| <TerminalDot tone="completed" /> | ||
| <SectionLabel>{label}</SectionLabel> | ||
| {matchedDocs.length === 0 ? ( | ||
| <p className="text-muted-foreground text-sm">No matching documents.</p> | ||
| ) : ( | ||
| <div className="flex flex-col gap-3"> | ||
| {matchedDocs.map((doc, index) => ( | ||
| <QueryResultRow doc={doc} key={`${doc.path}-${index}`} onOpenPath={openPath} /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </section> | ||
| ) | ||
| } | ||
|
|
||
| function QueryResultRow({doc, onOpenPath}: {doc: QueryToolModeMatchedDoc; onOpenPath: (path: string) => void}) { | ||
| const body = typeof doc.rendered_md === 'string' ? doc.rendered_md : undefined | ||
| const bodyRef = useRef<ComponentRef<'div'>>(null) | ||
| const [expanded, setExpanded] = useState(false) | ||
| const [overflowing, setOverflowing] = useState(false) | ||
|
|
||
| useLayoutEffect(() => { | ||
| const el = bodyRef.current | ||
| if (!el) return | ||
| setOverflowing(el.scrollHeight > el.clientHeight + 1) | ||
| }, [doc.rendered_md]) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (non-blocking): The "Show more/Show less" toggle is only computed when A |
||
|
|
||
| return ( | ||
| <Card className="ring-border bg-card flex flex-col gap-1.5 p-4" size="sm"> | ||
| <div className="flex items-start justify-between gap-3"> | ||
| <div className="flex min-w-0 items-center gap-2"> | ||
| <FileText className="text-muted-foreground size-3.5 shrink-0" /> | ||
| <span className="text-foreground/90 truncate text-sm font-medium">{doc.title}</span> | ||
| </div> | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={ | ||
| <Badge className="mono text-emerald-400 shrink-0" variant="outline"> | ||
| {doc.score.toFixed(2)} | ||
| </Badge> | ||
| } | ||
| /> | ||
| <TooltipContent>Match score</TooltipContent> | ||
| </Tooltip> | ||
| </div> | ||
| <button | ||
| className="text-muted-foreground hover:text-foreground mono w-fit cursor-pointer pl-5 text-left text-[11px] break-all hover:underline" | ||
| onClick={() => onOpenPath(doc.path)} | ||
| type="button" | ||
| > | ||
| {doc.path} | ||
| </button> | ||
| {body && ( | ||
| <div className="pl-5"> | ||
| <div className={cn('overflow-hidden', {'max-h-36': !expanded})} ref={bodyRef}> | ||
| <MarkdownInline className="text-foreground/70 text-xs">{body}</MarkdownInline> | ||
| </div> | ||
| {overflowing && ( | ||
| <button | ||
| className="text-muted-foreground hover:text-foreground mt-1.5 inline-flex cursor-pointer items-center gap-1 text-[11px]" | ||
| onClick={() => setExpanded((value) => !value)} | ||
| type="button" | ||
| > | ||
| {expanded ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />} | ||
| {expanded ? 'Show less' : 'Show more'} | ||
| </button> | ||
| )} | ||
| </div> | ||
| )} | ||
| </Card> | ||
| ) | ||
| } | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): This hook always queries the default branch (no
brancharg touseGetContextNodes()) and navigates to/contexts?path=…without abranchquery param. Two side-effects worth thinking about:findNodeByPath(nodes, path)) runs against the default-branch tree, so a result whose path exists only on the user's currently-selected branch will toast as "stale" even though clicking through would have shown valid content (or vice versa)./contexts?path=…is opened withoutbranch.For the local-only
query-tool-modeuse case today this is fine, but mirroring the branch-resolution thatuseContextTree/useTopicViewerNavigationuse would make the behavior consistent. Could readsearchParams.get('branch')(or read it from a shared store) and forward it to both the fetch and the navigation URL.