Skip to content

Commit 14cbafe

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

8 files changed

Lines changed: 140 additions & 10 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: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { useMemo, useCallback } from 'react';
1+
import { useMemo, useCallback, useState } from 'react';
22
import { EModelEndpoint, Constants } from 'librechat-data-provider';
3+
import type { TModelSpec } from 'librechat-data-provider';
34
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
4-
import { useGetAssistantDocsQuery, useGetEndpointsQuery } from '~/data-provider';
5+
import {
6+
useGetAssistantDocsQuery,
7+
useGetEndpointsQuery,
8+
useGetStartupConfig,
9+
} from '~/data-provider';
510
import { getIconEndpoint, getEntity } from '~/utils';
611
import { useSubmitMessage } from '~/hooks';
712

@@ -10,6 +15,8 @@ const ConversationStarters = () => {
1015
const agentsMap = useAgentsMapContext();
1116
const assistantMap = useAssistantsMapContext();
1217
const { data: endpointsConfig } = useGetEndpointsQuery();
18+
const { data: startupConfig } = useGetStartupConfig();
19+
const [showExamples, setShowExamples] = useState(false);
1320

1421
const endpointType = useMemo(() => {
1522
let ep = conversation?.endpoint ?? '';
@@ -41,7 +48,31 @@ const ConversationStarters = () => {
4148
assistant_id: conversation?.assistant_id,
4249
});
4350

51+
const currentSpec = useMemo(() => {
52+
const specs = startupConfig?.modelSpecs?.list ?? [];
53+
return specs.find(
54+
(spec: TModelSpec) =>
55+
spec.name === conversation?.spec || spec.preset?.agent_id === conversation?.agent_id,
56+
);
57+
}, [startupConfig?.modelSpecs?.list, conversation?.spec, conversation?.agent_id]);
58+
59+
const conversationStarterCategories = useMemo(() => {
60+
return (
61+
currentSpec?.conversationStarterCategories?.filter(
62+
(category) => category.label && category.starters?.length,
63+
) ?? []
64+
);
65+
}, [currentSpec?.conversationStarterCategories]);
66+
4467
const conversation_starters = useMemo(() => {
68+
if (conversationStarterCategories.length) {
69+
return [];
70+
}
71+
72+
if (currentSpec?.conversation_starters?.length) {
73+
return currentSpec.conversation_starters;
74+
}
75+
4576
if (entity?.conversation_starters?.length) {
4677
return entity.conversation_starters;
4778
}
@@ -51,18 +82,57 @@ const ConversationStarters = () => {
5182
}
5283

5384
return documentsMap.get(entity?.id ?? '')?.conversation_starters ?? [];
54-
}, [documentsMap, isAgent, entity]);
85+
}, [conversationStarterCategories.length, currentSpec, documentsMap, isAgent, entity]);
5586

5687
const { submitMessage } = useSubmitMessage();
5788
const sendConversationStarter = useCallback(
5889
(text: string) => submitMessage({ text }),
5990
[submitMessage],
6091
);
6192

62-
if (!conversation_starters.length) {
93+
if (!conversation_starters.length && !conversationStarterCategories.length) {
6394
return null;
6495
}
6596

97+
if (conversationStarterCategories.length) {
98+
return (
99+
<div className="mt-6 flex w-full max-w-5xl flex-col items-center px-4">
100+
<button
101+
type="button"
102+
onClick={() => setShowExamples((value) => !value)}
103+
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"
104+
aria-expanded={showExamples}
105+
>
106+
{showExamples ? 'Hide examples' : 'Show examples'}
107+
</button>
108+
{showExamples && (
109+
<div className="mt-5 grid w-full grid-cols-1 gap-4 md:grid-cols-3">
110+
{conversationStarterCategories.map((category) => (
111+
<section key={category.label} className="flex min-w-0 flex-col gap-3">
112+
<h2 className="text-center text-sm font-medium text-text-secondary">
113+
{category.label}
114+
</h2>
115+
<div className="flex flex-col gap-2">
116+
{category.starters.map((text: string, index: number) => (
117+
<button
118+
key={`${category.label}-${index}`}
119+
onClick={() => sendConversationStarter(text)}
120+
className="relative min-h-20 w-full cursor-pointer rounded-lg border border-border-medium px-3 py-3 text-left text-sm shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-surface-tertiary"
121+
>
122+
<span className="line-clamp-3 overflow-hidden break-words text-text-secondary">
123+
{text}
124+
</span>
125+
</button>
126+
))}
127+
</div>
128+
</section>
129+
))}
130+
</div>
131+
)}
132+
</div>
133+
);
134+
}
135+
66136
return (
67137
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
68138
{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: 23 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,16 @@ function ModelSelectorContent() {
7284
</button>
7385
);
7486

87+
const trigger = selectedSpecTooltip ? (
88+
<TooltipAnchor
89+
description={selectedSpecTooltip}
90+
side="bottom"
91+
render={triggerButton}
92+
/>
93+
) : (
94+
triggerButton
95+
);
96+
7597
return (
7698
<div className="relative flex w-full max-w-md flex-col items-center gap-2">
7799
<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) {

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,

packages/data-provider/src/models.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,21 @@ export type TModelSpec = {
2424
group?: string;
2525
showIconInMenu?: boolean;
2626
showIconInHeader?: boolean;
27+
showSwitchAgent?: boolean;
28+
conversationStarterCategories?: Array<{
29+
label: string;
30+
starters: string[];
31+
}>;
32+
conversation_starters?: string[];
2733
iconURL?: string | EModelEndpoint; // Allow using project-included icons
2834
authType?: AuthType;
2935
};
3036

37+
const tConversationStarterCategorySchema = z.object({
38+
label: z.string(),
39+
starters: z.array(z.string()),
40+
});
41+
3142
export const tModelSpecSchema = z.object({
3243
name: z.string(),
3344
label: z.string(),
@@ -38,6 +49,9 @@ export const tModelSpecSchema = z.object({
3849
group: z.string().optional(),
3950
showIconInMenu: z.boolean().optional(),
4051
showIconInHeader: z.boolean().optional(),
52+
showSwitchAgent: z.boolean().optional(),
53+
conversationStarterCategories: z.array(tConversationStarterCategorySchema).optional(),
54+
conversation_starters: z.array(z.string()).optional(),
4155
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
4256
authType: authTypeSchema.optional(),
4357
});

packages/data-schemas/src/app/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export async function loadDefaultInterface({
4848
prompts: interfaceConfig?.prompts,
4949
multiConvo: interfaceConfig?.multiConvo,
5050
agents: interfaceConfig?.agents,
51+
showSwitchAgent: interfaceConfig?.showSwitchAgent,
5152
temporaryChat: interfaceConfig?.temporaryChat,
5253
runCode: interfaceConfig?.runCode,
5354
webSearch: interfaceConfig?.webSearch,

0 commit comments

Comments
 (0)