| import React from 'react'; | |
| import { format } from 'date-fns'; | |
| import { Layers3, Crown, Zap } from 'lucide-react'; | |
| import { Tag, TooltipAnchor, Label } from '@librechat/client'; | |
| import type { TPrompt, TPromptGroup } from 'librechat-data-provider'; | |
| import { useLocalize } from '~/hooks'; | |
| import { cn } from '~/utils'; | |
| const CombinedStatusIcon = ({ description }: { description: string }) => ( | |
| <TooltipAnchor | |
| description={description} | |
| aria-label={description} | |
| render={ | |
| <div className="flex items-center justify-center"> | |
| <Crown className="h-4 w-4 text-amber-500" /> | |
| </div> | |
| } | |
| ></TooltipAnchor> | |
| ); | |
| const VersionTags = ({ tags }: { tags: string[] }) => { | |
| const localize = useLocalize(); | |
| const isLatestAndProduction = tags.includes('latest') && tags.includes('production'); | |
| if (isLatestAndProduction) { | |
| return ( | |
| <span className="absolute bottom-3 right-3"> | |
| <CombinedStatusIcon description={localize('com_ui_latest_production_version')} /> | |
| </span> | |
| ); | |
| } | |
| return ( | |
| <span className="flex gap-1 text-sm"> | |
| {tags.map((tag, i) => ( | |
| <TooltipAnchor | |
| description={ | |
| tag === 'production' | |
| ? localize('com_ui_currently_production') | |
| : localize('com_ui_latest_version') | |
| } | |
| key={`${tag}-${i}`} | |
| aria-label={ | |
| tag === 'production' | |
| ? localize('com_ui_currently_production') | |
| : localize('com_ui_latest_version') | |
| } | |
| render={ | |
| <Tag | |
| label={tag} | |
| className={cn( | |
| 'w-24 justify-center border border-transparent', | |
| tag === 'production' | |
| ? 'bg-green-100 text-green-500 dark:border-green-500 dark:bg-transparent dark:text-green-500' | |
| : 'bg-blue-100 text-blue-500 dark:border-blue-500 dark:bg-transparent dark:text-blue-500', | |
| )} | |
| labelClassName="flex items-center m-0 justify-center gap-1" | |
| LabelNode={(() => { | |
| if (tag === 'production') { | |
| return ( | |
| <div className="flex items-center"> | |
| <span className="slow-pulse size-2 rounded-full bg-green-400" /> | |
| </div> | |
| ); | |
| } | |
| if (tag === 'latest') { | |
| return ( | |
| <div className="flex items-center"> | |
| <Zap className="size-4" /> | |
| </div> | |
| ); | |
| } | |
| return null; | |
| })()} | |
| /> | |
| } | |
| ></TooltipAnchor> | |
| ))} | |
| </span> | |
| ); | |
| }; | |
| const VersionCard = ({ | |
| prompt, | |
| index, | |
| isSelected, | |
| totalVersions, | |
| onClick, | |
| authorName, | |
| tags, | |
| }: { | |
| prompt: TPrompt; | |
| index: number; | |
| isSelected: boolean; | |
| totalVersions: number; | |
| onClick: () => void; | |
| authorName?: string; | |
| tags: string[]; | |
| }) => { | |
| const localize = useLocalize(); | |
| return ( | |
| <button | |
| type="button" | |
| className={cn( | |
| 'group relative w-full rounded-lg border border-border-light p-4 transition-all duration-300', | |
| isSelected | |
| ? 'bg-surface-hover shadow-xl' | |
| : 'bg-surface-primary shadow-sm hover:bg-surface-secondary', | |
| )} | |
| onClick={onClick} | |
| aria-selected={isSelected} | |
| role="tab" | |
| aria-label={localize('com_ui_version_var', { 0: `${totalVersions - index}` })} | |
| > | |
| <div className="flex flex-col gap-2"> | |
| <div className="flex items-start justify-between lg:flex-col xl:flex-row"> | |
| <h3 className="font-bold text-text-primary"> | |
| {localize('com_ui_version_var', { 0: `${totalVersions - index}` })} | |
| </h3> | |
| <time className="text-xs text-text-secondary" dateTime={prompt.createdAt}> | |
| {format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')} | |
| </time> | |
| </div> | |
| <div className="flex items-center gap-1 lg:flex-col xl:flex-row"> | |
| {authorName && ( | |
| <Label className="text-left text-xs text-text-secondary">by {authorName}</Label> | |
| )} | |
| {tags.length > 0 && <VersionTags tags={tags} />} | |
| </div> | |
| </div> | |
| </button> | |
| ); | |
| }; | |
| const PromptVersions = ({ | |
| prompts, | |
| group, | |
| selectionIndex, | |
| setSelectionIndex, | |
| }: { | |
| prompts: TPrompt[]; | |
| group?: TPromptGroup; | |
| selectionIndex: number; | |
| setSelectionIndex: React.Dispatch<React.SetStateAction<number>>; | |
| }) => { | |
| const localize = useLocalize(); | |
| return ( | |
| <section className="my-6" aria-label="Prompt Versions"> | |
| <header className="mb-6"> | |
| <h2 className="flex items-center gap-2 text-base font-semibold text-text-primary"> | |
| <Layers3 className="h-5 w-5 text-green-500" /> | |
| {localize('com_ui_versions')} | |
| </h2> | |
| </header> | |
| <div className="flex flex-col gap-3" role="tablist" aria-label="Version history"> | |
| {prompts.map((prompt: TPrompt, index: number) => { | |
| const tags: string[] = []; | |
| if (index === 0) { | |
| tags.push('latest'); | |
| } | |
| if (prompt._id === group?.productionId) { | |
| tags.push('production'); | |
| } | |
| return ( | |
| <VersionCard | |
| key={prompt._id} | |
| prompt={prompt} | |
| index={index} | |
| isSelected={index === selectionIndex} | |
| totalVersions={prompts.length} | |
| onClick={() => setSelectionIndex(index)} | |
| authorName={group?.authorName} | |
| tags={tags} | |
| /> | |
| ); | |
| })} | |
| </div> | |
| </section> | |
| ); | |
| }; | |
| export default PromptVersions; | |