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

feat: Chat layout pt.1 (#61)

Browse files

<img width="1156" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/5669963/31574a2f-c287-4bd2-95e0-71f8af855b18">

app/chat/page.tsx CHANGED
@@ -22,7 +22,7 @@ import Loading from '@/components/ui/Loading';
22
  // const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
23
  const EXAMPLE_URL =
24
  'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png';
25
- const EXAMPLE_HEADER = 'Counting and find';
26
  const EXAMPLE_SUBHEADER =
27
  'number of flowers, area of largest and smallest flower';
28
  const EXAMPLE_PROMPT =
@@ -101,6 +101,7 @@ export default function Page() {
101
  const resp = await dbPostCreateChat({
102
  mediaUrl: example.url,
103
  initMessages: example.initMessages,
 
104
  });
105
  setUploading(false);
106
  if (resp) {
 
22
  // const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
23
  const EXAMPLE_URL =
24
  'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png';
25
+ const EXAMPLE_HEADER = 'Count and find';
26
  const EXAMPLE_SUBHEADER =
27
  'number of flowers, area of largest and smallest flower';
28
  const EXAMPLE_PROMPT =
 
101
  const resp = await dbPostCreateChat({
102
  mediaUrl: example.url,
103
  initMessages: example.initMessages,
104
+ title: example.heading,
105
  });
106
  setUploading(false);
107
  if (resp) {
components/chat/ChatClient.tsx CHANGED
@@ -25,7 +25,7 @@ export function Chat({ chat, session, isAdminView }: ChatProps) {
25
  return (
26
  <>
27
  <div className="h-full overflow-auto relative" ref={scrollRef}>
28
- <div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
29
  <ChatList
30
  messages={messages}
31
  session={session}
 
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}
components/chat/ChatList.tsx CHANGED
@@ -15,10 +15,10 @@ export interface ChatList {
15
 
16
  export function ChatList({ messages, session, isLoading }: ChatList) {
17
  return (
18
- <div className="relative mx-auto max-w-5xl px-8 pr-12">
19
  {!session && (
20
  <>
21
- <div className="group relative mb-4 flex items-center">
22
  <div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow">
23
  <IconExclamationTriangle />
24
  </div>
@@ -52,15 +52,11 @@ export function ChatList({ messages, session, isLoading }: ChatList) {
52
  {messages
53
  // .filter(message => message.role !== 'system')
54
  .map((message, index) => (
55
- <div key={index}>
56
- <ChatMessage
57
- message={message}
58
- isLoading={isLoading && index === messages.length - 1}
59
- />
60
- {index < messages.length - 1 && (
61
- <Separator className="my-4 md:my-8" />
62
- )}
63
- </div>
64
  ))}
65
  </div>
66
  );
 
15
 
16
  export function ChatList({ messages, session, isLoading }: ChatList) {
17
  return (
18
+ <div className="relative mx-auto max-w-5xl px-8 pt-6 border rounded-lg">
19
  {!session && (
20
  <>
21
+ <div className="group relative mb-6 flex items-center">
22
  <div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow">
23
  <IconExclamationTriangle />
24
  </div>
 
52
  {messages
53
  // .filter(message => message.role !== 'system')
54
  .map((message, index) => (
55
+ <ChatMessage
56
+ key={index}
57
+ message={message}
58
+ isLoading={isLoading && index === messages.length - 1}
59
+ />
 
 
 
 
60
  ))}
61
  </div>
62
  );
components/chat/ChatMessage.tsx CHANGED
@@ -9,7 +9,7 @@ import { useMemo, useState } from 'react';
9
  import { cn } from '@/lib/utils';
10
  import { CodeBlock } from '@/components/ui/CodeBlock';
11
  import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
12
- import { IconOpenAI, IconUser } from '@/components/ui/Icons';
13
  import { MessageBase } from '../../lib/types';
14
  import {
15
  Tooltip,
@@ -86,7 +86,7 @@ const Markdown: React.FC<{
86
  );
87
  }
88
  return (
89
- <p className="my-2 last:mb-0 whitespace-pre-line">{children}</p>
90
  );
91
  },
92
  img(props) {
@@ -96,28 +96,14 @@ const Markdown: React.FC<{
96
  );
97
  }
98
  return (
99
- <Tooltip>
100
- <TooltipTrigger asChild>
101
- <Img
102
- src={props.src ?? '/landing.png'}
103
- alt={props.alt ?? 'answer-image'}
104
- quality={100}
105
- className="cursor-zoom-in"
106
- sizes="(min-width: 66em) 25vw,
107
- (min-width: 44em) 40vw,
108
- 100vw"
109
- />
110
- </TooltipTrigger>
111
- <TooltipContent>
112
- <Img
113
- className="m-2"
114
- src={props.src ?? '/landing.png'}
115
- alt={props.alt ?? 'answer-image'}
116
- quality={100}
117
- width={500}
118
- />
119
- </TooltipContent>
120
- </Tooltip>
121
  );
122
  },
123
  code({ node, inline, className, children, ...props }) {
@@ -157,20 +143,24 @@ const Markdown: React.FC<{
157
  );
158
  };
159
 
160
- export function ChatMessage({
161
- message,
162
- isLoading,
163
- ...props
164
- }: ChatMessageProps) {
165
  const { logs, content } = useMemo(() => {
166
  return getCleanedUpMessages({
167
  content: message.content,
168
  role: message.role,
169
  });
170
  }, [message.content, message.role]);
 
 
 
171
  const [details, setDetails] = useState<string>('');
172
  return (
173
- <div className={cn('group relative mb-4 flex items-start')} {...props}>
 
 
 
 
 
174
  <div
175
  className={cn(
176
  'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
@@ -179,11 +169,10 @@ export function ChatMessage({
179
  : 'bg-primary text-primary-foreground',
180
  )}
181
  >
182
- {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
183
  </div>
184
  <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
185
  {logs && <Markdown content={logs} setDetails={setDetails} />}
186
- {/* <ChatMessageActions message={message} /> */}
187
  {isLoading && <Loading />}
188
  </div>
189
  <Dialog open={!!details} onOpenChange={open => !open && setDetails('')}>
 
9
  import { cn } from '@/lib/utils';
10
  import { CodeBlock } from '@/components/ui/CodeBlock';
11
  import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
12
+ import { IconLandingAI, IconUser } from '@/components/ui/Icons';
13
  import { MessageBase } from '../../lib/types';
14
  import {
15
  Tooltip,
 
86
  );
87
  }
88
  return (
89
+ <p className="mb-2 last:mb-0 whitespace-pre-line">{children}</p>
90
  );
91
  },
92
  img(props) {
 
96
  );
97
  }
98
  return (
99
+ <Img
100
+ src={props.src ?? '/landing.png'}
101
+ alt={props.alt ?? 'answer-image'}
102
+ quality={100}
103
+ sizes="(min-width: 66em) 15vw,
104
+ (min-width: 44em) 20vw,
105
+ 100vw"
106
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  );
108
  },
109
  code({ node, inline, className, children, ...props }) {
 
143
  );
144
  };
145
 
146
+ export function ChatMessage({ message, isLoading }: ChatMessageProps) {
 
 
 
 
147
  const { logs, content } = useMemo(() => {
148
  return getCleanedUpMessages({
149
  content: message.content,
150
  role: message.role,
151
  });
152
  }, [message.content, message.role]);
153
+ console.log('[Ming] logs:', logs);
154
+ console.log('[Ming] content:', content);
155
+ console.log('[Ming] raw:', message.content);
156
  const [details, setDetails] = useState<string>('');
157
  return (
158
+ <div
159
+ className={cn(
160
+ 'group relative mb-6 flex rounded-md bg-muted px-4 py-6 w-3/5',
161
+ message.role === 'user' ? 'ml-auto mr-0 w-3/5' : 'w-4/5',
162
+ )}
163
+ >
164
  <div
165
  className={cn(
166
  'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
 
169
  : 'bg-primary text-primary-foreground',
170
  )}
171
  >
172
+ {message.role === 'user' ? <IconUser /> : <IconLandingAI />}
173
  </div>
174
  <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
175
  {logs && <Markdown content={logs} setDetails={setDetails} />}
 
176
  {isLoading && <Loading />}
177
  </div>
178
  <Dialog open={!!details} onOpenChange={open => !open && setDetails('')}>
components/ui/Icons.tsx CHANGED
@@ -88,18 +88,60 @@ function IconNextChat({
88
  );
89
  }
90
 
91
- function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92
  return (
93
  <svg
94
- fill="currentColor"
 
95
  viewBox="0 0 24 24"
96
- role="img"
97
  xmlns="http://www.w3.org/2000/svg"
98
- className={cn('size-4', className)}
99
- {...props}
100
  >
101
- <title>OpenAI icon</title>
102
- <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </svg>
104
  );
105
  }
@@ -576,7 +618,7 @@ function IconExclamationTriangle({
576
  export {
577
  IconEdit,
578
  IconNextChat,
579
- IconOpenAI,
580
  IconVercel,
581
  IconGitHub,
582
  IconSeparator,
 
88
  );
89
  }
90
 
91
+ function IconLandingAI({ className, ...props }: React.ComponentProps<'svg'>) {
92
  return (
93
  <svg
94
+ width="24"
95
+ height="24"
96
  viewBox="0 0 24 24"
97
+ fill="none"
98
  xmlns="http://www.w3.org/2000/svg"
 
 
99
  >
100
+ <rect width="24" height="24" rx="4" fill="white" />
101
+ <path
102
+ d="M5 13.469V17.0762L7.84434 18.6274V15.0202L5 13.469Z"
103
+ fill="black"
104
+ />
105
+ <path
106
+ d="M5 9.2356V12.8428L7.84434 14.3921V10.7868L5 9.2356Z"
107
+ fill="black"
108
+ />
109
+ <path
110
+ d="M5 5.00391V8.60921L7.84434 10.1604V6.55509L5 5.00391Z"
111
+ fill="black"
112
+ />
113
+ <path
114
+ d="M15.1556 15.0202V18.6274L18 17.0762V13.469L15.1556 15.0202Z"
115
+ fill="black"
116
+ />
117
+ <path
118
+ d="M8.38708 10.7868V14.3921L11.2314 12.8428V9.2356L8.38708 10.7868Z"
119
+ fill="black"
120
+ />
121
+ <path
122
+ d="M8.38708 6.55509V10.1604L11.2314 8.60921V5.00391L8.38708 6.55509Z"
123
+ fill="black"
124
+ />
125
+ <path
126
+ d="M10.9421 4.54541L8.11669 3L5.29315 4.54541L8.11669 6.08889L10.9421 4.54541Z"
127
+ fill="black"
128
+ />
129
+ <path
130
+ d="M8.38708 15.3054V18.9127L11.2314 20.4619V16.8566L8.38708 15.3054Z"
131
+ fill="black"
132
+ />
133
+ <path
134
+ d="M11.7742 16.8566V20.4619L14.6186 18.9127V15.3054L11.7742 16.8566Z"
135
+ fill="black"
136
+ />
137
+ <path
138
+ d="M8.67645 14.8506L11.5 16.396L14.3235 14.8506L11.5 13.3071L8.67645 14.8506Z"
139
+ fill="black"
140
+ />
141
+ <path
142
+ d="M17.7144 13.0108L14.889 11.4673L12.0654 13.0108L14.889 14.5542L17.7144 13.0108Z"
143
+ fill="black"
144
+ />
145
  </svg>
146
  );
147
  }
 
618
  export {
619
  IconEdit,
620
  IconNextChat,
621
+ IconLandingAI,
622
  IconVercel,
623
  IconGitHub,
624
  IconSeparator,
lib/hooks/useScrollAnchor.tsx CHANGED
@@ -5,7 +5,7 @@ export const useScrollAnchor = () => {
5
  const scrollRef = useRef<HTMLDivElement>(null);
6
  const visibilityRef = useRef<HTMLDivElement>(null);
7
 
8
- const [isAtBottom, setIsAtBottom] = useState(true);
9
  const [isVisible, setIsVisible] = useState(false);
10
 
11
  const scrollToBottom = useCallback(() => {
@@ -17,39 +17,47 @@ export const useScrollAnchor = () => {
17
  }
18
  }, []);
19
 
20
- useEffect(() => {
21
- if (messagesRef.current) {
22
- if (isAtBottom && !isVisible) {
23
- messagesRef.current.scrollIntoView({
24
- block: 'end',
25
- });
26
- }
27
- }
28
- }, [isAtBottom, isVisible]);
29
 
30
- useEffect(() => {
31
- const { current } = scrollRef;
 
 
 
32
 
33
- if (current) {
34
- const handleScroll = (event: Event) => {
35
- const target = event.target as HTMLDivElement;
36
- const offset = 25;
37
- const isAtBottom =
38
- target.scrollTop + target.clientHeight >=
39
- target.scrollHeight - offset;
 
 
 
 
40
 
41
- setIsAtBottom(isAtBottom);
42
- };
43
 
44
- current.addEventListener('scroll', handleScroll, {
45
- passive: true,
46
- });
 
47
 
48
- return () => {
49
- current.removeEventListener('scroll', handleScroll);
50
- };
51
- }
52
- }, []);
53
 
54
  useEffect(() => {
55
  if (visibilityRef.current) {
@@ -81,7 +89,6 @@ export const useScrollAnchor = () => {
81
  scrollRef,
82
  visibilityRef,
83
  scrollToBottom,
84
- isAtBottom,
85
- isVisible,
86
  };
87
  };
 
5
  const scrollRef = useRef<HTMLDivElement>(null);
6
  const visibilityRef = useRef<HTMLDivElement>(null);
7
 
8
+ // const [isAtBottom, setIsAtBottom] = useState(true);
9
  const [isVisible, setIsVisible] = useState(false);
10
 
11
  const scrollToBottom = useCallback(() => {
 
17
  }
18
  }, []);
19
 
20
+ // useEffect(() => {
21
+ // if (messagesRef.current) {
22
+ // if (isAtBottom && !isVisible) {
23
+ // messagesRef.current.scrollIntoView({
24
+ // block: 'end',
25
+ // });
26
+ // }
27
+ // }
28
+ // }, [isAtBottom, isVisible]);
29
 
30
+ /**
31
+ * Seem to be broken, no time to fix
32
+ */
33
+ // useEffect(() => {
34
+ // const { current } = scrollRef;
35
 
36
+ // if (current) {
37
+ // const handleScroll = (event: Event) => {
38
+ // const target = event.target as HTMLDivElement;
39
+ // const offset = 100;
40
+ // console.log(
41
+ // '[Ming] ~ handleScroll ~ target.scrollTop + target.clientHeight:',
42
+ // target.scrollTop + target.clientHeight - target.scrollHeight,
43
+ // );
44
+ // const isAtBottom =
45
+ // target.scrollTop + target.clientHeight >=
46
+ // target.scrollHeight - offset;
47
 
48
+ // setIsAtBottom(isAtBottom);
49
+ // };
50
 
51
+ // current.addEventListener('scroll', handleScroll, {
52
+ // passive: true,
53
+ // });
54
+ // console.log('[Ming] ~ useEffect ~ current:', current);
55
 
56
+ // return () => {
57
+ // current.removeEventListener('scroll', handleScroll);
58
+ // };
59
+ // }
60
+ // }, []);
61
 
62
  useEffect(() => {
63
  if (visibilityRef.current) {
 
89
  scrollRef,
90
  visibilityRef,
91
  scrollToBottom,
92
+ isAtBottom: isVisible,
 
93
  };
94
  };