MingruiZhang commited on
Commit
bfbf1a7
1 Parent(s): 3a22cf3

feat: Component UI and lastUpdated timestamp (#21)

Browse files

<img width="1904" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/5669963/0565c7e2-a481-4246-8329-343a6c079fce">

app/api/upload/route.ts CHANGED
@@ -29,6 +29,7 @@ export async function POST(req: Request): Promise<Response> {
29
  id: id ?? nanoid(),
30
  user,
31
  messages: initMessages ?? [],
 
32
  };
33
 
34
  await createKVChat(payload);
 
29
  id: id ?? nanoid(),
30
  user,
31
  messages: initMessages ?? [],
32
+ updatedAt: Date.now(),
33
  };
34
 
35
  await createKVChat(payload);
app/chat/layout.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { auth } from '@/auth';
2
  import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
3
  import Loading from '@/components/ui/Loading';
4
  import { Suspense } from 'react';
@@ -8,8 +8,7 @@ interface ChatLayoutProps {
8
  }
9
 
10
  export default async function Layout({ children }: ChatLayoutProps) {
11
- const session = await auth();
12
- const email = session?.user?.email;
13
  return (
14
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
15
  <div
 
1
+ import { auth, authEmail } from '@/auth';
2
  import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
3
  import Loading from '@/components/ui/Loading';
4
  import { Suspense } from 'react';
 
8
  }
9
 
10
  export default async function Layout({ children }: ChatLayoutProps) {
11
+ const { email, isAdmin } = await authEmail();
 
12
  return (
13
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
14
  <div
auth.ts CHANGED
@@ -58,3 +58,9 @@ export const {
58
  signIn: '/sign-in', // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
59
  },
60
  });
 
 
 
 
 
 
 
58
  signIn: '/sign-in', // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
59
  },
60
  });
61
+
62
+ export async function authEmail() {
63
+ const session = await auth();
64
+ const email = session?.user?.email;
65
+ return { email, isAdmin: !!email?.endsWith('landing.ai') };
66
+ }
components/Header.tsx CHANGED
@@ -4,8 +4,14 @@ import Link from 'next/link';
4
  import { auth } from '@/auth';
5
  import { Button } from '@/components/ui/Button';
6
  import { UserMenu } from '@/components/UserMenu';
7
- import { IconSeparator } from './ui/Icons';
 
 
 
 
 
8
  import { LoginMenu } from './LoginMenu';
 
9
 
10
  export async function Header() {
11
  const session = await auth();
@@ -14,6 +20,16 @@ export async function Header() {
14
  {/* <Button variant="link" asChild className="mr-2">
15
  <Link href="/project">Projects</Link>
16
  </Button> */}
 
 
 
 
 
 
 
 
 
 
17
  {/* <Button variant="link" asChild className="mr-2">
18
  <Link href="/chat">Chat</Link>
19
  </Button> */}
 
4
  import { auth } from '@/auth';
5
  import { Button } from '@/components/ui/Button';
6
  import { UserMenu } from '@/components/UserMenu';
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipTrigger,
11
+ } from '@/components/ui/Tooltip';
12
+ import { IconPlus, IconSeparator } from '@/components/ui/Icons';
13
  import { LoginMenu } from './LoginMenu';
14
+ import { redirect } from 'next/navigation';
15
 
16
  export async function Header() {
17
  const session = await auth();
 
20
  {/* <Button variant="link" asChild className="mr-2">
21
  <Link href="/project">Projects</Link>
22
  </Button> */}
23
+ <Tooltip>
24
+ <TooltipTrigger asChild>
25
+ <Button variant="link" asChild className="mr-2">
26
+ <Link href="/chat">
27
+ <IconPlus />
28
+ </Link>
29
+ </Button>
30
+ </TooltipTrigger>
31
+ <TooltipContent>New chat</TooltipContent>
32
+ </Tooltip>
33
  {/* <Button variant="link" asChild className="mr-2">
34
  <Link href="/chat">Chat</Link>
35
  </Button> */}
components/chat-sidebar/ChatAdminToggle.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { chatViewMode } from '@/state';
4
+ import { useAtom } from 'jotai';
5
+ import React from 'react';
6
+
7
+ export interface ChatAdminToggleProps {}
8
+
9
+ const ChatAdminToggle: React.FC<ChatAdminToggleProps> = () => {
10
+ const modeAtom = useAtom(chatViewMode);
11
+ return null;
12
+ };
13
+
14
+ export default ChatAdminToggle;
components/chat-sidebar/ChatCard.tsx CHANGED
@@ -2,12 +2,13 @@
2
 
3
  import { PropsWithChildren } from 'react';
4
  import Link from 'next/link';
5
- import { useParams } from 'next/navigation';
6
  import { cn } from '@/lib/utils';
7
  import { ChatEntity } from '@/lib/types';
8
  import Image from 'next/image';
9
  import clsx from 'clsx';
10
  import Img from '../ui/Img';
 
11
 
12
  type ChatCardProps = PropsWithChildren<{
13
  chat: ChatEntity;
@@ -31,7 +32,8 @@ export const ChatCardLayout: React.FC<
31
 
32
  const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
33
  const { id: chatIdFromParam } = useParams();
34
- const { id, url, messages, user } = chat;
 
35
  const firstMessage = messages?.[0]?.content;
36
  const title = firstMessage
37
  ? firstMessage.length > 50
@@ -45,8 +47,11 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
45
  >
46
  <div className="overflow-hidden flex items-center size-full">
47
  <Img src={url} alt={`chat-${id}-card-image`} className="w-1/4 " />
48
- <div className="flex items-start h-full">
49
- <p className="text-sm w-3/4 ml-3">{title}</p>
 
 
 
50
  </div>
51
  </div>
52
  </ChatCardLayout>
 
2
 
3
  import { PropsWithChildren } from 'react';
4
  import Link from 'next/link';
5
+ import { useParams, usePathname, useRouter } from 'next/navigation';
6
  import { cn } from '@/lib/utils';
7
  import { ChatEntity } from '@/lib/types';
8
  import Image from 'next/image';
9
  import clsx from 'clsx';
10
  import Img from '../ui/Img';
11
+ // import { format } from 'date-fns';
12
 
13
  type ChatCardProps = PropsWithChildren<{
14
  chat: ChatEntity;
 
32
 
33
  const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
34
  const { id: chatIdFromParam } = useParams();
35
+ const pathname = usePathname();
36
+ const { id, url, messages, user, updatedAt } = chat;
37
  const firstMessage = messages?.[0]?.content;
38
  const title = firstMessage
39
  ? firstMessage.length > 50
 
47
  >
48
  <div className="overflow-hidden flex items-center size-full">
49
  <Img src={url} alt={`chat-${id}-card-image`} className="w-1/4 " />
50
+ <div className="flex items-start flex-col h-full ml-3 w-3/4">
51
+ <p className="text-sm mb-1">{title}</p>
52
+ <p className="text-xs text-gray-500">
53
+ {updatedAt ? new Date(1714027100904).toLocaleDateString() : '-'}
54
+ </p>
55
  </div>
56
  </div>
57
  </ChatCardLayout>
components/chat/ChatMessage.tsx CHANGED
@@ -33,7 +33,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
33
  </div>
34
  <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
35
  {logs && message.role !== 'user' && (
36
- <div className="bg-slate-100 dark:bg-slate-900 mb-4 p-4 max-h-[400px] overflow-y-scroll">
37
  <div className="text-xl font-bold">Thinking Process</div>
38
  <MemoizedReactMarkdown
39
  className="break-words text-sm"
 
33
  </div>
34
  <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
35
  {logs && message.role !== 'user' && (
36
+ <div className="bg-slate-100 dark:bg-slate-900 mb-4 p-4 max-h-[400px] overflow-auto">
37
  <div className="text-xl font-bold">Thinking Process</div>
38
  <MemoizedReactMarkdown
39
  className="break-words text-sm"
components/chat/ChatPanel.tsx DELETED
@@ -1,55 +0,0 @@
1
- import * as React from 'react';
2
- import { type UseChatHelpers } from 'ai/react';
3
-
4
- import { Button } from '@/components/ui/Button';
5
- import { PromptForm } from '@/components/chat/PromptForm';
6
- import { IconRefresh, IconStop } from '@/components/ui/Icons';
7
- import { MessageBase } from '../../lib/types';
8
-
9
- export interface ChatPanelProps
10
- extends Pick<
11
- UseChatHelpers,
12
- 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
13
- > {
14
- id?: string;
15
- title?: string;
16
- messages: MessageBase[];
17
- url?: string;
18
- }
19
-
20
- export function ChatPanel({
21
- id,
22
- title,
23
- isLoading,
24
- stop,
25
- append,
26
- reload,
27
- input,
28
- setInput,
29
- messages,
30
- url,
31
- }: ChatPanelProps) {
32
- return (
33
- <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]">
34
- <div className="mx-auto sm:max-w-3xl sm:px-4">
35
- <div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4">
36
- <PromptForm
37
- url={url}
38
- onSubmit={async value => {
39
- await append({
40
- id,
41
- content: value,
42
- role: 'user',
43
- });
44
- }}
45
- input={input}
46
- setInput={setInput}
47
- isLoading={isLoading}
48
- messages={messages}
49
- reload={reload}
50
- />
51
- </div>
52
- </div>
53
- </div>
54
- );
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat/Composer.tsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { type UseChatHelpers } from 'ai/react';
5
+ import Textarea from 'react-textarea-autosize';
6
+
7
+ import { Button } from '@/components/ui/Button';
8
+ import { MessageBase } from '../../lib/types';
9
+ import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit';
10
+ import Img from '../ui/Img';
11
+ import {
12
+ Tooltip,
13
+ TooltipContent,
14
+ TooltipTrigger,
15
+ } from '@/components/ui/Tooltip';
16
+ import {
17
+ IconArrowDown,
18
+ IconArrowElbow,
19
+ IconRefresh,
20
+ IconStop,
21
+ } from '@/components/ui/Icons';
22
+ import { cn } from '@/lib/utils';
23
+
24
+ export interface ComposerProps
25
+ extends Pick<
26
+ UseChatHelpers,
27
+ 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
28
+ > {
29
+ id?: string;
30
+ title?: string;
31
+ messages: MessageBase[];
32
+ url?: string;
33
+ isAtBottom: boolean;
34
+ scrollToBottom: () => void;
35
+ }
36
+
37
+ export function Composer({
38
+ id,
39
+ title,
40
+ isLoading,
41
+ stop,
42
+ append,
43
+ reload,
44
+ input,
45
+ setInput,
46
+ messages,
47
+ isAtBottom,
48
+ scrollToBottom,
49
+ url,
50
+ }: ComposerProps) {
51
+ const { formRef, onKeyDown } = useEnterSubmit();
52
+ const inputRef = React.useRef<HTMLTextAreaElement>(null);
53
+ React.useEffect(() => {
54
+ if (inputRef.current) {
55
+ inputRef.current.focus();
56
+ }
57
+ }, []);
58
+
59
+ return (
60
+ <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]">
61
+ <div className="mx-auto sm:max-w-3xl sm:px-4 h-full">
62
+ <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">
63
+ <form
64
+ onSubmit={async e => {
65
+ e.preventDefault();
66
+ if (!input?.trim()) {
67
+ return;
68
+ }
69
+ setInput('');
70
+ await append({
71
+ id,
72
+ content: input,
73
+ role: 'user',
74
+ });
75
+ scrollToBottom();
76
+ }}
77
+ ref={formRef}
78
+ className="h-full"
79
+ >
80
+ <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">
81
+ {url && (
82
+ <div className="w-1/5 p-2 h-full flex items-center justify-center relative">
83
+ <Tooltip>
84
+ <TooltipTrigger asChild>
85
+ <Img
86
+ src={url}
87
+ className="cursor-zoom-in"
88
+ alt="preview-image"
89
+ />
90
+ </TooltipTrigger>
91
+ <TooltipContent>
92
+ <Img
93
+ src={url}
94
+ className="m-2"
95
+ quality={100}
96
+ alt="zoomed-in-image"
97
+ />
98
+ </TooltipContent>
99
+ </Tooltip>
100
+ </div>
101
+ )}
102
+ <Textarea
103
+ ref={inputRef}
104
+ tabIndex={0}
105
+ onKeyDown={onKeyDown}
106
+ rows={1}
107
+ value={input}
108
+ disabled={isLoading}
109
+ onChange={e => setInput(e.target.value)}
110
+ placeholder={
111
+ isLoading
112
+ ? 'Vision Agent is thinking...'
113
+ : 'Ask questions about the images.'
114
+ }
115
+ spellCheck={false}
116
+ className="min-h-[60px] w-4/5 resize-none bg-transparent px-4 py-[1.3em] focus-within:outline-none sm:text-sm"
117
+ />
118
+ {/* Scroll to bottom Icon */}
119
+ <div
120
+ className={cn(
121
+ 'absolute top-3 right-4 transition-opacity duration-300',
122
+ isAtBottom ? 'opacity-0' : 'opacity-100',
123
+ )}
124
+ >
125
+ <Tooltip>
126
+ <TooltipTrigger asChild>
127
+ <Button
128
+ variant="outline"
129
+ size="icon"
130
+ className="bg-background"
131
+ onClick={() => scrollToBottom()}
132
+ >
133
+ <IconArrowDown />
134
+ </Button>
135
+ </TooltipTrigger>
136
+ <TooltipContent>Scroll to bottom</TooltipContent>
137
+ </Tooltip>
138
+ </div>
139
+ {/* Stop / Regenerate Icon */}
140
+ <div className="absolute bottom-14 right-4">
141
+ {isLoading ? (
142
+ <Tooltip>
143
+ <TooltipTrigger asChild>
144
+ <Button
145
+ variant="outline"
146
+ size="icon"
147
+ className="bg-background"
148
+ onClick={() => stop()}
149
+ >
150
+ <IconStop />
151
+ </Button>
152
+ </TooltipTrigger>
153
+ <TooltipContent>Stop generating</TooltipContent>
154
+ </Tooltip>
155
+ ) : (
156
+ messages?.length >= 2 && (
157
+ <Tooltip>
158
+ <TooltipTrigger asChild>
159
+ <Button
160
+ variant="outline"
161
+ size="icon"
162
+ className="bg-background"
163
+ onClick={() => reload()}
164
+ >
165
+ <IconRefresh />
166
+ </Button>
167
+ </TooltipTrigger>
168
+ <TooltipContent>Regenerate response</TooltipContent>
169
+ </Tooltip>
170
+ )
171
+ )}
172
+ </div>
173
+ {/* Submit Icon */}
174
+ <div className="absolute bottom-3 right-4">
175
+ <Tooltip>
176
+ <TooltipTrigger asChild>
177
+ <Button
178
+ type="submit"
179
+ size="icon"
180
+ disabled={isLoading || input === ''}
181
+ >
182
+ <IconArrowElbow />
183
+ </Button>
184
+ </TooltipTrigger>
185
+ <TooltipContent>Send message</TooltipContent>
186
+ </Tooltip>
187
+ </div>
188
+ </div>
189
+ </form>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
components/chat/PromptForm.tsx DELETED
@@ -1,124 +0,0 @@
1
- import * as React from 'react';
2
- import Textarea from 'react-textarea-autosize';
3
- import { UseChatHelpers } from 'ai/react';
4
- import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit';
5
- import { Button, buttonVariants } from '@/components/ui/Button';
6
- import {
7
- Tooltip,
8
- TooltipContent,
9
- TooltipTrigger,
10
- } from '@/components/ui/Tooltip';
11
- import {
12
- IconArrowElbow,
13
- IconPlus,
14
- IconRefresh,
15
- IconStop,
16
- } from '@/components/ui/Icons';
17
- import { useRouter } from 'next/navigation';
18
- import Img from '../ui/Img';
19
- import { MessageBase } from '@/lib/types';
20
-
21
- export interface PromptProps
22
- extends Pick<UseChatHelpers, 'input' | 'setInput' | 'reload'> {
23
- onSubmit: (value: string) => void;
24
- isLoading: boolean;
25
- url?: string;
26
- messages: MessageBase[];
27
- }
28
-
29
- export function PromptForm({
30
- onSubmit,
31
- input,
32
- setInput,
33
- isLoading,
34
- url,
35
- messages,
36
- reload,
37
- }: PromptProps) {
38
- const { formRef, onKeyDown } = useEnterSubmit();
39
- const inputRef = React.useRef<HTMLTextAreaElement>(null);
40
- const router = useRouter();
41
- React.useEffect(() => {
42
- if (inputRef.current) {
43
- inputRef.current.focus();
44
- }
45
- }, []);
46
-
47
- return (
48
- <form
49
- onSubmit={async e => {
50
- e.preventDefault();
51
- if (!input?.trim()) {
52
- return;
53
- }
54
- setInput('');
55
- await onSubmit(input);
56
- }}
57
- ref={formRef}
58
- >
59
- <div className="relative flex w-full px-8 pl-2 overflow-hidden max-h-60 grow bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2">
60
- {url && (
61
- <Tooltip>
62
- <TooltipTrigger asChild>
63
- <Img
64
- alt="prompt-image"
65
- src={url}
66
- className="w-1/5 my-4 mx-2 cursor-zoom-in"
67
- />
68
- </TooltipTrigger>
69
- <TooltipContent>
70
- <Img alt="prompt-hovered-image" src={url} className="m-2" />
71
- </TooltipContent>
72
- </Tooltip>
73
- )}
74
- <Textarea
75
- ref={inputRef}
76
- tabIndex={0}
77
- onKeyDown={onKeyDown}
78
- rows={1}
79
- value={input}
80
- onChange={e => setInput(e.target.value)}
81
- placeholder="Ask questions about the images."
82
- spellCheck={false}
83
- className="min-h-[60px] w-4/5 resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
84
- />
85
- <div className="absolute left-1/2 -translate-x-1/2 bottom-0 h-12 z-40">
86
- {isLoading ? (
87
- <Button
88
- variant="outline"
89
- onClick={() => stop()}
90
- className="bg-background"
91
- >
92
- <IconStop className="mr-2" />
93
- Stop generating
94
- </Button>
95
- ) : (
96
- messages?.length >= 2 && (
97
- <div className="flex space-x-2">
98
- <Button variant="outline" onClick={() => reload()}>
99
- <IconRefresh className="mr-2" />
100
- Regenerate response
101
- </Button>
102
- </div>
103
- )
104
- )}
105
- </div>
106
- <div className="absolute top-1/2 -translate-y-1/2 right-4">
107
- <Tooltip>
108
- <TooltipTrigger asChild>
109
- <Button
110
- type="submit"
111
- size="icon"
112
- disabled={isLoading || input === ''}
113
- >
114
- <IconArrowElbow />
115
- <span className="sr-only">Send message</span>
116
- </Button>
117
- </TooltipTrigger>
118
- <TooltipContent>Send message</TooltipContent>
119
- </Tooltip>
120
- </div>
121
- </div>
122
- </form>
123
- );
124
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat/index.tsx CHANGED
@@ -2,12 +2,11 @@
2
 
3
  import { cn } from '@/lib/utils';
4
  import { ChatList } from '@/components/chat/ChatList';
5
- import { ChatPanel } from '@/components/chat/ChatPanel';
6
  import { ChatEntity } from '@/lib/types';
7
  import Image from 'next/image';
8
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
9
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
10
- import { ButtonScrollToBottom } from '../ui/ButtonScrollToBottom';
11
 
12
  export interface ChatProps extends React.ComponentProps<'div'> {
13
  chat: ChatEntity;
@@ -29,7 +28,7 @@ export function Chat({ chat }: ChatProps) {
29
  <div className="h-px w-full" ref={visibilityRef} />
30
  </div>
31
  </div>
32
- <ChatPanel
33
  id={id}
34
  url={url}
35
  isLoading={isLoading}
@@ -39,8 +38,6 @@ export function Chat({ chat }: ChatProps) {
39
  messages={messages}
40
  input={input}
41
  setInput={setInput}
42
- />
43
- <ButtonScrollToBottom
44
  isAtBottom={isAtBottom}
45
  scrollToBottom={scrollToBottom}
46
  />
 
2
 
3
  import { cn } from '@/lib/utils';
4
  import { ChatList } from '@/components/chat/ChatList';
5
+ import { Composer } from '@/components/chat/Composer';
6
  import { ChatEntity } from '@/lib/types';
7
  import Image from 'next/image';
8
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
9
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
 
10
 
11
  export interface ChatProps extends React.ComponentProps<'div'> {
12
  chat: ChatEntity;
 
28
  <div className="h-px w-full" ref={visibilityRef} />
29
  </div>
30
  </div>
31
+ <Composer
32
  id={id}
33
  url={url}
34
  isLoading={isLoading}
 
38
  messages={messages}
39
  input={input}
40
  setInput={setInput}
 
 
41
  isAtBottom={isAtBottom}
42
  scrollToBottom={scrollToBottom}
43
  />
components/project/Chat.tsx CHANGED
@@ -4,7 +4,7 @@ import { MediaDetails } from '@/lib/fetch';
4
  import useChatWithMedia from '@/lib/hooks/useChatWithMedia';
5
  import React from 'react';
6
  import { ChatList } from '../chat/ChatList';
7
- import { ChatPanel } from '../chat/ChatPanel';
8
 
9
  export interface ChatProps {
10
  mediaList: MediaDetails[];
@@ -16,7 +16,7 @@ const Chat: React.FC<ChatProps> = ({ mediaList }) => {
16
  return (
17
  <>
18
  <ChatList messages={messages} />
19
- <ChatPanel
20
  isLoading={isLoading}
21
  stop={stop}
22
  append={append}
@@ -24,7 +24,7 @@ const Chat: React.FC<ChatProps> = ({ mediaList }) => {
24
  messages={messages}
25
  input={input}
26
  setInput={setInput}
27
- />
28
  </>
29
  );
30
  };
 
4
  import useChatWithMedia from '@/lib/hooks/useChatWithMedia';
5
  import React from 'react';
6
  import { ChatList } from '../chat/ChatList';
7
+ // import { ChatPanel } from '../chat/ChatPanel';
8
 
9
  export interface ChatProps {
10
  mediaList: MediaDetails[];
 
16
  return (
17
  <>
18
  <ChatList messages={messages} />
19
+ {/* <ChatPanel
20
  isLoading={isLoading}
21
  stop={stop}
22
  append={append}
 
24
  messages={messages}
25
  input={input}
26
  setInput={setInput}
27
+ /> */}
28
  </>
29
  );
30
  };
components/ui/ButtonScrollToBottom.tsx DELETED
@@ -1,36 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- import { cn } from '@/lib/utils';
6
- import { Button, type ButtonProps } from '@/components/ui/Button';
7
- import { IconArrowDown } from '@/components/ui/Icons';
8
-
9
- interface ButtonScrollToBottomProps extends ButtonProps {
10
- isAtBottom: boolean;
11
- scrollToBottom: () => void;
12
- }
13
-
14
- export function ButtonScrollToBottom({
15
- className,
16
- isAtBottom,
17
- scrollToBottom,
18
- ...props
19
- }: ButtonScrollToBottomProps) {
20
- return (
21
- <Button
22
- variant="outline"
23
- size="icon"
24
- className={cn(
25
- 'fixed bottom-16 right-4 z-10 bg-background transition-opacity duration-300',
26
- isAtBottom ? 'opacity-0' : 'opacity-100',
27
- className,
28
- )}
29
- onClick={() => scrollToBottom()}
30
- {...props}
31
- >
32
- <IconArrowDown />
33
- <span className="sr-only">Scroll to bottom</span>
34
- </Button>
35
- );
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/kv/chat.ts CHANGED
@@ -3,22 +3,13 @@
3
  import { revalidatePath } from 'next/cache';
4
  import { kv } from '@vercel/kv';
5
 
6
- import { auth } from '@/auth';
7
  import { ChatEntity, MessageBase } from '@/lib/types';
8
  import { notFound, redirect } from 'next/navigation';
9
  import { nanoid } from '../utils';
10
 
11
- async function authCheck() {
12
- const session = await auth();
13
- const email = session?.user?.email;
14
- // if (!email) {
15
- // redirect('/');
16
- // }
17
- return { email, isAdmin: !!email?.endsWith('landing.ai') };
18
- }
19
-
20
  export async function getKVChats() {
21
- const { email } = await authCheck();
22
 
23
  try {
24
  const pipeline = kv.pipeline();
@@ -30,16 +21,41 @@ export async function getKVChats() {
30
  pipeline.hgetall(chat);
31
  }
32
 
33
- const results = await pipeline.exec();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- return results as ChatEntity[];
36
  } catch (error) {
37
  return [];
38
  }
39
  }
40
 
41
  export async function getKVChat(id: string) {
42
- // const { email, isAdmin } = await authCheck();
43
  const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
44
 
45
  if (!chat) {
@@ -50,8 +66,8 @@ export async function getKVChat(id: string) {
50
  }
51
 
52
  export async function createKVChat(chat: ChatEntity) {
53
- // const { email, isAdmin } = await authCheck();
54
- const { email } = await authCheck();
55
 
56
  await kv.hmset(`chat:${chat.id}`, chat);
57
  if (email) {
@@ -76,6 +92,7 @@ export async function saveKVChatMessage(id: string, message: MessageBase) {
76
  await kv.hmset(`chat:${id}`, {
77
  ...chat,
78
  messages: [...messages, message],
 
79
  });
80
  revalidatePath('/chat', 'layout');
81
  }
 
3
  import { revalidatePath } from 'next/cache';
4
  import { kv } from '@vercel/kv';
5
 
6
+ import { auth, authEmail } from '@/auth';
7
  import { ChatEntity, MessageBase } from '@/lib/types';
8
  import { notFound, redirect } from 'next/navigation';
9
  import { nanoid } from '../utils';
10
 
 
 
 
 
 
 
 
 
 
11
  export async function getKVChats() {
12
+ const { email } = await authEmail();
13
 
14
  try {
15
  const pipeline = kv.pipeline();
 
21
  pipeline.hgetall(chat);
22
  }
23
 
24
+ const results = (await pipeline.exec()) as ChatEntity[];
25
+
26
+ return results.sort((r1, r2) => r2.updatedAt - r1.updatedAt);
27
+ } catch (error) {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ export async function adminGetAllKVChats() {
33
+ const { isAdmin } = await authEmail();
34
+
35
+ if (!isAdmin) {
36
+ notFound();
37
+ }
38
+
39
+ try {
40
+ const pipeline = kv.pipeline();
41
+ const chats: string[] = await kv.zrange(`user:chat:all`, 0, -1, {
42
+ rev: true,
43
+ });
44
+
45
+ for (const chat of chats) {
46
+ pipeline.hgetall(chat);
47
+ }
48
+
49
+ const results = (await pipeline.exec()) as ChatEntity[];
50
 
51
+ return results.sort((r1, r2) => r2.updatedAt - r1.updatedAt);
52
  } catch (error) {
53
  return [];
54
  }
55
  }
56
 
57
  export async function getKVChat(id: string) {
58
+ // const { email, isAdmin } = await authEmail();
59
  const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
60
 
61
  if (!chat) {
 
66
  }
67
 
68
  export async function createKVChat(chat: ChatEntity) {
69
+ // const { email, isAdmin } = await authEmail();
70
+ const { email } = await authEmail();
71
 
72
  await kv.hmset(`chat:${chat.id}`, chat);
73
  if (email) {
 
92
  await kv.hmset(`chat:${id}`, {
93
  ...chat,
94
  messages: [...messages, message],
95
+ updatedAt: Date.now(),
96
  });
97
  revalidatePath('/chat', 'layout');
98
  }
lib/types.ts CHANGED
@@ -3,8 +3,8 @@ import { type Message } from 'ai';
3
  export type ServerActionResult<Result> = Promise<
4
  | Result
5
  | {
6
- error: string;
7
- }
8
  >;
9
 
10
  /**
@@ -34,6 +34,7 @@ export type ChatEntity = {
34
  id: string;
35
  user: string; // email
36
  messages: MessageBase[];
 
37
  };
38
 
39
  export interface SignedPayload {
@@ -41,4 +42,4 @@ export interface SignedPayload {
41
  publicUrl: string;
42
  signedUrl: string;
43
  fields: Record<string, string>;
44
- }
 
3
  export type ServerActionResult<Result> = Promise<
4
  | Result
5
  | {
6
+ error: string;
7
+ }
8
  >;
9
 
10
  /**
 
34
  id: string;
35
  user: string; // email
36
  messages: MessageBase[];
37
+ updatedAt: number;
38
  };
39
 
40
  export interface SignedPayload {
 
42
  publicUrl: string;
43
  signedUrl: string;
44
  fields: Record<string, string>;
45
+ }
lib/utils.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { clsx, type ClassValue } from 'clsx';
2
  import { customAlphabet } from 'nanoid';
3
  import { twMerge } from 'tailwind-merge';
 
1
+ import { auth } from '@/auth';
2
  import { clsx, type ClassValue } from 'clsx';
3
  import { customAlphabet } from 'nanoid';
4
  import { twMerge } from 'tailwind-merge';
state/index.ts CHANGED
@@ -4,3 +4,5 @@ import { DatasetImageEntity } from '../lib/types';
4
  // list of image urls or base64 strings
5
  export const datasetAtom = atom<DatasetImageEntity[]>([]);
6
  // export const selectedImagesAtom = atom<number[]>([]);
 
 
 
4
  // list of image urls or base64 strings
5
  export const datasetAtom = atom<DatasetImageEntity[]>([]);
6
  // export const selectedImagesAtom = atom<number[]>([]);
7
+
8
+ export const chatViewMode = atom<'chat' | 'chat-all'>('chat');