1- import { useMemo , useCallback } from 'react' ;
1+ import { useMemo , useCallback , useState } from 'react' ;
2+ import { ChevronDown , HeartPulse , MapPin , Search } from 'lucide-react' ;
3+ import type { LucideIcon } from 'lucide-react' ;
24import { EModelEndpoint , Constants } from 'librechat-data-provider' ;
5+ import type { TModelSpec } from 'librechat-data-provider' ;
36import { useChatContext , useAgentsMapContext , useAssistantsMapContext } from '~/Providers' ;
4- import { useGetAssistantDocsQuery , useGetEndpointsQuery } from '~/data-provider' ;
7+ import {
8+ useGetAssistantDocsQuery ,
9+ useGetEndpointsQuery ,
10+ useGetStartupConfig ,
11+ } from '~/data-provider' ;
512import { getIconEndpoint , getEntity } from '~/utils' ;
13+ import { cn } from '~/utils/' ;
614import { useSubmitMessage } from '~/hooks' ;
715
16+ const categoryIcons : Record < string , LucideIcon > = {
17+ analyze : HeartPulse ,
18+ heartbeat : HeartPulse ,
19+ map : MapPin ,
20+ navigate : MapPin ,
21+ search : Search ,
22+ explore : Search ,
23+ } ;
24+
825const ConversationStarters = ( ) => {
926 const { conversation } = useChatContext ( ) ;
1027 const agentsMap = useAgentsMapContext ( ) ;
1128 const assistantMap = useAssistantsMapContext ( ) ;
1229 const { data : endpointsConfig } = useGetEndpointsQuery ( ) ;
30+ const { data : startupConfig } = useGetStartupConfig ( ) ;
31+ const [ showExamples , setShowExamples ] = useState ( false ) ;
32+ const [ expandedCategories , setExpandedCategories ] = useState < Set < string > > ( new Set ( ) ) ;
1333
1434 const endpointType = useMemo ( ( ) => {
1535 let ep = conversation ?. endpoint ?? '' ;
@@ -41,7 +61,31 @@ const ConversationStarters = () => {
4161 assistant_id : conversation ?. assistant_id ,
4262 } ) ;
4363
64+ const currentSpec = useMemo ( ( ) => {
65+ const specs = startupConfig ?. modelSpecs ?. list ?? [ ] ;
66+ return specs . find (
67+ ( spec : TModelSpec ) =>
68+ spec . name === conversation ?. spec || spec . preset ?. agent_id === conversation ?. agent_id ,
69+ ) ;
70+ } , [ startupConfig ?. modelSpecs ?. list , conversation ?. spec , conversation ?. agent_id ] ) ;
71+
72+ const conversationStarterCategories = useMemo ( ( ) => {
73+ return (
74+ currentSpec ?. conversationStarterCategories ?. filter (
75+ ( category ) => category . label && category . starters ?. length ,
76+ ) ?? [ ]
77+ ) ;
78+ } , [ currentSpec ?. conversationStarterCategories ] ) ;
79+
4480 const conversation_starters = useMemo ( ( ) => {
81+ if ( conversationStarterCategories . length ) {
82+ return [ ] ;
83+ }
84+
85+ if ( currentSpec ?. conversation_starters ?. length ) {
86+ return currentSpec . conversation_starters ;
87+ }
88+
4589 if ( entity ?. conversation_starters ?. length ) {
4690 return entity . conversation_starters ;
4791 }
@@ -51,18 +95,110 @@ const ConversationStarters = () => {
5195 }
5296
5397 return documentsMap . get ( entity ?. id ?? '' ) ?. conversation_starters ?? [ ] ;
54- } , [ documentsMap , isAgent , entity ] ) ;
98+ } , [ conversationStarterCategories . length , currentSpec , documentsMap , isAgent , entity ] ) ;
5599
56100 const { submitMessage } = useSubmitMessage ( ) ;
57101 const sendConversationStarter = useCallback (
58102 ( text : string ) => submitMessage ( { text } ) ,
59103 [ submitMessage ] ,
60104 ) ;
105+ const toggleCategory = useCallback ( ( label : string ) => {
106+ setExpandedCategories ( ( current ) => {
107+ const next = new Set ( current ) ;
108+ if ( next . has ( label ) ) {
109+ next . delete ( label ) ;
110+ } else {
111+ next . add ( label ) ;
112+ }
113+ return next ;
114+ } ) ;
115+ } , [ ] ) ;
61116
62- if ( ! conversation_starters . length ) {
117+ if ( ! conversation_starters . length && ! conversationStarterCategories . length ) {
63118 return null ;
64119 }
65120
121+ if ( conversationStarterCategories . length ) {
122+ return (
123+ < div className = "mt-6 flex w-full flex-col items-center px-4" >
124+ < button
125+ type = "button"
126+ onClick = { ( ) => setShowExamples ( ( value ) => ! value ) }
127+ className = "rounded-full border border-border-light bg-surface-secondary px-4 py-2 text-sm font-medium text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
128+ aria-expanded = { showExamples }
129+ >
130+ { showExamples ? 'Hide examples' : 'Show examples' }
131+ </ button >
132+ { showExamples && (
133+ < div className = "mt-5 flex w-full flex-col gap-3 sm:w-11/12 lg:w-4/5 xl:w-2/3" >
134+ { conversationStarterCategories . map ( ( category ) => {
135+ const isExpanded = expandedCategories . has ( category . label ) ;
136+ const Icon =
137+ categoryIcons [ category . icon ?. toLowerCase ( ) ?? '' ] ??
138+ categoryIcons [ category . label . split ( ' ' ) [ 0 ] ?. toLowerCase ( ) ?? '' ] ??
139+ Search ;
140+
141+ return (
142+ < section
143+ key = { category . label }
144+ className = { cn (
145+ 'min-w-0 rounded-lg border border-border-medium bg-surface-primary shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-200' ,
146+ isExpanded && 'bg-surface-secondary' ,
147+ ) }
148+ >
149+ < button
150+ type = "button"
151+ onClick = { ( ) => toggleCategory ( category . label ) }
152+ className = "flex min-h-16 w-full items-center justify-between gap-3 rounded-lg px-4 py-4 text-left transition-colors duration-200 hover:bg-surface-tertiary"
153+ aria-expanded = { isExpanded }
154+ >
155+ < span className = "flex min-w-0 items-center gap-3" >
156+ < span className = "flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border-light bg-surface-secondary" >
157+ < Icon className = "h-4 w-4 text-text-secondary" aria-hidden = "true" />
158+ </ span >
159+ < span className = "flex min-w-0 flex-col gap-1" >
160+ < span className = "text-sm font-semibold text-text-primary" >
161+ { category . label }
162+ </ span >
163+ { category . description && (
164+ < span className = "text-xs leading-5 text-text-secondary" >
165+ { category . description }
166+ </ span >
167+ ) }
168+ </ span >
169+ </ span >
170+ < ChevronDown
171+ className = { cn (
172+ 'h-4 w-4 shrink-0 text-text-secondary transition-transform duration-200' ,
173+ isExpanded && 'rotate-180' ,
174+ ) }
175+ aria-hidden = "true"
176+ />
177+ </ button >
178+ { isExpanded && (
179+ < div className = "scrollbar-thin flex max-h-72 flex-col gap-2 overflow-y-auto border-t border-border-light px-3 py-3 fade-in" >
180+ { category . starters . map ( ( text : string , index : number ) => (
181+ < button
182+ key = { `${ category . label } -${ index } ` }
183+ onClick = { ( ) => sendConversationStarter ( text ) }
184+ className = "relative min-h-16 w-full cursor-pointer rounded-md border border-border-light bg-transparent px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-surface-tertiary"
185+ >
186+ < span className = "line-clamp-3 overflow-hidden break-words text-text-secondary" >
187+ { text }
188+ </ span >
189+ </ button >
190+ ) ) }
191+ </ div >
192+ ) }
193+ </ section >
194+ ) ;
195+ } ) }
196+ </ div >
197+ ) }
198+ </ div >
199+ ) ;
200+ }
201+
66202 return (
67203 < div className = "mt-8 flex flex-wrap justify-center gap-3 px-4" >
68204 { conversation_starters
0 commit comments