MingruiZhang commited on
Commit
1f931d5
1 Parent(s): 4ec47c5

feat: Composer refactor and layout scroll (#62)

Browse files

![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/6f92a2d2-0f4d-49a5-ab3d-770706e3b0bf)

app/layout.tsx CHANGED
@@ -53,7 +53,9 @@ export default function RootLayout(props: RootLayoutProps) {
53
  >
54
  <div className="flex flex-col min-h-screen">
55
  <Header />
56
- <main className="flex flex-col flex-1 bg-muted/50">{children}</main>
 
 
57
  </div>
58
  <TailwindIndicator />
59
  </Providers>
 
53
  >
54
  <div className="flex flex-col min-h-screen">
55
  <Header />
56
+ <main className="flex max-h-[calc(100vh-64px)] flex-col flex-1 bg-muted/50 overflow-hidden">
57
+ {children}
58
+ </main>
59
  </div>
60
  <TailwindIndicator />
61
  </Providers>
components/chat/ChatClient.tsx CHANGED
@@ -1,20 +1,19 @@
1
  'use client';
2
 
3
- import { ChatList } from '@/components/chat/ChatList';
4
  import { Composer } from '@/components/chat/Composer';
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
  import { useState } from 'react';
9
  import { ChatWithMessages } from '@/lib/db/types';
 
10
 
11
- export interface ChatProps extends React.ComponentProps<'div'> {
12
  chat: ChatWithMessages;
13
- isAdminView?: boolean;
14
- session: Session | null;
15
  }
16
 
17
- export function Chat({ chat, session, isAdminView }: ChatProps) {
18
  const { mediaUrl, id } = chat;
19
  const { messages, append, reload, stop, isLoading, input, setInput } =
20
  useVisionAgent(chat);
@@ -23,34 +22,37 @@ export function Chat({ chat, session, isAdminView }: ChatProps) {
23
  useScrollAnchor();
24
 
25
  return (
26
- <>
27
- <div className="h-full overflow-auto relative" ref={scrollRef}>
28
- <div className="pb-[200px] pt-6" ref={messagesRef}>
29
- <ChatList
30
- messages={messages}
31
- session={session}
32
- isLoading={isLoading}
33
- />
34
- <div className="h-px w-full" ref={visibilityRef} />
35
- </div>
 
 
 
 
 
36
  </div>
37
- {!isAdminView && (
38
- <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
39
- <Composer
40
- id={id}
41
- url={mediaUrl}
42
- isLoading={isLoading}
43
- stop={stop}
44
- append={append}
45
- reload={reload}
46
- messages={messages}
47
- input={input}
48
- setInput={setInput}
49
- isAtBottom={isAtBottom}
50
- scrollToBottom={scrollToBottom}
51
- />
52
- </div>
53
- )}
54
- </>
55
  );
56
- }
 
 
 
1
  'use client';
2
 
3
+ // import { ChatList } from '@/components/chat/ChatList';
4
  import { Composer } from '@/components/chat/Composer';
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
  import { useState } from 'react';
9
  import { ChatWithMessages } from '@/lib/db/types';
10
+ import { ChatMessage } from './ChatMessage';
11
 
12
+ export interface ChatClientProps {
13
  chat: ChatWithMessages;
 
 
14
  }
15
 
16
+ const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
17
  const { mediaUrl, id } = chat;
18
  const { messages, append, reload, stop, isLoading, input, setInput } =
19
  useVisionAgent(chat);
 
22
  useScrollAnchor();
23
 
24
  return (
25
+ <div
26
+ className="h-full my-8 overflow-auto mx-auto max-w-5xl border rounded-lg relative"
27
+ ref={scrollRef}
28
+ >
29
+ <div className="overflow-auto h-full pt-6 px-6" ref={messagesRef}>
30
+ {messages
31
+ // .filter(message => message.role !== 'system')
32
+ .map((message, index) => (
33
+ <ChatMessage
34
+ key={index}
35
+ message={message}
36
+ isLoading={isLoading && index === messages.length - 1}
37
+ />
38
+ ))}
39
+ <div className="h-px w-full" ref={visibilityRef} />
40
  </div>
41
+ <div className="sticky bottom-3 w-full">
42
+ <Composer
43
+ id={id}
44
+ mediaUrl={mediaUrl}
45
+ isLoading={isLoading}
46
+ stop={stop}
47
+ append={append}
48
+ reload={reload}
49
+ messages={messages}
50
+ input={input}
51
+ setInput={setInput}
52
+ />
53
+ </div>
54
+ </div>
 
 
 
 
55
  );
56
+ };
57
+
58
+ export default ChatClient;
components/chat/ChatServer.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Chat } from './ChatClient';
2
  import { auth } from '@/auth';
3
  import { dbGetChat } from '@/lib/db/functions';
4
  import { redirect } from 'next/navigation';
@@ -15,6 +15,5 @@ export default async function ChatServer({ id }: ChatServerProps) {
15
  revalidatePath('/');
16
  redirect('/');
17
  }
18
- const session = await auth();
19
- return <Chat chat={chat} session={session} />;
20
  }
 
1
+ import ChatClient from './ChatClient';
2
  import { auth } from '@/auth';
3
  import { dbGetChat } from '@/lib/db/functions';
4
  import { redirect } from 'next/navigation';
 
15
  revalidatePath('/');
16
  redirect('/');
17
  }
18
+ return <ChatClient chat={chat} />;
 
19
  }
components/chat/Composer.tsx CHANGED
@@ -16,12 +16,14 @@ import {
16
  import {
17
  IconArrowDown,
18
  IconArrowElbow,
 
19
  IconRefresh,
20
  IconStop,
21
  } from '@/components/ui/Icons';
22
  import { cn } from '@/lib/utils';
23
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
24
  import { Switch } from '../ui/Switch';
 
25
 
26
  export interface ComposerProps
27
  extends Pick<
@@ -31,24 +33,17 @@ export interface ComposerProps
31
  id?: string;
32
  title?: string;
33
  messages: MessageBase[];
34
- url?: string;
35
- isAtBottom: boolean;
36
- scrollToBottom: () => void;
37
  }
38
 
39
  export function Composer({
40
  id,
41
- title,
42
  isLoading,
43
- stop,
44
  append,
45
- reload,
46
  input,
47
  setInput,
48
- messages,
49
- isAtBottom,
50
- scrollToBottom,
51
- url,
52
  }: ComposerProps) {
53
  const { formRef, onKeyDown } = useEnterSubmit();
54
  const inputRef = React.useRef<HTMLTextAreaElement>(null);
@@ -58,92 +53,62 @@ export function Composer({
58
  }
59
  }, []);
60
 
 
61
  return (
62
- // <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
63
- <div className="mx-auto sm:max-w-3xl sm:px-4 h-full">
64
- <div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4 h-full">
65
- <form
66
- onSubmit={async e => {
67
- e.preventDefault();
68
- if (!input?.trim()) {
69
- return;
70
- }
71
- setInput('');
72
- await append({
73
- id,
74
- content:
75
- input + (url ? '\n\n' + generateInputImageMarkdown(url) : ''),
76
- role: 'user',
77
- });
78
- scrollToBottom();
79
- }}
80
- ref={formRef}
81
- className="h-full"
82
- >
83
- <div className="relative flex px-8 pl-2 overflow-hidden size-full bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2 items-start">
84
- {url && (
85
- <div className="w-1/5 p-2 h-full flex items-center justify-center relative">
86
- <Tooltip>
87
- <TooltipTrigger asChild>
88
- <Img
89
- src={url}
90
- className="cursor-zoom-in"
91
- alt="preview-image"
92
- />
93
- </TooltipTrigger>
94
- <TooltipContent>
95
- <Img
96
- src={url}
97
- className="m-2"
98
- quality={100}
99
- width={500}
100
- alt="zoomed-in-image"
101
- />
102
- </TooltipContent>
103
- </Tooltip>
104
  </div>
105
- )}
106
- <div className="flex flex-col gap-2 w-4/5 px-4">
107
- <Textarea
108
- ref={inputRef}
109
- tabIndex={0}
110
- onKeyDown={onKeyDown}
111
- rows={1}
112
- value={input}
113
- disabled={isLoading}
114
- onChange={e => setInput(e.target.value)}
115
- placeholder={
116
- isLoading
117
- ? 'Vision Agent is thinking...'
118
- : 'Ask question about the image.'
119
- }
120
- spellCheck={false}
121
- className="min-h-[60px] resize-none bg-transparent py-[1.3em] focus-within:outline-none sm:text-sm"
122
- />
123
- </div>
124
- {/* Scroll to bottom Icon */}
125
- <div
126
- className={cn(
127
- 'absolute top-3 right-4 transition-opacity duration-300',
128
- isAtBottom ? 'opacity-0' : 'opacity-100',
129
- )}
130
- >
131
- <Tooltip>
132
- <TooltipTrigger asChild>
133
- <Button
134
- variant="outline"
135
- size="icon"
136
- className="bg-background"
137
- onClick={() => scrollToBottom()}
138
- >
139
- <IconArrowDown />
140
- </Button>
141
- </TooltipTrigger>
142
- <TooltipContent>Scroll to bottom</TooltipContent>
143
- </Tooltip>
144
- </div>
145
- {/* Stop / Regenerate Icon */}
146
- <div className="absolute bottom-14 right-4">
 
 
 
147
  {isLoading ? (
148
  <Tooltip>
149
  <TooltipTrigger asChild>
@@ -175,26 +140,39 @@ export function Composer({
175
  </Tooltip>
176
  )
177
  )}
178
- </div>
179
- {/* Submit Icon */}
180
- <div className="absolute bottom-3 right-4">
181
- <Tooltip>
182
- <TooltipTrigger asChild>
183
- <Button
184
- type="submit"
185
- size="icon"
186
- disabled={isLoading || input === ''}
187
- >
188
- <IconArrowElbow />
189
- </Button>
190
- </TooltipTrigger>
191
- <TooltipContent>Send message</TooltipContent>
192
- </Tooltip>
193
- </div>
194
- </div>
195
- </form>
196
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  </div>
198
- // </div>
199
  );
200
  }
 
16
  import {
17
  IconArrowDown,
18
  IconArrowElbow,
19
+ IconImage,
20
  IconRefresh,
21
  IconStop,
22
  } from '@/components/ui/Icons';
23
  import { cn } from '@/lib/utils';
24
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
25
  import { Switch } from '../ui/Switch';
26
+ import Chip from '../ui/Chip';
27
 
28
  export interface ComposerProps
29
  extends Pick<
 
33
  id?: string;
34
  title?: string;
35
  messages: MessageBase[];
36
+ mediaUrl?: string;
 
 
37
  }
38
 
39
  export function Composer({
40
  id,
 
41
  isLoading,
 
42
  append,
 
43
  input,
44
  setInput,
45
+ mediaUrl,
46
+ // isAtBottom,
 
 
47
  }: ComposerProps) {
48
  const { formRef, onKeyDown } = useEnterSubmit();
49
  const inputRef = React.useRef<HTMLTextAreaElement>(null);
 
53
  }
54
  }, []);
55
 
56
+ const mediaName = mediaUrl?.split('/').pop();
57
  return (
58
+ <div className="size-full mx-auto max-w-2xl px-6 py-3 space-y-4 bg-zinc-700 rounded-xl relative shadow-lg shadow-zinc-800/40">
59
+ {mediaUrl && (
60
+ <Tooltip>
61
+ <TooltipTrigger>
62
+ <Chip>
63
+ <div className="flex flex-row items-center">
64
+ <IconImage className="size-3 mr-1" />
65
+ <p>{mediaName ?? 'media(0)'}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
+ </Chip>
68
+ </TooltipTrigger>
69
+ <TooltipContent sideOffset={20}>
70
+ <Img
71
+ src={mediaUrl}
72
+ className="m-1"
73
+ quality={100}
74
+ alt="zoomed-in-image"
75
+ />
76
+ </TooltipContent>
77
+ </Tooltip>
78
+ )}
79
+ <form
80
+ onSubmit={async e => {
81
+ e.preventDefault();
82
+ if (!input?.trim()) {
83
+ return;
84
+ }
85
+ setInput('');
86
+ await append({
87
+ id,
88
+ content:
89
+ input +
90
+ (mediaUrl ? '\n\n' + generateInputImageMarkdown(mediaUrl) : ''),
91
+ role: 'user',
92
+ });
93
+ }}
94
+ ref={formRef}
95
+ className="h-full"
96
+ >
97
+ {/* <div className="border-gray-500 flex overflow-hidden size-full flex flex-row items-center"> */}
98
+ <Textarea
99
+ ref={inputRef}
100
+ tabIndex={0}
101
+ onKeyDown={onKeyDown}
102
+ rows={1}
103
+ value={input}
104
+ disabled={isLoading}
105
+ onChange={e => setInput(e.target.value)}
106
+ placeholder={isLoading ? '🤖 ✨ ...' : 'Message Vision Agent'}
107
+ spellCheck={false}
108
+ className="grow resize-none bg-transparent focus-within:outline-none text-sm"
109
+ />
110
+ {/* Stop / Regenerate Icon */}
111
+ {/* <div className="absolute bottom-14 right-4">
112
  {isLoading ? (
113
  <Tooltip>
114
  <TooltipTrigger asChild>
 
140
  </Tooltip>
141
  )
142
  )}
143
+ </div> */}
144
+ {/* </div> */}
145
+ {/* Submit Icon */}
146
+ <Tooltip>
147
+ <TooltipTrigger asChild>
148
+ <Button
149
+ type="submit"
150
+ size="icon"
151
+ className="size-6 absolute bottom-3 right-3"
152
+ disabled={isLoading || input === ''}
153
+ >
154
+ <IconArrowElbow className="size-3" />
155
+ </Button>
156
+ </TooltipTrigger>
157
+ <TooltipContent>Send message</TooltipContent>
158
+ </Tooltip>
159
+ </form>
160
+ {/* Scroll to bottom Icon */}
161
+ {/* <Tooltip>
162
+ <TooltipTrigger asChild>
163
+ <Button
164
+ size="icon"
165
+ className={cn(
166
+ 'absolute top-1 right-3 transition-opacity duration-300 size-6',
167
+ isAtBottom ? 'opacity-0' : 'opacity-100',
168
+ )}
169
+ onClick={() => scrollToBottom()}
170
+ >
171
+ <IconArrowDown className="size-3" />
172
+ </Button>
173
+ </TooltipTrigger>
174
+ <TooltipContent>Scroll to bottom</TooltipContent>
175
+ </Tooltip> */}
176
  </div>
 
177
  );
178
  }
components/ui/Chip.tsx CHANGED
@@ -1,28 +1,29 @@
1
  import { cn } from '@/lib/utils';
 
2
 
3
- export interface ChipProps {
4
  label?: string;
5
- value: string;
6
  color?: 'gray' | 'blue' | 'yellow' | 'purple';
7
  className?: string;
8
- }
9
 
10
  const Chip: React.FC<ChipProps> = ({
11
- label,
12
  value,
13
  className,
14
  color = 'gray',
 
15
  }) => {
16
  return (
17
  <div
18
  className={cn(
19
- 'inline-flex items-center px-1.5 rounded-full text-xs mr-2 bg-gray-100 text-gray-500',
20
  `bg-${color}-100 text-${color}-500`,
21
  className,
22
  )}
23
  >
24
- {label && <span className="font-medium">{label} :</span>}
25
  <span>{value}</span>
 
26
  </div>
27
  );
28
  };
 
1
  import { cn } from '@/lib/utils';
2
+ import React from 'react';
3
 
4
+ type ChipProps = {
5
  label?: string;
6
+ value?: string;
7
  color?: 'gray' | 'blue' | 'yellow' | 'purple';
8
  className?: string;
9
+ } & React.ComponentProps<'div'>;
10
 
11
  const Chip: React.FC<ChipProps> = ({
 
12
  value,
13
  className,
14
  color = 'gray',
15
+ children,
16
  }) => {
17
  return (
18
  <div
19
  className={cn(
20
+ 'inline-flex items-center rounded-full text-xs mr-2 bg-gray-100 text-gray-500 px-2 py-0.5',
21
  `bg-${color}-100 text-${color}-500`,
22
  className,
23
  )}
24
  >
 
25
  <span>{value}</span>
26
+ {children}
27
  </div>
28
  );
29
  };
components/ui/DropdownMenu.tsx CHANGED
@@ -51,7 +51,7 @@ const DropdownMenuSubContent = React.forwardRef<
51
  <DropdownMenuPrimitive.SubContent
52
  ref={ref}
53
  className={cn(
54
- 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
55
  className,
56
  )}
57
  {...props}
 
51
  <DropdownMenuPrimitive.SubContent
52
  ref={ref}
53
  className={cn(
54
+ 'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
55
  className,
56
  )}
57
  {...props}
components/ui/Icons.tsx CHANGED
@@ -4,90 +4,6 @@ import * as React from 'react';
4
 
5
  import { cn } from '@/lib/utils';
6
 
7
- function IconNextChat({
8
- className,
9
- inverted,
10
- ...props
11
- }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12
- const id = React.useId();
13
-
14
- return (
15
- <svg
16
- viewBox="0 0 17 17"
17
- fill="none"
18
- xmlns="http://www.w3.org/2000/svg"
19
- className={cn('size-4', className)}
20
- {...props}
21
- >
22
- <defs>
23
- <linearGradient
24
- id={`gradient-${id}-1`}
25
- x1="10.6889"
26
- y1="10.3556"
27
- x2="13.8445"
28
- y2="14.2667"
29
- gradientUnits="userSpaceOnUse"
30
- >
31
- <stop stopColor={inverted ? 'white' : 'black'} />
32
- <stop
33
- offset={1}
34
- stopColor={inverted ? 'white' : 'black'}
35
- stopOpacity={0}
36
- />
37
- </linearGradient>
38
- <linearGradient
39
- id={`gradient-${id}-2`}
40
- x1="11.7555"
41
- y1="4.8"
42
- x2="11.7376"
43
- y2="9.50002"
44
- gradientUnits="userSpaceOnUse"
45
- >
46
- <stop stopColor={inverted ? 'white' : 'black'} />
47
- <stop
48
- offset={1}
49
- stopColor={inverted ? 'white' : 'black'}
50
- stopOpacity={0}
51
- />
52
- </linearGradient>
53
- </defs>
54
- <path
55
- d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
56
- fill={inverted ? 'black' : 'white'}
57
- stroke={inverted ? 'black' : 'white'}
58
- strokeWidth={2}
59
- strokeLinecap="round"
60
- strokeLinejoin="round"
61
- />
62
- <mask
63
- id="mask0_91_2047"
64
- style={{ maskType: 'alpha' }}
65
- maskUnits="userSpaceOnUse"
66
- x={1}
67
- y={0}
68
- width={16}
69
- height={16}
70
- >
71
- <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
72
- </mask>
73
- <g mask="url(#mask0_91_2047)">
74
- <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
75
- <path
76
- d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
77
- fill={`url(#gradient-${id}-1)`}
78
- />
79
- <rect
80
- x="11.2222"
81
- y="4.8"
82
- width="1.06667"
83
- height="6.4"
84
- fill={`url(#gradient-${id}-2)`}
85
- />
86
- </g>
87
- </svg>
88
- );
89
- }
90
-
91
  function IconLandingAI({ className, ...props }: React.ComponentProps<'svg'>) {
92
  return (
93
  <svg
@@ -146,23 +62,6 @@ function IconLandingAI({ className, ...props }: React.ComponentProps<'svg'>) {
146
  );
147
  }
148
 
149
- function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
150
- return (
151
- <svg
152
- aria-label="Vercel logomark"
153
- role="img"
154
- viewBox="0 0 74 64"
155
- className={cn('size-4', className)}
156
- {...props}
157
- >
158
- <path
159
- d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
160
- fill="currentColor"
161
- ></path>
162
- </svg>
163
- );
164
- }
165
-
166
  function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
167
  return (
168
  <svg
@@ -615,11 +514,30 @@ function IconExclamationTriangle({
615
  );
616
  }
617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  export {
619
  IconEdit,
620
- IconNextChat,
621
  IconLandingAI,
622
- IconVercel,
623
  IconGitHub,
624
  IconSeparator,
625
  IconArrowDown,
@@ -647,4 +565,5 @@ export {
647
  IconLoading,
648
  IconDiscord,
649
  IconExclamationTriangle,
 
650
  };
 
4
 
5
  import { cn } from '@/lib/utils';
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  function IconLandingAI({ className, ...props }: React.ComponentProps<'svg'>) {
8
  return (
9
  <svg
 
62
  );
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
66
  return (
67
  <svg
 
514
  );
515
  }
516
 
517
+ function IconImage({ className, ...props }: React.ComponentProps<'svg'>) {
518
+ return (
519
+ <svg
520
+ data-testid="geist-icon"
521
+ height="16"
522
+ stroke-linejoin="round"
523
+ viewBox="0 0 16 16"
524
+ width="16"
525
+ className={cn('size-4', className)}
526
+ {...props}
527
+ >
528
+ <path
529
+ fill-rule="evenodd"
530
+ clip-rule="evenodd"
531
+ d="M14.5 2.5H1.5V9.18933L2.96966 7.71967L3.18933 7.5H3.49999H6.63001H6.93933L6.96966 7.46967L10.4697 3.96967L11.5303 3.96967L14.5 6.93934V2.5ZM8.00066 8.55999L9.53034 10.0897L10.0607 10.62L9.00001 11.6807L8.46968 11.1503L6.31935 9H3.81065L1.53032 11.2803L1.5 11.3106V12.5C1.5 13.0523 1.94772 13.5 2.5 13.5H13.5C14.0523 13.5 14.5 13.0523 14.5 12.5V9.06066L11 5.56066L8.03032 8.53033L8.00066 8.55999ZM4.05312e-06 10.8107V12.5C4.05312e-06 13.8807 1.11929 15 2.5 15H13.5C14.8807 15 16 13.8807 16 12.5V9.56066L16.5607 9L16.0303 8.46967L16 8.43934V2.5V1H14.5H1.5H4.05312e-06V2.5V10.6893L-0.0606689 10.75L4.05312e-06 10.8107Z"
532
+ fill="currentColor"
533
+ ></path>
534
+ </svg>
535
+ );
536
+ }
537
+
538
  export {
539
  IconEdit,
 
540
  IconLandingAI,
 
541
  IconGitHub,
542
  IconSeparator,
543
  IconArrowDown,
 
565
  IconLoading,
566
  IconDiscord,
567
  IconExclamationTriangle,
568
+ IconImage,
569
  };
components/ui/Tooltip.tsx CHANGED
@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
19
  ref={ref}
20
  sideOffset={sideOffset}
21
  className={cn(
22
- 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
23
  className,
24
  )}
25
  {...props}
 
19
  ref={ref}
20
  sideOffset={sideOffset}
21
  className={cn(
22
+ 'z-50 overflow-hidden rounded-md bg-muted px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
23
  className,
24
  )}
25
  {...props}