Spaces:
Running
Running
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
|
|
|