Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 111 additions & 9 deletions client/src/components/Chat/Landing.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { easings } from '@react-spring/web';
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
import type { TModelSpec, TConversation } from 'librechat-data-provider';
import { BirthdayIcon, TooltipAnchor, SplitText } from '@librechat/client';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useAuthContext } from '~/hooks';
import { getIconEndpoint, getEntity } from '~/utils';
import { useLocalize, useAuthContext, useDefaultConvo, useNewConvo } from '~/hooks';
import { getIconEndpoint, getEntity, getModelSpecIconURL, getConvoSwitchLogic } from '~/utils';
import store from '~/store';
import DOMPurify from 'dompurify';

const containerClassName =
Expand Down Expand Up @@ -36,6 +39,9 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
const { data: endpointsConfig } = useGetEndpointsQuery();
const { user } = useAuthContext();
const localize = useLocalize();
const { newConversation } = useNewConvo();
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);

const [textHasMultipleLines, setTextHasMultipleLines] = useState(false);
const [lineCount, setLineCount] = useState(1);
Expand Down Expand Up @@ -65,6 +71,74 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
const name = entity?.name ?? '';
const description = (conversation?.greeting || entity?.description) ?? '';

const modelSpecs = startupConfig?.modelSpecs?.list ?? [];

const otherSpec = useMemo(() => {
const switchableSpecs = modelSpecs.filter(
(s: TModelSpec) => s.showSwitchAgent && isAgentsEndpoint(s.preset?.endpoint),
);
if (switchableSpecs.length < 2) {
return undefined;
}
const currentAgentId = conversation?.agent_id;
return switchableSpecs.find((s: TModelSpec) => s.preset?.agent_id !== currentAgentId)
?? switchableSpecs[1];
}, [modelSpecs, conversation?.agent_id]);

const handleSwitchAgent = useCallback(() => {
if (!otherSpec) {
return;
}
const preset = { ...otherSpec.preset };
preset.iconURL = getModelSpecIconURL(otherSpec);
preset.spec = otherSpec.name;
const newEndpoint = preset.endpoint ?? '';
if (!newEndpoint) {
return;
}

const {
template,
shouldSwitch,
isNewModular,
newEndpointType,
isCurrentModular,
isExistingConversation,
} = getConvoSwitchLogic({
newEndpoint,
modularChat,
conversation,
endpointsConfig,
});

if (newEndpointType) {
preset.endpointType = newEndpointType;
}

const isModular = isCurrentModular && isNewModular && shouldSwitch;
if (isExistingConversation && isModular) {
template.endpointType = newEndpointType as EModelEndpoint | undefined;
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}), endpointType: template.endpointType },
preset: template,
cleanOutput: true,
});
newConversation({
template: currentConvo,
preset,
keepLatestMessage: true,
keepAddedConvos: true,
});
return;
}

newConversation({
template: { ...(template as Partial<TConversation>) },
preset,
keepAddedConvos: isModular,
});
}, [otherSpec, modularChat, conversation, endpointsConfig, getDefaultConversation, newConversation]);

const getGreeting = useCallback(() => {
if (typeof startupConfig?.interface?.customWelcome === 'string') {
const customWelcome = startupConfig.interface.customWelcome;
Expand Down Expand Up @@ -199,12 +273,40 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
/>
)}
</div>
{description && (
<div
className="animate-fadeIn mt-4 max-w-md text-center text-sm font-normal text-text-primary"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description || '') }}
/>
)}
{description && (() => {
const linkMatch = description.match(/<a\s+[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/i);
const learnMoreUrl = linkMatch ? linkMatch[1] : '';
const learnMoreText = linkMatch ? linkMatch[2] : '';
const cleanDesc = description.replace(/\s*(<br\s*\/?\s*>)*\s*<a\s+[^>]*>.*?<\/a>\s*$/i, '');
return (
<>
<div
className="animate-fadeIn mt-4 max-w-md text-center text-sm font-normal text-text-primary"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cleanDesc) }}
/>
<div className="animate-fadeIn mt-4 flex flex-row items-center gap-3">
{learnMoreUrl && (
<a
href={learnMoreUrl}
target="_blank"
rel="noopener noreferrer"
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"
>
{learnMoreText || 'Learn more'}
</a>
)}
{otherSpec && (
<button
onClick={handleSwitchAgent}
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"
>
{localize('com_ui_switch_agent')}
</button>
)}
</div>
</>
);
})()}
</div>
</div>
);
Expand Down
53 changes: 34 additions & 19 deletions client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { TooltipAnchor } from '@librechat/client';
import { getConfigDefaults, isAgentsEndpoint } from 'librechat-data-provider';
import type { TModelSpec } from 'librechat-data-provider';
import type { TModelSpec, TAgentsMap } from 'librechat-data-provider';
import type { ModelSelectorProps } from '~/common';
import {
renderModelSpecs,
Expand All @@ -22,37 +22,51 @@ function AgentButtonSelector({
specs,
selectedSpec,
endpointsConfig,
agentsMap,
onSelect,
}: {
specs: TModelSpec[];
selectedSpec: string | null;
endpointsConfig: any;
agentsMap: TAgentsMap | undefined;
onSelect: (spec: TModelSpec) => void;
}) {
const localize = useLocalize();
return (
<div className="relative inline-flex flex-row items-center gap-1.5">
<span className="text-sm text-text-secondary">{localize('com_ui_switch_agent')}</span>
{specs.map((spec) => {
const isSelected = selectedSpec === spec.name;
const description =
spec.description ||
agentsMap?.[spec.preset?.agent_id ?? '']?.description ||
spec.label ||
spec.name;
return (
<button
<TooltipAnchor
key={spec.name}
type="button"
onClick={() => onSelect(spec)}
className={cn(
'my-1 flex h-10 items-center gap-2 rounded-xl border px-3 py-2 text-sm transition-colors duration-200',
isSelected
? 'border-text-primary bg-surface-active-alt font-semibold text-text-primary'
: 'border-border-light bg-presentation text-text-secondary hover:bg-surface-active-alt hover:text-text-primary',
)}
aria-pressed={isSelected}
>
{(spec.showIconInHeader !== false) && (
<div className="flex flex-shrink-0 items-center justify-center overflow-hidden">
<SpecIcon currentSpec={spec} endpointsConfig={endpointsConfig} />
</div>
)}
<span className="truncate">{spec.name}</span>
</button>
description={description}
render={
<button
type="button"
onClick={() => onSelect(spec)}
className={cn(
'my-1 flex h-10 items-center gap-2 rounded-xl border px-3 py-2 text-sm transition-colors duration-200',
isSelected
? 'border-text-primary bg-surface-active-alt font-semibold text-text-primary'
: 'border-border-light bg-presentation text-text-secondary hover:bg-surface-active-alt hover:text-text-primary',
)}
aria-pressed={isSelected}
>
{(spec.showIconInHeader !== false) && (
<div className="flex flex-shrink-0 items-center justify-center overflow-hidden">
<SpecIcon currentSpec={spec} endpointsConfig={endpointsConfig} />
</div>
)}
<span className="truncate">{spec.name}</span>
</button>
}
/>
);
})}
</div>
Expand Down Expand Up @@ -96,6 +110,7 @@ function ModelSelectorContent() {
specs={modelSpecs}
selectedSpec={selectedValues.modelSpec}
endpointsConfig={endpointsConfig}
agentsMap={agentsMap}
onSelect={handleSelectSpec}
/>
);
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,7 @@
"com_ui_support_contact_name": "Name",
"com_ui_support_contact_name_min_length": "Name must be at least {{minLength}} characters",
"com_ui_support_contact_name_placeholder": "Support contact name",
"com_ui_switch_agent": "Switch Agent",
"com_ui_teach_or_explain": "Learning",
"com_ui_temporary": "Temporary Chat",
"com_ui_terms_and_conditions": "Terms and Conditions",
Expand Down
2 changes: 2 additions & 0 deletions packages/data-provider/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type TModelSpec = {
groupIcon?: string | EModelEndpoint;
showIconInMenu?: boolean;
showIconInHeader?: boolean;
showSwitchAgent?: boolean;
iconURL?: string | EModelEndpoint; // Allow using project-included icons
authType?: AuthType;
webSearch?: boolean;
Expand All @@ -50,6 +51,7 @@ export const tModelSpecSchema = z.object({
groupIcon: z.union([z.string(), eModelEndpointSchema]).optional(),
showIconInMenu: z.boolean().optional(),
showIconInHeader: z.boolean().optional(),
showSwitchAgent: z.boolean().optional(),
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
authType: authTypeSchema.optional(),
webSearch: z.boolean().optional(),
Expand Down
Loading