Skip to content

Commit d5a3a99

Browse files
committed
Add unified cBioAgent UI config controls
1 parent 27ae9bb commit d5a3a99

9 files changed

Lines changed: 219 additions & 11 deletions

File tree

client/src/components/Chat/Header.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export default function Header() {
3434
});
3535

3636
const isSmallScreen = useMediaQuery('(max-width: 768px)');
37+
const showModelSelector = useMemo(() => {
38+
const modelSpecs = startupConfig?.modelSpecs?.list ?? [];
39+
return interfaceConfig.modelSelect === true || modelSpecs.length !== 1;
40+
}, [interfaceConfig.modelSelect, startupConfig?.modelSpecs?.list]);
3741

3842
return (
3943
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
@@ -56,7 +60,7 @@ export default function Header() {
5660
!isSmallScreen ? 'transition-all duration-200 ease-in-out' : ''
5761
} ${!navVisible ? 'translate-x-0' : 'translate-x-[-100px]'}`}
5862
>
59-
<ModelSelector startupConfig={startupConfig} />
63+
{showModelSelector && <ModelSelector startupConfig={startupConfig} />}
6064
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}
6165
{hasAccessToBookmarks === true && <BookmarkMenu />}
6266
{hasAccessToMultiConvo === true && <AddMultiConvo />}

client/src/components/Chat/Input/ConversationStarters.tsx

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
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';
24
import { EModelEndpoint, Constants } from 'librechat-data-provider';
5+
import type { TModelSpec } from 'librechat-data-provider';
36
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
4-
import { useGetAssistantDocsQuery, useGetEndpointsQuery } from '~/data-provider';
7+
import {
8+
useGetAssistantDocsQuery,
9+
useGetEndpointsQuery,
10+
useGetStartupConfig,
11+
} from '~/data-provider';
512
import { getIconEndpoint, getEntity } from '~/utils';
13+
import { cn } from '~/utils/';
614
import { 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+
825
const 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

client/src/components/Chat/Landing.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,23 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
7474
assistant_id: conversation?.assistant_id,
7575
});
7676

77-
const name = entity?.name ?? '';
78-
const description = (conversation?.greeting || entity?.description) ?? '';
77+
const currentSpec = useMemo(() => {
78+
const specs = startupConfig?.modelSpecs?.list ?? [];
79+
return specs.find(
80+
(s: TModelSpec) =>
81+
s.name === conversation?.spec || s.preset?.agent_id === conversation?.agent_id,
82+
);
83+
}, [startupConfig?.modelSpecs?.list, conversation?.spec, conversation?.agent_id]);
84+
85+
const specName = currentSpec?.label?.split(/\s+-\s+/)[0] ?? '';
86+
const name = entity?.name ?? specName;
87+
const description =
88+
(conversation?.greeting || entity?.description || currentSpec?.description) ?? '';
7989

8090
const otherSpec = useMemo(() => {
91+
if (startupConfig?.interface?.showSwitchAgent === false) {
92+
return undefined;
93+
}
8194
const specs = startupConfig?.modelSpecs?.list ?? [];
8295
const switchableSpecs = specs.filter(
8396
(s: TModelSpec) => s.showSwitchAgent && isAgentsEndpoint(s.preset?.endpoint),
@@ -91,7 +104,11 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
91104
);
92105
const nextIndex = (currentIndex + 1) % switchableSpecs.length;
93106
return switchableSpecs[nextIndex];
94-
}, [startupConfig?.modelSpecs?.list, conversation?.agent_id]);
107+
}, [
108+
startupConfig?.interface?.showSwitchAgent,
109+
startupConfig?.modelSpecs?.list,
110+
conversation?.agent_id,
111+
]);
95112

96113
const handleSwitchAgent = useCallback(() => {
97114
if (!otherSpec) {

client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useMemo } from 'react';
22
import type { ModelSelectorProps } from '~/common';
3+
import { TooltipAnchor } from '@librechat/client';
34
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
45
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
56
import {
@@ -57,8 +58,19 @@ function ModelSelectorContent() {
5758
}),
5859
[localize, agentsMap, modelSpecs, selectedValues, mappedEndpoints],
5960
);
61+
const selectedSpec = useMemo(
62+
() => modelSpecs.find((spec) => spec.name === selectedValues.modelSpec),
63+
[modelSpecs, selectedValues.modelSpec],
64+
);
65+
const selectedSpecTooltip = useMemo(() => {
66+
if (!selectedSpec?.label) {
67+
return '';
68+
}
69+
const [, description] = selectedSpec.label.split(/\s+-\s+(.+)/);
70+
return description || selectedSpec.label;
71+
}, [selectedSpec?.label]);
6072

61-
const trigger = (
73+
const triggerButton = (
6274
<button
6375
className="my-1 flex h-10 w-full max-w-[70vw] items-center justify-center gap-2 rounded-xl border border-border-light bg-surface-secondary px-3 py-2 text-sm text-text-primary hover:bg-surface-tertiary"
6476
aria-label={localize('com_ui_select_model')}
@@ -72,6 +84,12 @@ function ModelSelectorContent() {
7284
</button>
7385
);
7486

87+
const trigger = selectedSpecTooltip ? (
88+
<TooltipAnchor description={selectedSpecTooltip} side="bottom" render={triggerButton} />
89+
) : (
90+
triggerButton
91+
);
92+
7593
return (
7694
<div className="relative flex w-full max-w-md flex-col items-center gap-2">
7795
<Menu

client/src/components/Chat/Menus/Endpoints/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export const getDisplayValue = ({
177177
}) => {
178178
if (selectedValues.modelSpec) {
179179
const spec = modelSpecs.find((s) => s.name === selectedValues.modelSpec);
180-
return spec?.label || spec?.name || localize('com_ui_select_model');
180+
return spec?.label?.split(/\s+-\s+/)[0] || spec?.name || localize('com_ui_select_model');
181181
}
182182

183183
if (selectedValues.model && selectedValues.endpoint) {

client/src/hooks/Input/useTextarea.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
1414
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
1515
import useGetSender from '~/hooks/Conversations/useGetSender';
1616
import useFileHandling from '~/hooks/Files/useFileHandling';
17-
import { useInteractionHealthCheck } from '~/data-provider';
17+
import { useGetStartupConfig, useInteractionHealthCheck } from '~/data-provider';
1818
import { useChatContext } from '~/Providers/ChatContext';
1919
import { globalAudioId } from '~/common';
2020
import { useLocalize } from '~/hooks';
@@ -37,6 +37,7 @@ export default function useTextarea({
3737
const getSender = useGetSender();
3838
const isComposing = useRef(false);
3939
const agentsMap = useAgentsMapContext();
40+
const { data: startupConfig } = useGetStartupConfig();
4041
const { handleFiles } = useFileHandling();
4142
const assistantMap = useAssistantsMapContext();
4243
const checkHealth = useInteractionHealthCheck();
@@ -55,6 +56,11 @@ export default function useTextarea({
5556
assistant_id: conversation?.assistant_id,
5657
});
5758
const entityName = entity?.name ?? '';
59+
const modelSpecPlaceholder =
60+
startupConfig?.modelSpecs?.list?.find(
61+
(spec) =>
62+
spec.name === conversation?.spec || spec.preset?.agent_id === conversation?.agent_id,
63+
)?.placeholder ?? '';
5864

5965
const isNotAppendable =
6066
(((latestMessage?.unfinished ?? false) && !isSubmitting) || (latestMessage?.error ?? false)) &&
@@ -83,6 +89,9 @@ export default function useTextarea({
8389
const currentEndpoint = conversation?.endpoint ?? '';
8490
const currentAgentId = conversation?.agent_id ?? '';
8591
const currentAssistantId = conversation?.assistant_id ?? '';
92+
if (isAgent && modelSpecPlaceholder) {
93+
return modelSpecPlaceholder;
94+
}
8695
if (isAgent && (!currentAgentId || !agentsMap?.[currentAgentId])) {
8796
return localize('com_endpoint_agent_placeholder');
8897
} else if (
@@ -132,6 +141,7 @@ export default function useTextarea({
132141
getSender,
133142
agentsMap,
134143
entityName,
144+
modelSpecPlaceholder,
135145
textAreaRef,
136146
isAssistant,
137147
assistantMap,

packages/data-provider/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ export const interfaceSchema = z
542542
presets: z.boolean().optional(),
543543
prompts: z.boolean().optional(),
544544
agents: z.boolean().optional(),
545+
showSwitchAgent: z.boolean().optional(),
545546
temporaryChat: z.boolean().optional(),
546547
temporaryChatRetention: z.number().min(1).max(8760).optional(),
547548
runCode: z.boolean().optional(),
@@ -572,6 +573,7 @@ export const interfaceSchema = z
572573
memories: true,
573574
prompts: true,
574575
agents: true,
576+
showSwitchAgent: true,
575577
temporaryChat: true,
576578
runCode: true,
577579
webSearch: true,

0 commit comments

Comments
 (0)