Spaces:
Running
Running
feat: Add shark example for quicker test / add time counter / add carousel (#77)
Browse files
- app/page.tsx +10 -2
- components/CodeResultDisplay.tsx +35 -21
- components/chat/ChatMessage.tsx +31 -7
- components/ui/carousel.tsx +262 -0
- lib/hooks/usePrevious.ts +20 -0
- package.json +1 -0
- pnpm-lock.yaml +28 -0
app/page.tsx
CHANGED
@@ -11,10 +11,18 @@ import { IconArrowUpRight } from '@/components/ui/Icons';
|
|
11 |
|
12 |
const EXAMPLES = [
|
13 |
{
|
14 |
-
title: 'Counting flowers',
|
15 |
mediaUrl:
|
16 |
'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png',
|
17 |
-
prompt:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
},
|
19 |
];
|
20 |
|
|
|
11 |
|
12 |
const EXAMPLES = [
|
13 |
{
|
14 |
+
title: 'Counting flowers in image',
|
15 |
mediaUrl:
|
16 |
'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png',
|
17 |
+
prompt:
|
18 |
+
'Draw box and output the image, return the number of flowers as output.',
|
19 |
+
},
|
20 |
+
{
|
21 |
+
title: 'Detecting sharks in video',
|
22 |
+
mediaUrl:
|
23 |
+
'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/shark3_short.mp4',
|
24 |
+
prompt:
|
25 |
+
'Can you detect any surfboards or sharks in the video, draw a green line between the shark and the nearest paddle boarder and add the distance between them in meters assuming 30 pixels is 1 meter. Make the line red if the shark is within 10 meters of a surfboard. Sample the video at 3 frames per second and save the output video as output.mp4.',
|
26 |
},
|
27 |
];
|
28 |
|
components/CodeResultDisplay.tsx
CHANGED
@@ -14,6 +14,14 @@ import { IconLog, IconTerminalWindow } from './ui/Icons';
|
|
14 |
import { Separator } from './ui/Separator';
|
15 |
import { ResultPayload } from '@/lib/types';
|
16 |
import Img from './ui/Img';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
export interface CodeResultDisplayProps {}
|
19 |
|
@@ -97,29 +105,35 @@ const CodeResultDisplay: React.FC<{
|
|
97 |
)}
|
98 |
{!!imageResults.length && (
|
99 |
<div className="p-4 text-xs lowercase bg-zinc-900 space-y-4 border-t border-muted">
|
100 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
<div className="flex flex-row space-x-4 overflow-auto">
|
102 |
{imageResults.map((png, index) => (
|
103 |
-
<
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
alt="result-image"
|
110 |
-
className="cursor-zoom-in"
|
111 |
-
/>
|
112 |
-
</DialogTrigger>
|
113 |
-
<DialogContent className="max-w-5xl">
|
114 |
-
<Img
|
115 |
-
src={png!}
|
116 |
-
width={1200}
|
117 |
-
height={800}
|
118 |
-
alt="result-image"
|
119 |
-
quality={100}
|
120 |
-
/>
|
121 |
-
</DialogContent>
|
122 |
-
</Dialog>
|
123 |
))}
|
124 |
</div>
|
125 |
</div>
|
|
|
14 |
import { Separator } from './ui/Separator';
|
15 |
import { ResultPayload } from '@/lib/types';
|
16 |
import Img from './ui/Img';
|
17 |
+
import {
|
18 |
+
Carousel,
|
19 |
+
CarouselContent,
|
20 |
+
CarouselItem,
|
21 |
+
CarouselNext,
|
22 |
+
CarouselPrevious,
|
23 |
+
} from './ui/carousel';
|
24 |
+
import Link from 'next/link';
|
25 |
|
26 |
export interface CodeResultDisplayProps {}
|
27 |
|
|
|
105 |
)}
|
106 |
{!!imageResults.length && (
|
107 |
<div className="p-4 text-xs lowercase bg-zinc-900 space-y-4 border-t border-muted">
|
108 |
+
<div className="flex items-center justify-between">
|
109 |
+
<p>image output</p>
|
110 |
+
<Dialog>
|
111 |
+
<DialogTrigger asChild>
|
112 |
+
<Button variant="ghost">View all</Button>
|
113 |
+
</DialogTrigger>
|
114 |
+
<DialogContent className="max-w-5xl flex justify-center items-center">
|
115 |
+
<Carousel className="w-3/4">
|
116 |
+
<CarouselContent>
|
117 |
+
{imageResults.map((png, index) => (
|
118 |
+
<CarouselItem key={'png' + index}>
|
119 |
+
<Img src={png!} width={1200} alt="result-image" />
|
120 |
+
</CarouselItem>
|
121 |
+
))}
|
122 |
+
</CarouselContent>
|
123 |
+
<CarouselPrevious />
|
124 |
+
<CarouselNext />
|
125 |
+
</Carousel>
|
126 |
+
</DialogContent>
|
127 |
+
</Dialog>
|
128 |
+
</div>
|
129 |
<div className="flex flex-row space-x-4 overflow-auto">
|
130 |
{imageResults.map((png, index) => (
|
131 |
+
<Img
|
132 |
+
key={'png' + index}
|
133 |
+
src={png!}
|
134 |
+
width={200}
|
135 |
+
alt="result-image"
|
136 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
))}
|
138 |
</div>
|
139 |
</div>
|
components/chat/ChatMessage.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { useMemo, useState } from 'react';
|
2 |
import { CodeBlock } from '@/components/ui/CodeBlock';
|
3 |
import {
|
4 |
IconCheckCircle,
|
@@ -29,6 +29,7 @@ import { selectedMessageId } from '@/state/chat';
|
|
29 |
import { Message } from '@prisma/client';
|
30 |
import { Separator } from '../ui/Separator';
|
31 |
import { cn } from '@/lib/utils';
|
|
|
32 |
|
33 |
export interface ChatMessageProps {
|
34 |
message: Message;
|
@@ -101,7 +102,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
101 |
{ChunkStatusToIconDict[section.status]}
|
102 |
</TableCell>
|
103 |
<TableCell className="font-medium">
|
104 |
-
{
|
105 |
</TableCell>
|
106 |
<TableCell className="text-right">
|
107 |
<ChunkPayloadAction payload={section.payload} />
|
@@ -130,11 +131,34 @@ const ChunkStatusToIconDict: Record<ChunkBody['status'], React.ReactElement> = {
|
|
130 |
running: <IconGlowingDot className="bg-teal-500/80" />,
|
131 |
failed: <IconCrossCircle className="text-red-500" />,
|
132 |
};
|
133 |
-
const
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
};
|
139 |
const ChunkPayloadAction: React.FC<{
|
140 |
payload: ChunkBody['payload'];
|
|
|
1 |
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
2 |
import { CodeBlock } from '@/components/ui/CodeBlock';
|
3 |
import {
|
4 |
IconCheckCircle,
|
|
|
29 |
import { Message } from '@prisma/client';
|
30 |
import { Separator } from '../ui/Separator';
|
31 |
import { cn } from '@/lib/utils';
|
32 |
+
import { usePrevious } from '@/lib/hooks/usePrevious';
|
33 |
|
34 |
export interface ChatMessageProps {
|
35 |
message: Message;
|
|
|
102 |
{ChunkStatusToIconDict[section.status]}
|
103 |
</TableCell>
|
104 |
<TableCell className="font-medium">
|
105 |
+
<ChunkTypeToText chunk={section} />
|
106 |
</TableCell>
|
107 |
<TableCell className="text-right">
|
108 |
<ChunkPayloadAction payload={section.payload} />
|
|
|
131 |
running: <IconGlowingDot className="bg-teal-500/80" />,
|
132 |
failed: <IconCrossCircle className="text-red-500" />,
|
133 |
};
|
134 |
+
const ChunkTypeToText: React.FC<{
|
135 |
+
chunk: ChunkBody;
|
136 |
+
}> = ({ chunk }) => {
|
137 |
+
const { status, type } = chunk;
|
138 |
+
|
139 |
+
const [seconds, setSeconds] = useState(0);
|
140 |
+
const isExecution = type === 'code' && status === 'running';
|
141 |
+
const timerId = useRef<NodeJS.Timeout>();
|
142 |
+
|
143 |
+
useEffect(() => {
|
144 |
+
if (isExecution) {
|
145 |
+
const timerId = setInterval(() => {
|
146 |
+
setSeconds(prevSeconds => Math.round((prevSeconds + 0.2) * 10) / 10);
|
147 |
+
}, 200);
|
148 |
+
return () => clearInterval(timerId);
|
149 |
+
}
|
150 |
+
}, [isExecution]);
|
151 |
+
|
152 |
+
if (type === 'plans') return <p>Creating instructions</p>;
|
153 |
+
if (type === 'tools') return <p>Retrieving tools</p>;
|
154 |
+
if (type === 'code' && status === 'started') return <p>Generating code</p>;
|
155 |
+
if (isExecution) return <p>Executing code ({seconds}s)</p>;
|
156 |
+
if (type === 'code' && status === 'completed')
|
157 |
+
return <p>Code execution success ({seconds}s)</p>;
|
158 |
+
if (type === 'code' && status === 'failed')
|
159 |
+
return <p>Code execution failure ({seconds}s)</p>;
|
160 |
+
|
161 |
+
return null;
|
162 |
};
|
163 |
const ChunkPayloadAction: React.FC<{
|
164 |
payload: ChunkBody['payload'];
|
components/ui/carousel.tsx
ADDED
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import * as React from 'react';
|
4 |
+
import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
|
5 |
+
import useEmblaCarousel, {
|
6 |
+
type UseEmblaCarouselType,
|
7 |
+
} from 'embla-carousel-react';
|
8 |
+
|
9 |
+
import { cn } from '@/lib/utils';
|
10 |
+
import { Button } from './Button';
|
11 |
+
|
12 |
+
type CarouselApi = UseEmblaCarouselType[1];
|
13 |
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
14 |
+
type CarouselOptions = UseCarouselParameters[0];
|
15 |
+
type CarouselPlugin = UseCarouselParameters[1];
|
16 |
+
|
17 |
+
type CarouselProps = {
|
18 |
+
opts?: CarouselOptions;
|
19 |
+
plugins?: CarouselPlugin;
|
20 |
+
orientation?: 'horizontal' | 'vertical';
|
21 |
+
setApi?: (api: CarouselApi) => void;
|
22 |
+
};
|
23 |
+
|
24 |
+
type CarouselContextProps = {
|
25 |
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
26 |
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
27 |
+
scrollPrev: () => void;
|
28 |
+
scrollNext: () => void;
|
29 |
+
canScrollPrev: boolean;
|
30 |
+
canScrollNext: boolean;
|
31 |
+
} & CarouselProps;
|
32 |
+
|
33 |
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
34 |
+
|
35 |
+
function useCarousel() {
|
36 |
+
const context = React.useContext(CarouselContext);
|
37 |
+
|
38 |
+
if (!context) {
|
39 |
+
throw new Error('useCarousel must be used within a <Carousel />');
|
40 |
+
}
|
41 |
+
|
42 |
+
return context;
|
43 |
+
}
|
44 |
+
|
45 |
+
const Carousel = React.forwardRef<
|
46 |
+
HTMLDivElement,
|
47 |
+
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
48 |
+
>(
|
49 |
+
(
|
50 |
+
{
|
51 |
+
orientation = 'horizontal',
|
52 |
+
opts,
|
53 |
+
setApi,
|
54 |
+
plugins,
|
55 |
+
className,
|
56 |
+
children,
|
57 |
+
...props
|
58 |
+
},
|
59 |
+
ref,
|
60 |
+
) => {
|
61 |
+
const [carouselRef, api] = useEmblaCarousel(
|
62 |
+
{
|
63 |
+
...opts,
|
64 |
+
axis: orientation === 'horizontal' ? 'x' : 'y',
|
65 |
+
},
|
66 |
+
plugins,
|
67 |
+
);
|
68 |
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
69 |
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
70 |
+
|
71 |
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
72 |
+
if (!api) {
|
73 |
+
return;
|
74 |
+
}
|
75 |
+
|
76 |
+
setCanScrollPrev(api.canScrollPrev());
|
77 |
+
setCanScrollNext(api.canScrollNext());
|
78 |
+
}, []);
|
79 |
+
|
80 |
+
const scrollPrev = React.useCallback(() => {
|
81 |
+
api?.scrollPrev();
|
82 |
+
}, [api]);
|
83 |
+
|
84 |
+
const scrollNext = React.useCallback(() => {
|
85 |
+
api?.scrollNext();
|
86 |
+
}, [api]);
|
87 |
+
|
88 |
+
const handleKeyDown = React.useCallback(
|
89 |
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
90 |
+
if (event.key === 'ArrowLeft') {
|
91 |
+
event.preventDefault();
|
92 |
+
scrollPrev();
|
93 |
+
} else if (event.key === 'ArrowRight') {
|
94 |
+
event.preventDefault();
|
95 |
+
scrollNext();
|
96 |
+
}
|
97 |
+
},
|
98 |
+
[scrollPrev, scrollNext],
|
99 |
+
);
|
100 |
+
|
101 |
+
React.useEffect(() => {
|
102 |
+
if (!api || !setApi) {
|
103 |
+
return;
|
104 |
+
}
|
105 |
+
|
106 |
+
setApi(api);
|
107 |
+
}, [api, setApi]);
|
108 |
+
|
109 |
+
React.useEffect(() => {
|
110 |
+
if (!api) {
|
111 |
+
return;
|
112 |
+
}
|
113 |
+
|
114 |
+
onSelect(api);
|
115 |
+
api.on('reInit', onSelect);
|
116 |
+
api.on('select', onSelect);
|
117 |
+
|
118 |
+
return () => {
|
119 |
+
api?.off('select', onSelect);
|
120 |
+
};
|
121 |
+
}, [api, onSelect]);
|
122 |
+
|
123 |
+
return (
|
124 |
+
<CarouselContext.Provider
|
125 |
+
value={{
|
126 |
+
carouselRef,
|
127 |
+
api: api,
|
128 |
+
opts,
|
129 |
+
orientation:
|
130 |
+
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
131 |
+
scrollPrev,
|
132 |
+
scrollNext,
|
133 |
+
canScrollPrev,
|
134 |
+
canScrollNext,
|
135 |
+
}}
|
136 |
+
>
|
137 |
+
<div
|
138 |
+
ref={ref}
|
139 |
+
onKeyDownCapture={handleKeyDown}
|
140 |
+
className={cn('relative', className)}
|
141 |
+
role="region"
|
142 |
+
aria-roledescription="carousel"
|
143 |
+
{...props}
|
144 |
+
>
|
145 |
+
{children}
|
146 |
+
</div>
|
147 |
+
</CarouselContext.Provider>
|
148 |
+
);
|
149 |
+
},
|
150 |
+
);
|
151 |
+
Carousel.displayName = 'Carousel';
|
152 |
+
|
153 |
+
const CarouselContent = React.forwardRef<
|
154 |
+
HTMLDivElement,
|
155 |
+
React.HTMLAttributes<HTMLDivElement>
|
156 |
+
>(({ className, ...props }, ref) => {
|
157 |
+
const { carouselRef, orientation } = useCarousel();
|
158 |
+
|
159 |
+
return (
|
160 |
+
<div ref={carouselRef} className="overflow-hidden">
|
161 |
+
<div
|
162 |
+
ref={ref}
|
163 |
+
className={cn(
|
164 |
+
'flex',
|
165 |
+
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
166 |
+
className,
|
167 |
+
)}
|
168 |
+
{...props}
|
169 |
+
/>
|
170 |
+
</div>
|
171 |
+
);
|
172 |
+
});
|
173 |
+
CarouselContent.displayName = 'CarouselContent';
|
174 |
+
|
175 |
+
const CarouselItem = React.forwardRef<
|
176 |
+
HTMLDivElement,
|
177 |
+
React.HTMLAttributes<HTMLDivElement>
|
178 |
+
>(({ className, ...props }, ref) => {
|
179 |
+
const { orientation } = useCarousel();
|
180 |
+
|
181 |
+
return (
|
182 |
+
<div
|
183 |
+
ref={ref}
|
184 |
+
role="group"
|
185 |
+
aria-roledescription="slide"
|
186 |
+
className={cn(
|
187 |
+
'min-w-0 shrink-0 grow-0 basis-full',
|
188 |
+
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
189 |
+
className,
|
190 |
+
)}
|
191 |
+
{...props}
|
192 |
+
/>
|
193 |
+
);
|
194 |
+
});
|
195 |
+
CarouselItem.displayName = 'CarouselItem';
|
196 |
+
|
197 |
+
const CarouselPrevious = React.forwardRef<
|
198 |
+
HTMLButtonElement,
|
199 |
+
React.ComponentProps<typeof Button>
|
200 |
+
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
201 |
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
202 |
+
|
203 |
+
return (
|
204 |
+
<Button
|
205 |
+
ref={ref}
|
206 |
+
variant={variant}
|
207 |
+
size={size}
|
208 |
+
className={cn(
|
209 |
+
'absolute h-8 w-8 rounded-full',
|
210 |
+
orientation === 'horizontal'
|
211 |
+
? '-left-12 top-1/2 -translate-y-1/2'
|
212 |
+
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
213 |
+
className,
|
214 |
+
)}
|
215 |
+
disabled={!canScrollPrev}
|
216 |
+
onClick={scrollPrev}
|
217 |
+
{...props}
|
218 |
+
>
|
219 |
+
<ArrowLeftIcon className="h-4 w-4" />
|
220 |
+
<span className="sr-only">Previous slide</span>
|
221 |
+
</Button>
|
222 |
+
);
|
223 |
+
});
|
224 |
+
CarouselPrevious.displayName = 'CarouselPrevious';
|
225 |
+
|
226 |
+
const CarouselNext = React.forwardRef<
|
227 |
+
HTMLButtonElement,
|
228 |
+
React.ComponentProps<typeof Button>
|
229 |
+
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
230 |
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
231 |
+
|
232 |
+
return (
|
233 |
+
<Button
|
234 |
+
ref={ref}
|
235 |
+
variant={variant}
|
236 |
+
size={size}
|
237 |
+
className={cn(
|
238 |
+
'absolute h-8 w-8 rounded-full',
|
239 |
+
orientation === 'horizontal'
|
240 |
+
? '-right-12 top-1/2 -translate-y-1/2'
|
241 |
+
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
242 |
+
className,
|
243 |
+
)}
|
244 |
+
disabled={!canScrollNext}
|
245 |
+
onClick={scrollNext}
|
246 |
+
{...props}
|
247 |
+
>
|
248 |
+
<ArrowRightIcon className="h-4 w-4" />
|
249 |
+
<span className="sr-only">Next slide</span>
|
250 |
+
</Button>
|
251 |
+
);
|
252 |
+
});
|
253 |
+
CarouselNext.displayName = 'CarouselNext';
|
254 |
+
|
255 |
+
export {
|
256 |
+
type CarouselApi,
|
257 |
+
Carousel,
|
258 |
+
CarouselContent,
|
259 |
+
CarouselItem,
|
260 |
+
CarouselPrevious,
|
261 |
+
CarouselNext,
|
262 |
+
};
|
lib/hooks/usePrevious.ts
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useRef } from 'react';
|
2 |
+
|
3 |
+
/**
|
4 |
+
* A simple hook to return the previous state of a useState
|
5 |
+
* https://usehooks.com/usePrevious/
|
6 |
+
* @param value the current state
|
7 |
+
* @returns the previous state
|
8 |
+
*/
|
9 |
+
export const usePrevious = <T>(value: T) => {
|
10 |
+
// The ref object is a generic container whose current property is mutable ...
|
11 |
+
// ... and can hold any value, similar to an instance property on a class
|
12 |
+
const ref = useRef<T>();
|
13 |
+
|
14 |
+
// Store current value in ref
|
15 |
+
useEffect(() => {
|
16 |
+
ref.current = value;
|
17 |
+
}, [value]); // Only re-run if value changes
|
18 |
+
// Return previous value (happens before update in useEffect above)
|
19 |
+
return ref.current;
|
20 |
+
};
|
package.json
CHANGED
@@ -32,6 +32,7 @@
|
|
32 |
"class-variance-authority": "^0.7.0",
|
33 |
"clsx": "^2.1.0",
|
34 |
"date-fns": "^3.6.0",
|
|
|
35 |
"focus-trap-react": "^10.2.3",
|
36 |
"framer-motion": "^10.18.0",
|
37 |
"geist": "^1.2.1",
|
|
|
32 |
"class-variance-authority": "^0.7.0",
|
33 |
"clsx": "^2.1.0",
|
34 |
"date-fns": "^3.6.0",
|
35 |
+
"embla-carousel-react": "^8.1.3",
|
36 |
"focus-trap-react": "^10.2.3",
|
37 |
"framer-motion": "^10.18.0",
|
38 |
"geist": "^1.2.1",
|
pnpm-lock.yaml
CHANGED
@@ -62,6 +62,9 @@ importers:
|
|
62 |
date-fns:
|
63 |
specifier: ^3.6.0
|
64 |
version: 3.6.0
|
|
|
|
|
|
|
65 |
focus-trap-react:
|
66 |
specifier: ^10.2.3
|
67 |
version: 10.2.3(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
@@ -1900,6 +1903,19 @@ packages:
|
|
1900 |
electron-to-chromium@1.4.789:
|
1901 |
resolution: {integrity: sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ==}
|
1902 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1903 |
emoji-regex@8.0.0:
|
1904 |
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
1905 |
|
@@ -6154,6 +6170,18 @@ snapshots:
|
|
6154 |
|
6155 |
electron-to-chromium@1.4.789: {}
|
6156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6157 |
emoji-regex@8.0.0: {}
|
6158 |
|
6159 |
emoji-regex@9.2.2: {}
|
|
|
62 |
date-fns:
|
63 |
specifier: ^3.6.0
|
64 |
version: 3.6.0
|
65 |
+
embla-carousel-react:
|
66 |
+
specifier: ^8.1.3
|
67 |
+
version: 8.1.3(react@18.2.0)
|
68 |
focus-trap-react:
|
69 |
specifier: ^10.2.3
|
70 |
version: 10.2.3(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
|
|
1903 |
electron-to-chromium@1.4.789:
|
1904 |
resolution: {integrity: sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ==}
|
1905 |
|
1906 |
+
embla-carousel-react@8.1.3:
|
1907 |
+
resolution: {integrity: sha512-YrezDPgxPDKa+OKMhSrwuPEU2OgF5147vFW473EWT3bx9DETV3W/RyWTxq0/2pf3M4VXkjqFNbS/W1xM8lTaVg==}
|
1908 |
+
peerDependencies:
|
1909 |
+
react: ^16.8.0 || ^17.0.1 || ^18.0.0
|
1910 |
+
|
1911 |
+
embla-carousel-reactive-utils@8.1.3:
|
1912 |
+
resolution: {integrity: sha512-D8tAK6NRQVEubMWb+b/BJ3VvGPsbEeEFOBM6cCCwfiyfLzNlacOAt0q2dtUEA9DbGxeWkB8ExgXzFRxhGV2Hig==}
|
1913 |
+
peerDependencies:
|
1914 |
+
embla-carousel: 8.1.3
|
1915 |
+
|
1916 |
+
embla-carousel@8.1.3:
|
1917 |
+
resolution: {integrity: sha512-GiRpKtzidV3v50oVMly8S+D7iE1r96ttt7fSlvtyKHoSkzrAnVcu8fX3c4j8Ol2hZSQlVfDqDIqdrFPs0u5TWQ==}
|
1918 |
+
|
1919 |
emoji-regex@8.0.0:
|
1920 |
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
1921 |
|
|
|
6170 |
|
6171 |
electron-to-chromium@1.4.789: {}
|
6172 |
|
6173 |
+
embla-carousel-react@8.1.3(react@18.2.0):
|
6174 |
+
dependencies:
|
6175 |
+
embla-carousel: 8.1.3
|
6176 |
+
embla-carousel-reactive-utils: 8.1.3(embla-carousel@8.1.3)
|
6177 |
+
react: 18.2.0
|
6178 |
+
|
6179 |
+
embla-carousel-reactive-utils@8.1.3(embla-carousel@8.1.3):
|
6180 |
+
dependencies:
|
6181 |
+
embla-carousel: 8.1.3
|
6182 |
+
|
6183 |
+
embla-carousel@8.1.3: {}
|
6184 |
+
|
6185 |
emoji-regex@8.0.0: {}
|
6186 |
|
6187 |
emoji-regex@9.2.2: {}
|