11'use client' ;
22
33import { useCallback , useMemo , useState , type FormEvent } from 'react' ;
4- import { Check , ChevronDownIcon , Plus , Sparkles , Trash2 } from 'lucide-react' ;
4+ import { Check , ChevronDownIcon , Plus , Search , Sparkles , Trash2 } from 'lucide-react' ;
55import { useTranslations } from 'next-intl' ;
66import * as AccordionPrimitive from '@radix-ui/react-accordion' ;
77import { useModelChannelList , type LLMChannel } from '@/api/endpoints/model' ;
@@ -43,8 +43,10 @@ function ModelPickerSection({
4343 autoAddDisabled : boolean ;
4444} ) {
4545 const t = useTranslations ( 'group' ) ;
46+ const [ searchKeyword , setSearchKeyword ] = useState ( '' ) ;
4647
4748 const selectedKeys = useMemo ( ( ) => new Set ( selectedMembers . map ( memberKey ) ) , [ selectedMembers ] ) ;
49+ const normalizedSearch = searchKeyword . trim ( ) . toLowerCase ( ) ;
4850
4951 const channels = useMemo ( ( ) => {
5052 const byId = new Map < number , { id : number ; name : string ; models : LLMChannel [ ] } > ( ) ;
@@ -59,21 +61,42 @@ function ModelPickerSection({
5961 . sort ( ( a , b ) => a . id - b . id ) ;
6062 } , [ modelChannels ] ) ;
6163
64+ const filteredChannels = useMemo ( ( ) => {
65+ if ( ! normalizedSearch ) return channels ;
66+ return channels . reduce < typeof channels > ( ( acc , channel ) => {
67+ if ( channel . name . toLowerCase ( ) . includes ( normalizedSearch ) ) {
68+ acc . push ( channel ) ;
69+ return acc ;
70+ }
71+
72+ const models = channel . models . filter ( ( model ) => model . name . toLowerCase ( ) . includes ( normalizedSearch ) ) ;
73+ if ( models . length > 0 ) acc . push ( { ...channel , models } ) ;
74+ return acc ;
75+ } , [ ] ) ;
76+ } , [ channels , normalizedSearch ] ) ;
77+
6278 return (
6379 < div className = "rounded-xl border border-border/50 bg-muted/30 flex flex-col min-h-0" >
64- < div className = "flex items-center justify-between px-3 py-2 border-b border-border/30 bg-muted/50" >
65- < span className = "text-sm font-medium text-foreground" >
80+ < div className = "grid grid-cols-[1fr_auto_1fr] items-center gap-2 px-3 py-2 border-b border-border/30 bg-muted/50" >
81+ < span className = "min-w-0 justify-self-start text-sm font-medium text-foreground" >
6682 { t ( 'form.addItem' ) }
67- < span className = "ml-1.5 text-xs text-muted-foreground font-normal" >
68- ({ selectedMembers . length } )
69- </ span >
7083 </ span >
7184
85+ < div className = "relative justify-self-center w-30" >
86+ < Search className = "pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
87+ < Input
88+ value = { searchKeyword }
89+ onChange = { ( event ) => setSearchKeyword ( event . target . value ) }
90+ className = "h-6 rounded-lg border-border/60 bg-background/70 pl-7 pr-2 text-xs shadow-none focus-visible:border-border/60 focus-visible:ring-0"
91+ aria-label = "search"
92+ />
93+ </ div >
94+
7295 < button
7396 type = "button"
7497 onClick = { onAutoAdd }
7598 className = { cn (
76- 'flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium transition-colors' ,
99+ 'justify-self-end shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium transition-colors' ,
77100 autoAddDisabled
78101 ? 'text-muted-foreground/50 cursor-not-allowed'
79102 : 'hover:bg-muted text-muted-foreground hover:text-foreground'
@@ -88,7 +111,7 @@ function ModelPickerSection({
88111
89112 < div className = "flex-1 min-h-0 overflow-y-auto p-2" >
90113 < Accordion type = "multiple" className = "w-full space-y-2" >
91- { channels . map ( ( channel ) => {
114+ { filteredChannels . map ( ( channel ) => {
92115 const total = channel . models . length ;
93116 const selectedCount = channel . models . reduce (
94117 ( acc , m ) => acc + ( selectedKeys . has ( memberKey ( m ) ) ? 1 : 0 ) ,
@@ -480,5 +503,3 @@ export function GroupEditor({
480503 </ form >
481504 ) ;
482505}
483-
484-
0 commit comments