Spaces:
Sleeping
Sleeping
wuyiqunLu
commited on
feat: support internal access to all chat (#45)
Browse files<img width="1326" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/98a7d32a-c9af-49a5-a680-a44014c7e8f3">
- app/all/chat/[id]/page.tsx +16 -0
- app/all/layout.tsx +38 -0
- app/all/page.tsx +5 -0
- app/chat/layout.tsx +4 -2
- app/chat/page.tsx +1 -2
- components/Header.tsx +5 -0
- components/chat-sidebar/ChatCard.tsx +4 -3
- components/chat-sidebar/ChatListSidebar.tsx +18 -11
- components/chat/index.tsx +21 -18
app/all/chat/[id]/page.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getKVChat } from '@/lib/kv/chat';
|
| 2 |
+
import { Chat } from '@/components/chat';
|
| 3 |
+
import { auth } from '@/auth';
|
| 4 |
+
|
| 5 |
+
interface PageProps {
|
| 6 |
+
params: {
|
| 7 |
+
id: string;
|
| 8 |
+
};
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default async function Page({ params }: PageProps) {
|
| 12 |
+
const { id: chatId } = params;
|
| 13 |
+
const chat = await getKVChat(chatId);
|
| 14 |
+
const session = await auth();
|
| 15 |
+
return <Chat chat={chat} session={session} isAdminView />;
|
| 16 |
+
}
|
app/all/layout.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Suspense } from 'react';
|
| 2 |
+
import Loading from '@/components/ui/Loading';
|
| 3 |
+
import { authEmail } from '@/auth';
|
| 4 |
+
import { redirect } from 'next/navigation';
|
| 5 |
+
import { adminGetAllKVChats } from '@/lib/kv/chat';
|
| 6 |
+
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
| 7 |
+
|
| 8 |
+
interface ChatLayoutProps {
|
| 9 |
+
children: React.ReactNode;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default async function Layout({ children }: ChatLayoutProps) {
|
| 13 |
+
const { isAdmin } = await authEmail();
|
| 14 |
+
|
| 15 |
+
if (!isAdmin) {
|
| 16 |
+
redirect('/');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const chats = await adminGetAllKVChats();
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
|
| 23 |
+
<div
|
| 24 |
+
data-state="open"
|
| 25 |
+
className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out translate-x-0 lg:flex lg:w-[250px] xl:w-[300px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
|
| 26 |
+
>
|
| 27 |
+
<Suspense fallback={<Loading />}>
|
| 28 |
+
<ChatSidebarList chats={chats} isAdminView />
|
| 29 |
+
</Suspense>
|
| 30 |
+
</div>
|
| 31 |
+
<Suspense fallback={<Loading />}>
|
| 32 |
+
<div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
|
| 33 |
+
{children}
|
| 34 |
+
</div>
|
| 35 |
+
</Suspense>
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
}
|
app/all/page.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface PageProps {}
|
| 2 |
+
|
| 3 |
+
export default async function Page({}: PageProps) {
|
| 4 |
+
return <div></div>;
|
| 5 |
+
}
|
app/chat/layout.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
-
import {
|
| 2 |
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
| 3 |
import Loading from '@/components/ui/Loading';
|
|
|
|
| 4 |
import { Suspense } from 'react';
|
| 5 |
|
| 6 |
interface ChatLayoutProps {
|
|
@@ -9,6 +10,7 @@ interface ChatLayoutProps {
|
|
| 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
|
|
@@ -16,7 +18,7 @@ export default async function Layout({ children }: ChatLayoutProps) {
|
|
| 16 |
className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out -translate-x-full data-[state=open]:translate-x-0 lg:flex lg:w-[250px] h-full flex-col overflow-auto py-2"
|
| 17 |
>
|
| 18 |
<Suspense fallback={<Loading />}>
|
| 19 |
-
<ChatSidebarList />
|
| 20 |
</Suspense>
|
| 21 |
</div>
|
| 22 |
<Suspense fallback={<Loading />}>
|
|
|
|
| 1 |
+
import { authEmail } from '@/auth';
|
| 2 |
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
| 3 |
import Loading from '@/components/ui/Loading';
|
| 4 |
+
import { getKVChats } from '@/lib/kv/chat';
|
| 5 |
import { Suspense } from 'react';
|
| 6 |
|
| 7 |
interface ChatLayoutProps {
|
|
|
|
| 10 |
|
| 11 |
export default async function Layout({ children }: ChatLayoutProps) {
|
| 12 |
const { email, isAdmin } = await authEmail();
|
| 13 |
+
const chats = await getKVChats();
|
| 14 |
return (
|
| 15 |
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
|
| 16 |
<div
|
|
|
|
| 18 |
className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out -translate-x-full data-[state=open]:translate-x-0 lg:flex lg:w-[250px] h-full flex-col overflow-auto py-2"
|
| 19 |
>
|
| 20 |
<Suspense fallback={<Loading />}>
|
| 21 |
+
<ChatSidebarList chats={chats} />
|
| 22 |
</Suspense>
|
| 23 |
</div>
|
| 24 |
<Suspense fallback={<Loading />}>
|
app/chat/page.tsx
CHANGED
|
@@ -2,9 +2,8 @@
|
|
| 2 |
|
| 3 |
import ImageSelector from '@/components/chat/ImageSelector';
|
| 4 |
import { generateInputImageMarkdown } from '@/lib/messageUtils';
|
| 5 |
-
import { ChatEntity
|
| 6 |
import { fetcher } from '@/lib/utils';
|
| 7 |
-
import Image from 'next/image';
|
| 8 |
import { useRouter } from 'next/navigation';
|
| 9 |
|
| 10 |
import {
|
|
|
|
| 2 |
|
| 3 |
import ImageSelector from '@/components/chat/ImageSelector';
|
| 4 |
import { generateInputImageMarkdown } from '@/lib/messageUtils';
|
| 5 |
+
import { ChatEntity } from '@/lib/types';
|
| 6 |
import { fetcher } from '@/lib/utils';
|
|
|
|
| 7 |
import { useRouter } from 'next/navigation';
|
| 8 |
|
| 9 |
import {
|
components/Header.tsx
CHANGED
|
@@ -38,6 +38,11 @@ export async function Header() {
|
|
| 38 |
</TooltipTrigger>
|
| 39 |
<TooltipContent>New chat</TooltipContent>
|
| 40 |
</Tooltip> */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
{isAdmin && (
|
| 42 |
<Button variant="link" asChild className="mr-2">
|
| 43 |
<Link href="/project">Projects (Internal)</Link>
|
|
|
|
| 38 |
</TooltipTrigger>
|
| 39 |
<TooltipContent>New chat</TooltipContent>
|
| 40 |
</Tooltip> */}
|
| 41 |
+
{isAdmin && (
|
| 42 |
+
<Button variant="link" asChild className="mr-2">
|
| 43 |
+
<Link href="/all">All Chats (Internal)</Link>
|
| 44 |
+
</Button>
|
| 45 |
+
)}
|
| 46 |
{isAdmin && (
|
| 47 |
<Button variant="link" asChild className="mr-2">
|
| 48 |
<Link href="/project">Projects (Internal)</Link>
|
components/chat-sidebar/ChatCard.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { cleanInputMessage } from '@/lib/messageUtils';
|
|
| 14 |
|
| 15 |
type ChatCardProps = PropsWithChildren<{
|
| 16 |
chat: ChatEntity;
|
|
|
|
| 17 |
}>;
|
| 18 |
|
| 19 |
export const ChatCardLayout: React.FC<
|
|
@@ -32,9 +33,8 @@ export const ChatCardLayout: React.FC<
|
|
| 32 |
);
|
| 33 |
};
|
| 34 |
|
| 35 |
-
const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
| 36 |
const { id: chatIdFromParam } = useParams();
|
| 37 |
-
const pathname = usePathname();
|
| 38 |
const { id, url, messages, user, updatedAt } = chat;
|
| 39 |
const firstMessage = cleanInputMessage(messages?.[0]?.content ?? '');
|
| 40 |
const title = firstMessage
|
|
@@ -44,7 +44,7 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
|
| 44 |
: '(No messages yet)';
|
| 45 |
return (
|
| 46 |
<ChatCardLayout
|
| 47 |
-
link={`/chat/${id}`}
|
| 48 |
classNames={chatIdFromParam === id && 'border-gray-500'}
|
| 49 |
>
|
| 50 |
<div className="overflow-hidden flex items-center size-full">
|
|
@@ -54,6 +54,7 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
|
| 54 |
<p className="text-xs text-gray-500">
|
| 55 |
{updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
|
| 56 |
</p>
|
|
|
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
</ChatCardLayout>
|
|
|
|
| 14 |
|
| 15 |
type ChatCardProps = PropsWithChildren<{
|
| 16 |
chat: ChatEntity;
|
| 17 |
+
isAdminView?: boolean;
|
| 18 |
}>;
|
| 19 |
|
| 20 |
export const ChatCardLayout: React.FC<
|
|
|
|
| 33 |
);
|
| 34 |
};
|
| 35 |
|
| 36 |
+
const ChatCard: React.FC<ChatCardProps> = ({ chat, isAdminView }) => {
|
| 37 |
const { id: chatIdFromParam } = useParams();
|
|
|
|
| 38 |
const { id, url, messages, user, updatedAt } = chat;
|
| 39 |
const firstMessage = cleanInputMessage(messages?.[0]?.content ?? '');
|
| 40 |
const title = firstMessage
|
|
|
|
| 44 |
: '(No messages yet)';
|
| 45 |
return (
|
| 46 |
<ChatCardLayout
|
| 47 |
+
link={isAdminView ? `/all/chat/${id}` : `/chat/${id}`}
|
| 48 |
classNames={chatIdFromParam === id && 'border-gray-500'}
|
| 49 |
>
|
| 50 |
<div className="overflow-hidden flex items-center size-full">
|
|
|
|
| 54 |
<p className="text-xs text-gray-500">
|
| 55 |
{updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
|
| 56 |
</p>
|
| 57 |
+
{isAdminView && <p className="text-xs text-gray-500">{user}</p>}
|
| 58 |
</div>
|
| 59 |
</div>
|
| 60 |
</ChatCardLayout>
|
components/chat-sidebar/ChatListSidebar.tsx
CHANGED
|
@@ -1,26 +1,33 @@
|
|
| 1 |
-
import { getKVChats } from '@/lib/kv/chat';
|
| 2 |
import ChatCard, { ChatCardLayout } from './ChatCard';
|
| 3 |
import { IconPlus } from '../ui/Icons';
|
| 4 |
import { auth } from '@/auth';
|
|
|
|
| 5 |
|
| 6 |
-
export interface ChatSidebarListProps {
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
export default async function ChatSidebarList({
|
|
|
|
|
|
|
|
|
|
| 9 |
const session = await auth();
|
| 10 |
if (!session || !session.user) {
|
| 11 |
return null;
|
| 12 |
}
|
| 13 |
-
const chats = await getKVChats();
|
| 14 |
return (
|
| 15 |
<>
|
| 16 |
-
|
| 17 |
-
<
|
| 18 |
-
<
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
{chats.map(chat => (
|
| 23 |
-
<ChatCard key={chat.id} chat={chat} />
|
| 24 |
))}
|
| 25 |
</>
|
| 26 |
);
|
|
|
|
|
|
|
| 1 |
import ChatCard, { ChatCardLayout } from './ChatCard';
|
| 2 |
import { IconPlus } from '../ui/Icons';
|
| 3 |
import { auth } from '@/auth';
|
| 4 |
+
import { ChatEntity } from '@/lib/types';
|
| 5 |
|
| 6 |
+
export interface ChatSidebarListProps {
|
| 7 |
+
chats: ChatEntity[];
|
| 8 |
+
isAdminView?: boolean;
|
| 9 |
+
}
|
| 10 |
|
| 11 |
+
export default async function ChatSidebarList({
|
| 12 |
+
chats,
|
| 13 |
+
isAdminView,
|
| 14 |
+
}: ChatSidebarListProps) {
|
| 15 |
const session = await auth();
|
| 16 |
if (!session || !session.user) {
|
| 17 |
return null;
|
| 18 |
}
|
|
|
|
| 19 |
return (
|
| 20 |
<>
|
| 21 |
+
{!isAdminView && (
|
| 22 |
+
<ChatCardLayout link="/chat">
|
| 23 |
+
<div className="overflow-hidden flex items-center size-full">
|
| 24 |
+
<IconPlus className="w-1/4 font-bold" />
|
| 25 |
+
<p className="text-sm w-3/4 ml-3 font-bold">New chat</p>
|
| 26 |
+
</div>
|
| 27 |
+
</ChatCardLayout>
|
| 28 |
+
)}
|
| 29 |
{chats.map(chat => (
|
| 30 |
+
<ChatCard key={chat.id} chat={chat} isAdminView={isAdminView} />
|
| 31 |
))}
|
| 32 |
</>
|
| 33 |
);
|
components/chat/index.tsx
CHANGED
|
@@ -10,10 +10,11 @@ import { useState } from 'react';
|
|
| 10 |
|
| 11 |
export interface ChatProps extends React.ComponentProps<'div'> {
|
| 12 |
chat: ChatEntity;
|
|
|
|
| 13 |
session: Session | null;
|
| 14 |
}
|
| 15 |
|
| 16 |
-
export function Chat({ chat, session }: ChatProps) {
|
| 17 |
const { url, id } = chat;
|
| 18 |
const [enableSelfReflection, setEnableSelfReflection] =
|
| 19 |
useState<boolean>(true);
|
|
@@ -35,23 +36,25 @@ export function Chat({ chat, session }: ChatProps) {
|
|
| 35 |
<div className="h-px w-full" ref={visibilityRef} />
|
| 36 |
</div>
|
| 37 |
</div>
|
| 38 |
-
|
| 39 |
-
<
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
</>
|
| 56 |
);
|
| 57 |
}
|
|
|
|
| 10 |
|
| 11 |
export interface ChatProps extends React.ComponentProps<'div'> {
|
| 12 |
chat: ChatEntity;
|
| 13 |
+
isAdminView?: boolean;
|
| 14 |
session: Session | null;
|
| 15 |
}
|
| 16 |
|
| 17 |
+
export function Chat({ chat, session, isAdminView }: ChatProps) {
|
| 18 |
const { url, id } = chat;
|
| 19 |
const [enableSelfReflection, setEnableSelfReflection] =
|
| 20 |
useState<boolean>(true);
|
|
|
|
| 36 |
<div className="h-px w-full" ref={visibilityRef} />
|
| 37 |
</div>
|
| 38 |
</div>
|
| 39 |
+
{!isAdminView && (
|
| 40 |
+
<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]">
|
| 41 |
+
<Composer
|
| 42 |
+
id={id}
|
| 43 |
+
url={url}
|
| 44 |
+
isLoading={isLoading}
|
| 45 |
+
stop={stop}
|
| 46 |
+
append={append}
|
| 47 |
+
reload={reload}
|
| 48 |
+
messages={messages}
|
| 49 |
+
input={input}
|
| 50 |
+
setInput={setInput}
|
| 51 |
+
isAtBottom={isAtBottom}
|
| 52 |
+
scrollToBottom={scrollToBottom}
|
| 53 |
+
enableSelfReflection={enableSelfReflection}
|
| 54 |
+
setEnableSelfReflection={setEnableSelfReflection}
|
| 55 |
+
/>
|
| 56 |
+
</div>
|
| 57 |
+
)}
|
| 58 |
</>
|
| 59 |
);
|
| 60 |
}
|