Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

feat: add threads sidebar interactivity #132

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
deleteThread,
userOwnsOrParticipatesInThread,
getThread,
getThreads,
} from "@/core/application/services/threadService";
import {
THREAD_DELETED_SUCCESSFULLY,
Expand Down Expand Up @@ -46,10 +47,10 @@ threads.post(
},
{
body: t.Object({
thread_name: t.String()
}),
thread_name: t.String(),
}),
beforeHandle: AuthMiddleware(["create_thread", "*"]),
},
}
);

threads.delete(
Expand Down Expand Up @@ -77,7 +78,7 @@ threads.delete(
},
{
beforeHandle: AuthMiddleware(["delete_own_thread", "*"]),
},
}
);

threads.get(
Expand All @@ -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 === "*");

Expand All @@ -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");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do remove console!

const decodedToken = await parseToken(bearer!);

if (decodedToken) {
const { userId } = decodedToken;

const threads = await getThreads(userId);
return threads;
}
},
{
beforeHandle: AuthMiddleware(["view_own_threads", "*"]),
}
);

/**
Expand All @@ -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) {
Expand All @@ -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", "*"]),
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,28 @@ export async function getThread(threadId: string): Promise<Thread | null> {
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<Thread[] | null>} A promise that resolves to the threads of a user or null if not found.
*/

export async function getThreads(userId: string): Promise<Thread[] | null> {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export async function validatorAdapter(
return new Response("Error", { status: 500 });
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={cn("w-96 rounded-md bg-white p-4 shadow-md", {
"animate-pulse": isFetchingAssistants
})}>
<Combobox
items={assistants?.map((assistant: any) => ({ value: assistant.name, label: assistant.name })) || []}
<div
className={cn("w-96 rounded-md bg-white p-4 shadow-md", {
"animate-pulse": isFetchingAssistants,
})}
>
<Combobox
items={
assistants?.map((assistant: any) => ({
value: assistant.name,
label: assistant.name,
})) || []
}
placeholder="Select assistant"
value={""}
onChange={(value) => console.log(value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
];
Original file line number Diff line number Diff line change
@@ -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 (
<div className="">
<p className="overflow-hidden whitespace-nowrap text-sm">{props.title}</p>
<div
className={cn(
"max-w-52 px-2 py-1 duration-200 hover:bg-gray-600/75 hover:text-white",
{ "bg-gray-600/75 text-white": props.isActive },
)}
>
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm ">
{props.name}
</p>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-64 rounded-md bg-white p-4 shadow-md">
<div className="mb-4 flex flex-col gap-2">
<Input placeholder="Filter your threads..." startIcon={Search} />
</div>
<ScrollArea className="relative h-4/5 overflow-clip">
{/* This is just for the overflow gradient */}
{/* This is just for the overflow gradient */}
<div className="pointer-events-none absolute right-0 top-0 h-4/5 w-8 bg-gradient-to-r from-transparent to-white"></div>
{/* This is just for the overflow gradient */}
{/* This is just for the overflow gradient */}
<div className="flex flex-col gap-4">
{THREADS_LIST_MOCK_DATA.map((thread, key) => {
return <ThreadsListItem key={thread.title + key} {...thread} />;
})}
{isFetchingThreads
? Array.from({ length: THREADS_LIST_MOCK_DATA.length }).map(
(_, idx) => <Skeleton key={idx} className="h-8" />,
)
: threads.map((thread: ThreadsListItemProps) => (
<div
key={thread.id}
onClick={() => handleSetActiveId(thread.name.toLowerCase())}
>
<ThreadsListItem
isActive={isActive(thread.name.toLowerCase())}
{...thread}
/>
</div>
))}
</div>
</ScrollArea>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
}
Loading
Loading