Files
upage-git/app/components/chat/ProgressCompilation.tsx
2025-09-24 17:02:44 +08:00

119 lines
3.8 KiB
TypeScript

import classNames from 'classnames';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useState } from 'react';
import type { ProgressAnnotation } from '~/types/message';
import { cubicEasingFn } from '~/utils/easings';
export default function ProgressCompilation({ data }: { data?: ProgressAnnotation[] }) {
const [expanded, setExpanded] = useState(false);
const progressList = useMemo(() => {
if (!data || data.length == 0) {
return [];
}
const progressMap = new Map<string, ProgressAnnotation>();
data.forEach((x) => {
const existingProgress = progressMap.get(x.label);
if (existingProgress && existingProgress.status === 'complete') {
return;
}
progressMap.set(x.label, x);
});
return Array.from(progressMap.values()).sort((a, b) => a.order - b.order);
}, [data]);
if (progressList.length === 0) {
return <></>;
}
return (
<AnimatePresence>
<div
className={classNames(
'bg-upage-elements-background-depth-2',
'border border-upage-elements-borderColor',
'shadow-lg rounded-lg relative w-full max-w-chat mx-auto z-prompt',
'p-1',
)}
>
<div
className={classNames(
'bg-upage-elements-item-backgroundAccent',
'py-1 px-1.5 rounded-md text-upage-elements-item-contentAccent',
'flex items-center',
)}
>
<div className="flex-1">
<AnimatePresence>
{expanded ? (
<motion.div
className="actions"
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: '0px' }}
transition={{ duration: 0.15 }}
>
{progressList.map((x, i) => {
return <ProgressItem key={i} progress={x} />;
})}
</motion.div>
) : (
<ProgressItem progress={progressList.slice(-1)[0]} />
)}
</AnimatePresence>
</div>
{progressList.length > 1 && (
<motion.button
initial={{ width: 0 }}
animate={{ width: 'auto' }}
exit={{ width: 0 }}
transition={{ duration: 0.15, ease: cubicEasingFn }}
className="p-1 rounded-lg bg-upage-elements-item-backgroundAccent hover:bg-upage-elements-artifacts-backgroundHover"
onClick={() => setExpanded((v) => !v)}
>
<div className={expanded ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
</motion.button>
)}
</div>
</div>
</AnimatePresence>
);
}
interface ProgressItemProps {
progress: ProgressAnnotation;
}
const ProgressItem = ({ progress }: ProgressItemProps) => {
return (
<motion.div
className={classNames('flex text-sm gap-3 items-center justify-between', {
'text-upage-elements-textSuccess': progress.status === 'complete',
'text-upage-elements-textError': progress.status === 'stopped',
})}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<div className="flex items-center gap-1.5">
<div>
{progress.status === 'in-progress' ? (
<div className="i-svg-spinners:90-ring-with-bg"></div>
) : progress.status === 'complete' ? (
<div className="i-ph:check"></div>
) : progress.status === 'stopped' ? (
<div className="i-ph:x"></div>
) : progress.status === 'warning' ? (
<div className="i-ph:warning"></div>
) : null}
</div>
{progress.message}
</div>
</motion.div>
);
};