MingruiZhang commited on
Commit
abb7588
1 Parent(s): c69ef3e
app/project/[projectId]/page.tsx CHANGED
@@ -1,6 +1,8 @@
1
  import MediaGrid from '@/components/project/MediaGrid';
2
  import { fetchProjectMedia } from '@/lib/fetch/clef';
3
  import { Suspense } from 'react';
 
 
4
 
5
  interface PageProps {
6
  params: {
@@ -11,24 +13,16 @@ interface PageProps {
11
  export default async function Page({ params }: PageProps) {
12
  const { projectId } = params;
13
 
 
 
14
  return (
15
  <div className="pb-[150px] pt-4 md:pt-10 h-full">
16
  <div className="flex h-full">
17
  <div className="w-1/2 relative border-r border-gray-300 overflow-auto">
18
- <Suspense
19
- fallback={
20
- <div className="flex justify-center items-center size-full text-sm">
21
- Loading media...
22
- </div>
23
- }
24
- >
25
- <MediaGrid projectId={Number(projectId)} />
26
- </Suspense>
27
  </div>
28
  <div className="w-1/2 relative overflow-auto">
29
- {/* <ChatList messages={messages} /> */}
30
- {/* <ChatScrollAnchor trackVisibility={isLoading} /> */}
31
- Chat
32
  </div>
33
  </div>
34
  </div>
 
1
  import MediaGrid from '@/components/project/MediaGrid';
2
  import { fetchProjectMedia } from '@/lib/fetch/clef';
3
  import { Suspense } from 'react';
4
+ import Loading from '../loading';
5
+ import Chat from '@/components/project/Chat';
6
 
7
  interface PageProps {
8
  params: {
 
13
  export default async function Page({ params }: PageProps) {
14
  const { projectId } = params;
15
 
16
+ const mediaList = await fetchProjectMedia({ projectId: Number(projectId) });
17
+
18
  return (
19
  <div className="pb-[150px] pt-4 md:pt-10 h-full">
20
  <div className="flex h-full">
21
  <div className="w-1/2 relative border-r border-gray-300 overflow-auto">
22
+ <MediaGrid mediaList={mediaList} />
 
 
 
 
 
 
 
 
23
  </div>
24
  <div className="w-1/2 relative overflow-auto">
25
+ <Chat mediaList={mediaList} />
 
 
26
  </div>
27
  </div>
28
  </div>
app/project/layout.tsx CHANGED
@@ -1,4 +1,6 @@
1
  import ProjectListSideBar from '@/components/sidebar/ProjectListSideBar';
 
 
2
 
3
  interface ChatLayoutProps {
4
  children: React.ReactNode;
@@ -7,10 +9,19 @@ interface ChatLayoutProps {
7
  export default async function Layout({ children }: ChatLayoutProps) {
8
  return (
9
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
10
- <ProjectListSideBar />
11
- <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]">
12
- {children}
 
 
 
 
13
  </div>
 
 
 
 
 
14
  </div>
15
  );
16
  }
 
1
  import ProjectListSideBar from '@/components/sidebar/ProjectListSideBar';
2
+ import { Suspense } from 'react';
3
+ import Loading from './loading';
4
 
5
  interface ChatLayoutProps {
6
  children: React.ReactNode;
 
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
+ <ProjectListSideBar />
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/project/loading.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { IconLoading } from '@/components/ui/Icons';
2
+
3
+ export default async function Loading() {
4
+ return (
5
+ <div className="flex justify-center items-center size-full text-sm">
6
+ <IconLoading />
7
+ </div>
8
+ );
9
+ }
components/chat/ChatList.tsx CHANGED
@@ -12,7 +12,7 @@ export function ChatList({ messages }: ChatList) {
12
  return (
13
  <div className="relative mx-auto max-w-3xl px-8 pr-12">
14
  {messages
15
- // .filter(message => message.role !== 'system')
16
  .map((message, index) => (
17
  <div key={index}>
18
  <ChatMessage message={message} />
 
12
  return (
13
  <div className="relative mx-auto max-w-3xl px-8 pr-12">
14
  {messages
15
+ .filter(message => message.role !== 'system')
16
  .map((message, index) => (
17
  <div key={index}>
18
  <ChatMessage message={message} />
components/chat/PromptForm.tsx CHANGED
@@ -72,7 +72,7 @@ export function PromptForm({
72
  rows={1}
73
  value={input}
74
  onChange={e => setInput(e.target.value)}
75
- placeholder="Ask questions about all or selected the images."
76
  spellCheck={false}
77
  className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
78
  />
 
72
  rows={1}
73
  value={input}
74
  onChange={e => setInput(e.target.value)}
75
+ placeholder="Ask questions about the images."
76
  spellCheck={false}
77
  className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
78
  />
components/project/Chat.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { MediaDetails } from '@/lib/fetch/clef';
4
+ import useChatWithMedia from '@/lib/hooks/useChatWithMedia';
5
+ import React from 'react';
6
+ import { ChatList } from '../chat/ChatList';
7
+ import { ChatScrollAnchor } from '../chat/ChatScrollAnchor';
8
+ import { ChatPanel } from '../chat/ChatPanel';
9
+
10
+ export interface ChatProps {
11
+ mediaList: MediaDetails[];
12
+ }
13
+
14
+ const Chat: React.FC<ChatProps> = ({ mediaList }) => {
15
+ const { messages, append, reload, stop, isLoading, input, setInput } =
16
+ useChatWithMedia(mediaList);
17
+ return (
18
+ <>
19
+ <ChatList messages={messages} />
20
+ <ChatScrollAnchor trackVisibility={isLoading} />
21
+ <ChatPanel
22
+ isLoading={isLoading}
23
+ stop={stop}
24
+ append={append}
25
+ reload={reload}
26
+ messages={messages}
27
+ input={input}
28
+ setInput={setInput}
29
+ />
30
+ </>
31
+ );
32
+ };
33
+
34
+ export default Chat;
components/project/MediaGrid.tsx CHANGED
@@ -1,12 +1,14 @@
1
- import { fetchProjectMedia } from '@/lib/fetch/clef';
2
  import MediaTile from './MediaTile';
3
 
4
- export default async function MediaGrid({ projectId }: { projectId: number }) {
5
- const mediaList = await fetchProjectMedia({ projectId });
6
-
 
 
7
  return (
8
  <div className="relative size-full p-6 max-w-3xl mx-auto">
9
- <div className="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-4">
10
  {mediaList.map(media => (
11
  <MediaTile key={media.id} media={media} />
12
  ))}
 
1
+ import { MediaDetails } from '@/lib/fetch/clef';
2
  import MediaTile from './MediaTile';
3
 
4
+ export default function MediaGrid({
5
+ mediaList,
6
+ }: {
7
+ mediaList: MediaDetails[];
8
+ }) {
9
  return (
10
  <div className="relative size-full p-6 max-w-3xl mx-auto">
11
+ <div className="columns-1 sm:columns-1 md:columns-2 lg:columns-2 xl:columns:3 gap-4 [&>img:not(:first-child)]:mt-4">
12
  {mediaList.map(media => (
13
  <MediaTile key={media.id} media={media} />
14
  ))}
components/project/MediaTile.tsx CHANGED
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
  import React from 'react';
4
  import Image from 'next/image';
5
  import {
@@ -13,26 +11,32 @@ export interface MediaTileProps {
13
  media: MediaDetails;
14
  }
15
 
16
- const MediaTile: React.FC<MediaTileProps> = ({ media }) => {
17
- const { url: imageSrc, id, name } = media;
 
 
 
 
 
 
 
 
18
  return (
19
  <Tooltip>
20
  <TooltipTrigger asChild>
21
- <div className="relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content">
22
- <Image
23
- src={imageSrc}
24
- draggable={false}
25
- alt="dataset images"
26
- width={500}
27
- height={500}
28
- objectFit="cover"
29
- className="rounded-xl"
30
- />
31
- </div>
32
  </TooltipTrigger>
33
- <TooltipContent>{name}</TooltipContent>
 
 
 
34
  </Tooltip>
35
  );
36
- };
37
-
38
- export default MediaTile;
 
 
 
1
  import React from 'react';
2
  import Image from 'next/image';
3
  import {
 
11
  media: MediaDetails;
12
  }
13
 
14
+ export default function MediaTile({ media }: MediaTileProps) {
15
+ const {
16
+ url,
17
+ thumbnails,
18
+ id,
19
+ name,
20
+ properties: { width, height },
21
+ } = media;
22
+ // const imageSrc = thumbnails.length ? thumbnails[thumbnails.length - 1] : url;
23
+ const imageSrc = url;
24
  return (
25
  <Tooltip>
26
  <TooltipTrigger asChild>
27
+ <Image
28
+ src={imageSrc}
29
+ draggable={false}
30
+ alt="dataset images"
31
+ width={width}
32
+ height={height}
33
+ className="w-full h-auto relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content"
34
+ />
 
 
 
35
  </TooltipTrigger>
36
+ <TooltipContent>
37
+ <p>{name}</p>
38
+ <p className="font-light text-xs">{`${width} x ${height}`}</p>
39
+ </TooltipContent>
40
  </Tooltip>
41
  );
42
+ }
 
 
components/sidebar/ProjectCard.tsx CHANGED
@@ -5,6 +5,7 @@ import { format } from 'date-fns';
5
  import Link from 'next/link';
6
  import { useParams } from 'next/navigation';
7
  import { cn } from '@/lib/utils';
 
8
 
9
  export interface ProjectCardProps {
10
  projectInfo: ProjectBaseInfo;
@@ -15,6 +16,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
15
  id,
16
  name,
17
  created_at,
 
18
  organization: { name: orgName },
19
  } = projectInfo;
20
 
@@ -29,7 +31,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
29
  )}
30
  href={`/project/${id}`}
31
  >
32
- <div>
33
  <p className="text-xs text-gray-500">{orgName}</p>
34
  <p className="text-sm font-medium text-black mb-1">{name}</p>
35
  <p className="text-xs text-gray-500">{formattedDate}</p>
 
5
  import Link from 'next/link';
6
  import { useParams } from 'next/navigation';
7
  import { cn } from '@/lib/utils';
8
+ import Chip from '../ui/Chip';
9
 
10
  export interface ProjectCardProps {
11
  projectInfo: ProjectBaseInfo;
 
16
  id,
17
  name,
18
  created_at,
19
+ label_type,
20
  organization: { name: orgName },
21
  } = projectInfo;
22
 
 
31
  )}
32
  href={`/project/${id}`}
33
  >
34
+ <div className="overflow-hidden">
35
  <p className="text-xs text-gray-500">{orgName}</p>
36
  <p className="text-sm font-medium text-black mb-1">{name}</p>
37
  <p className="text-xs text-gray-500">{formattedDate}</p>
components/sidebar/ProjectListSideBar.tsx CHANGED
@@ -1,7 +1,4 @@
1
- import { auth } from '@/auth';
2
  import { fetchRecentProjectList } from '@/lib/fetch/clef';
3
- import { redirect } from 'next/navigation';
4
- import { v5 as uuidV5 } from 'uuid';
5
  import ProjectCard from './ProjectCard';
6
 
7
  export interface ProjectListSideBarProps {}
@@ -9,14 +6,11 @@ export interface ProjectListSideBarProps {}
9
  const ProjectListSideBar: React.FC<ProjectListSideBarProps> = async () => {
10
  const recentProjects = await fetchRecentProjectList();
11
  return (
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
  {recentProjects.map(project => (
17
  <ProjectCard key={project.id} projectInfo={project} />
18
  ))}
19
- </div>
20
  );
21
  };
22
 
 
 
1
  import { fetchRecentProjectList } from '@/lib/fetch/clef';
 
 
2
  import ProjectCard from './ProjectCard';
3
 
4
  export interface ProjectListSideBarProps {}
 
6
  const ProjectListSideBar: React.FC<ProjectListSideBarProps> = async () => {
7
  const recentProjects = await fetchRecentProjectList();
8
  return (
9
+ <>
 
 
 
10
  {recentProjects.map(project => (
11
  <ProjectCard key={project.id} projectInfo={project} />
12
  ))}
13
+ </>
14
  );
15
  };
16
 
components/ui/Chip.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from '@/lib/utils';
2
+
3
+ export interface ChipProps {
4
+ label?: string;
5
+ value: string;
6
+ color?: 'gray' | 'blue';
7
+ className?: string;
8
+ }
9
+
10
+ const Chip: React.FC<ChipProps> = ({
11
+ label,
12
+ value,
13
+ color = 'gray',
14
+ className,
15
+ }) => {
16
+ return (
17
+ <div
18
+ className={cn(
19
+ `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-${color}-100 text-${color}-800 mr-1`,
20
+ className,
21
+ )}
22
+ >
23
+ {label && <span className="font-medium">{label} :</span>}
24
+ <span>{value}</span>
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export default Chip;
components/ui/Icons.tsx CHANGED
@@ -507,6 +507,27 @@ function IconChevronUpDown({
507
  );
508
  }
509
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  export {
511
  IconEdit,
512
  IconNextChat,
@@ -536,4 +557,5 @@ export {
536
  IconExternalLink,
537
  IconChevronUpDown,
538
  IconGoogle,
 
539
  };
 
507
  );
508
  }
509
 
510
+ 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-blue-600"
515
+ viewBox="0 0 100 101"
516
+ fill="none"
517
+ xmlns="http://www.w3.org/2000/svg"
518
+ >
519
+ <path
520
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
521
+ fill="currentColor"
522
+ />
523
+ <path
524
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
525
+ fill="currentFill"
526
+ />
527
+ </svg>
528
+ );
529
+ }
530
+
531
  export {
532
  IconEdit,
533
  IconNextChat,
 
557
  IconExternalLink,
558
  IconChevronUpDown,
559
  IconGoogle,
560
+ IconLoading,
561
  };
lib/fetch/clef.ts CHANGED
@@ -25,7 +25,7 @@ const clefApiBuilder = <Params extends object | void, Resp>(path: string) => {
25
  bucket: 'fake_bucket',
26
  };
27
 
28
- const baseURL = `https://app.dev.landing.ai/${path}`;
29
 
30
  // Create a URL object with query params
31
  const url = new URL(baseURL);
@@ -55,6 +55,7 @@ export type ProjectBaseInfo = {
55
  id: number;
56
  name: string;
57
  created_at: Date;
 
58
  organization: {
59
  id: number;
60
  name: string;
@@ -77,7 +78,7 @@ export type MediaDetails = {
77
  path: string;
78
  url: string;
79
  projectId: number;
80
- thumbnails: [string, string, string];
81
  properties: {
82
  width: number;
83
  height: number;
 
25
  bucket: 'fake_bucket',
26
  };
27
 
28
+ const baseURL = `https://app.landing.ai/${path}`;
29
 
30
  // Create a URL object with query params
31
  const url = new URL(baseURL);
 
55
  id: number;
56
  name: string;
57
  created_at: Date;
58
+ label_type: string;
59
  organization: {
60
  id: number;
61
  name: string;
 
78
  path: string;
79
  url: string;
80
  projectId: number;
81
+ thumbnails: string[];
82
  properties: {
83
  width: number;
84
  height: number;
lib/hooks/useChatWithDataset.ts CHANGED
@@ -98,7 +98,7 @@ const useChatWithDataset = () => {
98
  ...message,
99
  // @ts-ignore this is extra fields
100
  dataset: selectedDataset,
101
- } satisfies MessageWithSelectedDataset);
102
  };
103
 
104
  return {
 
98
  ...message,
99
  // @ts-ignore this is extra fields
100
  dataset: selectedDataset,
101
+ } as MessageWithSelectedDataset);
102
  };
103
 
104
  return {
lib/hooks/useChatWithMedia.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useChat, type Message } from 'ai/react';
2
+ import { toast } from 'react-hot-toast';
3
+ import { useEffect, useState } from 'react';
4
+ import { MediaDetails } from '../fetch/clef';
5
+ import { MessageWithSelectedDataset } from '../types';
6
+
7
+ const useChatWithMedia = (mediaList: MediaDetails[]) => {
8
+ const {
9
+ messages,
10
+ append,
11
+ reload,
12
+ stop,
13
+ isLoading,
14
+ input,
15
+ setInput,
16
+ setMessages,
17
+ } = useChat({
18
+ sendExtraMessageFields: true,
19
+ onResponse(response) {
20
+ if (response.status !== 200) {
21
+ toast.error(response.statusText);
22
+ }
23
+ },
24
+ initialMessages: [
25
+ {
26
+ id: 'system',
27
+ content: `For the full conversation, user have provided the following images: ${mediaList.map(media => media.name)}. Please help reply to user regarding these images`,
28
+ dataset: mediaList,
29
+ role: 'system',
30
+ },
31
+ ] as MessageWithSelectedDataset[],
32
+ });
33
+
34
+ const [loadingDots, setLoadingDots] = useState('');
35
+
36
+ useEffect(() => {
37
+ let loadingInterval: NodeJS.Timeout;
38
+
39
+ if (isLoading) {
40
+ loadingInterval = setInterval(() => {
41
+ setLoadingDots(prevMessage => {
42
+ switch (prevMessage) {
43
+ case '':
44
+ return '.';
45
+ case '.':
46
+ return '..';
47
+ case '..':
48
+ return '...';
49
+ case '...':
50
+ return '';
51
+ default:
52
+ return '';
53
+ }
54
+ });
55
+ }, 500);
56
+ }
57
+
58
+ return () => {
59
+ clearInterval(loadingInterval);
60
+ };
61
+ }, [isLoading]);
62
+
63
+ const assistantLoadingMessage = {
64
+ id: 'loading',
65
+ content: loadingDots,
66
+ role: 'assistant',
67
+ };
68
+
69
+ const messageWithLoading =
70
+ isLoading &&
71
+ messages.length &&
72
+ messages[messages.length - 1].role !== 'assistant'
73
+ ? [...messages, assistantLoadingMessage]
74
+ : messages;
75
+
76
+ return {
77
+ messages: messageWithLoading as MessageWithSelectedDataset[],
78
+ append,
79
+ reload,
80
+ stop,
81
+ isLoading,
82
+ input,
83
+ setInput,
84
+ };
85
+ };
86
+
87
+ export default useChatWithMedia;
lib/types.ts CHANGED
@@ -20,10 +20,10 @@ export type ServerActionResult<Result> = Promise<
20
 
21
  export type DatasetImageEntity = {
22
  url: string;
23
- selected: boolean;
24
  name: string;
25
  };
26
 
27
- export type MessageWithSelectedDataset = (Message | CreateMessage) & {
28
  dataset: DatasetImageEntity[];
29
  };
 
20
 
21
  export type DatasetImageEntity = {
22
  url: string;
23
+ selected?: boolean;
24
  name: string;
25
  };
26
 
27
+ export type MessageWithSelectedDataset = Message & {
28
  dataset: DatasetImageEntity[];
29
  };