diff --git a/bun.lockb b/bun.lockb index c10d524..ea54ea5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/akeru-server/package.json b/packages/akeru-server/package.json index 5a9a5ba..de9fde6 100644 --- a/packages/akeru-server/package.json +++ b/packages/akeru-server/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@elysiajs/bearer": "^1.0.2", + "@elysiajs/cors": "^1.0.2", "@elysiajs/stream": "^1.0.2", "@elysiajs/swagger": "^1.0.4", "dayjs": "^1.11.10", diff --git a/packages/akeru-server/scripts/createHumanUser.ts b/packages/akeru-server/scripts/createHumanUser.ts new file mode 100644 index 0000000..f8d7022 --- /dev/null +++ b/packages/akeru-server/scripts/createHumanUser.ts @@ -0,0 +1,51 @@ +import { + createApiToken, + createHumanUser, +} from "@/core/application/controllers/userController"; +import { createAssistant } from "@/core/application/services/assistantService"; +import { createThread } from "@/core/application/services/threadService"; +import { ulid } from "ulid"; + +async function createHumanUserWithApiKey() { + const userData = { + name: "Mr. Akeru Human User", + email: "thisistesting@akeru.com", + }; + + // Creating the human user for id + const userId = await createHumanUser(userData); + if (!userId) { + console.error("Failed to create super admin user"); + return; + } + + // creating user bearer token + const apiKey = await createApiToken(userId); + if (!apiKey) { + console.error("Failed to create API key for super admin user"); + return; + } + + // creating assistants associated with the user, creating three assistants + const { id: id_1 } = await createAssistant({ userId, id: ulid(), model: "gpt-4", name: "Homework assistant", fileIds: [], tools: [], instruction: "You are a homework helper. Do not answer irrelevant questions." }); + const { id: id_2 } = await createAssistant({ userId, id: ulid(), model: "gpt-4", name: "Tropical Plant Expert", fileIds: [], tools: [], instruction: "You specialise in tropical plants. Do not answer irrelevant questions." }); + const { id: id_3 } = await createAssistant({ userId, id: ulid(), model: "gpt-4", name: "Marine Biologist", fileIds: [], tools: [], instruction: "You specialise in Marine Biology. Do not answer irrelevant questions." }); + + // creating a few threads associated with said assistants + await createThread({ id: ulid(), createdBy: userId, participants: [], messageIds: [], name: 'Homework on Science' }) + await createThread({ id: ulid(), createdBy: userId, participants: [], messageIds: [], name: 'What are fishes?' }) + await createThread({ id: ulid(), createdBy: userId, participants: [], messageIds: [], name: 'Why do plants commonly die?' }) + + console.log(`human user created with ID: ${userId}`); + console.log(`API key for human user: ${apiKey}`); + + console.log(`assistant_id 1 created with ID: ${id_1}`); + console.log(`assistant_id 2 created with ID: ${id_2}`); + console.log(`assistant_id 3 created with ID: ${id_3}`); + + process.exit(); +} + +createHumanUserWithApiKey().catch((err) => { + console.error("Error creating super admin user with API key:", err); +}); diff --git a/packages/akeru-server/src/core/application/controllers/assistant/assistantController.ts b/packages/akeru-server/src/core/application/controllers/assistant/assistantController.ts index 2c0ec11..735a939 100644 --- a/packages/akeru-server/src/core/application/controllers/assistant/assistantController.ts +++ b/packages/akeru-server/src/core/application/controllers/assistant/assistantController.ts @@ -6,7 +6,10 @@ import { assignRole, createUser, } from "@/core/application/services/userService"; -import { createAssistant } from "@/core/application/services/assistantService"; +import { + createAssistant, + getAssistantData, +} from "@/core/application/services/assistantService"; import { AuthMiddleware } from "../../middlewares/authorizationMiddleware"; import bearer from "@elysiajs/bearer"; @@ -35,7 +38,7 @@ assistants.post( name, fileIds: [], tools: [], - instruction: body.instruction + instruction: body.instruction, }); return { @@ -48,8 +51,36 @@ assistants.post( body: t.Object({ name: t.String(), model: t.Literal("gpt-4"), // add more models here - instruction: t.String() + instruction: t.String(), }), beforeHandle: AuthMiddleware(["create_assistant", "*"]), } ); + +/** + * Based on the query for the endpoint fetch the appropriate details about the users assistants + */ +assistants.get( + "/assistant", + async ({ query, bearer }) => { + console.info("hit") + // only support ALL query for now + const decodedToken = await parseToken(bearer!); + if (!decodedToken) return; + + const { userId } = decodedToken; + + const assistantData = await getAssistantData({ + query: query.query, + userId: userId, + }); + + return assistantData; + }, + { + query: t.Object({ + query: t.Literal("ALL"), + }), + beforeHandle: AuthMiddleware(["view_assistants", "*"]), + } +); 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 cafc837..f8961e4 100644 --- a/packages/akeru-server/src/core/application/controllers/thread/threadController.ts +++ b/packages/akeru-server/src/core/application/controllers/thread/threadController.ts @@ -22,13 +22,14 @@ export const threads = new Elysia<"/thread">().use(bearer()); threads.post( "/thread", - async ({ bearer }) => { + async ({ bearer, body }) => { const decodedToken = await parseToken(bearer!); if (decodedToken) { const { userId } = decodedToken; // // Create a new thread const threadId = await createThread({ + name: body.thread_name, id: ulid(), // Generate a unique ID for the thread createdBy: userId, participants: [], // Initialize the participants list @@ -44,6 +45,9 @@ threads.post( } }, { + body: t.Object({ + thread_name: t.String() + }), beforeHandle: AuthMiddleware(["create_thread", "*"]), }, ); diff --git a/packages/akeru-server/src/core/application/controllers/thread/threadControllet.test.ts b/packages/akeru-server/src/core/application/controllers/thread/threadControllet.test.ts index 4135119..2209b0d 100644 --- a/packages/akeru-server/src/core/application/controllers/thread/threadControllet.test.ts +++ b/packages/akeru-server/src/core/application/controllers/thread/threadControllet.test.ts @@ -14,6 +14,9 @@ describe.only("threadController", async () => { "Content-Type": "application/json", }, method: "POST", + body: JSON.stringify({ + thread_name: "Akeru thread", + }), }); const response = await app.handle(request); diff --git a/packages/akeru-server/src/core/application/controllers/userController.ts b/packages/akeru-server/src/core/application/controllers/userController.ts index c8ae55f..56302e6 100644 --- a/packages/akeru-server/src/core/application/controllers/userController.ts +++ b/packages/akeru-server/src/core/application/controllers/userController.ts @@ -35,6 +35,38 @@ export async function createSuperAdmin( } } +/** + * Creates a new user with a random ID and assigns them the "super admin" role. + * Also creates the user in Redis. + * WARNING: This function should only be used in seeding scripts. + * @param {Object} userData - The data of the user to create. + * @returns {Promise} A promise that resolves to the ID of the new user if the user was created and assigned the role successfully, or null otherwise. + */ +export async function createHumanUser( + userData: Object +): Promise { + try { + const userId = ulid(); + const userCreated = await createUser(userId, userData); + if (!userCreated) { + return null; + } + + const roleAssigned = await assignRole(userId, "user"); + if (!roleAssigned) { + return null; + } + + // Create the user in Redis + await redis.set(`user:${userId}`, JSON.stringify(userData)); + + return userId; + } catch (err) { + console.error("Error creating super admin:", err); + return null; + } +} + /** * Creates a new API token for a user. * WARNING: This function should only be used in seeding scripts. diff --git a/packages/akeru-server/src/core/application/services/assistantService.ts b/packages/akeru-server/src/core/application/services/assistantService.ts index a6f0969..a09c7aa 100644 --- a/packages/akeru-server/src/core/application/services/assistantService.ts +++ b/packages/akeru-server/src/core/application/services/assistantService.ts @@ -1,4 +1,5 @@ import type { Assistant } from "@/core/domain/assistant"; +import { User } from "@/core/domain/user"; import { redis } from "@/infrastructure/adapters/redisAdapter"; /** @@ -15,7 +16,10 @@ export async function createAssistant(args: Assistant & { userId: string }) { const pipeline = redis.pipeline(); // Store the assistant data - pipeline.set(`assistant:${id}`, JSON.stringify({ tools, model, name, instruction })); + pipeline.set( + `assistant:${id}`, + JSON.stringify({ tools, model, name, instruction, id }) + ); // Store the relationship between the assistant and the user pipeline.sadd(`user:${userId}:assistants`, id); @@ -31,11 +35,30 @@ export async function createAssistant(args: Assistant & { userId: string }) { } // Parse the assistant data from JSON - return JSON.parse(assistantData); + return JSON.parse(assistantData) as Assistant; } -export async function getAssistantData(assistant_id: Assistant["id"]) { - const assistantData = await redis.get(`assistant:${assistant_id}`); +export async function getAssistantData(args: { + query: Assistant["id"] | "ALL"; + userId: User["id"]; +}) { + const { query, userId } = args; + + // if query is ALL then fetch all relevant assistant data + if (query === "ALL") { + const allAssistantData = await redis.smembers(`user:${userId}:assistants`); + return Promise.all( + allAssistantData.map(async (assistantId) => { + const assistantData = await redis.get(`assistant:${assistantId}`); + if (!assistantData) { + throw new Error("Failed to get assistant"); + } + return JSON.parse(assistantData) as Assistant; + }) + ); + } + + const assistantData = await redis.get(`user:${userId}:assistant:${query}`); if (!assistantData) { throw new Error("Failed to get assistant"); } diff --git a/packages/akeru-server/src/core/domain/roles.ts b/packages/akeru-server/src/core/domain/roles.ts index bc2868d..3c8e3cb 100644 --- a/packages/akeru-server/src/core/domain/roles.ts +++ b/packages/akeru-server/src/core/domain/roles.ts @@ -17,6 +17,7 @@ const roles: Record = { "view_own_threads", "create_message_in_own_thread", "create_assistant", + "view_assistants" ], }, assistant: { diff --git a/packages/akeru-server/src/core/domain/thread.ts b/packages/akeru-server/src/core/domain/thread.ts index d912303..41b309f 100644 --- a/packages/akeru-server/src/core/domain/thread.ts +++ b/packages/akeru-server/src/core/domain/thread.ts @@ -2,6 +2,7 @@ import { User } from "./user"; export type Thread = { id: string; // The ID of the thread + name: string; createdBy: string; // The ID of the user who created the thread startDate?: Date; // The start date of the thread participants: User['id'][]; // The IDs of the users participating in the thread diff --git a/packages/akeru-server/src/index.ts b/packages/akeru-server/src/index.ts index c521169..688e1f9 100644 --- a/packages/akeru-server/src/index.ts +++ b/packages/akeru-server/src/index.ts @@ -3,6 +3,7 @@ import { Elysia } from "elysia"; import { threads } from "@/core/application/controllers/thread/threadController"; import { assistants } from "@/core/application/controllers/assistant/assistantController"; import { users } from "./core/application/controllers/user/userController"; +import cors from "@elysiajs/cors"; export const name = "Akeru"; @@ -17,6 +18,7 @@ export const healthCheck = async () => { }; export const app = new Elysia() + .use(cors()) .use(assistants) .use(threads) .use(users) diff --git a/packages/akeru-server/src/infrastructure/config/redisConfig.ts b/packages/akeru-server/src/infrastructure/config/redisConfig.ts index b9dd9a0..d8ccbb9 100644 --- a/packages/akeru-server/src/infrastructure/config/redisConfig.ts +++ b/packages/akeru-server/src/infrastructure/config/redisConfig.ts @@ -5,5 +5,5 @@ export const redisConfig = { ? { password: process.env.REDIS_PASSWORD } : {}), // Redis password db: process.env.REDIS_DB ? parseInt(process.env.REDIS_DB) : 0, // Redis DB - tls: {}, + // tls: {}, }; diff --git a/packages/app-akeru/package.json b/packages/app-akeru/package.json index 828d91f..b2ff315 100644 --- a/packages/app-akeru/package.json +++ b/packages/app-akeru/package.json @@ -9,14 +9,17 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "geist": "^1.3.0", "lucide-react": "^0.378.0", "next": "14.2.3", @@ -24,8 +27,10 @@ "react": "^18", "react-dom": "^18", "sonner": "^1.4.41", + "swr": "^2.2.5", "tailwind-merge": "^2.3.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^4.5.2" }, "devDependencies": { "@types/node": "^20", diff --git a/packages/app-akeru/pnpm-lock.yaml b/packages/app-akeru/pnpm-lock.yaml index 237b6d8..5d031f2 100644 --- a/packages/app-akeru/pnpm-lock.yaml +++ b/packages/app-akeru/pnpm-lock.yaml @@ -1,6 +1,9 @@ lockfileVersion: '6.0' dependencies: + '@radix-ui/react-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) '@radix-ui/react-menubar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) @@ -19,12 +22,18 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.2)(react@18.0.0) + axios: + specifier: ^1.7.2 + version: 1.7.2 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 clsx: specifier: ^2.1.1 version: 2.1.1 + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) geist: specifier: ^1.3.0 version: 1.3.0(next@14.2.3) @@ -46,12 +55,18 @@ dependencies: sonner: specifier: ^1.4.41 version: 1.4.41(react-dom@18.0.0)(react@18.0.0) + swr: + specifier: ^2.2.5 + version: 2.2.5(react@18.0.0) tailwind-merge: specifier: ^2.3.0 version: 2.3.0 tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.1) + zustand: + specifier: ^4.5.2 + version: 4.5.2(@types/react@18.3.2)(react@18.0.0) devDependencies: '@types/node': @@ -393,6 +408,40 @@ packages: react: 18.0.0 dev: false + /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0): + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@types/react': 18.3.2 + '@types/react-dom': 18.0.0 + aria-hidden: 1.2.4 + react: 18.0.0 + react-dom: 18.0.0(react@18.0.0) + react-remove-scroll: 2.5.5(@types/react@18.3.2)(react@18.0.0) + dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.3.2)(react@18.0.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -1228,6 +1277,10 @@ packages: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} dev: true + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1240,6 +1293,16 @@ packages: engines: {node: '>=4'} dev: true + /axios@1.7.2: + resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: @@ -1344,6 +1407,21 @@ packages: engines: {node: '>=6'} dev: false + /cmdk@1.0.0(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0): + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.0.0)(@types/react@18.3.2)(react-dom@18.0.0)(react@18.0.0) + react: 18.0.0 + react-dom: 18.0.0(react@18.0.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1357,6 +1435,13 @@ packages: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} dev: true + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1457,6 +1542,11 @@ packages: object-keys: 1.1.1 dev: true + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1983,6 +2073,16 @@ packages: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -1996,6 +2096,15 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -2558,6 +2667,18 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -2959,6 +3080,10 @@ packages: react-is: 16.13.1 dev: true + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3359,6 +3484,16 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /swr@2.2.5(react@18.0.0): + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.0.0 + use-sync-external-store: 1.2.2(react@18.0.0) + dev: false + /tailwind-merge@2.3.0: resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} dependencies: @@ -3562,6 +3697,22 @@ packages: tslib: 2.6.2 dev: false + /use-sync-external-store@1.2.0(react@18.0.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.0.0 + dev: false + + /use-sync-external-store@1.2.2(react@18.0.0): + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.0.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3654,3 +3805,23 @@ packages: resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} engines: {node: '>= 14'} hasBin: true + + /zustand@4.5.2(@types/react@18.3.2)(react@18.0.0): + resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.3.2 + react: 18.0.0 + use-sync-external-store: 1.2.0(react@18.0.0) + dev: false 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 86ed4d2..6f91013 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 @@ -1,9 +1,25 @@ +"use client"; import React from "react"; +import useAssistant from "./use-assistant"; +import { cn } from "@/lib/utils"; +import { Combobox } from "@/components/ui/combobox"; + + const AssistantsList = () => { - return
- -
; + const { isFetchingAssistants, assistants } = useAssistant(); + return ( +
+ ({ value: assistant.name, label: assistant.name })) || []} + placeholder="Select assistant" + value={""} + onChange={(value) => console.log(value)} + /> +
+ ); }; export default AssistantsList; diff --git a/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/use-assistant.tsx b/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/use-assistant.tsx new file mode 100644 index 0000000..3e3d87c --- /dev/null +++ b/packages/app-akeru/src/app/(dashboard)/threads/features/assistants-list/use-assistant.tsx @@ -0,0 +1,20 @@ +import { axiosInstance } from "@/lib/axios"; +import useSWR from "swr"; + +const assistantFetcher = async (_: string) => { + const response = await axiosInstance.get("/assistant?query=ALL"); + return response.data; +}; + +/** + * Fetches all assistant related data, and provides all apis to mutate assistant data. + */ +export default function useAssistant() { + const { + data: assistants, + error, + isLoading: isFetchingAssistants, + mutate, + } = useSWR("assistant", assistantFetcher); + return { assistants, error, mutate, isFetchingAssistants }; +} diff --git a/packages/app-akeru/src/components/ui/combobox.tsx b/packages/app-akeru/src/components/ui/combobox.tsx new file mode 100644 index 0000000..e3d4dcc --- /dev/null +++ b/packages/app-akeru/src/components/ui/combobox.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +interface ComboboxProps { + items: Array<{ value: string, label: string }>; + placeholder: string; + value: string; + onChange: (value: string) => void; +} + +export function Combobox({ items, placeholder, value, onChange }: ComboboxProps) { + const [open, setOpen] = React.useState(false) + + return ( + + + + + + + + + No {placeholder.toLowerCase()} found. + + {items.map((item) => ( + { + onChange(currentValue === value ? "" : currentValue) + setOpen(false) + }} + > + + {item.label} + + ))} + + + + + + ) +} \ No newline at end of file diff --git a/packages/app-akeru/src/components/ui/command.tsx b/packages/app-akeru/src/components/ui/command.tsx new file mode 100644 index 0000000..007798a --- /dev/null +++ b/packages/app-akeru/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/packages/app-akeru/src/components/ui/dialog.tsx b/packages/app-akeru/src/components/ui/dialog.tsx new file mode 100644 index 0000000..01ff19c --- /dev/null +++ b/packages/app-akeru/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/packages/app-akeru/src/lib/axios.ts b/packages/app-akeru/src/lib/axios.ts new file mode 100644 index 0000000..3be58b4 --- /dev/null +++ b/packages/app-akeru/src/lib/axios.ts @@ -0,0 +1,14 @@ +// instantiate an axios instance +import axios from 'axios'; + +/** + * This is a temporary implementation for our swr fetchers until we have a solid auth system. + * The api token can be found within your local redis store. + */ +export const axiosInstance = axios.create({ + baseURL: process.env.NODE_ENV === "development" ? "http://localhost:8080" : "https://akeru-server.onrender.com", + timeout: 10000, + headers: { + Authorization: `Bearer ${process.env.NEXT_PUBLIC_API_TOKEN}` + } +}) \ No newline at end of file