Skip to content
Open
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
2 changes: 2 additions & 0 deletions client/src/components/Chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ChatFormValues } from '~/common';
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
import ConversationStarters from './Input/ConversationStarters';
import LimitBadge from './Input/LimitBadge';
import { useGetMessagesByConvoId } from '~/data-provider';
import MessagesView from './Messages/MessagesView';
import Presentation from './Presentation';
Expand Down Expand Up @@ -98,6 +99,7 @@ function ChatView({ index = 0 }: { index?: number }) {
)}
>
<ChatForm index={index} />
{isLandingPage && <LimitBadge />}
{isLandingPage ? <ConversationStarters /> : <Footer />}
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/Chat/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default function Header() {
});

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

return (
<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">
Expand All @@ -56,7 +60,7 @@ export default function Header() {
!isSmallScreen ? 'transition-all duration-200 ease-in-out' : ''
} ${!navVisible ? 'translate-x-0' : 'translate-x-[-100px]'}`}
>
<ModelSelector startupConfig={startupConfig} />
{showModelSelector && <ModelSelector startupConfig={startupConfig} />}
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}
{hasAccessToBookmarks === true && <BookmarkMenu />}
{hasAccessToMultiConvo === true && <AddMultiConvo />}
Expand Down
144 changes: 140 additions & 4 deletions client/src/components/Chat/Input/ConversationStarters.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import { useMemo, useCallback } from 'react';
import { useMemo, useCallback, useState } from 'react';
import { ChevronDown, HeartPulse, MapPin, Search } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { EModelEndpoint, Constants } from 'librechat-data-provider';
import type { TModelSpec } from 'librechat-data-provider';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery, useGetEndpointsQuery } from '~/data-provider';
import {
useGetAssistantDocsQuery,
useGetEndpointsQuery,
useGetStartupConfig,
} from '~/data-provider';
import { getIconEndpoint, getEntity } from '~/utils';
import { cn } from '~/utils/';
import { useSubmitMessage } from '~/hooks';

const categoryIcons: Record<string, LucideIcon> = {
analyze: HeartPulse,
heartbeat: HeartPulse,
map: MapPin,
navigate: MapPin,
search: Search,
explore: Search,
};

const ConversationStarters = () => {
const { conversation } = useChatContext();
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { data: endpointsConfig } = useGetEndpointsQuery();
const { data: startupConfig } = useGetStartupConfig();
const [showExamples, setShowExamples] = useState(false);
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());

const endpointType = useMemo(() => {
let ep = conversation?.endpoint ?? '';
Expand Down Expand Up @@ -41,7 +61,31 @@ const ConversationStarters = () => {
assistant_id: conversation?.assistant_id,
});

const currentSpec = useMemo(() => {
const specs = startupConfig?.modelSpecs?.list ?? [];
return specs.find(
(spec: TModelSpec) =>
spec.name === conversation?.spec || spec.preset?.agent_id === conversation?.agent_id,
);
}, [startupConfig?.modelSpecs?.list, conversation?.spec, conversation?.agent_id]);

const conversationStarterCategories = useMemo(() => {
return (
currentSpec?.conversationStarterCategories?.filter(
(category) => category.label && category.starters?.length,
) ?? []
);
}, [currentSpec?.conversationStarterCategories]);

const conversation_starters = useMemo(() => {
if (conversationStarterCategories.length) {
return [];
}

if (currentSpec?.conversation_starters?.length) {
return currentSpec.conversation_starters;
}

if (entity?.conversation_starters?.length) {
return entity.conversation_starters;
}
Expand All @@ -51,18 +95,110 @@ const ConversationStarters = () => {
}

return documentsMap.get(entity?.id ?? '')?.conversation_starters ?? [];
}, [documentsMap, isAgent, entity]);
}, [conversationStarterCategories.length, currentSpec, documentsMap, isAgent, entity]);

const { submitMessage } = useSubmitMessage();
const sendConversationStarter = useCallback(
(text: string) => submitMessage({ text }),
[submitMessage],
);
const toggleCategory = useCallback((label: string) => {
setExpandedCategories((current) => {
const next = new Set(current);
if (next.has(label)) {
next.delete(label);
} else {
next.add(label);
}
return next;
});
}, []);

if (!conversation_starters.length) {
if (!conversation_starters.length && !conversationStarterCategories.length) {
return null;
}

if (conversationStarterCategories.length) {
return (
<div className="mt-6 flex w-full flex-col items-center px-4">
<button
type="button"
onClick={() => setShowExamples((value) => !value)}
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"
aria-expanded={showExamples}
>
{showExamples ? 'Hide examples' : 'Show examples'}
</button>
{showExamples && (
<div className="mt-5 flex w-full flex-col gap-3 sm:w-11/12 lg:w-4/5 xl:w-2/3">
{conversationStarterCategories.map((category) => {
const isExpanded = expandedCategories.has(category.label);
const Icon =
categoryIcons[category.icon?.toLowerCase() ?? ''] ??
categoryIcons[category.label.split(' ')[0]?.toLowerCase() ?? ''] ??
Search;

return (
<section
key={category.label}
className={cn(
'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',
isExpanded && 'bg-surface-secondary',
)}
>
<button
type="button"
onClick={() => toggleCategory(category.label)}
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"
aria-expanded={isExpanded}
>
<span className="flex min-w-0 items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border-light bg-surface-secondary">
<Icon className="h-4 w-4 text-text-secondary" aria-hidden="true" />
</span>
<span className="flex min-w-0 flex-col gap-1">
<span className="text-sm font-semibold text-text-primary">
{category.label}
</span>
{category.description && (
<span className="text-xs leading-5 text-text-secondary">
{category.description}
</span>
)}
</span>
</span>
<ChevronDown
className={cn(
'h-4 w-4 shrink-0 text-text-secondary transition-transform duration-200',
isExpanded && 'rotate-180',
)}
aria-hidden="true"
/>
</button>
{isExpanded && (
<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">
{category.starters.map((text: string, index: number) => (
<button
key={`${category.label}-${index}`}
onClick={() => sendConversationStarter(text)}
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"
>
<span className="line-clamp-3 overflow-hidden break-words text-text-secondary">
{text}
</span>
</button>
))}
</div>
)}
</section>
);
})}
</div>
)}
</div>
);
}

return (
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
{conversation_starters
Expand Down
46 changes: 46 additions & 0 deletions client/src/components/Chat/Input/LimitBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { BarChart3, Timer } from 'lucide-react';
import type { TModelSpec } from 'librechat-data-provider';
import { useChatContext } from '~/Providers';
import { useGetStartupConfig } from '~/data-provider';

export default function LimitBadge() {
const { conversation } = useChatContext();
const { data: startupConfig } = useGetStartupConfig();

const currentSpec = startupConfig?.modelSpecs?.list?.find(
(spec: TModelSpec) =>
spec.name === conversation?.spec || spec.preset?.agent_id === conversation?.agent_id,
);
const limitBadge = currentSpec?.limitBadge;

if (!limitBadge?.messages && !limitBadge?.tokens) {
return null;
}

return (
<div className="mt-3 flex w-full justify-center px-4">
<div
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"
aria-label={[limitBadge.messages, limitBadge.tokens].filter(Boolean).join(', ')}
>
{limitBadge.messages && (
<span className="inline-flex min-w-0 items-center gap-1.5">
<Timer className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">{limitBadge.messages}</span>
</span>
)}
{limitBadge.messages && limitBadge.tokens && (
<span className="text-text-secondary" aria-hidden="true">
&middot;
</span>
)}
{limitBadge.tokens && (
<span className="inline-flex min-w-0 items-center gap-1.5">
<BarChart3 className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">{limitBadge.tokens}</span>
</span>
)}
</div>
</div>
);
}
57 changes: 38 additions & 19 deletions client/src/components/Chat/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,23 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
assistant_id: conversation?.assistant_id,
});

const name = entity?.name ?? '';
const description = (conversation?.greeting || entity?.description) ?? '';
const currentSpec = useMemo(() => {
const specs = startupConfig?.modelSpecs?.list ?? [];
return specs.find(
(s: TModelSpec) =>
s.name === conversation?.spec || s.preset?.agent_id === conversation?.agent_id,
);
}, [startupConfig?.modelSpecs?.list, conversation?.spec, conversation?.agent_id]);

const specName = currentSpec?.label?.split(/\s+-\s+/)[0] ?? '';
const name = entity?.name ?? specName;
const description =
(conversation?.greeting || entity?.description || currentSpec?.description) ?? '';

const otherSpec = useMemo(() => {
if (startupConfig?.interface?.showSwitchAgent === false) {
return undefined;
}
const specs = startupConfig?.modelSpecs?.list ?? [];
const switchableSpecs = specs.filter(
(s: TModelSpec) => s.showSwitchAgent && isAgentsEndpoint(s.preset?.endpoint),
Expand All @@ -91,7 +104,11 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
);
const nextIndex = (currentIndex + 1) % switchableSpecs.length;
return switchableSpecs[nextIndex];
}, [startupConfig?.modelSpecs?.list, conversation?.agent_id]);
}, [
startupConfig?.interface?.showSwitchAgent,
startupConfig?.modelSpecs?.list,
conversation?.agent_id,
]);

const handleSwitchAgent = useCallback(() => {
if (!otherSpec) {
Expand Down Expand Up @@ -298,30 +315,32 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
);
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">
<div className="animate-fadeIn mt-4 max-w-md text-left text-sm font-normal text-text-primary">
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cleanDesc) }} />
{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>
<>
{' '}
<a
href={learnMoreUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-blue-500 underline-offset-2 hover:underline dark:text-blue-400"
>
{learnMoreText || 'Learn more'}
</a>
</>
)}
{otherSpec && (
</div>
{otherSpec && (
<div className="animate-fadeIn mt-4 flex flex-row items-center gap-3">
<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>
)}
</>
);
})()}
Expand Down
20 changes: 19 additions & 1 deletion client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useMemo } from 'react';
import type { ModelSelectorProps } from '~/common';
import { TooltipAnchor } from '@librechat/client';
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
import {
Expand Down Expand Up @@ -57,8 +58,19 @@ function ModelSelectorContent() {
}),
[localize, agentsMap, modelSpecs, selectedValues, mappedEndpoints],
);
const selectedSpec = useMemo(
() => modelSpecs.find((spec) => spec.name === selectedValues.modelSpec),
[modelSpecs, selectedValues.modelSpec],
);
const selectedSpecTooltip = useMemo(() => {
if (!selectedSpec?.label) {
return '';
}
const [, description] = selectedSpec.label.split(/\s+-\s+(.+)/);
return description || selectedSpec.label;
}, [selectedSpec?.label]);

const trigger = (
const triggerButton = (
<button
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"
aria-label={localize('com_ui_select_model')}
Expand All @@ -72,6 +84,12 @@ function ModelSelectorContent() {
</button>
);

const trigger = selectedSpecTooltip ? (
<TooltipAnchor description={selectedSpecTooltip} side="bottom" render={triggerButton} />
) : (
triggerButton
);

return (
<div className="relative flex w-full max-w-md flex-col items-center gap-2">
<Menu
Expand Down
Loading
Loading