From 5980d98019eda40cda0d5c88d460029d8033503a Mon Sep 17 00:00:00 2001 From: h Date: Wed, 4 Feb 2026 17:26:41 +0100 Subject: [PATCH] feat(*): add data injection --- backend/src/bot/handlers/__init__.py | 3 +- .../bot/handlers/initialize/initializer.py | 1 + backend/src/bot/handlers/inject/__init__.py | 10 + backend/src/bot/handlers/inject/collection.py | 97 ++++++++++ backend/src/bot/handlers/inject/handler.py | 172 ++++++++++++++++++ backend/src/bot/handlers/message/handler.py | 33 +++- backend/src/bot/modules/ai/prompts.py | 43 +++++ frontend/src/lib/convex/_generated/api.d.ts | 4 + frontend/src/lib/convex/inject.ts | 109 +++++++++++ frontend/src/lib/convex/injectConnections.ts | 102 +++++++++++ frontend/src/lib/convex/schema.ts | 27 ++- frontend/src/lib/convex/users.ts | 50 +++++ 12 files changed, 648 insertions(+), 3 deletions(-) create mode 100644 backend/src/bot/handlers/inject/__init__.py create mode 100644 backend/src/bot/handlers/inject/collection.py create mode 100644 backend/src/bot/handlers/inject/handler.py create mode 100644 frontend/src/lib/convex/inject.ts create mode 100644 frontend/src/lib/convex/injectConnections.ts diff --git a/backend/src/bot/handlers/__init__.py b/backend/src/bot/handlers/__init__.py index 3e2fe58..ee07607 100644 --- a/backend/src/bot/handlers/__init__.py +++ b/backend/src/bot/handlers/__init__.py @@ -1,6 +1,6 @@ from aiogram import Router -from . import apikey, chat, initialize, message, proxy, rag, start +from . import apikey, chat, initialize, inject, message, proxy, rag, start router = Router() @@ -10,6 +10,7 @@ router.include_routers( apikey.router, chat.router, rag.router, + inject.router, proxy.router, message.router, ) diff --git a/backend/src/bot/handlers/initialize/initializer.py b/backend/src/bot/handlers/initialize/initializer.py index accd0f8..6a83827 100644 --- a/backend/src/bot/handlers/initialize/initializer.py +++ b/backend/src/bot/handlers/initialize/initializer.py @@ -18,6 +18,7 @@ async def startup(bot: Bot) -> None: types.BotCommand(command="/presets", description="Show prompt presets"), types.BotCommand(command="/preset", description="Apply a preset"), types.BotCommand(command="/proxy", description="Proxy chat to another bot"), + types.BotCommand(command="/inject", description="Inject knowledge base"), ] ) logger.info(f"[green]Started as[/] @{(await bot.me()).username}") diff --git a/backend/src/bot/handlers/inject/__init__.py b/backend/src/bot/handlers/inject/__init__.py new file mode 100644 index 0000000..83c9f46 --- /dev/null +++ b/backend/src/bot/handlers/inject/__init__.py @@ -0,0 +1,10 @@ +from aiogram import Router + +from .collection import router as collection_router +from .handler import router as command_router + +router = Router() + +router.include_routers(command_router, collection_router) + +__all__ = ["router"] diff --git a/backend/src/bot/handlers/inject/collection.py b/backend/src/bot/handlers/inject/collection.py new file mode 100644 index 0000000..5175dee --- /dev/null +++ b/backend/src/bot/handlers/inject/collection.py @@ -0,0 +1,97 @@ +import io + +from aiogram import Bot, F, Router, types +from aiogram.filters import Filter +from convex import ConvexInt64 + +from utils import env +from utils.convex import ConvexClient + +router = Router() +convex = ConvexClient(env.convex_url) + + +class InInjectCollectionMode(Filter): + async def __call__(self, message: types.Message) -> bool | dict: + if not message.from_user: + return False + + user = await convex.query( + "users:getByTelegramId", {"telegramId": ConvexInt64(message.from_user.id)} + ) + + if not user or not user.get("injectCollectionMode"): + return False + + return { + "inject_user": user, + "inject_collection_mode": user["injectCollectionMode"], + } + + +in_collection_mode = InInjectCollectionMode() + + +@router.message(in_collection_mode, F.text & ~F.text.startswith("/")) +async def on_text_in_collection_mode( + message: types.Message, inject_user: dict, inject_collection_mode: dict +) -> None: + if not message.text: + return + + db_id = inject_collection_mode["injectDatabaseId"] + + db = await convex.query("inject:getDatabaseById", {"injectDatabaseId": db_id}) + db_name = db["name"] if db else "database" + + await convex.mutation( + "inject:setContent", {"injectDatabaseId": db_id, "content": message.text} + ) + + await convex.mutation( + "users:stopInjectCollectionMode", {"userId": inject_user["_id"]} + ) + + await message.answer( + f"✓ Text saved to '{db_name}'.\n\n" + f"Connect it with: /inject connect {db_name}", + parse_mode="HTML", + ) + + +@router.message(in_collection_mode, F.document) +async def on_document_in_collection_mode( + message: types.Message, bot: Bot, inject_user: dict, inject_collection_mode: dict +) -> None: + if not message.document: + return + + doc = message.document + db_id = inject_collection_mode["injectDatabaseId"] + + db = await convex.query("inject:getDatabaseById", {"injectDatabaseId": db_id}) + db_name = db["name"] if db else "database" + + file = await bot.get_file(doc.file_id) + if not file.file_path: + await message.answer("Failed to download file.") + return + + buffer = io.BytesIO() + await bot.download_file(file.file_path, buffer) + text = buffer.getvalue().decode("utf-8") + + await convex.mutation( + "inject:setContent", {"injectDatabaseId": db_id, "content": text} + ) + + await convex.mutation( + "users:stopInjectCollectionMode", {"userId": inject_user["_id"]} + ) + + file_name = doc.file_name or "file" + await message.answer( + f"✓ '{file_name}' saved to '{db_name}'.\n\n" + f"Connect it with: /inject connect {db_name}", + parse_mode="HTML", + ) diff --git a/backend/src/bot/handlers/inject/handler.py b/backend/src/bot/handlers/inject/handler.py new file mode 100644 index 0000000..6e02d3f --- /dev/null +++ b/backend/src/bot/handlers/inject/handler.py @@ -0,0 +1,172 @@ +from aiogram import Router, types +from aiogram.filters import Command +from convex import ConvexInt64 + +from utils import env +from utils.convex import ConvexClient + +router = Router() +convex = ConvexClient(env.convex_url) + + +@router.message(Command("inject")) +async def on_inject(message: types.Message) -> None: + if not message.from_user or not message.text: + return + + args = message.text.split()[1:] + + if not args: + await show_usage(message) + return + + user = await convex.query( + "users:getByTelegramId", {"telegramId": ConvexInt64(message.from_user.id)} + ) + + if not user: + await message.answer("Use /apikey first to set your Gemini API key.") + return + + user_id = user["_id"] + command = args[0] + + if command == "list": + await list_databases(message, user_id) + return + + if len(args) < 2: # noqa: PLR2004 + await show_usage(message) + return + + db_name = args[1] + + if command == "create": + await create_database(message, user_id, db_name) + elif command == "connect": + await connect_database(message, user_id, db_name) + elif command == "disconnect": + await disconnect_database(message, user_id, db_name) + elif command == "clear": + await clear_database(message, user_id, db_name) + else: + await show_usage(message) + + +async def show_usage(message: types.Message) -> None: + await message.answer( + "Inject Commands:\n\n" + "/inject list - List inject databases\n\n" + "/inject create <name> - Create and upload one file\n" + "/inject connect <name> - Connect to all chats\n" + "/inject disconnect <name> - Disconnect\n" + "/inject clear <name> - Delete database", + parse_mode="HTML", + ) + + +async def list_databases(message: types.Message, user_id: str) -> None: + databases = await convex.query("inject:listDatabases", {"userId": user_id}) + connections = await convex.query( + "injectConnections:getActiveForUser", {"userId": user_id} + ) + + connected_db_ids = {conn["injectDatabaseId"] for conn in connections} + + if not databases: + await message.answer( + "No inject databases found.\n\n" + "Create one with: /inject create mydb", + parse_mode="HTML", + ) + return + + lines = ["Your inject databases:\n"] + for db in databases: + status = "" + if db["_id"] in connected_db_ids: + status += " (connected)" + if not db.get("content"): + status += " (empty)" + lines.append(f"• {db['name']}{status}") + + await message.answer("\n".join(lines), parse_mode="HTML") + + +async def create_database(message: types.Message, user_id: str, db_name: str) -> None: + collection_mode = await convex.query( + "users:getInjectCollectionMode", {"userId": user_id} + ) + + if collection_mode: + await message.answer( + "Already waiting for a file. Send a file or text to complete." + ) + return + + db_id = await convex.mutation( + "inject:createDatabase", {"userId": user_id, "name": db_name} + ) + + await convex.mutation( + "users:startInjectCollectionMode", + {"userId": user_id, "injectDatabaseId": db_id}, + ) + + await message.answer( + f"Waiting for content for '{db_name}'\n\n" + "Send a file (json, txt, csv, etc.) or a text message.\n" + "It will be saved automatically.", + parse_mode="HTML", + ) + + +async def connect_database(message: types.Message, user_id: str, db_name: str) -> None: + db = await convex.query("inject:getDatabase", {"userId": user_id, "name": db_name}) + + if not db: + await message.answer( + f"Database '{db_name}' not found.\n" + f"Create it with: /inject create {db_name}", + parse_mode="HTML", + ) + return + + await convex.mutation( + "injectConnections:connect", + {"userId": user_id, "injectDatabaseId": db["_id"], "isGlobal": True}, + ) + + await message.answer(f"✓ '{db_name}' connected to all your chats.") + + +async def disconnect_database( + message: types.Message, user_id: str, db_name: str +) -> None: + db = await convex.query("inject:getDatabase", {"userId": user_id, "name": db_name}) + + if not db: + await message.answer(f"Database '{db_name}' not found.") + return + + result = await convex.mutation( + "injectConnections:disconnect", + {"userId": user_id, "injectDatabaseId": db["_id"]}, + ) + + if result: + await message.answer(f"✓ '{db_name}' disconnected.") + else: + await message.answer(f"'{db_name}' was not connected.") + + +async def clear_database(message: types.Message, user_id: str, db_name: str) -> None: + db = await convex.query("inject:getDatabase", {"userId": user_id, "name": db_name}) + + if not db: + await message.answer(f"Database '{db_name}' not found.") + return + + await convex.mutation("inject:deleteDatabase", {"injectDatabaseId": db["_id"]}) + + await message.answer(f"✓ '{db_name}' deleted.") diff --git a/backend/src/bot/handlers/message/handler.py b/backend/src/bot/handlers/message/handler.py index f338344..b509469 100644 --- a/backend/src/bot/handlers/message/handler.py +++ b/backend/src/bot/handlers/message/handler.py @@ -265,6 +265,19 @@ async def process_message_from_web( # noqa: C901, PLR0912, PLR0913, PLR0915 if db: rag_db_names.append(db["name"]) + inject_connections = await convex.query( + "injectConnections:getActiveForUser", {"userId": convex_user_id} + ) + inject_content = "" + if inject_connections: + for conn in inject_connections: + db = await convex.query( + "inject:getDatabaseById", {"injectDatabaseId": conn["injectDatabaseId"]} + ) + if db and db.get("content"): + inject_content += db["content"] + "\n\n" + inject_content = inject_content.strip() + assistant_message_id = await convex.mutation( "messages:create", { @@ -281,6 +294,8 @@ async def process_message_from_web( # noqa: C901, PLR0912, PLR0913, PLR0915 ) system_prompt = SUMMARIZE_PROMPT if is_summarize else user.get("systemPrompt") + if system_prompt and inject_content: + system_prompt = system_prompt.replace("{theory_database}", inject_content) text_agent = create_text_agent( api_key=api_key, model_name=model_name, @@ -441,6 +456,19 @@ async def process_message( # noqa: C901, PLR0912, PLR0913, PLR0915 if db: rag_db_names.append(db["name"]) + inject_connections = await convex.query( + "injectConnections:getActiveForUser", {"userId": convex_user_id} + ) + inject_content = "" + if inject_connections: + for conn in inject_connections: + db = await convex.query( + "inject:getDatabaseById", {"injectDatabaseId": conn["injectDatabaseId"]} + ) + if db and db.get("content"): + inject_content += db["content"] + "\n\n" + inject_content = inject_content.strip() + if not skip_user_message: await convex.mutation( "messages:create", @@ -467,10 +495,13 @@ async def process_message( # noqa: C901, PLR0912, PLR0913, PLR0915 "messages:getHistoryForAI", {"chatId": active_chat_id, "limit": 50} ) + system_prompt = user.get("systemPrompt") + if system_prompt and inject_content: + system_prompt = system_prompt.replace("{theory_database}", inject_content) text_agent = create_text_agent( api_key=api_key, model_name=model_name, - system_prompt=user.get("systemPrompt"), + system_prompt=system_prompt, rag_db_names=rag_db_names if rag_db_names else None, ) diff --git a/backend/src/bot/modules/ai/prompts.py b/backend/src/bot/modules/ai/prompts.py index ad0f7ae..b1911a8 100644 --- a/backend/src/bot/modules/ai/prompts.py +++ b/backend/src/bot/modules/ai/prompts.py @@ -22,6 +22,48 @@ for example Group A: 1, Group A: 2a, Group B: 2b, etc. Or, Theory: 1, Theory: 2a, Practice: 1, etc. Only output identifiers that exist in the image.""" +PROOFS_SYSTEM = """ +You are an Examination Engine designed for Apple Watch output. +CONTEXT: You have a loaded JSON database of theoretical knowledge below. + + +{theory_database} + + +*** PROTOCOL: BATCH PROCESSING *** + +1. IMAGE INPUT (Primary Mode): + - **DETECT ALL** tasks/questions visible in the image. + - **SOLVE ALL** of them sequentially in a single response. + - **ORDER:** Follow the numbering on the exam sheet (Ex 1, Ex 2, ...). + - **SEPARATOR:** Use "---" between tasks. + +2. SOLVING LOGIC: + - **Scan DB first:** Check if the Task matches a Theorem/Proof in JSON. + - IF MATCH: Output `statement` AND `proof` VERBATIM from JSON + (as requested in task) + - IF PARTIAL MATCH (e.g., specific function): + Use JSON method but plug in the numbers. + - **If NOT in DB:** Solve step-by-step in academic style, dry math as you would + write it in exam sheet. + - **Style:** Dry, formal, "notebook" style. No conversational filler. + +3. APPLE WATCH FORMATTING (Strict): + - **Line Width:** MAX 25-30 chars. Force line breaks (`\\`) often. + - **Math:** Standard LaTeX blocks `$$...$$` or inline `$..$`. + - **Structure:** + **Ex. X ([Topic])** + [Solution/Proof] + --- + **Ex. Y ([Topic])** + [Solution/Proof] + +4. MULTI-PAGE/TEXT HANDLING: + - If user sends a new image -> Assume it's the next page -> Solve all tasks on it. + - If user types text (e.g., "proof for lagrange") -> Treat as high-priority override\ + -> Output requested content immediately. + - Ignore typos in text input (fuzzy match). +""" RAGTHEORY_SYSTEM = """You help answer theoretical exam questions. @@ -74,4 +116,5 @@ Max 2-3 sentences. This is for Apple Watch display.""" PRESETS: dict[str, tuple[str, str]] = { "exam": (EXAM_SYSTEM, EXAM_FOLLOW_UP), "ragtheory": (RAGTHEORY_SYSTEM, EXAM_FOLLOW_UP), + "proofs": (PROOFS_SYSTEM, EXAM_FOLLOW_UP), } diff --git a/frontend/src/lib/convex/_generated/api.d.ts b/frontend/src/lib/convex/_generated/api.d.ts index 5daf8a5..f32e8b5 100644 --- a/frontend/src/lib/convex/_generated/api.d.ts +++ b/frontend/src/lib/convex/_generated/api.d.ts @@ -11,6 +11,8 @@ import type * as chats from "../chats.js"; import type * as devicePairings from "../devicePairings.js"; import type * as http from "../http.js"; +import type * as inject from "../inject.js"; +import type * as injectConnections from "../injectConnections.js"; import type * as messages from "../messages.js"; import type * as pairingRequests from "../pairingRequests.js"; import type * as pendingGenerations from "../pendingGenerations.js"; @@ -30,6 +32,8 @@ declare const fullApi: ApiFromModules<{ chats: typeof chats; devicePairings: typeof devicePairings; http: typeof http; + inject: typeof inject; + injectConnections: typeof injectConnections; messages: typeof messages; pairingRequests: typeof pairingRequests; pendingGenerations: typeof pendingGenerations; diff --git a/frontend/src/lib/convex/inject.ts b/frontend/src/lib/convex/inject.ts new file mode 100644 index 0000000..65d750c --- /dev/null +++ b/frontend/src/lib/convex/inject.ts @@ -0,0 +1,109 @@ +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; + +export const createDatabase = mutation({ + args: { userId: v.id('users'), name: v.string() }, + returns: v.id('injectDatabases'), + handler: async (ctx, args) => { + const existing = await ctx.db + .query('injectDatabases') + .withIndex('by_user_id_and_name', (q) => q.eq('userId', args.userId).eq('name', args.name)) + .unique(); + + if (existing) { + return existing._id; + } + + return await ctx.db.insert('injectDatabases', { + userId: args.userId, + name: args.name, + createdAt: Date.now() + }); + } +}); + +export const getDatabase = query({ + args: { userId: v.id('users'), name: v.string() }, + returns: v.union( + v.object({ + _id: v.id('injectDatabases'), + _creationTime: v.number(), + userId: v.id('users'), + name: v.string(), + content: v.optional(v.string()), + createdAt: v.number() + }), + v.null() + ), + handler: async (ctx, args) => { + return await ctx.db + .query('injectDatabases') + .withIndex('by_user_id_and_name', (q) => q.eq('userId', args.userId).eq('name', args.name)) + .unique(); + } +}); + +export const getDatabaseById = query({ + args: { injectDatabaseId: v.id('injectDatabases') }, + returns: v.union( + v.object({ + _id: v.id('injectDatabases'), + _creationTime: v.number(), + userId: v.id('users'), + name: v.string(), + content: v.optional(v.string()), + createdAt: v.number() + }), + v.null() + ), + handler: async (ctx, args) => { + return await ctx.db.get(args.injectDatabaseId); + } +}); + +export const listDatabases = query({ + args: { userId: v.id('users') }, + returns: v.array( + v.object({ + _id: v.id('injectDatabases'), + _creationTime: v.number(), + userId: v.id('users'), + name: v.string(), + content: v.optional(v.string()), + createdAt: v.number() + }) + ), + handler: async (ctx, args) => { + return await ctx.db + .query('injectDatabases') + .withIndex('by_user_id', (q) => q.eq('userId', args.userId)) + .collect(); + } +}); + +export const setContent = mutation({ + args: { injectDatabaseId: v.id('injectDatabases'), content: v.string() }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.patch(args.injectDatabaseId, { content: args.content }); + return null; + } +}); + +export const deleteDatabase = mutation({ + args: { injectDatabaseId: v.id('injectDatabases') }, + returns: v.null(), + handler: async (ctx, args) => { + const connections = await ctx.db + .query('injectConnections') + .withIndex('by_inject_database_id', (q) => q.eq('injectDatabaseId', args.injectDatabaseId)) + .collect(); + + for (const conn of connections) { + await ctx.db.delete(conn._id); + } + + await ctx.db.delete(args.injectDatabaseId); + return null; + } +}); diff --git a/frontend/src/lib/convex/injectConnections.ts b/frontend/src/lib/convex/injectConnections.ts new file mode 100644 index 0000000..47d4709 --- /dev/null +++ b/frontend/src/lib/convex/injectConnections.ts @@ -0,0 +1,102 @@ +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; + +export const connect = mutation({ + args: { + userId: v.id('users'), + injectDatabaseId: v.id('injectDatabases'), + isGlobal: v.optional(v.boolean()) + }, + returns: v.id('injectConnections'), + handler: async (ctx, args) => { + const existing = await ctx.db + .query('injectConnections') + .withIndex('by_user_id_and_inject_database_id', (q) => + q.eq('userId', args.userId).eq('injectDatabaseId', args.injectDatabaseId) + ) + .unique(); + + if (existing) { + return existing._id; + } + + return await ctx.db.insert('injectConnections', { + userId: args.userId, + injectDatabaseId: args.injectDatabaseId, + isGlobal: args.isGlobal ?? true, + createdAt: Date.now() + }); + } +}); + +export const disconnect = mutation({ + args: { + userId: v.id('users'), + injectDatabaseId: v.id('injectDatabases') + }, + returns: v.boolean(), + handler: async (ctx, args) => { + const existing = await ctx.db + .query('injectConnections') + .withIndex('by_user_id_and_inject_database_id', (q) => + q.eq('userId', args.userId).eq('injectDatabaseId', args.injectDatabaseId) + ) + .unique(); + + if (!existing) { + return false; + } + + await ctx.db.delete(existing._id); + return true; + } +}); + +export const getActiveForUser = query({ + args: { userId: v.id('users') }, + returns: v.array( + v.object({ + _id: v.id('injectConnections'), + _creationTime: v.number(), + userId: v.id('users'), + injectDatabaseId: v.id('injectDatabases'), + isGlobal: v.boolean(), + createdAt: v.number() + }) + ), + handler: async (ctx, args) => { + return await ctx.db + .query('injectConnections') + .withIndex('by_user_id', (q) => q.eq('userId', args.userId)) + .collect(); + } +}); + +export const getByInjectDatabaseId = query({ + args: { injectDatabaseId: v.id('injectDatabases') }, + returns: v.array( + v.object({ + _id: v.id('injectConnections'), + _creationTime: v.number(), + userId: v.id('users'), + injectDatabaseId: v.id('injectDatabases'), + isGlobal: v.boolean(), + createdAt: v.number() + }) + ), + handler: async (ctx, args) => { + return await ctx.db + .query('injectConnections') + .withIndex('by_inject_database_id', (q) => q.eq('injectDatabaseId', args.injectDatabaseId)) + .collect(); + } +}); + +export const deleteConnection = mutation({ + args: { connectionId: v.id('injectConnections') }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.delete(args.connectionId); + return null; + } +}); diff --git a/frontend/src/lib/convex/schema.ts b/frontend/src/lib/convex/schema.ts index 2147f9d..f066952 100644 --- a/frontend/src/lib/convex/schema.ts +++ b/frontend/src/lib/convex/schema.ts @@ -16,6 +16,12 @@ export default defineSchema({ ragDatabaseId: v.id('ragDatabases'), activeSince: v.number() }) + ), + injectCollectionMode: v.optional( + v.object({ + injectDatabaseId: v.id('injectDatabases'), + activeSince: v.number() + }) ) }).index('by_telegram_id', ['telegramId']), @@ -122,5 +128,24 @@ export default defineSchema({ }) .index('by_user_id', ['userId']) .index('by_user_id_and_rag_database_id', ['userId', 'ragDatabaseId']) - .index('by_rag_database_id', ['ragDatabaseId']) + .index('by_rag_database_id', ['ragDatabaseId']), + + injectDatabases: defineTable({ + userId: v.id('users'), + name: v.string(), + content: v.optional(v.string()), + createdAt: v.number() + }) + .index('by_user_id', ['userId']) + .index('by_user_id_and_name', ['userId', 'name']), + + injectConnections: defineTable({ + userId: v.id('users'), + injectDatabaseId: v.id('injectDatabases'), + isGlobal: v.boolean(), + createdAt: v.number() + }) + .index('by_user_id', ['userId']) + .index('by_user_id_and_inject_database_id', ['userId', 'injectDatabaseId']) + .index('by_inject_database_id', ['injectDatabaseId']) }); diff --git a/frontend/src/lib/convex/users.ts b/frontend/src/lib/convex/users.ts index 7b4ad5a..d38c361 100644 --- a/frontend/src/lib/convex/users.ts +++ b/frontend/src/lib/convex/users.ts @@ -22,6 +22,12 @@ export const getById = query({ ragDatabaseId: v.id('ragDatabases'), activeSince: v.number() }) + ), + injectCollectionMode: v.optional( + v.object({ + injectDatabaseId: v.id('injectDatabases'), + activeSince: v.number() + }) ) }), v.null() @@ -50,6 +56,12 @@ export const getByTelegramId = query({ ragDatabaseId: v.id('ragDatabases'), activeSince: v.number() }) + ), + injectCollectionMode: v.optional( + v.object({ + injectDatabaseId: v.id('injectDatabases'), + activeSince: v.number() + }) ) }), v.null() @@ -177,3 +189,41 @@ export const getRagCollectionMode = query({ return user?.ragCollectionMode ?? null; } }); + +export const startInjectCollectionMode = mutation({ + args: { userId: v.id('users'), injectDatabaseId: v.id('injectDatabases') }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.patch(args.userId, { + injectCollectionMode: { + injectDatabaseId: args.injectDatabaseId, + activeSince: Date.now() + } + }); + return null; + } +}); + +export const stopInjectCollectionMode = mutation({ + args: { userId: v.id('users') }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.patch(args.userId, { injectCollectionMode: undefined }); + return null; + } +}); + +export const getInjectCollectionMode = query({ + args: { userId: v.id('users') }, + returns: v.union( + v.object({ + injectDatabaseId: v.id('injectDatabases'), + activeSince: v.number() + }), + v.null() + ), + handler: async (ctx, args) => { + const user = await ctx.db.get(args.userId); + return user?.injectCollectionMode ?? null; + } +});