Spaces:
Sleeping
Sleeping
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 +2 -1
- components/chat/ChatClient.tsx +1 -1
- components/chat/ChatList.tsx +7 -11
- components/chat/ChatMessage.tsx +21 -32
- components/ui/Icons.tsx +50 -8
- lib/hooks/useScrollAnchor.tsx +38 -31
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 = '
|
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-
|
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
|
19 |
{!session && (
|
20 |
<>
|
21 |
-
<div className="group relative mb-
|
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 |
-
<
|
56 |
-
|
57 |
-
|
58 |
-
|
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 {
|
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="
|
90 |
);
|
91 |
},
|
92 |
img(props) {
|
@@ -96,28 +96,14 @@ const Markdown: React.FC<{
|
|
96 |
);
|
97 |
}
|
98 |
return (
|
99 |
-
<
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
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 /> : <
|
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
|
92 |
return (
|
93 |
<svg
|
94 |
-
|
|
|
95 |
viewBox="0 0 24 24"
|
96 |
-
|
97 |
xmlns="http://www.w3.org/2000/svg"
|
98 |
-
className={cn('size-4', className)}
|
99 |
-
{...props}
|
100 |
>
|
101 |
-
<
|
102 |
-
<path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
</svg>
|
104 |
);
|
105 |
}
|
@@ -576,7 +618,7 @@ function IconExclamationTriangle({
|
|
576 |
export {
|
577 |
IconEdit,
|
578 |
IconNextChat,
|
579 |
-
|
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 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
}, [isAtBottom, isVisible]);
|
29 |
|
30 |
-
|
31 |
-
|
|
|
|
|
|
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
40 |
|
41 |
-
|
42 |
-
|
43 |
|
44 |
-
|
45 |
-
|
46 |
-
|
|
|
47 |
|
48 |
-
|
49 |
-
|
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 |
};
|