Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Commit
β’
c3e8f3d
1
Parent(s):
7eeb895
KV template
Browse files- app/api/chat/route.ts +2 -9
- app/api/upload/route.ts +32 -0
- app/chat/layout.tsx +27 -0
- app/chat/page.tsx +1 -21
- app/layout.tsx +1 -0
- app/page.tsx +15 -7
- app/project/[projectId]/page.tsx +1 -1
- app/project/layout.tsx +2 -2
- components/Header.tsx +2 -2
- components/chat-sidebar/ChatCard.tsx +29 -0
- components/chat-sidebar/ChatListSidebar.tsx +16 -0
- components/chat/ImageSelector.tsx +57 -0
- components/chat/index.tsx +8 -4
- components/{sidebar β project-sidebar}/ProjectCard.tsx +0 -0
- components/{sidebar β project-sidebar}/ProjectListSideBar.tsx +0 -0
- components/ui/Icons.tsx +1 -1
- app/project/loading.tsx β components/ui/Loading.tsx +1 -1
- lib/fetch/index.ts +9 -21
- lib/kv/chat.ts +69 -0
- lib/types.ts +0 -2
- lib/utils.ts +30 -29
- package.json +2 -1
- pnpm-lock.yaml +0 -0
app/api/chat/route.ts
CHANGED
@@ -8,7 +8,7 @@ import {
|
|
8 |
ChatCompletionContentPartImage,
|
9 |
} from 'openai/resources';
|
10 |
import { MessageWithSelectedDataset } from '../../../lib/types';
|
11 |
-
import { postAgentChat } from '@/lib/fetch';
|
12 |
|
13 |
export const runtime = 'edge';
|
14 |
|
@@ -21,6 +21,7 @@ export async function POST(req: Request) {
|
|
21 |
const { messages } = json as {
|
22 |
messages: MessageWithSelectedDataset[];
|
23 |
};
|
|
|
24 |
|
25 |
const session = await auth();
|
26 |
if (!session?.user?.email) {
|
@@ -29,14 +30,6 @@ export async function POST(req: Request) {
|
|
29 |
});
|
30 |
}
|
31 |
|
32 |
-
// const lastMessage = messages[messages.length - 1];
|
33 |
-
// const firstMessage = messages[0];
|
34 |
-
|
35 |
-
// const resp = await postAgentChat({
|
36 |
-
// input: lastMessage.content,
|
37 |
-
// image: firstMessage.dataset?.[0]?.url,
|
38 |
-
// });
|
39 |
-
|
40 |
const formattedMessage: ChatCompletionMessageParam[] = messages.map(
|
41 |
message => {
|
42 |
const { dataset, ...rest } = message;
|
|
|
8 |
ChatCompletionContentPartImage,
|
9 |
} from 'openai/resources';
|
10 |
import { MessageWithSelectedDataset } from '../../../lib/types';
|
11 |
+
// import { postAgentChat } from '@/lib/fetch';
|
12 |
|
13 |
export const runtime = 'edge';
|
14 |
|
|
|
21 |
const { messages } = json as {
|
22 |
messages: MessageWithSelectedDataset[];
|
23 |
};
|
24 |
+
console.log('[Ming] ~ POST ~ messages:', messages);
|
25 |
|
26 |
const session = await auth();
|
27 |
if (!session?.user?.email) {
|
|
|
30 |
});
|
31 |
}
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
const formattedMessage: ChatCompletionMessageParam[] = messages.map(
|
34 |
message => {
|
35 |
const { dataset, ...rest } = message;
|
app/api/upload/route.ts
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { auth } from '@/auth';
|
2 |
+
import { nanoid } from '@/lib/utils';
|
3 |
+
import { kv } from '@vercel/kv';
|
4 |
+
import { format } from 'date-fns';
|
5 |
+
|
6 |
+
export async function POST(req: Request) {
|
7 |
+
const session = await auth();
|
8 |
+
console.log('[Ming] ~ POST ~ session:', session);
|
9 |
+
const email = session?.user?.email;
|
10 |
+
if (!email) {
|
11 |
+
return new Response('Unauthorized', {
|
12 |
+
status: 401,
|
13 |
+
});
|
14 |
+
}
|
15 |
+
|
16 |
+
const json = await req.json();
|
17 |
+
console.log('[Ming] ~ POST ~ json:', json);
|
18 |
+
|
19 |
+
const id = nanoid();
|
20 |
+
|
21 |
+
await kv.hmset(`chat:${id}`, json);
|
22 |
+
await kv.zadd(`user:chat:${email}`, {
|
23 |
+
score: Date.now(),
|
24 |
+
member: `chat:${id}`,
|
25 |
+
});
|
26 |
+
await kv.zadd('user:chat:all', {
|
27 |
+
score: Date.now(),
|
28 |
+
member: `chat:${id}`,
|
29 |
+
});
|
30 |
+
|
31 |
+
return 'success';
|
32 |
+
}
|
app/chat/layout.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
2 |
+
import Loading from '@/components/ui/Loading';
|
3 |
+
import { Suspense } from 'react';
|
4 |
+
|
5 |
+
interface ChatLayoutProps {
|
6 |
+
children: React.ReactNode;
|
7 |
+
}
|
8 |
+
|
9 |
+
export default async function Layout({ children }: ChatLayoutProps) {
|
10 |
+
return (
|
11 |
+
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
|
12 |
+
<div
|
13 |
+
data-state="open"
|
14 |
+
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"
|
15 |
+
>
|
16 |
+
<Suspense fallback={<Loading />}>
|
17 |
+
<ChatSidebarList />
|
18 |
+
</Suspense>
|
19 |
+
</div>
|
20 |
+
<Suspense fallback={<Loading />}>
|
21 |
+
<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]">
|
22 |
+
{children}
|
23 |
+
</div>
|
24 |
+
</Suspense>
|
25 |
+
</div>
|
26 |
+
);
|
27 |
+
}
|
app/chat/page.tsx
CHANGED
@@ -1,27 +1,7 @@
|
|
1 |
-
'use client';
|
2 |
-
|
3 |
import { nanoid } from '@/lib/utils';
|
4 |
import { Chat } from '@/components/chat';
|
5 |
-
import { ThemeToggle } from '../../components/ThemeToggle';
|
6 |
-
import { useAtomValue } from 'jotai';
|
7 |
-
import { datasetAtom } from '../../state';
|
8 |
-
import { EmptyScreen } from '../../components/chat/EmptyScreen';
|
9 |
|
10 |
export default function Page() {
|
11 |
const id = nanoid();
|
12 |
-
|
13 |
-
|
14 |
-
if (!dataset.length)
|
15 |
-
return (
|
16 |
-
<div className="pb-[150px] pt-4 md:pt-10 h-full">
|
17 |
-
<EmptyScreen />
|
18 |
-
</div>
|
19 |
-
);
|
20 |
-
|
21 |
-
return (
|
22 |
-
<>
|
23 |
-
<Chat id={id} />
|
24 |
-
<ThemeToggle />
|
25 |
-
</>
|
26 |
-
);
|
27 |
}
|
|
|
|
|
|
|
1 |
import { nanoid } from '@/lib/utils';
|
2 |
import { Chat } from '@/components/chat';
|
|
|
|
|
|
|
|
|
3 |
|
4 |
export default function Page() {
|
5 |
const id = nanoid();
|
6 |
+
return <Chat id={id} />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
}
|
app/layout.tsx
CHANGED
@@ -7,6 +7,7 @@ import { cn } from '@/lib/utils';
|
|
7 |
import { TailwindIndicator } from '@/components/TailwindIndicator';
|
8 |
import { Providers } from '@/components/Providers';
|
9 |
import { Header } from '@/components/Header';
|
|
|
10 |
|
11 |
export const metadata = {
|
12 |
metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
|
|
|
7 |
import { TailwindIndicator } from '@/components/TailwindIndicator';
|
8 |
import { Providers } from '@/components/Providers';
|
9 |
import { Header } from '@/components/Header';
|
10 |
+
import { ThemeToggle } from '@/components/ThemeToggle';
|
11 |
|
12 |
export const metadata = {
|
13 |
metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
|
app/page.tsx
CHANGED
@@ -1,9 +1,17 @@
|
|
1 |
-
|
|
|
2 |
|
3 |
-
export default function Page() {
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
}
|
|
|
1 |
+
import { auth } from '@/auth';
|
2 |
+
import { redirect } from 'next/navigation';
|
3 |
|
4 |
+
export default async function Page() {
|
5 |
+
const session = await auth();
|
6 |
+
if (!session) {
|
7 |
+
return null;
|
8 |
+
}
|
9 |
+
|
10 |
+
redirect('/chat');
|
11 |
+
|
12 |
+
// return (
|
13 |
+
// <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
|
14 |
+
// Welcome to Insight Playground
|
15 |
+
// </div>
|
16 |
+
// );
|
17 |
}
|
app/project/[projectId]/page.tsx
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import MediaGrid from '@/components/project/MediaGrid';
|
2 |
import { fetchProjectMedia } from '@/lib/fetch';
|
3 |
import { Suspense } from 'react';
|
4 |
-
import Loading from '
|
5 |
import Chat from '@/components/project/Chat';
|
6 |
|
7 |
interface PageProps {
|
|
|
1 |
import MediaGrid from '@/components/project/MediaGrid';
|
2 |
import { fetchProjectMedia } from '@/lib/fetch';
|
3 |
import { Suspense } from 'react';
|
4 |
+
import Loading from '../../../components/ui/Loading';
|
5 |
import Chat from '@/components/project/Chat';
|
6 |
|
7 |
interface PageProps {
|
app/project/layout.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
-
import ProjectListSideBar from '@/components/sidebar/ProjectListSideBar';
|
2 |
import { Suspense } from 'react';
|
3 |
-
import Loading from '
|
4 |
|
5 |
interface ChatLayoutProps {
|
6 |
children: React.ReactNode;
|
|
|
1 |
+
import ProjectListSideBar from '@/components/project-sidebar/ProjectListSideBar';
|
2 |
import { Suspense } from 'react';
|
3 |
+
import Loading from '@/components/ui/Loading';
|
4 |
|
5 |
interface ChatLayoutProps {
|
6 |
children: React.ReactNode;
|
components/Header.tsx
CHANGED
@@ -15,9 +15,9 @@ export async function Header() {
|
|
15 |
|
16 |
return (
|
17 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
18 |
-
<Button variant="link" asChild className="mr-2">
|
19 |
<Link href="/project">Projects</Link>
|
20 |
-
</Button>
|
21 |
<Button variant="link" asChild className="mr-2">
|
22 |
<Link href="/chat">Chat</Link>
|
23 |
</Button>
|
|
|
15 |
|
16 |
return (
|
17 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
18 |
+
{/* <Button variant="link" asChild className="mr-2">
|
19 |
<Link href="/project">Projects</Link>
|
20 |
+
</Button> */}
|
21 |
<Button variant="link" asChild className="mr-2">
|
22 |
<Link href="/chat">Chat</Link>
|
23 |
</Button>
|
components/chat-sidebar/ChatCard.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import Link from 'next/link';
|
4 |
+
import { useParams } from 'next/navigation';
|
5 |
+
import { cn } from '@/lib/utils';
|
6 |
+
|
7 |
+
export interface ChatCardProps {
|
8 |
+
id: string;
|
9 |
+
title: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
const ChatCard: React.FC<ChatCardProps> = ({ id, title }) => {
|
13 |
+
const { chatId: chatIdFromParam } = useParams();
|
14 |
+
return (
|
15 |
+
<Link
|
16 |
+
className={cn(
|
17 |
+
'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
|
18 |
+
chatIdFromParam === id && 'border-gray-500',
|
19 |
+
)}
|
20 |
+
href={`/chat/${id}`}
|
21 |
+
>
|
22 |
+
<div className="overflow-hidden">
|
23 |
+
<p className="text-sm font-medium text-black mb-1">{title}</p>
|
24 |
+
</div>
|
25 |
+
</Link>
|
26 |
+
);
|
27 |
+
};
|
28 |
+
|
29 |
+
export default ChatCard;
|
components/chat-sidebar/ChatListSidebar.tsx
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { getKVChats } from '@/lib/kv/chat';
|
2 |
+
import ChatCard from './ChatCard';
|
3 |
+
|
4 |
+
export interface ChatSidebarListProps {}
|
5 |
+
|
6 |
+
export default async function ChatSidebarList({}: ChatSidebarListProps) {
|
7 |
+
const chats = await getKVChats();
|
8 |
+
console.log('[Ming] ~ ChatSidebarList ~ chats:', chats);
|
9 |
+
return (
|
10 |
+
<>
|
11 |
+
{chats.map(chat => (
|
12 |
+
<ChatCard key={chat.id} id={chat.id} title={chat.title} />
|
13 |
+
))}
|
14 |
+
</>
|
15 |
+
);
|
16 |
+
}
|
components/chat/ImageSelector.tsx
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import Image from 'next/image';
|
3 |
+
import useImageUpload from '../../lib/hooks/useImageUpload';
|
4 |
+
import { fetcher } from '@/lib/utils';
|
5 |
+
|
6 |
+
export interface ImageSelectorProps {}
|
7 |
+
|
8 |
+
const examples = [
|
9 |
+
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
|
10 |
+
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
|
11 |
+
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
|
12 |
+
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
|
13 |
+
];
|
14 |
+
|
15 |
+
const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
16 |
+
const { getRootProps, getInputProps } = useImageUpload();
|
17 |
+
return (
|
18 |
+
<div className="mx-auto max-w-2xl px-4">
|
19 |
+
<div className="rounded-lg border bg-background p-8">
|
20 |
+
<h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
|
21 |
+
<p>Lets start by choosing an image</p>
|
22 |
+
<div
|
23 |
+
{...getRootProps()}
|
24 |
+
className="dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer"
|
25 |
+
>
|
26 |
+
<input {...getInputProps()} />
|
27 |
+
<p className="text-gray-400 text-lg">
|
28 |
+
Drag or drop image here, or click to select images
|
29 |
+
</p>
|
30 |
+
</div>
|
31 |
+
<p className="mt-4 mb-2">
|
32 |
+
You can also choose from below examples we provided
|
33 |
+
</p>
|
34 |
+
<div className="flex">
|
35 |
+
{examples.map((example, index) => (
|
36 |
+
<Image
|
37 |
+
src={example}
|
38 |
+
key={index}
|
39 |
+
width={120}
|
40 |
+
height={120}
|
41 |
+
alt="example images"
|
42 |
+
className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
|
43 |
+
onClick={() =>
|
44 |
+
fetcher('/api/upload', {
|
45 |
+
method: 'POST',
|
46 |
+
body: JSON.stringify({ url: example }),
|
47 |
+
})
|
48 |
+
}
|
49 |
+
/>
|
50 |
+
))}
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
);
|
55 |
+
};
|
56 |
+
|
57 |
+
export default ImageSelector;
|
components/chat/index.tsx
CHANGED
@@ -5,21 +5,25 @@ import { ChatPanel } from '@/components/chat/ChatPanel';
|
|
5 |
import { ChatScrollAnchor } from '@/components/chat/ChatScrollAnchor';
|
6 |
import ImageList from './ImageList';
|
7 |
import useChatWithDataset from '../../lib/hooks/useChatWithDataset';
|
|
|
|
|
|
|
8 |
|
9 |
export interface ChatProps extends React.ComponentProps<'div'> {
|
10 |
-
id
|
11 |
}
|
12 |
|
13 |
export function Chat({ id, className }: ChatProps) {
|
14 |
const { messages, append, reload, stop, isLoading, input, setInput } =
|
15 |
-
|
16 |
|
17 |
return (
|
18 |
<>
|
19 |
<div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
|
20 |
<div className="flex h-full">
|
21 |
-
<div className="w-1/2 relative border-r
|
22 |
-
<ImageList />
|
|
|
23 |
</div>
|
24 |
<div className="w-1/2 relative overflow-auto">
|
25 |
<ChatList messages={messages} />
|
|
|
5 |
import { ChatScrollAnchor } from '@/components/chat/ChatScrollAnchor';
|
6 |
import ImageList from './ImageList';
|
7 |
import useChatWithDataset from '../../lib/hooks/useChatWithDataset';
|
8 |
+
import { useChat } from 'ai/react';
|
9 |
+
import { Button } from '../ui/Button';
|
10 |
+
import ImageSelector from './ImageSelector';
|
11 |
|
12 |
export interface ChatProps extends React.ComponentProps<'div'> {
|
13 |
+
id: string;
|
14 |
}
|
15 |
|
16 |
export function Chat({ id, className }: ChatProps) {
|
17 |
const { messages, append, reload, stop, isLoading, input, setInput } =
|
18 |
+
useChat();
|
19 |
|
20 |
return (
|
21 |
<>
|
22 |
<div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
|
23 |
<div className="flex h-full">
|
24 |
+
<div className="w-1/2 relative border-r border-gray-400 overflow-auto">
|
25 |
+
{/* <ImageList /> */}
|
26 |
+
<ImageSelector />
|
27 |
</div>
|
28 |
<div className="w-1/2 relative overflow-auto">
|
29 |
<ChatList messages={messages} />
|
components/{sidebar β project-sidebar}/ProjectCard.tsx
RENAMED
File without changes
|
components/{sidebar β project-sidebar}/ProjectListSideBar.tsx
RENAMED
File without changes
|
components/ui/Icons.tsx
CHANGED
@@ -511,7 +511,7 @@ function IconLoading({ className, ...props }: React.ComponentProps<'svg'>) {
|
|
511 |
return (
|
512 |
<svg
|
513 |
aria-hidden="true"
|
514 |
-
className="size-8 text-gray-200 animate-spin dark:text-gray-600 fill-
|
515 |
viewBox="0 0 100 101"
|
516 |
fill="none"
|
517 |
xmlns="http://www.w3.org/2000/svg"
|
|
|
511 |
return (
|
512 |
<svg
|
513 |
aria-hidden="true"
|
514 |
+
className="size-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600"
|
515 |
viewBox="0 0 100 101"
|
516 |
fill="none"
|
517 |
xmlns="http://www.w3.org/2000/svg"
|
app/project/loading.tsx β components/ui/Loading.tsx
RENAMED
@@ -1,6 +1,6 @@
|
|
1 |
import { IconLoading } from '@/components/ui/Icons';
|
2 |
|
3 |
-
export default
|
4 |
return (
|
5 |
<div className="flex justify-center items-center size-full text-sm">
|
6 |
<IconLoading />
|
|
|
1 |
import { IconLoading } from '@/components/ui/Icons';
|
2 |
|
3 |
+
export default function Loading() {
|
4 |
return (
|
5 |
<div className="flex justify-center items-center size-full text-sm">
|
6 |
<IconLoading />
|
lib/fetch/index.ts
CHANGED
@@ -7,7 +7,7 @@ interface ApiResponse<T> {
|
|
7 |
data: T;
|
8 |
}
|
9 |
|
10 |
-
const
|
11 |
path: string,
|
12 |
options?: {
|
13 |
// default to GET
|
@@ -18,18 +18,19 @@ const apiBuilder = <Params extends object | void, Resp>(
|
|
18 |
) => {
|
19 |
return async (params: Params): Promise<Resp> => {
|
20 |
const session = await auth();
|
21 |
-
|
|
|
|
|
22 |
throw new Response('Unauthorized', {
|
23 |
status: 401,
|
24 |
});
|
25 |
}
|
26 |
|
27 |
-
const adminEmail = session.user.email;
|
28 |
const sessionUser = {
|
29 |
-
id: uuidV5(
|
30 |
orgId: '-1024',
|
31 |
-
email:
|
32 |
-
username:
|
33 |
userRole: 'adminPortal',
|
34 |
bucket: 'fake_bucket',
|
35 |
};
|
@@ -63,7 +64,6 @@ const apiBuilder = <Params extends object | void, Resp>(
|
|
63 |
|
64 |
fetchParams.body = formData;
|
65 |
}
|
66 |
-
console.log('[Ming] ~ return ~ fetchParams:', fetchParams, url.toString());
|
67 |
|
68 |
const res = await fetch(url.toString(), fetchParams);
|
69 |
|
@@ -94,7 +94,7 @@ export type ProjectBaseInfo = {
|
|
94 |
* 3. projects not containing media or only contain sample media
|
95 |
* @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
|
96 |
*/
|
97 |
-
export const fetchRecentProjectList =
|
98 |
'api/admin/projects/recent',
|
99 |
);
|
100 |
|
@@ -117,19 +117,7 @@ export type MediaDetails = {
|
|
117 |
* Randomly fetch 10 media from a given project
|
118 |
* @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
|
119 |
*/
|
120 |
-
export const fetchProjectMedia =
|
121 |
{ projectId: number },
|
122 |
MediaDetails[]
|
123 |
>('api/admin/project/media');
|
124 |
-
|
125 |
-
/**
|
126 |
-
* Call vision agent
|
127 |
-
* @author https://github.com/landing-ai/public-rest-api/pull/36
|
128 |
-
*/
|
129 |
-
export const postAgentChat = apiBuilder<
|
130 |
-
{ input: string; image: string },
|
131 |
-
MediaDetails[]
|
132 |
-
>('v1/agent/chat?agent_class=vision_agent', {
|
133 |
-
method: 'POST',
|
134 |
-
prefix: 'api.dev',
|
135 |
-
});
|
|
|
7 |
data: T;
|
8 |
}
|
9 |
|
10 |
+
const clefApiBuilder = <Params extends object | void, Resp>(
|
11 |
path: string,
|
12 |
options?: {
|
13 |
// default to GET
|
|
|
18 |
) => {
|
19 |
return async (params: Params): Promise<Resp> => {
|
20 |
const session = await auth();
|
21 |
+
const email = session?.user?.email;
|
22 |
+
|
23 |
+
if (!email || !email.endsWith('@landing.ai')) {
|
24 |
throw new Response('Unauthorized', {
|
25 |
status: 401,
|
26 |
});
|
27 |
}
|
28 |
|
|
|
29 |
const sessionUser = {
|
30 |
+
id: uuidV5(email, uuidV5.URL),
|
31 |
orgId: '-1024',
|
32 |
+
email: email,
|
33 |
+
username: email.split('@')[0],
|
34 |
userRole: 'adminPortal',
|
35 |
bucket: 'fake_bucket',
|
36 |
};
|
|
|
64 |
|
65 |
fetchParams.body = formData;
|
66 |
}
|
|
|
67 |
|
68 |
const res = await fetch(url.toString(), fetchParams);
|
69 |
|
|
|
94 |
* 3. projects not containing media or only contain sample media
|
95 |
* @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
|
96 |
*/
|
97 |
+
export const fetchRecentProjectList = clefApiBuilder<void, ProjectBaseInfo[]>(
|
98 |
'api/admin/projects/recent',
|
99 |
);
|
100 |
|
|
|
117 |
* Randomly fetch 10 media from a given project
|
118 |
* @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
|
119 |
*/
|
120 |
+
export const fetchProjectMedia = clefApiBuilder<
|
121 |
{ projectId: number },
|
122 |
MediaDetails[]
|
123 |
>('api/admin/project/media');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib/kv/chat.ts
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use server';
|
2 |
+
|
3 |
+
import { revalidatePath } from 'next/cache';
|
4 |
+
import { redirect } from 'next/navigation';
|
5 |
+
import { kv } from '@vercel/kv';
|
6 |
+
|
7 |
+
import { auth } from '@/auth';
|
8 |
+
import { type Chat } from '@/lib/types';
|
9 |
+
|
10 |
+
export async function getKVChats() {
|
11 |
+
const session = await auth();
|
12 |
+
const email = session?.user?.email;
|
13 |
+
|
14 |
+
if (!email) {
|
15 |
+
return [];
|
16 |
+
}
|
17 |
+
|
18 |
+
try {
|
19 |
+
const pipeline = kv.pipeline();
|
20 |
+
const chats: string[] = await kv.zrange(`user:chat:${email}`, 0, -1, {
|
21 |
+
rev: true,
|
22 |
+
});
|
23 |
+
|
24 |
+
for (const chat of chats) {
|
25 |
+
pipeline.hgetall(chat);
|
26 |
+
}
|
27 |
+
|
28 |
+
const results = await pipeline.exec();
|
29 |
+
|
30 |
+
return results as Chat[];
|
31 |
+
} catch (error) {
|
32 |
+
return [];
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
export async function getKVChat(id: string, userId: string) {
|
37 |
+
const chat = await kv.hgetall<Chat>(`chat:${id}`);
|
38 |
+
|
39 |
+
if (!chat || (userId && chat.userId !== userId)) {
|
40 |
+
return null;
|
41 |
+
}
|
42 |
+
|
43 |
+
return chat;
|
44 |
+
}
|
45 |
+
|
46 |
+
export async function removeKVChat({ id, path }: { id: string; path: string }) {
|
47 |
+
const session = await auth();
|
48 |
+
|
49 |
+
if (!session) {
|
50 |
+
return {
|
51 |
+
error: 'Unauthorized',
|
52 |
+
};
|
53 |
+
}
|
54 |
+
|
55 |
+
//Convert uid to string for consistent comparison with session.user.id
|
56 |
+
const uid = String(await kv.hget(`chat:${id}`, 'userId'));
|
57 |
+
|
58 |
+
if (uid !== session?.user?.id) {
|
59 |
+
return {
|
60 |
+
error: 'Unauthorized',
|
61 |
+
};
|
62 |
+
}
|
63 |
+
|
64 |
+
await kv.del(`chat:${id}`);
|
65 |
+
await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`);
|
66 |
+
|
67 |
+
revalidatePath('/');
|
68 |
+
return revalidatePath(path);
|
69 |
+
}
|
lib/types.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import { type Message } from 'ai';
|
2 |
-
import { CreateMessage } from 'ai/react/dist';
|
3 |
|
4 |
export interface Chat extends Record<string, any> {
|
5 |
id: string;
|
@@ -8,7 +7,6 @@ export interface Chat extends Record<string, any> {
|
|
8 |
userId: string;
|
9 |
path: string;
|
10 |
messages: Message[];
|
11 |
-
sharePath?: string;
|
12 |
}
|
13 |
|
14 |
export type ServerActionResult<Result> = Promise<
|
|
|
1 |
import { type Message } from 'ai';
|
|
|
2 |
|
3 |
export interface Chat extends Record<string, any> {
|
4 |
id: string;
|
|
|
7 |
userId: string;
|
8 |
path: string;
|
9 |
messages: Message[];
|
|
|
10 |
}
|
11 |
|
12 |
export type ServerActionResult<Result> = Promise<
|
lib/utils.ts
CHANGED
@@ -1,43 +1,44 @@
|
|
1 |
-
import { clsx, type ClassValue } from 'clsx'
|
2 |
-
import { customAlphabet } from 'nanoid'
|
3 |
-
import { twMerge } from 'tailwind-merge'
|
4 |
|
5 |
export function cn(...inputs: ClassValue[]) {
|
6 |
-
|
7 |
}
|
8 |
|
9 |
export const nanoid = customAlphabet(
|
10 |
-
|
11 |
-
|
12 |
-
) // 7-character random string
|
13 |
|
14 |
export async function fetcher<JSON = any>(
|
15 |
-
|
16 |
-
|
17 |
): Promise<JSON> {
|
18 |
-
|
|
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
|
33 |
-
|
34 |
}
|
35 |
|
36 |
export function formatDate(input: string | number | Date): string {
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
}
|
|
|
1 |
+
import { clsx, type ClassValue } from 'clsx';
|
2 |
+
import { customAlphabet } from 'nanoid';
|
3 |
+
import { twMerge } from 'tailwind-merge';
|
4 |
|
5 |
export function cn(...inputs: ClassValue[]) {
|
6 |
+
return twMerge(clsx(inputs));
|
7 |
}
|
8 |
|
9 |
export const nanoid = customAlphabet(
|
10 |
+
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
11 |
+
7,
|
12 |
+
); // 7-character random string
|
13 |
|
14 |
export async function fetcher<JSON = any>(
|
15 |
+
input: RequestInfo,
|
16 |
+
init?: RequestInit,
|
17 |
): Promise<JSON> {
|
18 |
+
const res = await fetch(input, init);
|
19 |
+
console.log('[Ming] ~ res:', res);
|
20 |
|
21 |
+
if (!res.ok) {
|
22 |
+
const json = await res.json();
|
23 |
+
if (json.error) {
|
24 |
+
const error = new Error(json.error) as Error & {
|
25 |
+
status: number;
|
26 |
+
};
|
27 |
+
error.status = res.status;
|
28 |
+
throw error;
|
29 |
+
} else {
|
30 |
+
throw new Error('An unexpected error occurred');
|
31 |
+
}
|
32 |
+
}
|
33 |
|
34 |
+
return res.json();
|
35 |
}
|
36 |
|
37 |
export function formatDate(input: string | number | Date): string {
|
38 |
+
const date = new Date(input);
|
39 |
+
return date.toLocaleDateString('en-US', {
|
40 |
+
month: 'long',
|
41 |
+
day: 'numeric',
|
42 |
+
year: 'numeric',
|
43 |
+
});
|
44 |
}
|
package.json
CHANGED
@@ -20,6 +20,7 @@
|
|
20 |
"@radix-ui/react-slot": "^1.0.2",
|
21 |
"@radix-ui/react-switch": "^1.0.3",
|
22 |
"@radix-ui/react-tooltip": "^1.0.7",
|
|
|
23 |
"ai": "^2.2.31",
|
24 |
"class-variance-authority": "^0.7.0",
|
25 |
"clsx": "^2.1.0",
|
@@ -66,5 +67,5 @@
|
|
66 |
"tailwindcss-animate": "^1.0.7",
|
67 |
"typescript": "^5.3.3"
|
68 |
},
|
69 |
-
"packageManager": "pnpm@
|
70 |
}
|
|
|
20 |
"@radix-ui/react-slot": "^1.0.2",
|
21 |
"@radix-ui/react-switch": "^1.0.3",
|
22 |
"@radix-ui/react-tooltip": "^1.0.7",
|
23 |
+
"@vercel/kv": "^1.0.1",
|
24 |
"ai": "^2.2.31",
|
25 |
"class-variance-authority": "^0.7.0",
|
26 |
"clsx": "^2.1.0",
|
|
|
67 |
"tailwindcss-animate": "^1.0.7",
|
68 |
"typescript": "^5.3.3"
|
69 |
},
|
70 |
+
"packageManager": "pnpm@9.0.1"
|
71 |
}
|
pnpm-lock.yaml
CHANGED
The diff for this file is too large to render.
See raw diff
|
|