mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-04 15:20:18 +01:00
✨ feat: Refactor FilterPrompts and List components for improved search handling and sorting functionality
This commit is contained in:
parent
3e698338aa
commit
84f62eb70c
3 changed files with 192 additions and 175 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { ListFilter, User, Share2 } from 'lucide-react';
|
import { ListFilter, User, Share2 } from 'lucide-react';
|
||||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { SystemCategories } from 'librechat-data-provider';
|
import { SystemCategories } from 'librechat-data-provider';
|
||||||
import { usePromptGroupsNav, useLocalize, useCategories } from '~/hooks';
|
import { usePromptGroupsNav, useLocalize, useCategories } from '~/hooks';
|
||||||
|
|
@ -19,7 +19,6 @@ export default function FilterPrompts({
|
||||||
const setCategory = useSetRecoilState(store.promptsCategory);
|
const setCategory = useSetRecoilState(store.promptsCategory);
|
||||||
const categoryFilter = useRecoilValue(store.promptsCategory);
|
const categoryFilter = useRecoilValue(store.promptsCategory);
|
||||||
const { categories } = useCategories('h-4 w-4');
|
const { categories } = useCategories('h-4 w-4');
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
|
|
||||||
const filterOptions = useMemo(() => {
|
const filterOptions = useMemo(() => {
|
||||||
const baseOptions: Option[] = [
|
const baseOptions: Option[] = [
|
||||||
|
|
@ -41,36 +40,27 @@ export default function FilterPrompts({
|
||||||
{ divider: true, value: null },
|
{ divider: true, value: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
const categoryOptions = categories
|
const categoryOptions = categories?.length
|
||||||
? [...categories]
|
? [...categories]
|
||||||
: [
|
: [{ value: SystemCategories.NO_CATEGORY, label: localize('com_ui_no_category') }];
|
||||||
{
|
|
||||||
value: SystemCategories.NO_CATEGORY,
|
|
||||||
label: localize('com_ui_no_category'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return [...baseOptions, ...categoryOptions];
|
return [...baseOptions, ...categoryOptions];
|
||||||
}, [categories, localize]);
|
}, [categories, localize]);
|
||||||
|
|
||||||
const onSelect = useCallback(
|
const onSelect = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
if (value === SystemCategories.ALL) {
|
setCategory(value === SystemCategories.ALL ? '' : value);
|
||||||
setCategory('');
|
|
||||||
} else {
|
|
||||||
setCategory(value);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[setCategory],
|
[setCategory],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSearchChange = useCallback(
|
||||||
setIsSearching(true);
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const timeout = setTimeout(() => {
|
setDisplayName(e.target.value);
|
||||||
setIsSearching(false);
|
setName(e.target.value);
|
||||||
}, 500);
|
},
|
||||||
return () => clearTimeout(timeout);
|
[setName],
|
||||||
}, [displayName]);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex w-full gap-2 text-text-primary', className)}>
|
<div className={cn('flex w-full gap-2 text-text-primary', className)}>
|
||||||
|
|
@ -86,11 +76,7 @@ export default function FilterPrompts({
|
||||||
/>
|
/>
|
||||||
<AnimatedSearchInput
|
<AnimatedSearchInput
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={(e) => {
|
onChange={handleSearchChange}
|
||||||
setDisplayName(e.target.value);
|
|
||||||
setName(e.target.value);
|
|
||||||
}}
|
|
||||||
isSearching={isSearching}
|
|
||||||
placeholder={localize('com_ui_filter_prompts_name')}
|
placeholder={localize('com_ui_filter_prompts_name')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,105 @@
|
||||||
import type { TPromptGroup, TStartupConfig } from 'librechat-data-provider';
|
import type { TPromptGroup, TStartupConfig } from 'librechat-data-provider';
|
||||||
import { RankablePromptList, RankingProvider } from '~/components/Prompts/Groups/RankingComponent';
|
import {
|
||||||
|
RankablePromptList,
|
||||||
|
SortedPromptList,
|
||||||
|
RankingProvider,
|
||||||
|
} from '~/components/Prompts/Groups/RankingComponent';
|
||||||
import DashGroupItem from '~/components/Prompts/Groups/DashGroupItem';
|
import DashGroupItem from '~/components/Prompts/Groups/DashGroupItem';
|
||||||
import ChatGroupItem from '~/components/Prompts/Groups/ChatGroupItem';
|
import ChatGroupItem from '~/components/Prompts/Groups/ChatGroupItem';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
import { Button, Skeleton } from '~/components/ui';
|
import { Skeleton } from '~/components/ui';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
interface ListProps {
|
||||||
|
groups?: TPromptGroup[];
|
||||||
|
isChatRoute: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
enableRanking?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function List({
|
export default function List({
|
||||||
groups = [],
|
groups = [],
|
||||||
isChatRoute,
|
isChatRoute,
|
||||||
isLoading,
|
isLoading,
|
||||||
enableRanking = true,
|
enableRanking = true,
|
||||||
}: {
|
}: ListProps) {
|
||||||
groups?: TPromptGroup[];
|
|
||||||
isChatRoute: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
enableRanking?: boolean;
|
|
||||||
}) {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { data: startupConfig = {} as Partial<TStartupConfig> } = useGetStartupConfig();
|
const { data: startupConfig = {} as Partial<TStartupConfig> } = useGetStartupConfig();
|
||||||
const { instanceProjectId } = startupConfig;
|
const { instanceProjectId } = startupConfig;
|
||||||
|
|
||||||
const renderGroupItem = (group: TPromptGroup) => {
|
const renderGroupItem = useMemo(
|
||||||
|
() => (group: TPromptGroup) => {
|
||||||
|
const Component = isChatRoute ? ChatGroupItem : DashGroupItem;
|
||||||
|
return <Component key={group._id} group={group} instanceProjectId={instanceProjectId} />;
|
||||||
|
},
|
||||||
|
[isChatRoute, instanceProjectId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyMessage = localize('com_ui_nothing_found');
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<RankingProvider>
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<div className="flex-grow overflow-y-auto">
|
||||||
|
<div className="overflow-y-auto overflow-x-hidden">
|
||||||
|
{isChatRoute ? (
|
||||||
|
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
||||||
|
) : (
|
||||||
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
|
<Skeleton
|
||||||
|
key={index}
|
||||||
|
className="w-100 mx-2 my-2 flex h-14 rounded-lg border-0 p-4"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RankingProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.length === 0) {
|
||||||
|
return (
|
||||||
|
<RankingProvider>
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<div className="flex-grow overflow-y-auto">
|
||||||
|
<div className="overflow-y-auto overflow-x-hidden">
|
||||||
|
{isChatRoute ? (
|
||||||
|
<div className="my-2 flex h-[84px] w-full items-center justify-center rounded-2xl border border-border-light bg-transparent px-3 pb-4 pt-3 text-text-primary">
|
||||||
|
{emptyMessage}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="my-12 flex w-full items-center justify-center text-lg font-semibold text-text-primary">
|
||||||
|
{emptyMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RankingProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldUseRanking = !isChatRoute && enableRanking;
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
if (isChatRoute) {
|
if (isChatRoute) {
|
||||||
return <ChatGroupItem key={group._id} group={group} instanceProjectId={instanceProjectId} />;
|
return <SortedPromptList groups={groups} renderItem={renderGroupItem} />;
|
||||||
}
|
}
|
||||||
return <DashGroupItem key={group._id} group={group} instanceProjectId={instanceProjectId} />;
|
if (shouldUseRanking) {
|
||||||
|
return <RankablePromptList groups={groups} renderItem={renderGroupItem} />;
|
||||||
|
}
|
||||||
|
return groups.map((group) => renderGroupItem(group));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RankingProvider>
|
<RankingProvider>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex-grow overflow-y-auto">
|
<div className="flex-grow overflow-y-auto">
|
||||||
<div className="overflow-y-auto overflow-x-hidden">
|
<div className="overflow-y-auto overflow-x-hidden">{renderContent()}</div>
|
||||||
{isLoading && isChatRoute && (
|
|
||||||
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
|
||||||
)}
|
|
||||||
{isLoading &&
|
|
||||||
!isChatRoute &&
|
|
||||||
Array.from({ length: 10 }).map((_, index: number) => (
|
|
||||||
<Skeleton
|
|
||||||
key={index}
|
|
||||||
className="w-100 mx-2 my-2 flex h-14 rounded-lg border-0 p-4"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{!isLoading && groups.length === 0 && isChatRoute && (
|
|
||||||
<div className="my-2 flex h-[84px] w-full items-center justify-center rounded-2xl border border-border-light bg-transparent px-3 pb-4 pt-3 text-text-primary">
|
|
||||||
{localize('com_ui_nothing_found')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isLoading && groups.length === 0 && !isChatRoute && (
|
|
||||||
<div className="my-12 flex w-full items-center justify-center text-lg font-semibold text-text-primary">
|
|
||||||
{localize('com_ui_nothing_found')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isLoading && groups.length > 0 && !isChatRoute && enableRanking ? (
|
|
||||||
<RankablePromptList groups={groups} renderItem={renderGroupItem} />
|
|
||||||
) : (
|
|
||||||
!isLoading && groups.map((group) => renderGroupItem(group))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RankingProvider>
|
</RankingProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useCallback, useEffect, useState, useRef, ReactNode } from 'react';
|
import React, { useCallback, useEffect, useState, useRef, ReactNode } from 'react';
|
||||||
import { DndProvider, useDrag, useDrop, useDragLayer } from 'react-dnd';
|
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
||||||
import { GripVertical } from 'lucide-react';
|
import { GripVertical } from 'lucide-react';
|
||||||
import { useUpdatePromptRankings, useGetUserPromptPreferences } from '~/data-provider';
|
import { useDrag, useDrop, useDragLayer } from 'react-dnd';
|
||||||
import type { TPromptGroup } from 'librechat-data-provider';
|
import type { TPromptGroup } from 'librechat-data-provider';
|
||||||
|
import { useUpdatePromptRankings, useGetUserPromptPreferences } from '~/data-provider';
|
||||||
|
import CategoryIcon from './CategoryIcon';
|
||||||
|
import { Label } from '~/components';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const ITEM_TYPE = 'PROMPT_GROUP';
|
const ITEM_TYPE = 'PROMPT_GROUP';
|
||||||
|
|
@ -20,8 +21,31 @@ interface DragItem {
|
||||||
index: number;
|
index: number;
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
group: TPromptGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortGroups = (groups: TPromptGroup[], rankings: any[], favorites: string[]) => {
|
||||||
|
const rankingMap = new Map(rankings.map((ranking) => [ranking.promptGroupId, ranking.order]));
|
||||||
|
|
||||||
|
return [...groups].sort((a, b) => {
|
||||||
|
const aId = a._id ?? '';
|
||||||
|
const bId = b._id ?? '';
|
||||||
|
const aIsFavorite = favorites.includes(aId);
|
||||||
|
const bIsFavorite = favorites.includes(bId);
|
||||||
|
|
||||||
|
if (aIsFavorite && !bIsFavorite) return -1;
|
||||||
|
if (!aIsFavorite && bIsFavorite) return 1;
|
||||||
|
|
||||||
|
const aRank = rankingMap.get(aId);
|
||||||
|
const bRank = rankingMap.get(bId);
|
||||||
|
if (aRank !== undefined && bRank !== undefined) return aRank - bRank;
|
||||||
|
if (aRank !== undefined) return -1;
|
||||||
|
if (bRank !== undefined) return 1;
|
||||||
|
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function DraggablePromptItem({
|
function DraggablePromptItem({
|
||||||
group,
|
group,
|
||||||
index,
|
index,
|
||||||
|
|
@ -30,46 +54,33 @@ function DraggablePromptItem({
|
||||||
children,
|
children,
|
||||||
}: DraggablePromptItemProps) {
|
}: DraggablePromptItemProps) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isOver, setIsOver] = useState(false);
|
|
||||||
|
|
||||||
const [{ isDragging }, drag, preview] = useDrag({
|
const [{ isDragging }, drag, preview] = useDrag({
|
||||||
type: ITEM_TYPE,
|
type: ITEM_TYPE,
|
||||||
item: () => ({ type: ITEM_TYPE, index, id: group._id }),
|
item: { type: ITEM_TYPE, index, id: group._id, group },
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ isOverCurrent }, drop] = useDrop<DragItem, void, { isOverCurrent: boolean }>({
|
const [{ isOver }, drop] = useDrop<DragItem, void, { isOver: boolean }>({
|
||||||
accept: ITEM_TYPE,
|
accept: ITEM_TYPE,
|
||||||
hover: (item, monitor) => {
|
hover: (item, monitor) => {
|
||||||
if (!ref.current) return;
|
if (!ref.current || item.index === index) return;
|
||||||
|
|
||||||
const dragIndex = item.index;
|
|
||||||
const hoverIndex = index;
|
|
||||||
if (dragIndex === hoverIndex) return;
|
|
||||||
|
|
||||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
const hoverMiddleY = hoverBoundingRect.height / 2;
|
||||||
const clientOffset = monitor.getClientOffset();
|
const clientOffset = monitor.getClientOffset();
|
||||||
if (!clientOffset) return;
|
if (!clientOffset) return;
|
||||||
|
|
||||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||||
|
if (item.index < index && hoverClientY < hoverMiddleY * 0.8) return;
|
||||||
|
if (item.index > index && hoverClientY > hoverMiddleY * 1.2) return;
|
||||||
|
|
||||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY * 0.8) return;
|
moveItem(item.index, index);
|
||||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY * 1.2) return;
|
item.index = index;
|
||||||
|
|
||||||
moveItem(dragIndex, hoverIndex);
|
|
||||||
item.index = hoverIndex;
|
|
||||||
},
|
},
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({ isOver: monitor.isOver({ shallow: true }) }),
|
||||||
isOverCurrent: monitor.isOver({ shallow: true }),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsOver(isOverCurrent);
|
|
||||||
}, [isOverCurrent]);
|
|
||||||
|
|
||||||
drag(drop(ref));
|
drag(drop(ref));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -85,15 +96,11 @@ function DraggablePromptItem({
|
||||||
isAnyDragging && !isDragging && 'transition-transform',
|
isAnyDragging && !isDragging && 'transition-transform',
|
||||||
isOver && !isDragging && 'scale-[1.02]',
|
isOver && !isDragging && 'scale-[1.02]',
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||||
transform: isDragging ? 'scale(1.05)' : 'scale(1)',
|
|
||||||
cursor: isDragging ? 'grabbing' : 'grab',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute left-2 top-1/2 z-10 -translate-y-1/2 transition-all duration-200',
|
'absolute left-2 top-1/2 z-10 -translate-y-1/2 opacity-0 group-hover:opacity-100',
|
||||||
'opacity-0 group-hover:opacity-100',
|
|
||||||
isDragging && 'opacity-100',
|
isDragging && 'opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -105,35 +112,14 @@ function DraggablePromptItem({
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomDragLayer() {
|
function CustomDragLayer() {
|
||||||
const { item, itemType, currentOffset, isDragging } = useDragLayer((monitor) => ({
|
const { itemType, item, currentOffset, isDragging } = useDragLayer((monitor) => ({
|
||||||
item: monitor.getItem(),
|
|
||||||
itemType: monitor.getItemType(),
|
itemType: monitor.getItemType(),
|
||||||
|
item: monitor.getItem() as DragItem,
|
||||||
currentOffset: monitor.getSourceClientOffset(),
|
currentOffset: monitor.getSourceClientOffset(),
|
||||||
isDragging: monitor.isDragging(),
|
isDragging: monitor.isDragging(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!isDragging || !currentOffset || itemType !== ITEM_TYPE) {
|
if (!isDragging || !currentOffset || itemType !== ITEM_TYPE || !item?.group) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderPreview = () => {
|
|
||||||
if (item && typeof item.id === 'string') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'rgba(230, 245, 255, 0.9)',
|
|
||||||
padding: '10px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
||||||
display: 'inline-block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`Moving: ${item.id}`}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <div>Dragging...</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -152,12 +138,56 @@ function CustomDragLayer() {
|
||||||
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
|
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderPreview()}
|
<div className="mx-2 my-2 flex h-[60px] w-[430px] min-w-[300px] cursor-pointer rounded-lg border border-border-light bg-surface-primary p-3 opacity-90 shadow-lg">
|
||||||
|
<div className="flex items-center gap-2 truncate pr-2">
|
||||||
|
<CategoryIcon
|
||||||
|
category={item.group.category ?? ''}
|
||||||
|
className="icon-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label className="text-md cursor-pointer truncate font-semibold text-text-primary">
|
||||||
|
{item.group.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SortedPromptList({
|
||||||
|
groups,
|
||||||
|
renderItem,
|
||||||
|
}: {
|
||||||
|
groups: TPromptGroup[];
|
||||||
|
renderItem: (group: TPromptGroup) => ReactNode;
|
||||||
|
}) {
|
||||||
|
const { data: preferences } = useGetUserPromptPreferences();
|
||||||
|
const [sortedGroups, setSortedGroups] = useState<TPromptGroup[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!groups?.length) {
|
||||||
|
setSortedGroups([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankings = preferences?.rankings || [];
|
||||||
|
const favorites = preferences?.favorites || [];
|
||||||
|
setSortedGroups(sortGroups(groups, rankings, favorites));
|
||||||
|
}, [groups, preferences]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{sortedGroups.map((group) => (
|
||||||
|
<div key={group._id} className="transition-all duration-300 ease-in-out">
|
||||||
|
{renderItem(group)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface RankablePromptListProps {
|
interface RankablePromptListProps {
|
||||||
groups: TPromptGroup[];
|
groups: TPromptGroup[];
|
||||||
renderItem: (group: TPromptGroup) => ReactNode;
|
renderItem: (group: TPromptGroup) => ReactNode;
|
||||||
|
|
@ -167,7 +197,6 @@ interface RankablePromptListProps {
|
||||||
function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePromptListProps) {
|
function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePromptListProps) {
|
||||||
const { data: preferences } = useGetUserPromptPreferences();
|
const { data: preferences } = useGetUserPromptPreferences();
|
||||||
const updateRankings = useUpdatePromptRankings();
|
const updateRankings = useUpdatePromptRankings();
|
||||||
|
|
||||||
const [sortedGroups, setSortedGroups] = useState<TPromptGroup[]>([]);
|
const [sortedGroups, setSortedGroups] = useState<TPromptGroup[]>([]);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -177,54 +206,29 @@ function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePro
|
||||||
setSortedGroups([]);
|
setSortedGroups([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rankings = preferences?.rankings || [];
|
const rankings = preferences?.rankings || [];
|
||||||
const favorites = preferences?.favorites || [];
|
const favorites = preferences?.favorites || [];
|
||||||
const rankingMap = new Map(rankings.map((ranking) => [ranking.promptGroupId, ranking.order]));
|
setSortedGroups(sortGroups(groups, rankings, favorites));
|
||||||
|
|
||||||
const sorted = [...groups].sort((a, b) => {
|
|
||||||
const aId = a._id ?? '';
|
|
||||||
const bId = b._id ?? '';
|
|
||||||
const aIsFavorite = favorites.includes(aId);
|
|
||||||
const bIsFavorite = favorites.includes(bId);
|
|
||||||
|
|
||||||
if (aIsFavorite && !bIsFavorite) return -1;
|
|
||||||
if (!aIsFavorite && bIsFavorite) return 1;
|
|
||||||
|
|
||||||
const aRank = rankingMap.get(aId);
|
|
||||||
const bRank = rankingMap.get(bId);
|
|
||||||
if (aRank !== undefined && bRank !== undefined) {
|
|
||||||
return aRank - bRank;
|
|
||||||
}
|
|
||||||
if (aRank !== undefined) return -1;
|
|
||||||
if (bRank !== undefined) return 1;
|
|
||||||
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
setSortedGroups(sorted);
|
|
||||||
}, [groups, preferences]);
|
}, [groups, preferences]);
|
||||||
|
|
||||||
const moveItem = useCallback(
|
const moveItem = useCallback(
|
||||||
(dragIndex: number, hoverIndex: number) => {
|
(dragIndex: number, hoverIndex: number) => {
|
||||||
if (dragIndex === hoverIndex) return;
|
if (dragIndex === hoverIndex) return;
|
||||||
|
|
||||||
setSortedGroups((prevGroups) => {
|
setSortedGroups((prevGroups) => {
|
||||||
const newGroups = [...prevGroups];
|
const newGroups = [...prevGroups];
|
||||||
const draggedItem = newGroups[dragIndex];
|
const [draggedItem] = newGroups.splice(dragIndex, 1);
|
||||||
newGroups.splice(dragIndex, 1);
|
|
||||||
newGroups.splice(hoverIndex, 0, draggedItem);
|
newGroups.splice(hoverIndex, 0, draggedItem);
|
||||||
return newGroups;
|
return newGroups;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (saveTimeoutRef.current) {
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
||||||
clearTimeout(saveTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTimeoutRef.current = setTimeout(() => {
|
saveTimeoutRef.current = setTimeout(() => {
|
||||||
setSortedGroups((currentGroups) => {
|
setSortedGroups((currentGroups) => {
|
||||||
const newRankings = currentGroups
|
const newRankings = currentGroups
|
||||||
.map((group, index) =>
|
.map((group, index) => (group._id ? { promptGroupId: group._id, order: index } : null))
|
||||||
typeof group._id === 'string' ? { promptGroupId: group._id, order: index } : null,
|
|
||||||
)
|
|
||||||
.filter(
|
.filter(
|
||||||
(ranking): ranking is { promptGroupId: string; order: number } => ranking !== null,
|
(ranking): ranking is { promptGroupId: string; order: number } => ranking !== null,
|
||||||
);
|
);
|
||||||
|
|
@ -232,12 +236,8 @@ function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePro
|
||||||
if (newRankings.length > 0) {
|
if (newRankings.length > 0) {
|
||||||
updateRankings
|
updateRankings
|
||||||
.mutateAsync({ rankings: newRankings })
|
.mutateAsync({ rankings: newRankings })
|
||||||
.then(() => {
|
.then(() => onRankingChange?.(newRankings.map((r) => r.promptGroupId)))
|
||||||
onRankingChange?.(newRankings.map((r) => r.promptGroupId));
|
.catch(console.error);
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to update rankings:', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return currentGroups;
|
return currentGroups;
|
||||||
});
|
});
|
||||||
|
|
@ -246,14 +246,6 @@ function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePro
|
||||||
[updateRankings, onRankingChange],
|
[updateRankings, onRankingChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (saveTimeoutRef.current) {
|
|
||||||
clearTimeout(saveTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleDragStart = () => setIsDragging(true);
|
const handleDragStart = () => setIsDragging(true);
|
||||||
const handleDragEnd = () => setIsDragging(false);
|
const handleDragEnd = () => setIsDragging(false);
|
||||||
|
|
@ -264,6 +256,7 @@ function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePro
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('dragstart', handleDragStart);
|
document.removeEventListener('dragstart', handleDragStart);
|
||||||
document.removeEventListener('dragend', handleDragEnd);
|
document.removeEventListener('dragend', handleDragEnd);
|
||||||
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -273,9 +266,7 @@ function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePro
|
||||||
<div
|
<div
|
||||||
key={group._id || index}
|
key={group._id || index}
|
||||||
className="transition-all duration-300 ease-in-out"
|
className="transition-all duration-300 ease-in-out"
|
||||||
style={{
|
style={{ transform: `translateY(${isDragging ? '2px' : '0'})` }}
|
||||||
transform: `translateY(${isDragging ? '2px' : '0'})`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DraggablePromptItem
|
<DraggablePromptItem
|
||||||
group={group}
|
group={group}
|
||||||
|
|
@ -293,11 +284,11 @@ function RankablePromptList({ groups, renderItem, onRankingChange }: RankablePro
|
||||||
|
|
||||||
function RankingProvider({ children }: { children: ReactNode }) {
|
function RankingProvider({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<div>
|
||||||
<CustomDragLayer />
|
<CustomDragLayer />
|
||||||
{children}
|
{children}
|
||||||
</DndProvider>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RankablePromptList, RankingProvider };
|
export { RankablePromptList, SortedPromptList, RankingProvider };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue