diff --git a/bun.lockb b/bun.lockb index ea54ea5..6bfc9e8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/akeru-server/src/core/application/controllers/thread/threadController.ts b/packages/akeru-server/src/core/application/controllers/thread/threadController.ts index f8961e4..16ae3b3 100644 --- a/packages/akeru-server/src/core/application/controllers/thread/threadController.ts +++ b/packages/akeru-server/src/core/application/controllers/thread/threadController.ts @@ -7,6 +7,7 @@ import { deleteThread, userOwnsOrParticipatesInThread, getThread, + getThreads, } from "@/core/application/services/threadService"; import { THREAD_DELETED_SUCCESSFULLY, @@ -46,10 +47,10 @@ threads.post( }, { body: t.Object({ - thread_name: t.String() - }), + thread_name: t.String(), + }), beforeHandle: AuthMiddleware(["create_thread", "*"]), - }, + } ); threads.delete( @@ -77,7 +78,7 @@ threads.delete( }, { beforeHandle: AuthMiddleware(["delete_own_thread", "*"]), - }, + } ); threads.get( @@ -90,7 +91,7 @@ threads.get( const threadId = params.id; const isParticipant = await userOwnsOrParticipatesInThread( threadId, - userId, + userId ); const isSuperUser = decodedToken.permissions.some((p) => p.key === "*"); @@ -106,7 +107,28 @@ threads.get( }, { beforeHandle: AuthMiddleware(["view_own_threads", "*"]), + } +); + +/** + * Return all threads associated to a specific user + */ +threads.get( + "/thread", + async ({ bearer, set }) => { + console.info("all threads baby"); + const decodedToken = await parseToken(bearer!); + + if (decodedToken) { + const { userId } = decodedToken; + + const threads = await getThreads(userId); + return threads; + } }, + { + beforeHandle: AuthMiddleware(["view_own_threads", "*"]), + } ); /** @@ -123,9 +145,9 @@ threads.post( const isSuperUser = permissions.some((p) => p.key === "*"); const isParticipant = await userOwnsOrParticipatesInThread( threadId, - userId, + userId ); - + // Check if the user has the permission to add a message // if the user has * they can send a message anywhere, if not they need to be in conversation if (isSuperUser || isParticipant) { @@ -144,40 +166,45 @@ threads.post( message: t.String(), }), beforeHandle: AuthMiddleware(["create_message_in_own_thread", "*"]), - }, + } ); /** * This runs and responds once with anything that's in the thread */ -threads.post("/thread/:id/run", async ({ params, bearer, set, body }) => { - const decodedToken = await parseToken(bearer!); - - if(decodedToken) { - const { userId, permissions } = decodedToken - const threadId = params.id; - const isSuperUser = permissions.some((p) => p.key === "*"); - const isParticipant = await userOwnsOrParticipatesInThread(threadId, userId); - - - if(isSuperUser || isParticipant) { - // run the assistant with thread once, and get a single response - // this also adds the message to the thread - const response = await runAssistantWithThread({ - thread_id: threadId, - assistant_id: body.assistant_id - }) - set.status = 200 - return response - } - - set.status = 403 - return UNAUTHORIZED_USER_NOT_PARTICIPANT; - } +threads.post( + "/thread/:id/run", + async ({ params, bearer, set, body }) => { + const decodedToken = await parseToken(bearer!); + + if (decodedToken) { + const { userId, permissions } = decodedToken; + const threadId = params.id; + const isSuperUser = permissions.some((p) => p.key === "*"); + const isParticipant = await userOwnsOrParticipatesInThread( + threadId, + userId + ); -}, { - body: t.Object({ - assistant_id: t.String() - }), - beforeHandle: AuthMiddleware(['create_message_in_own_thread', '*']) -}) + if (isSuperUser || isParticipant) { + // run the assistant with thread once, and get a single response + // this also adds the message to the thread + const response = await runAssistantWithThread({ + thread_id: threadId, + assistant_id: body.assistant_id, + }); + set.status = 200; + return response; + } + + set.status = 403; + return UNAUTHORIZED_USER_NOT_PARTICIPANT; + } + }, + { + body: t.Object({ + assistant_id: t.String(), + }), + beforeHandle: AuthMiddleware(["create_message_in_own_thread", "*"]), + } +); diff --git a/packages/akeru-server/src/core/application/services/threadService.ts b/packages/akeru-server/src/core/application/services/threadService.ts index dbe3c2a..16b357d 100644 --- a/packages/akeru-server/src/core/application/services/threadService.ts +++ b/packages/akeru-server/src/core/application/services/threadService.ts @@ -96,5 +96,28 @@ export async function getThread(threadId: string): Promise { return null; } - return JSON.parse(threadData); + return JSON.parse(threadData) as Thread; +} + +/** + * Retrieves threads from Redis. + * @param {string} userId - The ID of the user threads to retrieve. + * @returns {Promise} A promise that resolves to the threads of a user or null if not found. + */ + +export async function getThreads(userId: string): Promise { + const threadIds = await getUserThreads(userId); + + if (!threadIds.length) { + return null; + } + + const threads = await Promise.all( + threadIds.map(async (threadId) => { + const thread = await getThread(threadId); + return thread as Thread; + }) + ); + + return threads; } diff --git a/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts b/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts index 1f5f64b..786838c 100644 --- a/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts +++ b/packages/akeru-server/src/infrastructure/adapters/validators/validatorAdapter.ts @@ -41,3 +41,4 @@ export async function validatorAdapter( return new Response("Error", { status: 500 }); } } + diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/assistants-list.tsx b/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/assistants-list.tsx index 6f91013..f4f608f 100644 --- a/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/assistants-list.tsx +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/assistants-list.tsx @@ -4,16 +4,21 @@ import useAssistant from "./use-assistant"; import { cn } from "@/lib/utils"; import { Combobox } from "@/components/ui/combobox"; - - const AssistantsList = () => { const { isFetchingAssistants, assistants } = useAssistant(); return ( -
- ({ value: assistant.name, label: assistant.name })) || []} +
+ ({ + value: assistant.name, + label: assistant.name, + })) || [] + } placeholder="Select assistant" value={""} onChange={(value) => console.log(value)} diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/test-data.ts b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/test-data.ts index a381904..ef4da7c 100644 --- a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/test-data.ts +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/test-data.ts @@ -2,43 +2,35 @@ import { ThreadsListItemProps } from "./threads-list-item"; export const THREADS_LIST_MOCK_DATA: ThreadsListItemProps[] = [ { - title: "Understanding React Hooks", - description: "A deep dive into the use of hooks in React for state and lifecycle management.", - date: "2024-05-01" + id: "1", + name: "Understanding React Hooks", }, { - title: "Announcing TypeScript 5.0", - description: "Explore the new features and improvements in the latest TypeScript release.", - date: "2024-04-15" + id: "2", + name: "Announcing TypeScript 5.0", }, { - title: "Getting Started with Python", - description: "An introductory guide to programming in Python for beginners.", - date: "2024-03-20" + id: "3", + name: "Getting Started with Python", }, { - title: "AI Breakthrough in Natural Language Processing", - description: "How the latest advancements in AI are revolutionizing language understanding.", - date: "2024-02-10" + id: "4", + name: "AI Breakthrough in Natural Language Processing", }, { - title: "Join the Annual Developer Conference", - description: "Meet and network with other developers at this year's conference. Keynotes, workshops, and more!", - date: "2024-06-05" + id: "5", + name: "Join the Annual Developer Conference", }, { - title: "Hiring Frontend Engineers", - description: "We are looking for talented frontend engineers to join our team. Apply now!", - date: "2024-05-20" + id: "6", + name: "Hiring Frontend Engineers", }, { - title: "Top 10 Books for Software Engineers", - description: "A curated list of must-read books for anyone in the software development field.", - date: "2024-01-30" + id: "7", + name: "Top 10 Books for Software Engineers", }, { - title: "Critical Security Patch Released", - description: "A new security patch has been released to address vulnerabilities in the system. Update immediately.", - date: "2024-04-05" - } + id: "8", + name: "Critical Security Patch Released", + }, ]; diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list-item.tsx b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list-item.tsx index e99657d..0891c6b 100644 --- a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list-item.tsx +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list-item.tsx @@ -1,16 +1,26 @@ +import { cn } from "@/lib/utils"; import React from "react"; export interface ThreadsListItemProps { - title: string; - description: string; - date: string; + id: string; + name: string; + createdBy?: string; + participants?: string[]; + messageIds?: string[]; isActive?: boolean; } const ThreadsListItem = (props: ThreadsListItemProps) => { return ( -
-

{props.title}

+
+

+ {props.name} +

); }; diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list.tsx b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list.tsx index 8293498..8ff00b3 100644 --- a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list.tsx +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/threads-list.tsx @@ -1,24 +1,56 @@ -import React from "react"; +"use client"; + +import React, { useState, useEffect } from "react"; import { THREADS_LIST_MOCK_DATA } from "./test-data"; import { ScrollArea } from "@/components/ui/scroll-area"; -import ThreadsListItem from "./threads-list-item"; +import ThreadsListItem, { ThreadsListItemProps } from "./threads-list-item"; import { Input } from "@/components/ui/input"; import { Search } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import useThreads from "./use-threads"; +import { useThreadStore } from "./useThreadStore"; +import { useRouter } from "next/navigation"; const ThreadsList = () => { + const router = useRouter(); + const activeThread = useThreadStore((state) => state.activeId); + const setActiveThread = useThreadStore((state) => state.setActiveId); + + const { isFetchingThreads, threads } = useThreads(); + + const handleSetActiveId = (id: string) => { + const parsedString = id.replace(/\s+/g, "-"); + setActiveThread(id); + router.push(`/threads?${parsedString}`); + }; + + const isActive = (id: string) => id === activeThread; + return (
- {/* This is just for the overflow gradient */} + {/* This is just for the overflow gradient */}
- {/* This is just for the overflow gradient */} + {/* This is just for the overflow gradient */}
- {THREADS_LIST_MOCK_DATA.map((thread, key) => { - return ; - })} + {isFetchingThreads + ? Array.from({ length: THREADS_LIST_MOCK_DATA.length }).map( + (_, idx) => , + ) + : threads.map((thread: ThreadsListItemProps) => ( +
handleSetActiveId(thread.name.toLowerCase())} + > + +
+ ))}
diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/use-threads.tsx b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/use-threads.tsx new file mode 100644 index 0000000..a51b7dd --- /dev/null +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/use-threads.tsx @@ -0,0 +1,20 @@ +import { axiosInstance } from "@/lib/axios"; +import useSWR from "swr"; + +const threadsFetcher = async (_: string) => { + const response = await axiosInstance.get("/thread"); + return response.data; +}; + +/** + * Fetches all threads related data, and provides all apis to mutate thread data. + */ +export default function useThreads() { + const { + data: threads, + error, + isLoading: isFetchingThreads, + mutate, + } = useSWR("threads", threadsFetcher); + return { threads, error, mutate, isFetchingThreads }; +} diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/useThreadStore.ts b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/useThreadStore.ts new file mode 100644 index 0000000..a0e78be --- /dev/null +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/thread-list/useThreadStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface ThreadState { + activeId: string; + setActiveId: (id: string) => void; +} + +export const useThreadStore = create((set) => ({ + activeId: "", + setActiveId: (id: string) => set({ activeId: id }), +})); diff --git a/packages/app-akeru/src/lib/utils.ts b/packages/app-akeru/src/lib/utils.ts index d084cca..365058c 100644 --- a/packages/app-akeru/src/lib/utils.ts +++ b/packages/app-akeru/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/packages/bun.lockb b/packages/bun.lockb new file mode 100644 index 0000000..ea54ea5 Binary files /dev/null and b/packages/bun.lockb differ