mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 01:40:15 +01:00
✨ feat: Refactor Favorites functionality to support new data structure and enhance UI interactions
This commit is contained in:
parent
d2faf9c67d
commit
4c10fcd118
13 changed files with 347 additions and 175 deletions
|
|
@ -35,11 +35,16 @@ const getFavoritesController = async (req, res) => {
|
||||||
return res.status(404).json({ message: 'User not found' });
|
return res.status(404).json({ message: 'User not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const favorites = user.favorites || {};
|
let favorites = user.favorites || [];
|
||||||
res.status(200).json({
|
|
||||||
agents: favorites.agents || [],
|
// Ensure favorites is an array (migration/dev fix)
|
||||||
models: favorites.models || [],
|
if (!Array.isArray(favorites)) {
|
||||||
});
|
favorites = [];
|
||||||
|
// Optionally update the DB to fix it permanently
|
||||||
|
await User.findByIdAndUpdate(userId, { $set: { favorites: [] } });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(favorites);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching favorites:', error);
|
console.error('Error fetching favorites:', error);
|
||||||
res.status(500).json({ message: 'Internal server error' });
|
res.status(500).json({ message: 'Internal server error' });
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Star } from 'lucide-react';
|
|
||||||
import { Label } from '@librechat/client';
|
import { Label } from '@librechat/client';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
import { useLocalize, TranslationKeys, useAgentCategories, useFavorites } from '~/hooks';
|
import { useLocalize, TranslationKeys, useAgentCategories } from '~/hooks';
|
||||||
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
|
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
|
||||||
|
|
||||||
interface AgentCardProps {
|
interface AgentCardProps {
|
||||||
|
|
@ -17,13 +16,6 @@ interface AgentCardProps {
|
||||||
const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' }) => {
|
const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { categories } = useAgentCategories();
|
const { categories } = useAgentCategories();
|
||||||
const { isFavoriteAgent, toggleFavoriteAgent } = useFavorites();
|
|
||||||
const isFavorite = isFavoriteAgent(agent.id);
|
|
||||||
|
|
||||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleFavoriteAgent(agent.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryLabel = useMemo(() => {
|
const categoryLabel = useMemo(() => {
|
||||||
if (!agent.category) return '';
|
if (!agent.category) return '';
|
||||||
|
|
@ -57,16 +49,6 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div className="absolute right-2 top-2 z-10">
|
|
||||||
<button
|
|
||||||
onClick={handleFavoriteClick}
|
|
||||||
className="rounded-full p-1 hover:bg-surface-hover"
|
|
||||||
>
|
|
||||||
<Star
|
|
||||||
className={cn('h-5 w-5', isFavorite ? 'fill-yellow-400 text-yellow-400' : 'text-text-secondary')}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Left column: Avatar and Category */}
|
{/* Left column: Avatar and Category */}
|
||||||
|
|
@ -93,7 +75,9 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
|
||||||
<p
|
<p
|
||||||
id={`agent-${agent.id}-description`}
|
id={`agent-${agent.id}-description`}
|
||||||
className="line-clamp-3 text-sm leading-relaxed text-text-primary"
|
className="line-clamp-3 text-sm leading-relaxed text-text-primary"
|
||||||
{...(agent.description ? { 'aria-label': `Description: ${agent.description}` } : {})}
|
{...(agent.description
|
||||||
|
? { 'aria-label': `Description: ${agent.description}` }
|
||||||
|
: {})}
|
||||||
>
|
>
|
||||||
{agent.description ?? ''}
|
{agent.description ?? ''}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { Link } from 'lucide-react';
|
import { Link, Pin, PinOff } from 'lucide-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { OGDialog, OGDialogContent, Button, useToastContext } from '@librechat/client';
|
import { OGDialog, OGDialogContent, Button, useToastContext } from '@librechat/client';
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,8 +11,8 @@ import {
|
||||||
AgentListResponse,
|
AgentListResponse,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
|
import { useLocalize, useDefaultConvo, useFavorites } from '~/hooks';
|
||||||
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
|
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
|
||||||
import { useLocalize, useDefaultConvo } from '~/hooks';
|
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
|
|
||||||
interface SupportContact {
|
interface SupportContact {
|
||||||
|
|
@ -39,6 +39,14 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
const getDefaultConversation = useDefaultConvo();
|
const getDefaultConversation = useDefaultConvo();
|
||||||
const { conversation, newConversation } = useChatContext();
|
const { conversation, newConversation } = useChatContext();
|
||||||
|
const { isFavoriteAgent, toggleFavoriteAgent } = useFavorites();
|
||||||
|
const isFavorite = isFavoriteAgent(agent?.id);
|
||||||
|
|
||||||
|
const handleFavoriteClick = () => {
|
||||||
|
if (agent) {
|
||||||
|
toggleFavoriteAgent(agent.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to chat with the selected agent
|
* Navigate to chat with the selected agent
|
||||||
|
|
@ -133,18 +141,6 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
|
||||||
return (
|
return (
|
||||||
<OGDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
<OGDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
<OGDialogContent ref={dialogRef} className="max-h-[90vh] w-11/12 max-w-lg overflow-y-auto">
|
<OGDialogContent ref={dialogRef} className="max-h-[90vh] w-11/12 max-w-lg overflow-y-auto">
|
||||||
{/* Copy link button - positioned next to close button */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-11 top-4 h-4 w-4 rounded-sm p-0 opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
||||||
aria-label={localize('com_agents_copy_link')}
|
|
||||||
onClick={handleCopyLink}
|
|
||||||
title={localize('com_agents_copy_link')}
|
|
||||||
>
|
|
||||||
<Link />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Agent avatar - top center */}
|
{/* Agent avatar - top center */}
|
||||||
<div className="mt-6 flex justify-center">{renderAgentAvatar(agent, { size: 'xl' })}</div>
|
<div className="mt-6 flex justify-center">{renderAgentAvatar(agent, { size: 'xl' })}</div>
|
||||||
|
|
||||||
|
|
@ -168,7 +164,25 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action button */}
|
{/* Action button */}
|
||||||
<div className="mb-4 mt-6 flex justify-center">
|
<div className="mb-4 mt-6 flex justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleFavoriteClick}
|
||||||
|
title={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
|
||||||
|
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
|
||||||
|
>
|
||||||
|
{isFavorite ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCopyLink}
|
||||||
|
title={localize('com_agents_copy_link')}
|
||||||
|
aria-label={localize('com_agents_copy_link')}
|
||||||
|
>
|
||||||
|
<Link className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button className="w-full max-w-xs" onClick={handleStartChat} disabled={!agent}>
|
<Button className="w-full max-w-xs" onClick={handleStartChat} disabled={!agent}>
|
||||||
{localize('com_agents_start_chat')}
|
{localize('com_agents_start_chat')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EarthIcon, Star } from 'lucide-react';
|
import { EarthIcon, Pin, PinOff } from 'lucide-react';
|
||||||
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
|
import { useModelSelectorContext } from '../ModelSelectorContext';
|
||||||
|
import { CustomMenuItem as MenuItem } from '../CustomMenu';
|
||||||
import type { Endpoint } from '~/common';
|
import type { Endpoint } from '~/common';
|
||||||
import { useFavorites } from '~/hooks';
|
import { useFavorites } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import { useModelSelectorContext } from '../ModelSelectorContext';
|
|
||||||
import { CustomMenuItem as MenuItem } from '../CustomMenu';
|
|
||||||
|
|
||||||
interface EndpointModelItemProps {
|
interface EndpointModelItemProps {
|
||||||
modelId: string | null;
|
modelId: string | null;
|
||||||
|
|
@ -15,7 +15,8 @@ interface EndpointModelItemProps {
|
||||||
|
|
||||||
export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps) {
|
export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps) {
|
||||||
const { handleSelectModel } = useModelSelectorContext();
|
const { handleSelectModel } = useModelSelectorContext();
|
||||||
const { isFavoriteModel, toggleFavoriteModel } = useFavorites();
|
const { isFavoriteModel, toggleFavoriteModel, isFavoriteAgent, toggleFavoriteAgent } =
|
||||||
|
useFavorites();
|
||||||
let isGlobal = false;
|
let isGlobal = false;
|
||||||
let modelName = modelId;
|
let modelName = modelId;
|
||||||
const avatarUrl = endpoint?.modelIcons?.[modelId ?? ''] || null;
|
const avatarUrl = endpoint?.modelIcons?.[modelId ?? ''] || null;
|
||||||
|
|
@ -35,13 +36,43 @@ export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps)
|
||||||
modelName = endpoint.assistantNames[modelId];
|
modelName = endpoint.assistantNames[modelId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFavorite = isFavoriteModel(modelId ?? '', endpoint.value);
|
const isAgent = isAgentsEndpoint(endpoint.value);
|
||||||
|
const isFavorite = isAgent
|
||||||
|
? isFavoriteAgent(modelId ?? '')
|
||||||
|
: isFavoriteModel(modelId ?? '', endpoint.value);
|
||||||
|
|
||||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (modelId) {
|
if (modelId) {
|
||||||
toggleFavoriteModel({ model: modelId, endpoint: endpoint.value, label: modelName ?? undefined });
|
if (isAgent) {
|
||||||
|
toggleFavoriteAgent(modelId);
|
||||||
|
} else {
|
||||||
|
toggleFavoriteModel({ model: modelId, endpoint: endpoint.value });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAvatar = () => {
|
||||||
|
if (avatarUrl) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
||||||
|
<img src={avatarUrl} alt={modelName ?? ''} className="h-full w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value)) &&
|
||||||
|
endpoint.icon
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
||||||
|
{endpoint.icon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full" />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -51,34 +82,22 @@ export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps)
|
||||||
className="group flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm"
|
className="group flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm"
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 items-center gap-2 px-1 py-1">
|
<div className="flex w-full min-w-0 items-center gap-2 px-1 py-1">
|
||||||
{avatarUrl ? (
|
{renderAvatar()}
|
||||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
|
||||||
<img src={avatarUrl} alt={modelName ?? ''} className="h-full w-full object-cover" />
|
|
||||||
</div>
|
|
||||||
) : (isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value)) &&
|
|
||||||
endpoint.icon ? (
|
|
||||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
|
||||||
{endpoint.icon}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-full" />
|
|
||||||
)}
|
|
||||||
<span className="truncate">{modelName}</span>
|
<span className="truncate">{modelName}</span>
|
||||||
{isGlobal && <EarthIcon className="ml-1 size-4 text-surface-submit" />}
|
{isGlobal && <EarthIcon className="ml-1 size-4 text-surface-submit" />}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleFavoriteClick}
|
onClick={handleFavoriteClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full p-1 hover:bg-surface-hover',
|
'rounded-md p-1 hover:bg-surface-hover',
|
||||||
isFavorite ? 'visible' : 'invisible group-hover:visible',
|
isFavorite ? 'visible' : 'invisible group-hover:visible',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Star
|
{isFavorite ? (
|
||||||
className={cn(
|
<PinOff className="h-4 w-4 text-text-secondary" />
|
||||||
'h-4 w-4',
|
) : (
|
||||||
isFavorite ? 'fill-yellow-400 text-yellow-400' : 'text-text-secondary',
|
<Pin className="h-4 w-4 text-text-secondary" />
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex h-12 w-full items-center border-l-2 transition-[background-color] duration-200 md:h-9',
|
'group relative flex h-12 w-full items-center border-l-2 md:h-9',
|
||||||
isActiveConvo
|
isActiveConvo || isPopoverActive
|
||||||
? 'rounded-r-lg border-l-border-xheavy bg-surface-active-alt'
|
? 'rounded-r-lg border-l-border-xheavy bg-surface-active-alt'
|
||||||
: 'rounded-lg border-l-transparent hover:bg-surface-active-alt',
|
: 'rounded-lg border-l-transparent hover:bg-surface-active-alt',
|
||||||
)}
|
)}
|
||||||
|
|
@ -173,6 +173,7 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||||
) : (
|
) : (
|
||||||
<ConvoLink
|
<ConvoLink
|
||||||
isActiveConvo={isActiveConvo}
|
isActiveConvo={isActiveConvo}
|
||||||
|
isPopoverActive={isPopoverActive}
|
||||||
title={title}
|
title={title}
|
||||||
onRename={handleRename}
|
onRename={handleRename}
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { cn } from '~/utils';
|
||||||
|
|
||||||
interface ConvoLinkProps {
|
interface ConvoLinkProps {
|
||||||
isActiveConvo: boolean;
|
isActiveConvo: boolean;
|
||||||
|
isPopoverActive: boolean;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
onRename: () => void;
|
onRename: () => void;
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
|
@ -12,6 +13,7 @@ interface ConvoLinkProps {
|
||||||
|
|
||||||
const ConvoLink: React.FC<ConvoLinkProps> = ({
|
const ConvoLink: React.FC<ConvoLinkProps> = ({
|
||||||
isActiveConvo,
|
isActiveConvo,
|
||||||
|
isPopoverActive,
|
||||||
title,
|
title,
|
||||||
onRename,
|
onRename,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
|
|
@ -22,7 +24,7 @@ const ConvoLink: React.FC<ConvoLinkProps> = ({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex grow items-center gap-2 overflow-hidden rounded-lg px-2',
|
'flex grow items-center gap-2 overflow-hidden rounded-lg px-2',
|
||||||
isActiveConvo ? 'bg-surface-active-alt' : '',
|
isActiveConvo || isPopoverActive ? 'bg-surface-active-alt' : '',
|
||||||
)}
|
)}
|
||||||
title={title ?? undefined}
|
title={title ?? undefined}
|
||||||
aria-current={isActiveConvo ? 'page' : undefined}
|
aria-current={isActiveConvo ? 'page' : undefined}
|
||||||
|
|
@ -47,7 +49,7 @@ const ConvoLink: React.FC<ConvoLinkProps> = ({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
|
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
|
||||||
isActiveConvo
|
isActiveConvo || isPopoverActive
|
||||||
? 'from-surface-active-alt'
|
? 'from-surface-active-alt'
|
||||||
: 'from-surface-primary-alt from-0% to-transparent group-hover:from-surface-active-alt group-hover:from-40%',
|
: 'from-surface-primary-alt from-0% to-transparent group-hover:from-surface-active-alt group-hover:from-40%',
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import * as Menu from '@ariakit/react/menu';
|
import * as Menu from '@ariakit/react/menu';
|
||||||
import { Ellipsis, Trash } from 'lucide-react';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Ellipsis, PinOff } from 'lucide-react';
|
||||||
import { DropdownPopup } from '@librechat/client';
|
import { DropdownPopup } from '@librechat/client';
|
||||||
|
import type { FavoriteModel } from '~/store/favorites';
|
||||||
|
import type t from 'librechat-data-provider';
|
||||||
|
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
||||||
import { useNewConvo, useFavorites, useLocalize } from '~/hooks';
|
import { useNewConvo, useFavorites, useLocalize } from '~/hooks';
|
||||||
import { renderAgentAvatar, cn } from '~/utils';
|
import { renderAgentAvatar, cn } from '~/utils';
|
||||||
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
|
||||||
import type t from 'librechat-data-provider';
|
|
||||||
import type { FavoriteModel } from '~/store/favorites';
|
|
||||||
|
|
||||||
type FavoriteItemProps = {
|
type FavoriteItemProps = {
|
||||||
item: t.Agent | FavoriteModel;
|
item: t.Agent | FavoriteModel;
|
||||||
|
|
@ -68,7 +68,7 @@ export default function FavoriteItem({ item, type }: FavoriteItemProps) {
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (type === 'agent') {
|
if (type === 'agent') {
|
||||||
return renderAgentAvatar(item as t.Agent, { size: 20, className: 'mr-2' });
|
return renderAgentAvatar(item as t.Agent, { size: 'icon', className: 'mr-2' });
|
||||||
}
|
}
|
||||||
const model = item as FavoriteModel;
|
const model = item as FavoriteModel;
|
||||||
return (
|
return (
|
||||||
|
|
@ -87,22 +87,25 @@ export default function FavoriteItem({ item, type }: FavoriteItemProps) {
|
||||||
if (type === 'agent') {
|
if (type === 'agent') {
|
||||||
return (item as t.Agent).name;
|
return (item as t.Agent).name;
|
||||||
}
|
}
|
||||||
return (item as FavoriteModel).label || (item as FavoriteModel).model;
|
return (item as FavoriteModel).model;
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuId = React.useId();
|
const menuId = React.useId();
|
||||||
|
|
||||||
const dropdownItems = [
|
const dropdownItems = [
|
||||||
{
|
{
|
||||||
label: localize('com_ui_remove'),
|
label: localize('com_ui_unpin'),
|
||||||
onClick: handleRemove,
|
onClick: handleRemove,
|
||||||
icon: <Trash className="h-4 w-4 text-text-primary" />,
|
icon: <PinOff className="h-4 w-4 text-text-secondary" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group relative flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm text-text-primary hover:bg-surface-hover"
|
className={cn(
|
||||||
|
'group relative flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm text-text-primary hover:bg-surface-active-alt',
|
||||||
|
isPopoverActive ? 'bg-surface-active-alt' : '',
|
||||||
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
data-testid="favorite-item"
|
data-testid="favorite-item"
|
||||||
>
|
>
|
||||||
|
|
@ -126,13 +129,13 @@ export default function FavoriteItem({ item, type }: FavoriteItemProps) {
|
||||||
trigger={
|
trigger={
|
||||||
<Menu.MenuButton
|
<Menu.MenuButton
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-7 w-7 items-center justify-center rounded-md hover:bg-surface-hover focus:bg-surface-hover',
|
'flex h-7 w-7 items-center justify-center rounded-md',
|
||||||
isPopoverActive ? 'bg-surface-hover' : '',
|
isPopoverActive ? 'bg-surface-active-alt' : '',
|
||||||
)}
|
)}
|
||||||
aria-label={localize('com_ui_options')}
|
aria-label={localize('com_ui_options')}
|
||||||
data-testid="favorite-options-button"
|
data-testid="favorite-options-button"
|
||||||
>
|
>
|
||||||
<Ellipsis className="h-4 w-4 text-text-secondary hover:text-text-primary" />
|
<Ellipsis className="h-4 w-4 text-text-secondary" />
|
||||||
</Menu.MenuButton>
|
</Menu.MenuButton>
|
||||||
}
|
}
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,195 @@
|
||||||
import React from 'react';
|
import React, { useRef, useCallback, useMemo } from 'react';
|
||||||
import { useQueries } from '@tanstack/react-query';
|
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
import { useFavorites, useLocalStorage } from '~/hooks';
|
import { useQueries, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { InfiniteData } from '@tanstack/react-query';
|
||||||
|
import type t from 'librechat-data-provider';
|
||||||
|
import { useFavorites, useLocalStorage, useLocalize } from '~/hooks';
|
||||||
import FavoriteItem from './FavoriteItem';
|
import FavoriteItem from './FavoriteItem';
|
||||||
|
|
||||||
|
interface DraggableFavoriteItemProps {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
moveItem: (dragIndex: number, hoverIndex: number) => void;
|
||||||
|
onDrop: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraggableFavoriteItem = ({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
moveItem,
|
||||||
|
onDrop,
|
||||||
|
children,
|
||||||
|
}: DraggableFavoriteItemProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [{ handlerId }, drop] = useDrop({
|
||||||
|
accept: 'favorite-item',
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
handlerId: monitor.getHandlerId(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hover(item: { index: number; id: string }, monitor) {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dragIndex = item.index;
|
||||||
|
const hoverIndex = index;
|
||||||
|
|
||||||
|
if (dragIndex === hoverIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||||
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = (clientOffset as any).y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveItem(dragIndex, hoverIndex);
|
||||||
|
item.index = hoverIndex;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ isDragging }, drag] = useDrag({
|
||||||
|
type: 'favorite-item',
|
||||||
|
item: () => {
|
||||||
|
return { id, index };
|
||||||
|
},
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
end: () => {
|
||||||
|
onDrop();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const opacity = isDragging ? 0 : 1;
|
||||||
|
drag(drop(ref));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function FavoritesList() {
|
export default function FavoritesList() {
|
||||||
const { favorites } = useFavorites();
|
const localize = useLocalize();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { favorites, reorderFavorites, persistFavorites } = useFavorites();
|
||||||
const [isExpanded, setIsExpanded] = useLocalStorage('favoritesExpanded', true);
|
const [isExpanded, setIsExpanded] = useLocalStorage('favoritesExpanded', true);
|
||||||
|
|
||||||
|
const agentIds = favorites.map((f) => f.agentId).filter(Boolean) as string[];
|
||||||
|
|
||||||
const agentQueries = useQueries({
|
const agentQueries = useQueries({
|
||||||
queries: (favorites.agents || []).map((agentId) => ({
|
queries: agentIds.map((agentId) => ({
|
||||||
queryKey: [QueryKeys.agent, agentId],
|
queryKey: [QueryKeys.agent, agentId],
|
||||||
queryFn: () => dataService.getAgentById({ agent_id: agentId }),
|
queryFn: () => dataService.getAgentById({ agent_id: agentId }),
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const favoriteAgents = agentQueries
|
const agentsMap = useMemo(() => {
|
||||||
.map((query) => query.data)
|
const map: Record<string, t.Agent> = {};
|
||||||
.filter((agent) => agent !== undefined);
|
|
||||||
|
|
||||||
const favoriteModels = favorites.models || [];
|
const addToMap = (agent: t.Agent) => {
|
||||||
|
if (agent && agent.id && !map[agent.id]) {
|
||||||
|
map[agent.id] = agent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if ((favorites.agents || []).length === 0 && (favorites.models || []).length === 0) {
|
const marketplaceData = queryClient.getQueriesData<InfiniteData<t.AgentListResponse>>([
|
||||||
|
QueryKeys.marketplaceAgents,
|
||||||
|
]);
|
||||||
|
marketplaceData.forEach(([_, data]) => {
|
||||||
|
data?.pages.forEach((page) => {
|
||||||
|
page.data.forEach(addToMap);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const agentsListData = queryClient.getQueriesData<t.AgentListResponse>([QueryKeys.agents]);
|
||||||
|
agentsListData.forEach(([_, data]) => {
|
||||||
|
if (data && Array.isArray(data.data)) {
|
||||||
|
data.data.forEach(addToMap);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
agentQueries.forEach((query) => {
|
||||||
|
if (query.data) {
|
||||||
|
map[query.data.id] = query.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, [agentQueries, queryClient]);
|
||||||
|
|
||||||
|
const moveItem = useCallback(
|
||||||
|
(dragIndex: number, hoverIndex: number) => {
|
||||||
|
const newFavorites = [...favorites];
|
||||||
|
const [draggedItem] = newFavorites.splice(dragIndex, 1);
|
||||||
|
newFavorites.splice(hoverIndex, 0, draggedItem);
|
||||||
|
reorderFavorites(newFavorites);
|
||||||
|
},
|
||||||
|
[favorites, reorderFavorites],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDrop = useCallback(() => {
|
||||||
|
persistFavorites(favorites);
|
||||||
|
}, [favorites, persistFavorites]);
|
||||||
|
|
||||||
|
if (favorites.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root
|
<Collapsible.Root open={isExpanded} onOpenChange={setIsExpanded} className="flex flex-col py-2">
|
||||||
open={isExpanded}
|
|
||||||
onOpenChange={setIsExpanded}
|
|
||||||
className="flex flex-col py-2"
|
|
||||||
>
|
|
||||||
<Collapsible.Trigger className="group flex w-full items-center gap-2 px-3 py-1 text-xs font-bold text-text-secondary hover:text-text-primary">
|
<Collapsible.Trigger className="group flex w-full items-center gap-2 px-3 py-1 text-xs font-bold text-text-secondary hover:text-text-primary">
|
||||||
<ChevronRight className="h-3 w-3 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
<ChevronRight className="h-3 w-3 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
||||||
<span className="select-none">Favorites</span>
|
<span className="select-none">{localize('com_ui_pinned')}</span>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content className="collapsible-content">
|
<Collapsible.Content className="collapsible-content">
|
||||||
<div className="mt-1 flex flex-col gap-1">
|
<div className="mt-1 flex flex-col gap-1">
|
||||||
{favoriteAgents.map((agent) => (
|
{favorites.map((fav, index) => {
|
||||||
<FavoriteItem key={agent!.id} item={agent!} type="agent" />
|
if (fav.agentId) {
|
||||||
))}
|
const agent = agentsMap[fav.agentId];
|
||||||
{favoriteModels.map((model) => (
|
if (!agent) return null;
|
||||||
<FavoriteItem key={`${model.endpoint}-${model.model}`} item={model} type="model" />
|
return (
|
||||||
))}
|
<DraggableFavoriteItem
|
||||||
|
key={fav.agentId}
|
||||||
|
id={fav.agentId}
|
||||||
|
index={index}
|
||||||
|
moveItem={moveItem}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<FavoriteItem item={agent} type="agent" />
|
||||||
|
</DraggableFavoriteItem>
|
||||||
|
);
|
||||||
|
} else if (fav.model && fav.endpoint) {
|
||||||
|
return (
|
||||||
|
<DraggableFavoriteItem
|
||||||
|
key={`${fav.endpoint}-${fav.model}`}
|
||||||
|
id={`${fav.endpoint}-${fav.model}`}
|
||||||
|
index={index}
|
||||||
|
moveItem={moveItem}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<FavoriteItem item={fav as any} type="model" />
|
||||||
|
</DraggableFavoriteItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
import type { FavoriteModel } from '~/store/favorites';
|
|
||||||
import { useGetFavoritesQuery, useUpdateFavoritesMutation } from '~/data-provider';
|
import { useGetFavoritesQuery, useUpdateFavoritesMutation } from '~/data-provider';
|
||||||
|
|
||||||
export default function useFavorites() {
|
export default function useFavorites() {
|
||||||
|
|
@ -11,73 +10,71 @@ export default function useFavorites() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getFavoritesQuery.data) {
|
if (getFavoritesQuery.data) {
|
||||||
setFavorites({
|
if (Array.isArray(getFavoritesQuery.data)) {
|
||||||
agents: getFavoritesQuery.data.agents || [],
|
const mapped = getFavoritesQuery.data.map((f: any) => {
|
||||||
models: getFavoritesQuery.data.models || [],
|
if (f.agentId || (f.model && f.endpoint)) return f;
|
||||||
|
if (f.type === 'agent' && f.id) return { agentId: f.id };
|
||||||
|
// Drop label and map legacy model format
|
||||||
|
if (f.type === 'model') return { model: f.model, endpoint: f.endpoint };
|
||||||
|
return f;
|
||||||
});
|
});
|
||||||
|
setFavorites(mapped);
|
||||||
|
} else {
|
||||||
|
// Handle legacy format or invalid data
|
||||||
|
setFavorites([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [getFavoritesQuery.data, setFavorites]);
|
}, [getFavoritesQuery.data, setFavorites]);
|
||||||
|
|
||||||
const saveFavorites = (newFavorites: typeof favorites) => {
|
const saveFavorites = (newFavorites: typeof favorites) => {
|
||||||
setFavorites(newFavorites);
|
const cleaned = newFavorites.map((f) => {
|
||||||
updateFavoritesMutation.mutate(newFavorites);
|
if (f.agentId) return { agentId: f.agentId };
|
||||||
|
if (f.model && f.endpoint) return { model: f.model, endpoint: f.endpoint };
|
||||||
|
return f;
|
||||||
|
});
|
||||||
|
setFavorites(cleaned);
|
||||||
|
updateFavoritesMutation.mutate(cleaned);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFavoriteAgent = (id: string) => {
|
const addFavoriteAgent = (agentId: string) => {
|
||||||
const agents = favorites?.agents || [];
|
if (favorites.some((f) => f.agentId === agentId)) return;
|
||||||
if (agents.includes(id)) return;
|
const newFavorites = [...favorites, { agentId }];
|
||||||
const newFavorites = {
|
|
||||||
...favorites,
|
|
||||||
agents: [...agents, id],
|
|
||||||
};
|
|
||||||
saveFavorites(newFavorites);
|
saveFavorites(newFavorites);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFavoriteAgent = (id: string) => {
|
const removeFavoriteAgent = (agentId: string) => {
|
||||||
const agents = favorites?.agents || [];
|
const newFavorites = favorites.filter((f) => f.agentId !== agentId);
|
||||||
const newFavorites = {
|
|
||||||
...favorites,
|
|
||||||
agents: agents.filter((item) => item !== id),
|
|
||||||
};
|
|
||||||
saveFavorites(newFavorites);
|
saveFavorites(newFavorites);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFavoriteModel = (model: FavoriteModel) => {
|
const addFavoriteModel = (model: { model: string; endpoint: string }) => {
|
||||||
const models = favorites?.models || [];
|
if (favorites.some((f) => f.model === model.model && f.endpoint === model.endpoint)) return;
|
||||||
if (models.some((m) => m.model === model.model && m.endpoint === model.endpoint)) return;
|
const newFavorites = [...favorites, { model: model.model, endpoint: model.endpoint }];
|
||||||
const newFavorites = {
|
|
||||||
...favorites,
|
|
||||||
models: [...models, model],
|
|
||||||
};
|
|
||||||
saveFavorites(newFavorites);
|
saveFavorites(newFavorites);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFavoriteModel = (model: string, endpoint: string) => {
|
const removeFavoriteModel = (model: string, endpoint: string) => {
|
||||||
const models = favorites?.models || [];
|
const newFavorites = favorites.filter((f) => !(f.model === model && f.endpoint === endpoint));
|
||||||
const newFavorites = {
|
|
||||||
...favorites,
|
|
||||||
models: models.filter((m) => !(m.model === model && m.endpoint === endpoint)),
|
|
||||||
};
|
|
||||||
saveFavorites(newFavorites);
|
saveFavorites(newFavorites);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFavoriteAgent = (id: string) => {
|
const isFavoriteAgent = (agentId: string) => {
|
||||||
return (favorites?.agents || []).includes(id);
|
return favorites.some((f) => f.agentId === agentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFavoriteModel = (model: string, endpoint: string) => {
|
const isFavoriteModel = (model: string, endpoint: string) => {
|
||||||
return (favorites?.models || []).some((m) => m.model === model && m.endpoint === endpoint);
|
return favorites.some((f) => f.model === model && f.endpoint === endpoint);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFavoriteAgent = (id: string) => {
|
const toggleFavoriteAgent = (agentId: string) => {
|
||||||
if (isFavoriteAgent(id)) {
|
if (isFavoriteAgent(agentId)) {
|
||||||
removeFavoriteAgent(id);
|
removeFavoriteAgent(agentId);
|
||||||
} else {
|
} else {
|
||||||
addFavoriteAgent(id);
|
addFavoriteAgent(agentId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFavoriteModel = (model: FavoriteModel) => {
|
const toggleFavoriteModel = (model: { model: string; endpoint: string }) => {
|
||||||
if (isFavoriteModel(model.model, model.endpoint)) {
|
if (isFavoriteModel(model.model, model.endpoint)) {
|
||||||
removeFavoriteModel(model.model, model.endpoint);
|
removeFavoriteModel(model.model, model.endpoint);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -85,6 +82,14 @@ export default function useFavorites() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reorderFavorites = (newFavorites: typeof favorites) => {
|
||||||
|
setFavorites(newFavorites);
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistFavorites = (newFavorites: typeof favorites) => {
|
||||||
|
updateFavoritesMutation.mutate(newFavorites);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favorites,
|
favorites,
|
||||||
addFavoriteAgent,
|
addFavoriteAgent,
|
||||||
|
|
@ -95,5 +100,7 @@ export default function useFavorites() {
|
||||||
isFavoriteModel,
|
isFavoriteModel,
|
||||||
toggleFavoriteAgent,
|
toggleFavoriteAgent,
|
||||||
toggleFavoriteModel,
|
toggleFavoriteModel,
|
||||||
|
reorderFavorites,
|
||||||
|
persistFavorites,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -851,7 +851,7 @@
|
||||||
"com_ui_date_yesterday": "Yesterday",
|
"com_ui_date_yesterday": "Yesterday",
|
||||||
"com_ui_decline": "I do not accept",
|
"com_ui_decline": "I do not accept",
|
||||||
"com_ui_default_post_request": "Default (POST request)",
|
"com_ui_default_post_request": "Default (POST request)",
|
||||||
"com_ui_remove": "Remove",
|
"com_ui_unpin": "Unpin",
|
||||||
"com_ui_delete": "Delete",
|
"com_ui_delete": "Delete",
|
||||||
"com_ui_delete_action": "Delete Action",
|
"com_ui_delete_action": "Delete Action",
|
||||||
"com_ui_delete_action_confirm": "Are you sure you want to delete this action?",
|
"com_ui_delete_action_confirm": "Are you sure you want to delete this action?",
|
||||||
|
|
@ -1129,6 +1129,8 @@
|
||||||
"com_ui_prev": "Prev",
|
"com_ui_prev": "Prev",
|
||||||
"com_ui_preview": "Preview",
|
"com_ui_preview": "Preview",
|
||||||
"com_ui_privacy_policy": "Privacy policy",
|
"com_ui_privacy_policy": "Privacy policy",
|
||||||
|
"com_ui_pin": "Pin",
|
||||||
|
"com_ui_pinned": "Pinned",
|
||||||
"com_ui_privacy_policy_url": "Privacy Policy URL",
|
"com_ui_privacy_policy_url": "Privacy Policy URL",
|
||||||
"com_ui_prompt": "Prompt",
|
"com_ui_prompt": "Prompt",
|
||||||
"com_ui_prompt_group_button": "{{name}} prompt, {{category}} category",
|
"com_ui_prompt_group_button": "{{name}} prompt, {{category}} category",
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import { atom } from 'recoil';
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export type Favorite = {
|
||||||
|
agentId?: string;
|
||||||
|
model?: string;
|
||||||
|
endpoint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FavoriteModel = {
|
export type FavoriteModel = {
|
||||||
model: string;
|
model: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
label?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FavoritesState = {
|
export type FavoritesState = Favorite[];
|
||||||
agents: string[];
|
|
||||||
models: FavoriteModel[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const favorites = atom<FavoritesState>({
|
const favorites = atom<FavoritesState>({
|
||||||
key: 'favorites',
|
key: 'favorites',
|
||||||
default: {
|
default: [],
|
||||||
agents: [],
|
|
||||||
models: [],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const getAgentAvatarUrl = (agent: t.Agent | null | undefined): string | n
|
||||||
export const renderAgentAvatar = (
|
export const renderAgentAvatar = (
|
||||||
agent: t.Agent | null | undefined,
|
agent: t.Agent | null | undefined,
|
||||||
options: {
|
options: {
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
className?: string;
|
className?: string;
|
||||||
showBorder?: boolean;
|
showBorder?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
|
|
@ -40,6 +40,7 @@ export const renderAgentAvatar = (
|
||||||
|
|
||||||
// Size mappings for responsive design
|
// Size mappings for responsive design
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
|
icon: 'h-5 w-5',
|
||||||
sm: 'h-12 w-12 sm:h-14 sm:w-14',
|
sm: 'h-12 w-12 sm:h-14 sm:w-14',
|
||||||
md: 'h-16 w-16 sm:h-20 sm:w-20 md:h-24 md:w-24',
|
md: 'h-16 w-16 sm:h-20 sm:w-20 md:h-24 md:w-24',
|
||||||
lg: 'h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28',
|
lg: 'h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28',
|
||||||
|
|
@ -47,6 +48,7 @@ export const renderAgentAvatar = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconSizeClasses = {
|
const iconSizeClasses = {
|
||||||
|
icon: 'h-4 w-4',
|
||||||
sm: 'h-6 w-6 sm:h-7 sm:w-7',
|
sm: 'h-6 w-6 sm:h-7 sm:w-7',
|
||||||
md: 'h-6 w-6 sm:h-8 sm:w-8 md:h-10 md:w-10',
|
md: 'h-6 w-6 sm:h-8 sm:w-8 md:h-10 md:w-10',
|
||||||
lg: 'h-8 w-8 sm:h-10 sm:w-10 md:h-12 md:w-12',
|
lg: 'h-8 w-8 sm:h-10 sm:w-10 md:h-12 md:w-12',
|
||||||
|
|
@ -54,6 +56,7 @@ export const renderAgentAvatar = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeholderSizeClasses = {
|
const placeholderSizeClasses = {
|
||||||
|
icon: 'h-5 w-5',
|
||||||
sm: 'h-10 w-10 sm:h-12 sm:w-12',
|
sm: 'h-10 w-10 sm:h-12 sm:w-12',
|
||||||
md: 'h-12 w-12 sm:h-16 sm:w-16 md:h-20 md:w-20',
|
md: 'h-12 w-12 sm:h-16 sm:w-16 md:h-20 md:w-20',
|
||||||
lg: 'h-16 w-16 sm:h-20 sm:w-20 md:h-24 md:w-24',
|
lg: 'h-16 w-16 sm:h-20 sm:w-20 md:h-24 md:w-24',
|
||||||
|
|
|
||||||
|
|
@ -142,27 +142,16 @@ const userSchema = new Schema<IUser>(
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
favorites: {
|
favorites: {
|
||||||
type: {
|
|
||||||
agents: {
|
|
||||||
type: [String],
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
type: [
|
type: [
|
||||||
{
|
{
|
||||||
model: String,
|
_id: false,
|
||||||
endpoint: String,
|
agentId: String, // for agent
|
||||||
label: String,
|
model: String, // for model
|
||||||
|
endpoint: String, // for model
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
default: {
|
|
||||||
agents: [],
|
|
||||||
models: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** Field for external source identification (for consistency with TPrincipal schema) */
|
/** Field for external source identification (for consistency with TPrincipal schema) */
|
||||||
idOnTheSource: {
|
idOnTheSource: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue