MingruiZhang commited on
Commit
abc1963
·
unverified ·
1 Parent(s): ae074fc

feat: Add shark example for quicker test / add time counter / add carousel (#77)

Browse files

![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/d197d4df-70e6-4b9a-9b65-5b23e775f74a)

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: 'Count the number of flowers in this image.',
 
 
 
 
 
 
 
 
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
- <p>image output</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  <div className="flex flex-row space-x-4 overflow-auto">
102
  {imageResults.map((png, index) => (
103
- <Dialog key={'png' + index}>
104
- <DialogTrigger asChild>
105
- <Img
106
- key={'png' + index}
107
- src={png!}
108
- width={200}
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
- {ChunkTypeToTextDict[section.type]}
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 ChunkTypeToTextDict: Record<ChunkBody['type'], string> = {
134
- plans: 'Creating instructions',
135
- tools: 'Retrieving tools',
136
- code: 'Generating code',
137
- final_code: 'Final result',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: {}