Skip to content

Commit 7263e91

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

11 files changed

Lines changed: 295 additions & 27 deletions

File tree

client/src/components/Chat/ChatView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ChatFormValues } from '~/common';
99
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
1010
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
1111
import ConversationStarters from './Input/ConversationStarters';
12+
import LimitBadge from './Input/LimitBadge';
1213
import { useGetMessagesByConvoId } from '~/data-provider';
1314
import MessagesView from './Messages/MessagesView';
1415
import Presentation from './Presentation';
@@ -98,6 +99,7 @@ function ChatView({ index = 0 }: { index?: number }) {
9899
)}
99100
>
100101
<ChatForm index={index} />
102+
{isLandingPage && <LimitBadge />}
101103
{isLandingPage ? <ConversationStarters /> : <Footer />}
102104
</div>
103105
</div>

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
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { BarChart3, Timer } from 'lucide-react';
2+
import type { TModelSpec } from 'librechat-data-provider';
3+
import { useChatContext } from '~/Providers';
4+
import { useGetStartupConfig } from '~/data-provider';
5+
6+
export default function LimitBadge() {
7+
const { conversation } = useChatContext();
8+
const { data: startupConfig } = useGetStartupConfig();
9+
10+
const currentSpec = startupConfig?.modelSpecs?.list?.find(
11+
(spec: TModelSpec) =>
12+
spec.name === conversation?.spec || spec.preset?.agent_id === conversation?.agent_id,
13+
);
14+
const limitBadge = currentSpec?.limitBadge;
15+
16+
if (!limitBadge?.messages && !limitBadge?.tokens) {
17+
return null;
18+
}
19+
20+
return (
21+
<div className="mt-3 flex w-full justify-center px-4">
22+
<div
23+
className="inline-flex max-w-full items-center gap-3 rounded-full border border-border-light bg-surface-secondary px-4 py-2 text-sm font-medium text-text-secondary"
24+
aria-label={[limitBadge.messages, limitBadge.tokens].filter(Boolean).join(', ')}
25+
>
26+
{limitBadge.messages && (
27+
<span className="inline-flex min-w-0 items-center gap-1.5">
28+
<Timer className="h-4 w-4 shrink-0" aria-hidden="true" />
29+
<span className="truncate">{limitBadge.messages}</span>
30+
</span>
31+
)}
32+
{limitBadge.messages && limitBadge.tokens && (
33+
<span className="text-text-secondary" aria-hidden="true">
34+
&middot;
35+
</span>
36+
)}
37+
{limitBadge.tokens && (
38+
<span className="inline-flex min-w-0 items-center gap-1.5">
39+
<BarChart3 className="h-4 w-4 shrink-0" aria-hidden="true" />
40+
<span className="truncate">{limitBadge.tokens}</span>
41+
</span>
42+
)}
43+
</div>
44+
</div>
45+
);
46+
}

client/src/components/Chat/Landing.tsx

Lines changed: 38 additions & 19 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) {
@@ -298,30 +315,32 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
298315
);
299316
return (
300317
<>
301-
<div
302-
className="animate-fadeIn mt-4 max-w-md text-center text-sm font-normal text-text-primary"
303-
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cleanDesc) }}
304-
/>
305-
<div className="animate-fadeIn mt-4 flex flex-row items-center gap-3">
318+
<div className="animate-fadeIn mt-4 max-w-md text-left text-sm font-normal text-text-primary">
319+
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cleanDesc) }} />
306320
{learnMoreUrl && (
307-
<a
308-
href={learnMoreUrl}
309-
target="_blank"
310-
rel="noopener noreferrer"
311-
className="rounded-full bg-gray-600 px-4 py-2 text-sm font-medium text-gray-50 transition-colors duration-200 hover:bg-gray-700"
312-
>
313-
{learnMoreText || 'Learn more'}
314-
</a>
321+
<>
322+
{' '}
323+
<a
324+
href={learnMoreUrl}
325+
target="_blank"
326+
rel="noopener noreferrer"
327+
className="font-medium text-blue-500 underline-offset-2 hover:underline dark:text-blue-400"
328+
>
329+
{learnMoreText || 'Learn more'}
330+
</a>
331+
</>
315332
)}
316-
{otherSpec && (
333+
</div>
334+
{otherSpec && (
335+
<div className="animate-fadeIn mt-4 flex flex-row items-center gap-3">
317336
<button
318337
onClick={handleSwitchAgent}
319338
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"
320339
>
321340
{localize('com_ui_switch_agent')}
322341
</button>
323-
)}
324-
</div>
342+
</div>
343+
)}
325344
</>
326345
);
327346
})()}

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

0 commit comments

Comments
 (0)