11"use client" ;
22
33import { ChatInput } from "@llamaindex/chat-ui" ;
4- import { Brain , Check , FolderOpen , Zap } from "lucide-react" ;
4+ import { Brain , Check , FolderOpen , Minus , Plus , Zap } from "lucide-react" ;
55import { useParams } from "next/navigation" ;
66import React , { Suspense , useCallback , useState } from "react" ;
77import { DocumentsDataTable } from "@/components/chat/DocumentsDataTable" ;
@@ -15,13 +15,15 @@ import {
1515 DialogTitle ,
1616 DialogTrigger ,
1717} from "@/components/ui/dialog" ;
18+ import { Input } from "@/components/ui/input" ;
1819import {
1920 Select ,
2021 SelectContent ,
2122 SelectItem ,
2223 SelectTrigger ,
2324 SelectValue ,
2425} from "@/components/ui/select" ;
26+ import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from "@/components/ui/tooltip" ;
2527import { getConnectorIcon } from "@/contracts/enums/connectorIcons" ;
2628import { useDocumentTypes } from "@/hooks/use-document-types" ;
2729import type { Document } from "@/hooks/use-documents" ;
@@ -447,6 +449,119 @@ const SearchModeSelector = React.memo(
447449
448450SearchModeSelector . displayName = "SearchModeSelector" ;
449451
452+ const TopKSelector = React . memo (
453+ ( { topK = 10 , onTopKChange } : { topK ?: number ; onTopKChange ?: ( topK : number ) => void } ) => {
454+ const MIN_VALUE = 1 ;
455+ const MAX_VALUE = 100 ;
456+
457+ const handleIncrement = React . useCallback ( ( ) => {
458+ if ( topK < MAX_VALUE ) {
459+ onTopKChange ?.( topK + 1 ) ;
460+ }
461+ } , [ topK , onTopKChange ] ) ;
462+
463+ const handleDecrement = React . useCallback ( ( ) => {
464+ if ( topK > MIN_VALUE ) {
465+ onTopKChange ?.( topK - 1 ) ;
466+ }
467+ } , [ topK , onTopKChange ] ) ;
468+
469+ const handleInputChange = React . useCallback (
470+ ( e : React . ChangeEvent < HTMLInputElement > ) => {
471+ const value = e . target . value ;
472+ // Allow empty input for editing
473+ if ( value === "" ) {
474+ return ;
475+ }
476+ const numValue = parseInt ( value , 10 ) ;
477+ if ( ! isNaN ( numValue ) && numValue >= MIN_VALUE && numValue <= MAX_VALUE ) {
478+ onTopKChange ?.( numValue ) ;
479+ }
480+ } ,
481+ [ onTopKChange ]
482+ ) ;
483+
484+ const handleInputBlur = React . useCallback (
485+ ( e : React . FocusEvent < HTMLInputElement > ) => {
486+ const value = e . target . value ;
487+ if ( value === "" ) {
488+ // Reset to default if empty
489+ onTopKChange ?.( 10 ) ;
490+ return ;
491+ }
492+ const numValue = parseInt ( value , 10 ) ;
493+ if ( isNaN ( numValue ) || numValue < MIN_VALUE ) {
494+ onTopKChange ?.( MIN_VALUE ) ;
495+ } else if ( numValue > MAX_VALUE ) {
496+ onTopKChange ?.( MAX_VALUE ) ;
497+ }
498+ } ,
499+ [ onTopKChange ]
500+ ) ;
501+
502+ return (
503+ < TooltipProvider >
504+ < Tooltip delayDuration = { 200 } >
505+ < TooltipTrigger asChild >
506+ < div className = "flex items-center h-8 border rounded-md bg-background hover:bg-accent/50 transition-colors" >
507+ < Button
508+ type = "button"
509+ variant = "ghost"
510+ size = "icon"
511+ className = "h-full w-7 rounded-l-md rounded-r-none hover:bg-accent border-r"
512+ onClick = { handleDecrement }
513+ disabled = { topK <= MIN_VALUE }
514+ >
515+ < Minus className = "h-3.5 w-3.5" />
516+ </ Button >
517+ < div className = "flex flex-col items-center justify-center px-2 min-w-[60px]" >
518+ < Input
519+ type = "number"
520+ value = { topK }
521+ onChange = { handleInputChange }
522+ onBlur = { handleInputBlur }
523+ min = { MIN_VALUE }
524+ max = { MAX_VALUE }
525+ className = "h-5 w-full px-1 text-center text-sm font-semibold border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
526+ />
527+ < span className = "text-[10px] text-muted-foreground leading-none" > Results</ span >
528+ </ div >
529+ < Button
530+ type = "button"
531+ variant = "ghost"
532+ size = "icon"
533+ className = "h-full w-7 rounded-r-md rounded-l-none hover:bg-accent border-l"
534+ onClick = { handleIncrement }
535+ disabled = { topK >= MAX_VALUE }
536+ >
537+ < Plus className = "h-3.5 w-3.5" />
538+ </ Button >
539+ </ div >
540+ </ TooltipTrigger >
541+ < TooltipContent side = "top" className = "max-w-xs" >
542+ < div className = "space-y-2" >
543+ < p className = "text-sm font-semibold" > Results per Source</ p >
544+ < p className = "text-xs text-muted-foreground leading-relaxed" >
545+ Control how many results to fetch from each data source. Set a higher number to get
546+ more information, or a lower number for faster, more focused results.
547+ </ p >
548+ < div className = "flex items-center gap-2 text-xs text-muted-foreground pt-1 border-t" >
549+ < span > Recommended: 5-20</ span >
550+ < span > •</ span >
551+ < span >
552+ Range: { MIN_VALUE } -{ MAX_VALUE }
553+ </ span >
554+ </ div >
555+ </ div >
556+ </ TooltipContent >
557+ </ Tooltip >
558+ </ TooltipProvider >
559+ ) ;
560+ }
561+ ) ;
562+
563+ TopKSelector . displayName = "TopKSelector" ;
564+
450565const LLMSelector = React . memo ( ( ) => {
451566 const { search_space_id } = useParams ( ) ;
452567 const searchSpaceId = Number ( search_space_id ) ;
@@ -604,13 +719,17 @@ const CustomChatInputOptions = React.memo(
604719 selectedConnectors,
605720 searchMode,
606721 onSearchModeChange,
722+ topK,
723+ onTopKChange,
607724 } : {
608725 onDocumentSelectionChange ?: ( documents : Document [ ] ) => void ;
609726 selectedDocuments ?: Document [ ] ;
610727 onConnectorSelectionChange ?: ( connectorTypes : string [ ] ) => void ;
611728 selectedConnectors ?: string [ ] ;
612729 searchMode ?: "DOCUMENTS" | "CHUNKS" ;
613730 onSearchModeChange ?: ( mode : "DOCUMENTS" | "CHUNKS" ) => void ;
731+ topK ?: number ;
732+ onTopKChange ?: ( topK : number ) => void ;
614733 } ) => {
615734 // Memoize the loading fallback to prevent recreation
616735 const loadingFallback = React . useMemo (
@@ -637,6 +756,8 @@ const CustomChatInputOptions = React.memo(
637756 < div className = "h-4 w-px bg-border hidden sm:block" />
638757 < SearchModeSelector searchMode = { searchMode } onSearchModeChange = { onSearchModeChange } />
639758 < div className = "h-4 w-px bg-border hidden sm:block" />
759+ < TopKSelector topK = { topK } onTopKChange = { onTopKChange } />
760+ < div className = "h-4 w-px bg-border hidden sm:block" />
640761 < LLMSelector />
641762 </ div >
642763 ) ;
@@ -653,13 +774,17 @@ export const ChatInputUI = React.memo(
653774 selectedConnectors,
654775 searchMode,
655776 onSearchModeChange,
777+ topK,
778+ onTopKChange,
656779 } : {
657780 onDocumentSelectionChange ?: ( documents : Document [ ] ) => void ;
658781 selectedDocuments ?: Document [ ] ;
659782 onConnectorSelectionChange ?: ( connectorTypes : string [ ] ) => void ;
660783 selectedConnectors ?: string [ ] ;
661784 searchMode ?: "DOCUMENTS" | "CHUNKS" ;
662785 onSearchModeChange ?: ( mode : "DOCUMENTS" | "CHUNKS" ) => void ;
786+ topK ?: number ;
787+ onTopKChange ?: ( topK : number ) => void ;
663788 } ) => {
664789 return (
665790 < ChatInput >
@@ -674,6 +799,8 @@ export const ChatInputUI = React.memo(
674799 selectedConnectors = { selectedConnectors }
675800 searchMode = { searchMode }
676801 onSearchModeChange = { onSearchModeChange }
802+ topK = { topK }
803+ onTopKChange = { onTopKChange }
677804 />
678805 </ ChatInput >
679806 ) ;
0 commit comments