diff options
| author | Fuwn <[email protected]> | 2026-02-04 01:05:42 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-04 01:05:42 -0800 |
| commit | a05b8d45a04693b48d7db0ad5a158e8f8fda689e (patch) | |
| tree | 7deef70f1b080015ab7f0809c15a2c46f112c1cc /packages/web/src/server | |
| parent | feat(sdk): Support memory project reassignment in update (diff) | |
| download | archived-imemio-a05b8d45a04693b48d7db0ad5a158e8f8fda689e.tar.xz archived-imemio-a05b8d45a04693b48d7db0ad5a158e8f8fda689e.zip | |
feat(web): Enhance memory dashboard with projects and search
Diffstat (limited to 'packages/web/src/server')
| -rw-r--r-- | packages/web/src/server/api/root.ts | 2 | ||||
| -rw-r--r-- | packages/web/src/server/api/routers/memory.ts | 142 | ||||
| -rw-r--r-- | packages/web/src/server/api/routers/project.ts | 79 |
3 files changed, 216 insertions, 7 deletions
diff --git a/packages/web/src/server/api/root.ts b/packages/web/src/server/api/root.ts index 919ba21..c382d36 100644 --- a/packages/web/src/server/api/root.ts +++ b/packages/web/src/server/api/root.ts @@ -1,12 +1,14 @@ import { apiKeyRouter } from "~/server/api/routers/api-key"; import { memoryRouter } from "~/server/api/routers/memory"; import { postRouter } from "~/server/api/routers/post"; +import { projectRouter } from "~/server/api/routers/project"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; export const appRouter = createTRPCRouter({ apiKey: apiKeyRouter, memory: memoryRouter, post: postRouter, + project: projectRouter, }); // export type definition of API diff --git a/packages/web/src/server/api/routers/memory.ts b/packages/web/src/server/api/routers/memory.ts index e6d206b..eebe0f8 100644 --- a/packages/web/src/server/api/routers/memory.ts +++ b/packages/web/src/server/api/routers/memory.ts @@ -1,9 +1,21 @@ -import { SupabaseProjectStore, SupabaseStore } from "@imemio/sdk"; +import { EmbeddingService, SupabaseProjectStore, SupabaseStore } from "@imemio/sdk"; import type { SupabaseClient } from "@supabase/supabase-js"; +import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { env } from "~/env"; import { createClient } from "~/lib/supabase/server"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +function getEmbeddingService(): EmbeddingService | null { + const openaiApiKey = env.OPENAI_API_KEY; + + if (!openaiApiKey) { + return null; + } + + return new EmbeddingService({ apiKey: openaiApiKey }); +} + const defaultProjectName = "default"; async function getOrCreateDefaultProject( @@ -47,14 +59,53 @@ export const memoryRouter = createTRPCRouter({ return memories; }), - create: protectedProcedure - .input(z.object({ content: z.string().min(1) })) - .mutation(async ({ context, input }) => { + listWithProjects: protectedProcedure + .input( + z.object({ + projectId: z.string().optional(), + sortOrder: z.enum(["asc", "desc"]).optional().default("desc"), + }), + ) + .query(async ({ ctx: context, input }) => { const supabaseClient = await createClient(); - const projectId = await getOrCreateDefaultProject( + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); + const projectStore = new SupabaseProjectStore( supabaseClient, context.user.id, ); + const memories = await memoryStore.list( + input.projectId ? { projectId: input.projectId } : undefined, + ); + const projectsResult = await projectStore.list(); + const projects = projectsResult.success ? projectsResult.value : []; + const projectMap = new Map(projects.map((project) => [project.id, project])); + const memoriesWithProjects = memories.map((memory) => ({ + ...memory, + project: projectMap.get(memory.projectId) ?? null, + })); + const sortedMemories = memoriesWithProjects.sort((memoryA, memoryB) => { + const comparison = + new Date(memoryA.createdAt).getTime() - + new Date(memoryB.createdAt).getTime(); + + return input.sortOrder === "desc" ? -comparison : comparison; + }); + + return { memories: sortedMemories, projects }; + }), + + create: protectedProcedure + .input( + z.object({ + content: z.string().min(1), + projectId: z.string().optional(), + }), + ) + .mutation(async ({ ctx: context, input }) => { + const supabaseClient = await createClient(); + const projectId = + input.projectId ?? + (await getOrCreateDefaultProject(supabaseClient, context.user.id)); const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const memory = await memoryStore.create({ content: input.content, @@ -64,17 +115,94 @@ export const memoryRouter = createTRPCRouter({ return memory; }), + update: protectedProcedure + .input( + z.object({ + id: z.string(), + content: z.string().min(1).optional(), + projectId: z.string().optional(), + }), + ) + .mutation(async ({ ctx: context, input }) => { + const supabaseClient = await createClient(); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); + const updateData: { content?: string; projectId?: string } = {}; + + if (input.content !== undefined) { + updateData.content = input.content; + } + + if (input.projectId !== undefined) { + updateData.projectId = input.projectId; + } + + const updateResult = await memoryStore.update(input.id, updateData); + + if (!updateResult.success) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Memory not found: ${input.id}`, + }); + } + + return updateResult.value; + }), + delete: protectedProcedure .input(z.object({ id: z.string() })) - .mutation(async ({ context, input }) => { + .mutation(async ({ ctx: context, input }) => { const supabaseClient = await createClient(); const memoryStore = new SupabaseStore(supabaseClient, context.user.id); const deleteResult = await memoryStore.delete(input.id); if (!deleteResult.success) { - throw new Error(`Memory not found: ${input.id}`); + throw new TRPCError({ + code: "NOT_FOUND", + message: `Memory not found: ${input.id}`, + }); } return { success: true }; }), + + search: protectedProcedure + .input( + z.object({ + query: z.string().min(1), + projectId: z.string().optional(), + limit: z.number().optional().default(20), + }), + ) + .query(async ({ ctx: context, input }) => { + const embeddingService = getEmbeddingService(); + + if (!embeddingService) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: + "Search is not available. Configure OPENAI_API_KEY to enable semantic search.", + }); + } + + const supabaseClient = await createClient(); + const memoryStore = new SupabaseStore(supabaseClient, context.user.id); + const projectStore = new SupabaseProjectStore( + supabaseClient, + context.user.id, + ); + const queryEmbedding = await embeddingService.generate(input.query); + const searchResults = await memoryStore.search(queryEmbedding, { + projectId: input.projectId, + limit: input.limit, + }); + const projectsResult = await projectStore.list(); + const projects = projectsResult.success ? projectsResult.value : []; + const projectMap = new Map(projects.map((project) => [project.id, project])); + const resultsWithProjects = searchResults.map((result) => ({ + ...result, + project: projectMap.get(result.projectId) ?? null, + })); + + return { results: resultsWithProjects, projects }; + }), }); diff --git a/packages/web/src/server/api/routers/project.ts b/packages/web/src/server/api/routers/project.ts new file mode 100644 index 0000000..c76070d --- /dev/null +++ b/packages/web/src/server/api/routers/project.ts @@ -0,0 +1,79 @@ +import { SupabaseProjectStore } from "@imemio/sdk"; +import { z } from "zod"; +import { createClient } from "~/lib/supabase/server"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; + +const globalProjectName = "global"; +export const projectRouter = createTRPCRouter({ + list: protectedProcedure.query(async ({ ctx: context }) => { + const supabaseClient = await createClient(); + const projectStore = new SupabaseProjectStore( + supabaseClient, + context.user.id, + ); + const result = await projectStore.list(); + + if (!result.success) { + return []; + } + + return result.value; + }), + + create: protectedProcedure + .input( + z.object({ + name: z.string().min(1), + description: z.string().optional(), + isGlobal: z.boolean().optional().default(false), + }), + ) + .mutation(async ({ ctx: context, input }) => { + const supabaseClient = await createClient(); + const projectStore = new SupabaseProjectStore( + supabaseClient, + context.user.id, + ); + const result = await projectStore.create({ + name: input.name, + description: input.description, + isGlobal: input.isGlobal, + }); + + if (!result.success) { + throw new Error("Failed to create project"); + } + + return result.value; + }), + + getOrCreateGlobal: protectedProcedure.query(async ({ ctx: context }) => { + const supabaseClient = await createClient(); + const projectStore = new SupabaseProjectStore( + supabaseClient, + context.user.id, + ); + const listResult = await projectStore.list(); + + if (listResult.success) { + const existingGlobal = listResult.value.find( + (project) => project.isGlobal, + ); + + if (existingGlobal) { + return existingGlobal; + } + } + + const createResult = await projectStore.create({ + name: globalProjectName, + isGlobal: true, + }); + + if (!createResult.success) { + throw new Error("Failed to create global project"); + } + + return createResult.value; + }), +}); |