diff --git a/.gitignore b/.gitignore index 33bfac6..1cbc373 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +public/pdf.worker.min.mjs diff --git a/drizzle/0036_motionless_rick_jones.sql b/drizzle/0036_motionless_rick_jones.sql new file mode 100644 index 0000000..a7b82fe --- /dev/null +++ b/drizzle/0036_motionless_rick_jones.sql @@ -0,0 +1,7 @@ +-- Switch full-text search to the 'german' config (stemming + stop words). +-- A generated column cannot be altered in place; drop + re-add rewrites the +-- table and recomputes the vectors. Dropping the column also drops the GIN +-- index, so it is recreated afterwards. +ALTER TABLE "material_chunk" drop column "content_tsv";--> statement-breakpoint +ALTER TABLE "material_chunk" ADD COLUMN "content_tsv" "tsvector" GENERATED ALWAYS AS (to_tsvector('german', "material_chunk"."content")) STORED;--> statement-breakpoint +CREATE INDEX "material_chunk_tsv_idx" ON "material_chunk" USING gin ("content_tsv"); diff --git a/drizzle/0037_typical_overlord.sql b/drizzle/0037_typical_overlord.sql new file mode 100644 index 0000000..6769949 --- /dev/null +++ b/drizzle/0037_typical_overlord.sql @@ -0,0 +1,2 @@ +ALTER TABLE "ai_conversation" ADD COLUMN "material_id" text;--> statement-breakpoint +ALTER TABLE "ai_conversation" ADD CONSTRAINT "ai_conversation_material_id_material_id_fk" FOREIGN KEY ("material_id") REFERENCES "public"."material"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0036_snapshot.json b/drizzle/meta/0036_snapshot.json new file mode 100644 index 0000000..abbea1f --- /dev/null +++ b/drizzle/meta/0036_snapshot.json @@ -0,0 +1,5815 @@ +{ + "id": "646344f3-aa87-45b9-8f9f-a668339bee23", + "prevId": "3ac42aee-7be4-49fa-8409-1e9d11bcecd3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversation": { + "name": "ai_conversation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New conversation'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_conversation_userId_idx": { + "name": "ai_conversation_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversation_user_id_user_id_fk": { + "name": "ai_conversation_user_id_user_id_fk", + "tableFrom": "ai_conversation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_conversation_module_id_module_id_fk": { + "name": "ai_conversation_module_id_module_id_fk", + "tableFrom": "ai_conversation", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_message": { + "name": "ai_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_message_conversationId_idx": { + "name": "ai_message_conversationId_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_message_conversation_id_ai_conversation_id_fk": { + "name": "ai_message_conversation_id_ai_conversation_id_fk", + "tableFrom": "ai_message", + "tableTo": "ai_conversation", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_log": { + "name": "ai_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'chat'" + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_usage_userId_idx": { + "name": "ai_usage_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_usage_createdAt_idx": { + "name": "ai_usage_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_log_user_id_user_id_fk": { + "name": "ai_usage_log_user_id_user_id_fk", + "tableFrom": "ai_usage_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_ai_key": { + "name": "user_ai_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_ai_key_user_id_user_id_fk": { + "name": "user_ai_key_user_id_user_id_fk", + "tableFrom": "user_ai_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_config": { + "name": "app_config", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignment": { + "name": "assignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'graded'" + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "points_achieved": { + "name": "points_achieved", + "type": "numeric(7, 2)", + "primaryKey": false, + "notNull": false + }, + "points_max": { + "name": "points_max", + "type": "numeric(7, 2)", + "primaryKey": false, + "notNull": false + }, + "subtasks": { + "name": "subtasks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assignment_module_idx": { + "name": "assignment_module_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_user_idx": { + "name": "assignment_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assignment_user_id_user_id_fk": { + "name": "assignment_user_id_user_id_fk", + "tableFrom": "assignment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignment_module_id_module_id_fk": { + "name": "assignment_module_id_module_id_fk", + "tableFrom": "assignment", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignment_material": { + "name": "assignment_material", + "schema": "", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "assignment_material_assignment_id_assignment_id_fk": { + "name": "assignment_material_assignment_id_assignment_id_fk", + "tableFrom": "assignment_material", + "tableTo": "assignment", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignment_material_material_id_material_id_fk": { + "name": "assignment_material_material_id_material_id_fk", + "tableFrom": "assignment_material", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assignment_material_assignment_id_material_id_pk": { + "name": "assignment_material_assignment_id_material_id_pk", + "columns": [ + "assignment_id", + "material_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_label": { + "name": "entity_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before": { + "name": "before", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after": { + "name": "after", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "undone": { + "name": "undone", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_user_created_idx": { + "name": "audit_log_user_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generation_coverage": { + "name": "generation_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "produced_count": { + "name": "produced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "generation_coverage_target_topic_idx": { + "name": "generation_coverage_target_topic_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "generation_coverage_job_idx": { + "name": "generation_coverage_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "generation_coverage_topic_id_outline_topic_id_fk": { + "name": "generation_coverage_topic_id_outline_topic_id_fk", + "tableFrom": "generation_coverage", + "tableTo": "outline_topic", + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_coverage_job_id_generation_job_id_fk": { + "name": "generation_coverage_job_id_generation_job_id_fk", + "tableFrom": "generation_coverage", + "tableTo": "generation_job", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generation_job": { + "name": "generation_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "outline_version": { + "name": "outline_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "topics_total": { + "name": "topics_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "topics_done": { + "name": "topics_done", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "produced_count": { + "name": "produced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "batch_ref": { + "name": "batch_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_model": { + "name": "batch_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "generation_job_user_idx": { + "name": "generation_job_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "generation_job_target_idx": { + "name": "generation_job_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "generation_job_user_id_user_id_fk": { + "name": "generation_job_user_id_user_id_fk", + "tableFrom": "generation_job", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_job_module_id_module_id_fk": { + "name": "generation_job_module_id_module_id_fk", + "tableFrom": "generation_job", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assessment_attempt": { + "name": "assessment_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "result_percent": { + "name": "result_percent", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assessment_attempt_assessmentId_idx": { + "name": "assessment_attempt_assessmentId_idx", + "columns": [ + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assessment_attempt_assessment_id_module_assessment_id_fk": { + "name": "assessment_attempt_assessment_id_module_assessment_id_fk", + "tableFrom": "assessment_attempt", + "tableTo": "module_assessment", + "columnsFrom": [ + "assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.degree_program": { + "name": "degree_program", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "degree_type": { + "name": "degree_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "institution": { + "name": "institution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_ects": { + "name": "target_ects", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "grading_system": { + "name": "grading_system", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'german'" + }, + "grade_goal": { + "name": "grade_goal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grade_scale": { + "name": "grade_scale", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thesis_max_attempts": { + "name": "thesis_max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "degree_program_userId_idx": { + "name": "degree_program_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "degree_program_user_id_user_id_fk": { + "name": "degree_program_user_id_user_id_fk", + "tableFrom": "degree_program", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_resource": { + "name": "external_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'website'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_note": { + "name": "encrypted_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "external_resource_userId_idx": { + "name": "external_resource_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_resource_moduleId_idx": { + "name": "external_resource_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_resource_user_id_user_id_fk": { + "name": "external_resource_user_id_user_id_fk", + "tableFrom": "external_resource", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_resource_program_id_degree_program_id_fk": { + "name": "external_resource_program_id_degree_program_id_fk", + "tableFrom": "external_resource", + "tableTo": "degree_program", + "columnsFrom": [ + "program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_resource_module_id_module_id_fk": { + "name": "external_resource_module_id_module_id_fk", + "tableFrom": "external_resource", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "graded_at": { + "name": "graded_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "grade_moduleId_idx": { + "name": "grade_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "grade_module_id_module_id_fk": { + "name": "grade_module_id_module_id_fk", + "tableFrom": "grade", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module_assessment": { + "name": "module_assessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exam'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "module_assessment_moduleId_idx": { + "name": "module_assessment_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "module_assessment_module_id_module_id_fk": { + "name": "module_assessment_module_id_module_id_fk", + "tableFrom": "module_assessment", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "module_assessment_module_id_unique": { + "name": "module_assessment_module_id_unique", + "nullsNotDistinct": false, + "columns": [ + "module_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module_contact": { + "name": "module_contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "module_contact_moduleId_idx": { + "name": "module_contact_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "module_contact_module_id_module_id_fk": { + "name": "module_contact_module_id_module_id_fk", + "tableFrom": "module_contact", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.semester": { + "name": "semester", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "semester_programId_idx": { + "name": "semester_programId_idx", + "columns": [ + { + "expression": "program_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "semester_program_id_degree_program_id_fk": { + "name": "semester_program_id_degree_program_id_fk", + "tableFrom": "semester", + "tableTo": "degree_program", + "columnsFrom": [ + "program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event": { + "name": "event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_offsets": { + "name": "reminder_offsets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "recurrence_until": { + "name": "recurrence_until", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "recurrence_weekdays": { + "name": "recurrence_weekdays", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recurrence_interval": { + "name": "recurrence_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_userId_idx": { + "name": "event_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "event_startsAt_idx": { + "name": "event_startsAt_idx", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_user_id_user_id_fk": { + "name": "event_user_id_user_id_fk", + "tableFrom": "event", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_module_id_module_id_fk": { + "name": "event_module_id_module_id_fk", + "tableFrom": "event", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module": { + "name": "module", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "semester_id": { + "name": "semester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ects": { + "name": "ects", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "instructor": { + "name": "instructor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exam_type": { + "name": "exam_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "is_thesis": { + "name": "is_thesis", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "pass_fail": { + "name": "pass_fail", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bonus_type": { + "name": "bonus_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "bonus_value": { + "name": "bonus_value", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "bonus_min_avg_percent": { + "name": "bonus_min_avg_percent", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "bonus_min_completed_share": { + "name": "bonus_min_completed_share", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "module_semesterId_idx": { + "name": "module_semesterId_idx", + "columns": [ + { + "expression": "semester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "module_semester_id_semester_id_fk": { + "name": "module_semester_id_semester_id_fk", + "tableFrom": "module", + "tableTo": "semester", + "columnsFrom": [ + "semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_prefs": { + "name": "user_prefs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ics_token": { + "name": "ics_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_program_id": { + "name": "active_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_semester_id": { + "name": "active_semester_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_model": { + "name": "preferred_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weekly_goal_minutes": { + "name": "weekly_goal_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_prefs_user_id_user_id_fk": { + "name": "user_prefs_user_id_user_id_fk", + "tableFrom": "user_prefs", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_prefs_active_program_id_degree_program_id_fk": { + "name": "user_prefs_active_program_id_degree_program_id_fk", + "tableFrom": "user_prefs", + "tableTo": "degree_program", + "columnsFrom": [ + "active_program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_prefs_active_semester_id_semester_id_fk": { + "name": "user_prefs_active_semester_id_semester_id_fk", + "tableFrom": "user_prefs", + "tableTo": "semester", + "columnsFrom": [ + "active_semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_prefs_ics_token_unique": { + "name": "user_prefs_ics_token_unique", + "nullsNotDistinct": false, + "columns": [ + "ics_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_content": { + "name": "text_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_storage_path": { + "name": "text_storage_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "char_count": { + "name": "char_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extraction_status": { + "name": "extraction_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "extraction_error": { + "name": "extraction_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chunks_total": { + "name": "chunks_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "chunks_embedded": { + "name": "chunks_embedded", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "material_userId_idx": { + "name": "material_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_moduleId_idx": { + "name": "material_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folderId_idx": { + "name": "material_folderId_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_contentHash_idx": { + "name": "material_contentHash_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "material_user_id_user_id_fk": { + "name": "material_user_id_user_id_fk", + "tableFrom": "material", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_module_id_module_id_fk": { + "name": "material_module_id_module_id_fk", + "tableFrom": "material", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_folder_id_material_folder_id_fk": { + "name": "material_folder_id_material_folder_id_fk", + "tableFrom": "material", + "tableTo": "material_folder", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material_annotation": { + "name": "material_annotation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rect": { + "name": "rect", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'yellow'" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "material_annotation_material_idx": { + "name": "material_annotation_material_idx", + "columns": [ + { + "expression": "material_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "material_annotation_material_id_material_id_fk": { + "name": "material_annotation_material_id_material_id_fk", + "tableFrom": "material_annotation", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_annotation_user_id_user_id_fk": { + "name": "material_annotation_user_id_user_id_fk", + "tableFrom": "material_annotation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material_folder": { + "name": "material_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "material_folder_userId_idx": { + "name": "material_folder_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folder_moduleId_idx": { + "name": "material_folder_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folder_parentId_idx": { + "name": "material_folder_parentId_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folder_sibling_uniq": { + "name": "material_folder_sibling_uniq", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "material_folder_user_id_user_id_fk": { + "name": "material_folder_user_id_user_id_fk", + "tableFrom": "material_folder", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_folder_module_id_module_id_fk": { + "name": "material_folder_module_id_module_id_fk", + "tableFrom": "material_folder", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_folder_parent_id_material_folder_id_fk": { + "name": "material_folder_parent_id_material_folder_id_fk", + "tableFrom": "material_folder", + "tableTo": "material_folder", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material_chunk": { + "name": "material_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "parent_chunk_id": { + "name": "parent_chunk_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contextual_header": { + "name": "contextual_header", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('german', \"material_chunk\".\"content\")", + "type": "stored" + } + } + }, + "indexes": { + "material_chunk_materialId_idx": { + "name": "material_chunk_materialId_idx", + "columns": [ + { + "expression": "material_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_chunk_material_level_idx": { + "name": "material_chunk_material_level_idx", + "columns": [ + { + "expression": "material_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_chunk_tsv_idx": { + "name": "material_chunk_tsv_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "material_chunk_material_id_material_id_fk": { + "name": "material_chunk_material_id_material_id_fk", + "tableFrom": "material_chunk", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_chunk_parent_chunk_id_material_chunk_id_fk": { + "name": "material_chunk_parent_chunk_id_material_chunk_id_fk", + "tableFrom": "material_chunk", + "tableTo": "material_chunk", + "columnsFrom": [ + "parent_chunk_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answer_log": { + "name": "answer_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correct": { + "name": "correct", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answer_log_attemptId_idx": { + "name": "answer_log_attemptId_idx", + "columns": [ + { + "expression": "attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answer_log_attempt_id_quiz_attempt_id_fk": { + "name": "answer_log_attempt_id_quiz_attempt_id_fk", + "tableFrom": "answer_log", + "tableTo": "quiz_attempt", + "columnsFrom": [ + "attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answer_log_question_id_question_id_fk": { + "name": "answer_log_question_id_question_id_fk", + "tableFrom": "answer_log", + "tableTo": "question", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deck": { + "name": "deck", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deck_userId_idx": { + "name": "deck_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_user_id_user_id_fk": { + "name": "deck_user_id_user_id_fk", + "tableFrom": "deck", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_module_id_module_id_fk": { + "name": "deck_module_id_module_id_fk", + "tableFrom": "deck", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.flashcard": { + "name": "flashcard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "front": { + "name": "front", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "back": { + "name": "back", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due": { + "name": "due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stability": { + "name": "stability", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "difficulty": { + "name": "difficulty", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "elapsed_days": { + "name": "elapsed_days", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_days": { + "name": "scheduled_days", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "learning_steps": { + "name": "learning_steps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reps": { + "name": "reps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lapses": { + "name": "lapses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_review": { + "name": "last_review", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "flashcard_deckId_idx": { + "name": "flashcard_deckId_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flashcard_due_idx": { + "name": "flashcard_due_idx", + "columns": [ + { + "expression": "due", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "flashcard_deck_id_deck_id_fk": { + "name": "flashcard_deck_id_deck_id_fk", + "tableFrom": "flashcard", + "tableTo": "deck", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question": { + "name": "question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "quiz_id": { + "name": "quiz_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "correct_index": { + "name": "correct_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "question_quizId_idx": { + "name": "question_quizId_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "question_quiz_id_quiz_id_fk": { + "name": "question_quiz_id_quiz_id_fk", + "tableFrom": "question", + "tableTo": "quiz", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz": { + "name": "quiz", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quiz_userId_idx": { + "name": "quiz_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_user_id_user_id_fk": { + "name": "quiz_user_id_user_id_fk", + "tableFrom": "quiz", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_module_id_module_id_fk": { + "name": "quiz_module_id_module_id_fk", + "tableFrom": "quiz", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_attempt": { + "name": "quiz_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "quiz_id": { + "name": "quiz_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "quiz_attempt_quizId_idx": { + "name": "quiz_attempt_quizId_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quiz_attempt_userId_idx": { + "name": "quiz_attempt_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_attempt_quiz_id_quiz_id_fk": { + "name": "quiz_attempt_quiz_id_quiz_id_fk", + "tableFrom": "quiz_attempt", + "tableTo": "quiz", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_attempt_user_id_user_id_fk": { + "name": "quiz_attempt_user_id_user_id_fk", + "tableFrom": "quiz_attempt", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_log": { + "name": "review_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "card_id": { + "name": "card_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_log_cardId_idx": { + "name": "review_log_cardId_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_log_userId_idx": { + "name": "review_log_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_log_card_id_flashcard_id_fk": { + "name": "review_log_card_id_flashcard_id_fk", + "tableFrom": "review_log", + "tableTo": "flashcard", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_log_user_id_user_id_fk": { + "name": "review_log_user_id_user_id_fk", + "tableFrom": "review_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.study_plan": { + "name": "study_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "study_plan_userId_idx": { + "name": "study_plan_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_plan_user_id_user_id_fk": { + "name": "study_plan_user_id_user_id_fk", + "tableFrom": "study_plan", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "study_plan_module_id_module_id_fk": { + "name": "study_plan_module_id_module_id_fk", + "tableFrom": "study_plan", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.study_plan_item": { + "name": "study_plan_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_date": { + "name": "scheduled_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "study_plan_item_planId_idx": { + "name": "study_plan_item_planId_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_plan_item_plan_id_study_plan_id_fk": { + "name": "study_plan_item_plan_id_study_plan_id_fk", + "tableFrom": "study_plan_item", + "tableTo": "study_plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thesis_milestone": { + "name": "thesis_milestone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "thesis_id": { + "name": "thesis_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thesis_milestone_thesisId_idx": { + "name": "thesis_milestone_thesisId_idx", + "columns": [ + { + "expression": "thesis_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thesis_milestone_thesis_id_thesis_project_id_fk": { + "name": "thesis_milestone_thesis_id_thesis_project_id_fk", + "tableFrom": "thesis_milestone", + "tableTo": "thesis_project", + "columnsFrom": [ + "thesis_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thesis_project": { + "name": "thesis_project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "semester_id": { + "name": "semester_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thesis_type": { + "name": "thesis_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'topic'" + }, + "research_question": { + "name": "research_question", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outline": { + "name": "outline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "superseded_by_id": { + "name": "superseded_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thesis_project_userId_idx": { + "name": "thesis_project_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "thesis_active_per_program_uq": { + "name": "thesis_active_per_program_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "program_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"thesis_project\".\"superseded_by_id\" is null and \"thesis_project\".\"program_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thesis_project_user_id_user_id_fk": { + "name": "thesis_project_user_id_user_id_fk", + "tableFrom": "thesis_project", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thesis_project_program_id_degree_program_id_fk": { + "name": "thesis_project_program_id_degree_program_id_fk", + "tableFrom": "thesis_project", + "tableTo": "degree_program", + "columnsFrom": [ + "program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thesis_project_semester_id_semester_id_fk": { + "name": "thesis_project_semester_id_semester_id_fk", + "tableFrom": "thesis_project", + "tableTo": "semester", + "columnsFrom": [ + "semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "thesis_project_superseded_by_id_thesis_project_id_fk": { + "name": "thesis_project_superseded_by_id_thesis_project_id_fk", + "tableFrom": "thesis_project", + "tableTo": "thesis_project", + "columnsFrom": [ + "superseded_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_prefs": { + "name": "notification_prefs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email_reminders": { + "name": "email_reminders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "push_reminders": { + "name": "push_reminders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "channels": { + "name": "channels", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_prefs_user_id_user_id_fk": { + "name": "notification_prefs_user_id_user_id_fk", + "tableFrom": "notification_prefs", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_sent": { + "name": "notification_sent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_sent_user_id_user_id_fk": { + "name": "notification_sent_user_id_user_id_fk", + "tableFrom": "notification_sent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_sent_user_key": { + "name": "notification_sent_user_key", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_subscription": { + "name": "push_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_subscription_userId_idx": { + "name": "push_subscription_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "push_subscription_endpoint_unique": { + "name": "push_subscription_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "endpoint" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_sent": { + "name": "reminder_sent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "offset_minutes": { + "name": "offset_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurrence_date": { + "name": "occurrence_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_sent_event_id_event_id_fk": { + "name": "reminder_sent_event_id_event_id_fk", + "tableFrom": "reminder_sent", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reminder_sent_event_offset_occurrence": { + "name": "reminder_sent_event_offset_occurrence", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "offset_minutes", + "occurrence_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite": { + "name": "invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "used_count": { + "name": "used_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invite_created_by_user_id_fk": { + "name": "invite_created_by_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.study_session": { + "name": "study_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pomodoro'" + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "study_session_userId_idx": { + "name": "study_session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "study_session_startedAt_idx": { + "name": "study_session_startedAt_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_session_user_id_user_id_fk": { + "name": "study_session_user_id_user_id_fk", + "tableFrom": "study_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "study_session_module_id_module_id_fk": { + "name": "study_session_module_id_module_id_fk", + "tableFrom": "study_session", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.semester_plan": { + "name": "semester_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semester_id": { + "name": "semester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "availability": { + "name": "availability", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "semester_plan_user_idx": { + "name": "semester_plan_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "semester_plan_user_id_user_id_fk": { + "name": "semester_plan_user_id_user_id_fk", + "tableFrom": "semester_plan", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "semester_plan_semester_id_semester_id_fk": { + "name": "semester_plan_semester_id_semester_id_fk", + "tableFrom": "semester_plan", + "tableTo": "semester", + "columnsFrom": [ + "semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "semester_plan_semester_id_unique": { + "name": "semester_plan_semester_id_unique", + "nullsNotDistinct": false, + "columns": [ + "semester_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.semester_plan_item": { + "name": "semester_plan_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_id": { + "name": "assignment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'study'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60 + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "semester_plan_item_plan_idx": { + "name": "semester_plan_item_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "semester_plan_item_plan_id_semester_plan_id_fk": { + "name": "semester_plan_item_plan_id_semester_plan_id_fk", + "tableFrom": "semester_plan_item", + "tableTo": "semester_plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "semester_plan_item_module_id_module_id_fk": { + "name": "semester_plan_item_module_id_module_id_fk", + "tableFrom": "semester_plan_item", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "semester_plan_item_assignment_id_assignment_id_fk": { + "name": "semester_plan_item_assignment_id_assignment_id_fk", + "tableFrom": "semester_plan_item", + "tableTo": "assignment", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module_outline": { + "name": "module_outline", + "schema": "", + "columns": { + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "topic_count": { + "name": "topic_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "module_outline_module_id_module_id_fk": { + "name": "module_outline_module_id_module_id_fk", + "tableFrom": "module_outline", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "module_outline_user_id_user_id_fk": { + "name": "module_outline_user_id_user_id_fk", + "tableFrom": "module_outline", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outline_topic": { + "name": "outline_topic", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_key": { + "name": "title_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_material_ids": { + "name": "source_material_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outline_topic_module_idx": { + "name": "outline_topic_module_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outline_topic_module_version_idx": { + "name": "outline_topic_module_version_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outline_topic_module_id_module_id_fk": { + "name": "outline_topic_module_id_module_id_fk", + "tableFrom": "outline_topic", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "outline_topic_user_id_user_id_fk": { + "name": "outline_topic_user_id_user_id_fk", + "tableFrom": "outline_topic", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "outline_topic_parent_id_outline_topic_id_fk": { + "name": "outline_topic_parent_id_outline_topic_id_fk", + "tableFrom": "outline_topic", + "tableTo": "outline_topic", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0037_snapshot.json b/drizzle/meta/0037_snapshot.json new file mode 100644 index 0000000..daade25 --- /dev/null +++ b/drizzle/meta/0037_snapshot.json @@ -0,0 +1,5834 @@ +{ + "id": "944c503f-f066-4ffd-8200-d67223d0a6d7", + "prevId": "646344f3-aa87-45b9-8f9f-a668339bee23", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversation": { + "name": "ai_conversation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New conversation'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_conversation_userId_idx": { + "name": "ai_conversation_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversation_user_id_user_id_fk": { + "name": "ai_conversation_user_id_user_id_fk", + "tableFrom": "ai_conversation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_conversation_module_id_module_id_fk": { + "name": "ai_conversation_module_id_module_id_fk", + "tableFrom": "ai_conversation", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ai_conversation_material_id_material_id_fk": { + "name": "ai_conversation_material_id_material_id_fk", + "tableFrom": "ai_conversation", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_message": { + "name": "ai_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_message_conversationId_idx": { + "name": "ai_message_conversationId_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_message_conversation_id_ai_conversation_id_fk": { + "name": "ai_message_conversation_id_ai_conversation_id_fk", + "tableFrom": "ai_message", + "tableTo": "ai_conversation", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_log": { + "name": "ai_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'chat'" + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_usage_userId_idx": { + "name": "ai_usage_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_usage_createdAt_idx": { + "name": "ai_usage_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_log_user_id_user_id_fk": { + "name": "ai_usage_log_user_id_user_id_fk", + "tableFrom": "ai_usage_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_ai_key": { + "name": "user_ai_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_ai_key_user_id_user_id_fk": { + "name": "user_ai_key_user_id_user_id_fk", + "tableFrom": "user_ai_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_config": { + "name": "app_config", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignment": { + "name": "assignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'graded'" + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "points_achieved": { + "name": "points_achieved", + "type": "numeric(7, 2)", + "primaryKey": false, + "notNull": false + }, + "points_max": { + "name": "points_max", + "type": "numeric(7, 2)", + "primaryKey": false, + "notNull": false + }, + "subtasks": { + "name": "subtasks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assignment_module_idx": { + "name": "assignment_module_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_user_idx": { + "name": "assignment_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assignment_user_id_user_id_fk": { + "name": "assignment_user_id_user_id_fk", + "tableFrom": "assignment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignment_module_id_module_id_fk": { + "name": "assignment_module_id_module_id_fk", + "tableFrom": "assignment", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignment_material": { + "name": "assignment_material", + "schema": "", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "assignment_material_assignment_id_assignment_id_fk": { + "name": "assignment_material_assignment_id_assignment_id_fk", + "tableFrom": "assignment_material", + "tableTo": "assignment", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignment_material_material_id_material_id_fk": { + "name": "assignment_material_material_id_material_id_fk", + "tableFrom": "assignment_material", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assignment_material_assignment_id_material_id_pk": { + "name": "assignment_material_assignment_id_material_id_pk", + "columns": [ + "assignment_id", + "material_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_label": { + "name": "entity_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before": { + "name": "before", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after": { + "name": "after", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "undone": { + "name": "undone", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_user_created_idx": { + "name": "audit_log_user_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generation_coverage": { + "name": "generation_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "produced_count": { + "name": "produced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "generation_coverage_target_topic_idx": { + "name": "generation_coverage_target_topic_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "generation_coverage_job_idx": { + "name": "generation_coverage_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "generation_coverage_topic_id_outline_topic_id_fk": { + "name": "generation_coverage_topic_id_outline_topic_id_fk", + "tableFrom": "generation_coverage", + "tableTo": "outline_topic", + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_coverage_job_id_generation_job_id_fk": { + "name": "generation_coverage_job_id_generation_job_id_fk", + "tableFrom": "generation_coverage", + "tableTo": "generation_job", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generation_job": { + "name": "generation_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "outline_version": { + "name": "outline_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "topics_total": { + "name": "topics_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "topics_done": { + "name": "topics_done", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "produced_count": { + "name": "produced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "batch_ref": { + "name": "batch_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_model": { + "name": "batch_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "generation_job_user_idx": { + "name": "generation_job_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "generation_job_target_idx": { + "name": "generation_job_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "generation_job_user_id_user_id_fk": { + "name": "generation_job_user_id_user_id_fk", + "tableFrom": "generation_job", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_job_module_id_module_id_fk": { + "name": "generation_job_module_id_module_id_fk", + "tableFrom": "generation_job", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assessment_attempt": { + "name": "assessment_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "result_percent": { + "name": "result_percent", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assessment_attempt_assessmentId_idx": { + "name": "assessment_attempt_assessmentId_idx", + "columns": [ + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assessment_attempt_assessment_id_module_assessment_id_fk": { + "name": "assessment_attempt_assessment_id_module_assessment_id_fk", + "tableFrom": "assessment_attempt", + "tableTo": "module_assessment", + "columnsFrom": [ + "assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.degree_program": { + "name": "degree_program", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "degree_type": { + "name": "degree_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "institution": { + "name": "institution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_ects": { + "name": "target_ects", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "grading_system": { + "name": "grading_system", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'german'" + }, + "grade_goal": { + "name": "grade_goal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grade_scale": { + "name": "grade_scale", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thesis_max_attempts": { + "name": "thesis_max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "degree_program_userId_idx": { + "name": "degree_program_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "degree_program_user_id_user_id_fk": { + "name": "degree_program_user_id_user_id_fk", + "tableFrom": "degree_program", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_resource": { + "name": "external_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'website'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_note": { + "name": "encrypted_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "external_resource_userId_idx": { + "name": "external_resource_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_resource_moduleId_idx": { + "name": "external_resource_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_resource_user_id_user_id_fk": { + "name": "external_resource_user_id_user_id_fk", + "tableFrom": "external_resource", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_resource_program_id_degree_program_id_fk": { + "name": "external_resource_program_id_degree_program_id_fk", + "tableFrom": "external_resource", + "tableTo": "degree_program", + "columnsFrom": [ + "program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_resource_module_id_module_id_fk": { + "name": "external_resource_module_id_module_id_fk", + "tableFrom": "external_resource", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grade": { + "name": "grade", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "graded_at": { + "name": "graded_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "grade_moduleId_idx": { + "name": "grade_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "grade_module_id_module_id_fk": { + "name": "grade_module_id_module_id_fk", + "tableFrom": "grade", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module_assessment": { + "name": "module_assessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exam'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "module_assessment_moduleId_idx": { + "name": "module_assessment_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "module_assessment_module_id_module_id_fk": { + "name": "module_assessment_module_id_module_id_fk", + "tableFrom": "module_assessment", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "module_assessment_module_id_unique": { + "name": "module_assessment_module_id_unique", + "nullsNotDistinct": false, + "columns": [ + "module_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module_contact": { + "name": "module_contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "module_contact_moduleId_idx": { + "name": "module_contact_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "module_contact_module_id_module_id_fk": { + "name": "module_contact_module_id_module_id_fk", + "tableFrom": "module_contact", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.semester": { + "name": "semester", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "semester_programId_idx": { + "name": "semester_programId_idx", + "columns": [ + { + "expression": "program_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "semester_program_id_degree_program_id_fk": { + "name": "semester_program_id_degree_program_id_fk", + "tableFrom": "semester", + "tableTo": "degree_program", + "columnsFrom": [ + "program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event": { + "name": "event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_offsets": { + "name": "reminder_offsets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "recurrence_until": { + "name": "recurrence_until", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "recurrence_weekdays": { + "name": "recurrence_weekdays", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recurrence_interval": { + "name": "recurrence_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_userId_idx": { + "name": "event_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "event_startsAt_idx": { + "name": "event_startsAt_idx", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_user_id_user_id_fk": { + "name": "event_user_id_user_id_fk", + "tableFrom": "event", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_module_id_module_id_fk": { + "name": "event_module_id_module_id_fk", + "tableFrom": "event", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module": { + "name": "module", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "semester_id": { + "name": "semester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ects": { + "name": "ects", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "instructor": { + "name": "instructor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exam_type": { + "name": "exam_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "is_thesis": { + "name": "is_thesis", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "pass_fail": { + "name": "pass_fail", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bonus_type": { + "name": "bonus_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "bonus_value": { + "name": "bonus_value", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "bonus_min_avg_percent": { + "name": "bonus_min_avg_percent", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "bonus_min_completed_share": { + "name": "bonus_min_completed_share", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "module_semesterId_idx": { + "name": "module_semesterId_idx", + "columns": [ + { + "expression": "semester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "module_semester_id_semester_id_fk": { + "name": "module_semester_id_semester_id_fk", + "tableFrom": "module", + "tableTo": "semester", + "columnsFrom": [ + "semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_prefs": { + "name": "user_prefs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ics_token": { + "name": "ics_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_program_id": { + "name": "active_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_semester_id": { + "name": "active_semester_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_model": { + "name": "preferred_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weekly_goal_minutes": { + "name": "weekly_goal_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_prefs_user_id_user_id_fk": { + "name": "user_prefs_user_id_user_id_fk", + "tableFrom": "user_prefs", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_prefs_active_program_id_degree_program_id_fk": { + "name": "user_prefs_active_program_id_degree_program_id_fk", + "tableFrom": "user_prefs", + "tableTo": "degree_program", + "columnsFrom": [ + "active_program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_prefs_active_semester_id_semester_id_fk": { + "name": "user_prefs_active_semester_id_semester_id_fk", + "tableFrom": "user_prefs", + "tableTo": "semester", + "columnsFrom": [ + "active_semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_prefs_ics_token_unique": { + "name": "user_prefs_ics_token_unique", + "nullsNotDistinct": false, + "columns": [ + "ics_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_content": { + "name": "text_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_storage_path": { + "name": "text_storage_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "char_count": { + "name": "char_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extraction_status": { + "name": "extraction_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "extraction_error": { + "name": "extraction_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chunks_total": { + "name": "chunks_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "chunks_embedded": { + "name": "chunks_embedded", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "material_userId_idx": { + "name": "material_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_moduleId_idx": { + "name": "material_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folderId_idx": { + "name": "material_folderId_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_contentHash_idx": { + "name": "material_contentHash_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "material_user_id_user_id_fk": { + "name": "material_user_id_user_id_fk", + "tableFrom": "material", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_module_id_module_id_fk": { + "name": "material_module_id_module_id_fk", + "tableFrom": "material", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_folder_id_material_folder_id_fk": { + "name": "material_folder_id_material_folder_id_fk", + "tableFrom": "material", + "tableTo": "material_folder", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material_annotation": { + "name": "material_annotation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rect": { + "name": "rect", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'yellow'" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "material_annotation_material_idx": { + "name": "material_annotation_material_idx", + "columns": [ + { + "expression": "material_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "material_annotation_material_id_material_id_fk": { + "name": "material_annotation_material_id_material_id_fk", + "tableFrom": "material_annotation", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_annotation_user_id_user_id_fk": { + "name": "material_annotation_user_id_user_id_fk", + "tableFrom": "material_annotation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material_folder": { + "name": "material_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "material_folder_userId_idx": { + "name": "material_folder_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folder_moduleId_idx": { + "name": "material_folder_moduleId_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folder_parentId_idx": { + "name": "material_folder_parentId_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_folder_sibling_uniq": { + "name": "material_folder_sibling_uniq", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "material_folder_user_id_user_id_fk": { + "name": "material_folder_user_id_user_id_fk", + "tableFrom": "material_folder", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_folder_module_id_module_id_fk": { + "name": "material_folder_module_id_module_id_fk", + "tableFrom": "material_folder", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_folder_parent_id_material_folder_id_fk": { + "name": "material_folder_parent_id_material_folder_id_fk", + "tableFrom": "material_folder", + "tableTo": "material_folder", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material_chunk": { + "name": "material_chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "parent_chunk_id": { + "name": "parent_chunk_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contextual_header": { + "name": "contextual_header", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('german', \"material_chunk\".\"content\")", + "type": "stored" + } + } + }, + "indexes": { + "material_chunk_materialId_idx": { + "name": "material_chunk_materialId_idx", + "columns": [ + { + "expression": "material_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_chunk_material_level_idx": { + "name": "material_chunk_material_level_idx", + "columns": [ + { + "expression": "material_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "material_chunk_tsv_idx": { + "name": "material_chunk_tsv_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "material_chunk_material_id_material_id_fk": { + "name": "material_chunk_material_id_material_id_fk", + "tableFrom": "material_chunk", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "material_chunk_parent_chunk_id_material_chunk_id_fk": { + "name": "material_chunk_parent_chunk_id_material_chunk_id_fk", + "tableFrom": "material_chunk", + "tableTo": "material_chunk", + "columnsFrom": [ + "parent_chunk_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answer_log": { + "name": "answer_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correct": { + "name": "correct", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answer_log_attemptId_idx": { + "name": "answer_log_attemptId_idx", + "columns": [ + { + "expression": "attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answer_log_attempt_id_quiz_attempt_id_fk": { + "name": "answer_log_attempt_id_quiz_attempt_id_fk", + "tableFrom": "answer_log", + "tableTo": "quiz_attempt", + "columnsFrom": [ + "attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answer_log_question_id_question_id_fk": { + "name": "answer_log_question_id_question_id_fk", + "tableFrom": "answer_log", + "tableTo": "question", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deck": { + "name": "deck", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deck_userId_idx": { + "name": "deck_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_user_id_user_id_fk": { + "name": "deck_user_id_user_id_fk", + "tableFrom": "deck", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_module_id_module_id_fk": { + "name": "deck_module_id_module_id_fk", + "tableFrom": "deck", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.flashcard": { + "name": "flashcard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "front": { + "name": "front", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "back": { + "name": "back", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due": { + "name": "due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stability": { + "name": "stability", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "difficulty": { + "name": "difficulty", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "elapsed_days": { + "name": "elapsed_days", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_days": { + "name": "scheduled_days", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "learning_steps": { + "name": "learning_steps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reps": { + "name": "reps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lapses": { + "name": "lapses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_review": { + "name": "last_review", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "flashcard_deckId_idx": { + "name": "flashcard_deckId_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flashcard_due_idx": { + "name": "flashcard_due_idx", + "columns": [ + { + "expression": "due", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "flashcard_deck_id_deck_id_fk": { + "name": "flashcard_deck_id_deck_id_fk", + "tableFrom": "flashcard", + "tableTo": "deck", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question": { + "name": "question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "quiz_id": { + "name": "quiz_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "correct_index": { + "name": "correct_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reference_answer": { + "name": "reference_answer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "question_quizId_idx": { + "name": "question_quizId_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "question_quiz_id_quiz_id_fk": { + "name": "question_quiz_id_quiz_id_fk", + "tableFrom": "question", + "tableTo": "quiz", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz": { + "name": "quiz", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quiz_userId_idx": { + "name": "quiz_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_user_id_user_id_fk": { + "name": "quiz_user_id_user_id_fk", + "tableFrom": "quiz", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_module_id_module_id_fk": { + "name": "quiz_module_id_module_id_fk", + "tableFrom": "quiz", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_attempt": { + "name": "quiz_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "quiz_id": { + "name": "quiz_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "quiz_attempt_quizId_idx": { + "name": "quiz_attempt_quizId_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quiz_attempt_userId_idx": { + "name": "quiz_attempt_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_attempt_quiz_id_quiz_id_fk": { + "name": "quiz_attempt_quiz_id_quiz_id_fk", + "tableFrom": "quiz_attempt", + "tableTo": "quiz", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_attempt_user_id_user_id_fk": { + "name": "quiz_attempt_user_id_user_id_fk", + "tableFrom": "quiz_attempt", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_log": { + "name": "review_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "card_id": { + "name": "card_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_log_cardId_idx": { + "name": "review_log_cardId_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_log_userId_idx": { + "name": "review_log_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_log_card_id_flashcard_id_fk": { + "name": "review_log_card_id_flashcard_id_fk", + "tableFrom": "review_log", + "tableTo": "flashcard", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_log_user_id_user_id_fk": { + "name": "review_log_user_id_user_id_fk", + "tableFrom": "review_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.study_plan": { + "name": "study_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "study_plan_userId_idx": { + "name": "study_plan_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_plan_user_id_user_id_fk": { + "name": "study_plan_user_id_user_id_fk", + "tableFrom": "study_plan", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "study_plan_module_id_module_id_fk": { + "name": "study_plan_module_id_module_id_fk", + "tableFrom": "study_plan", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.study_plan_item": { + "name": "study_plan_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_date": { + "name": "scheduled_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "study_plan_item_planId_idx": { + "name": "study_plan_item_planId_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_plan_item_plan_id_study_plan_id_fk": { + "name": "study_plan_item_plan_id_study_plan_id_fk", + "tableFrom": "study_plan_item", + "tableTo": "study_plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thesis_milestone": { + "name": "thesis_milestone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "thesis_id": { + "name": "thesis_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thesis_milestone_thesisId_idx": { + "name": "thesis_milestone_thesisId_idx", + "columns": [ + { + "expression": "thesis_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thesis_milestone_thesis_id_thesis_project_id_fk": { + "name": "thesis_milestone_thesis_id_thesis_project_id_fk", + "tableFrom": "thesis_milestone", + "tableTo": "thesis_project", + "columnsFrom": [ + "thesis_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thesis_project": { + "name": "thesis_project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "semester_id": { + "name": "semester_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thesis_type": { + "name": "thesis_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'topic'" + }, + "research_question": { + "name": "research_question", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outline": { + "name": "outline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "superseded_by_id": { + "name": "superseded_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thesis_project_userId_idx": { + "name": "thesis_project_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "thesis_active_per_program_uq": { + "name": "thesis_active_per_program_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "program_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"thesis_project\".\"superseded_by_id\" is null and \"thesis_project\".\"program_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thesis_project_user_id_user_id_fk": { + "name": "thesis_project_user_id_user_id_fk", + "tableFrom": "thesis_project", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thesis_project_program_id_degree_program_id_fk": { + "name": "thesis_project_program_id_degree_program_id_fk", + "tableFrom": "thesis_project", + "tableTo": "degree_program", + "columnsFrom": [ + "program_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thesis_project_semester_id_semester_id_fk": { + "name": "thesis_project_semester_id_semester_id_fk", + "tableFrom": "thesis_project", + "tableTo": "semester", + "columnsFrom": [ + "semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "thesis_project_superseded_by_id_thesis_project_id_fk": { + "name": "thesis_project_superseded_by_id_thesis_project_id_fk", + "tableFrom": "thesis_project", + "tableTo": "thesis_project", + "columnsFrom": [ + "superseded_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_prefs": { + "name": "notification_prefs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email_reminders": { + "name": "email_reminders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "push_reminders": { + "name": "push_reminders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "channels": { + "name": "channels", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_prefs_user_id_user_id_fk": { + "name": "notification_prefs_user_id_user_id_fk", + "tableFrom": "notification_prefs", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_sent": { + "name": "notification_sent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_sent_user_id_user_id_fk": { + "name": "notification_sent_user_id_user_id_fk", + "tableFrom": "notification_sent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_sent_user_key": { + "name": "notification_sent_user_key", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_subscription": { + "name": "push_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_subscription_userId_idx": { + "name": "push_subscription_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "push_subscription_endpoint_unique": { + "name": "push_subscription_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "endpoint" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_sent": { + "name": "reminder_sent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "offset_minutes": { + "name": "offset_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurrence_date": { + "name": "occurrence_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_sent_event_id_event_id_fk": { + "name": "reminder_sent_event_id_event_id_fk", + "tableFrom": "reminder_sent", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reminder_sent_event_offset_occurrence": { + "name": "reminder_sent_event_offset_occurrence", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "offset_minutes", + "occurrence_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite": { + "name": "invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "used_count": { + "name": "used_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invite_created_by_user_id_fk": { + "name": "invite_created_by_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.study_session": { + "name": "study_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pomodoro'" + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "study_session_userId_idx": { + "name": "study_session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "study_session_startedAt_idx": { + "name": "study_session_startedAt_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_session_user_id_user_id_fk": { + "name": "study_session_user_id_user_id_fk", + "tableFrom": "study_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "study_session_module_id_module_id_fk": { + "name": "study_session_module_id_module_id_fk", + "tableFrom": "study_session", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.semester_plan": { + "name": "semester_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semester_id": { + "name": "semester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "availability": { + "name": "availability", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "semester_plan_user_idx": { + "name": "semester_plan_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "semester_plan_user_id_user_id_fk": { + "name": "semester_plan_user_id_user_id_fk", + "tableFrom": "semester_plan", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "semester_plan_semester_id_semester_id_fk": { + "name": "semester_plan_semester_id_semester_id_fk", + "tableFrom": "semester_plan", + "tableTo": "semester", + "columnsFrom": [ + "semester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "semester_plan_semester_id_unique": { + "name": "semester_plan_semester_id_unique", + "nullsNotDistinct": false, + "columns": [ + "semester_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.semester_plan_item": { + "name": "semester_plan_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_id": { + "name": "assignment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'study'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60 + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "semester_plan_item_plan_idx": { + "name": "semester_plan_item_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "semester_plan_item_plan_id_semester_plan_id_fk": { + "name": "semester_plan_item_plan_id_semester_plan_id_fk", + "tableFrom": "semester_plan_item", + "tableTo": "semester_plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "semester_plan_item_module_id_module_id_fk": { + "name": "semester_plan_item_module_id_module_id_fk", + "tableFrom": "semester_plan_item", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "semester_plan_item_assignment_id_assignment_id_fk": { + "name": "semester_plan_item_assignment_id_assignment_id_fk", + "tableFrom": "semester_plan_item", + "tableTo": "assignment", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.module_outline": { + "name": "module_outline", + "schema": "", + "columns": { + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "topic_count": { + "name": "topic_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "module_outline_module_id_module_id_fk": { + "name": "module_outline_module_id_module_id_fk", + "tableFrom": "module_outline", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "module_outline_user_id_user_id_fk": { + "name": "module_outline_user_id_user_id_fk", + "tableFrom": "module_outline", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outline_topic": { + "name": "outline_topic", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "module_id": { + "name": "module_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_key": { + "name": "title_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_material_ids": { + "name": "source_material_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outline_topic_module_idx": { + "name": "outline_topic_module_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outline_topic_module_version_idx": { + "name": "outline_topic_module_version_idx", + "columns": [ + { + "expression": "module_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outline_topic_module_id_module_id_fk": { + "name": "outline_topic_module_id_module_id_fk", + "tableFrom": "outline_topic", + "tableTo": "module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "outline_topic_user_id_user_id_fk": { + "name": "outline_topic_user_id_user_id_fk", + "tableFrom": "outline_topic", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "outline_topic_parent_id_outline_topic_id_fk": { + "name": "outline_topic_parent_id_outline_topic_id_fk", + "tableFrom": "outline_topic", + "tableTo": "outline_topic", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d7e1d49..9830c70 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -253,6 +253,20 @@ "when": 1784359449655, "tag": "0035_fair_killmonger", "breakpoints": true + }, + { + "idx": 36, + "version": "7", + "when": 1784365495581, + "tag": "0036_motionless_rick_jones", + "breakpoints": true + }, + { + "idx": 37, + "version": "7", + "when": 1784365763129, + "tag": "0037_typical_overlord", + "breakpoints": true } ] } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..53c4114 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,8 @@ const eslintConfig = defineConfig([ "out/**", "build/**", "next-env.d.ts", + // Generated at build time by scripts/copy-pdf-worker.mjs (minified vendor code). + "public/pdf.worker.min.mjs", ]), ]); diff --git a/messages/de.json b/messages/de.json index 50130a4..e54dea3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -123,7 +123,8 @@ "unban": "Entsperren", "delete": "Löschen", "deleteConfirm": "Benutzer und alle zugehörigen Daten unwiderruflich löschen?", - "updated": "Benutzer aktualisiert." + "updated": "Benutzer aktualisiert.", + "storageUnwritable": "Der Datei-Speicher ist nicht beschreibbar — Uploads schlagen fehl. Prüfe das Upload-Volume (UPLOAD_DIR) und dessen Berechtigungen." }, "auth": { "title": "Anmeldung & SSO", @@ -194,7 +195,25 @@ "annStatusFailed": "Fehlgeschlagen", "annRebuild": "Index (neu) aufbauen", "annRebuilding": "Index-Aufbau gestartet – läuft im Hintergrund.", - "annNeedEmbedding": "Zuerst ein Standard-Embedding-Modell konfigurieren." + "annNeedEmbedding": "Zuerst ein Standard-Embedding-Modell konfigurieren.", + "reembedTitle": "Embedding-Backfill", + "reembedDescription": "Nach einem Wechsel des Embedding-Modells müssen bestehende Materialien neu eingebettet werden — sonst findet die AI-Suche sie nicht mehr.", + "reembedStaleWarning": "{count} Material(ien) sind mit einem anderen Modell eingebettet und derzeit nicht per Vektorsuche auffindbar.", + "reembedRunning": "Re-Embedding läuft… {done}/{total}", + "reembedDone": "Abgeschlossen: {done} neu eingebettet, {failed} fehlgeschlagen.", + "reembedFailed": "Fehlgeschlagen", + "reembedIdle": "Kein Backfill nötig oder noch nie gelaufen.", + "reembedStart": "Jetzt neu einbetten", + "reembedStarted": "Re-Embedding gestartet.", + "usageByFeature": "Nutzung nach Feature & Modell (30 Tage)", + "usageFeature": "Feature", + "usageModel": "Modell", + "usageCalls": "Aufrufe", + "testConnection": "Verbindung testen", + "testOk": "Verbindung erfolgreich — der API-Key funktioniert.", + "testFailed": "Verbindungstest fehlgeschlagen: {error}", + "rerank": "LLM-Reranking der Suche", + "rerankHint": "Sortiert Suchtreffer vor der Nutzung mit dem Standard-Modell neu — präzisere Ergebnisse, kostet einen kleinen Zusatzaufruf pro Suche." }, "email": { "title": "E-Mail (SMTP)", @@ -354,7 +373,9 @@ "input": "Input", "output": "Output", "limit": "Limit", - "unlimited": "unbegrenzt" + "unlimited": "unbegrenzt", + "utilization": "Auslastung (dieser Monat)", + "byFeature": "Nach Feature (dieser Monat)" }, "security": { "title": "Sicherheit", @@ -764,11 +785,16 @@ "download": "Herunterladen", "open": "Öffnen", "viewer": { - "unsupported": "Vorschau für diesen Dateityp nicht verfügbar — bitte herunterladen." + "unsupported": "Vorschau für diesen Dateityp nicht verfügbar — bitte herunterladen.", + "fileMissing": "Datei nicht mehr vorhanden", + "fileMissingHint": "Der Eintrag existiert, aber die Datei fehlt auf dem Server (z. B. durch ein früheres Redeploy ohne persistentes Upload-Volume verloren). Bitte lade die Datei neu hoch." }, "annotator": { "hint": "Auf der Seite ziehen zum Markieren; Markierung anklicken für Notiz oder Löschen.", - "notePlaceholder": "Notiz zu dieser Markierung …" + "notePlaceholder": "Notiz zu dieser Markierung …", + "fileMissing": "Die Datei fehlt auf dem Server — vermutlich ging sie bei einem früheren Redeploy verloren. Bitte lade sie neu hoch.", + "explain": "Erklären", + "explainEmpty": "In der Markierung wurde kein Text gefunden." }, "stats": { "files": "{count, plural, =1 {# Datei} other {# Dateien}}", @@ -791,7 +817,22 @@ "uploadingCount": "Lädt hoch … {done}/{total}", "uploadHint": "Wähle Dateien oder einen ganzen Ordner. Ordner behalten ihre Struktur; .zip-Archive werden automatisch entpackt.", "chooseFiles": "Dateien wählen", - "chooseFolder": "Ordner wählen" + "chooseFolder": "Ordner wählen", + "processing": { + "pending": "Wartet", + "extracting": "Extrahiert", + "embedding": "Indexiert", + "summarizing": "Fasst zusammen", + "failed": "Verarbeitung fehlgeschlagen", + "skipped": "Nicht verarbeitet", + "skippedHint": "Aus dieser Datei konnte kein Text gewonnen werden.", + "retry": "Neu verarbeiten" + }, + "notIndexedHint": "Es ist kein Embedding-Modell konfiguriert — Materialien werden nicht für die AI-Suche indexiert. Ein Admin kann das unter Admin → AI einrichten.", + "uploadRateLimited": "Zu viele Uploads in kurzer Zeit — bitte kurz warten.", + "uploadNetworkError": "Netzwerkfehler beim Upload — Verbindung prüfen und erneut versuchen.", + "uploadServerError": "{name}: Serverfehler beim Speichern. Prüfe im Admin-Bereich, ob der Datei-Speicher beschreibbar ist.", + "askDocument": "Im Chat fragen" }, "search": { "modules": "Module", @@ -878,7 +919,18 @@ "history": "Verlauf" }, "errorGeneric": "Die Anfrage ist fehlgeschlagen. Bitte versuche es erneut.", - "errorLimit": "Dein monatliches AI-Kontingent ist aufgebraucht." + "errorLimit": "Dein monatliches AI-Kontingent ist aufgebraucht.", + "errorAuth": "Der AI-Anbieter hat die Anfrage abgelehnt (API-Key ungültig oder abgelaufen). Bitte prüfe die AI-Einstellungen.", + "sources": "Quellen", + "regenerate": "Antwort neu generieren", + "stop": "Stoppen", + "modeLabel": "Modus", + "voice": { + "start": "Spracheingabe starten", + "stop": "Aufnahme beenden", + "empty": "Keine Sprache erkannt.", + "micDenied": "Mikrofonzugriff wurde verweigert." + } }, "learn": { "title": "Lernen", @@ -1193,7 +1245,10 @@ "analyzeTitle": "KI-Auswertung", "analyzing": "Analysiere deine Lernhistorie …", "capped": "Session mit {shown} — insgesamt {total} fällig", - "swipeHint": "Nach rechts wischen = Gut, nach links = Nochmal" + "swipeHint": "Nach rechts wischen = Gut, nach links = Nochmal", + "weakTopicsQuiz": "Quiz aus Schwachthemen", + "weakTopicsDeck": "Deck aus Schwachthemen", + "weakTopicsDeckName": "Schwachthemen-Wiederholung" }, "moduleTabs": { "overview": "Übersicht", diff --git a/messages/en.json b/messages/en.json index bd5fa51..a526be9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -123,7 +123,8 @@ "unban": "Unban", "delete": "Delete", "deleteConfirm": "Permanently delete this user and all associated data?", - "updated": "User updated." + "updated": "User updated.", + "storageUnwritable": "File storage is not writable — uploads will fail. Check the upload volume (UPLOAD_DIR) and its permissions." }, "auth": { "title": "Sign-in & SSO", @@ -194,7 +195,25 @@ "annStatusFailed": "Failed", "annRebuild": "Build / rebuild index", "annRebuilding": "Index build started — running in the background.", - "annNeedEmbedding": "Configure a default embedding model first." + "annNeedEmbedding": "Configure a default embedding model first.", + "reembedTitle": "Embedding backfill", + "reembedDescription": "After changing the embedding model, existing materials must be re-embedded — otherwise AI search can no longer find them.", + "reembedStaleWarning": "{count} material(s) are embedded with a different model and currently invisible to vector search.", + "reembedRunning": "Re-embedding… {done}/{total}", + "reembedDone": "Finished: {done} re-embedded, {failed} failed.", + "reembedFailed": "Failed", + "reembedIdle": "No backfill needed or never run.", + "reembedStart": "Re-embed now", + "reembedStarted": "Re-embedding started.", + "usageByFeature": "Usage by feature & model (30 days)", + "usageFeature": "Feature", + "usageModel": "Model", + "usageCalls": "Calls", + "testConnection": "Test connection", + "testOk": "Connection successful — the API key works.", + "testFailed": "Connection test failed: {error}", + "rerank": "LLM re-ranking for search", + "rerankHint": "Re-orders search hits with the default model before use — more precise results at the cost of one small extra call per search." }, "email": { "title": "Email (SMTP)", @@ -354,7 +373,9 @@ "input": "Input", "output": "Output", "limit": "Limit", - "unlimited": "unlimited" + "unlimited": "unlimited", + "utilization": "Utilization (this month)", + "byFeature": "By feature (this month)" }, "security": { "title": "Security", @@ -764,11 +785,16 @@ "download": "Download", "open": "Open", "viewer": { - "unsupported": "No preview available for this file type — please download." + "unsupported": "No preview available for this file type — please download.", + "fileMissing": "File no longer available", + "fileMissingHint": "The entry exists, but the file is missing on the server (e.g. lost in an earlier redeploy without a persistent upload volume). Please upload it again." }, "annotator": { "hint": "Drag on a page to highlight; click a highlight to add a note or delete it.", - "notePlaceholder": "Note for this highlight …" + "notePlaceholder": "Note for this highlight …", + "fileMissing": "The file is missing on the server — it was probably lost in an earlier redeploy. Please upload it again.", + "explain": "Explain", + "explainEmpty": "No text found in the highlighted area." }, "stats": { "files": "{count, plural, =1 {# file} other {# files}}", @@ -791,7 +817,22 @@ "uploadingCount": "Uploading … {done}/{total}", "uploadHint": "Choose files or a whole folder. Folders keep their structure; .zip archives are unpacked automatically.", "chooseFiles": "Choose files", - "chooseFolder": "Choose folder" + "chooseFolder": "Choose folder", + "processing": { + "pending": "Queued", + "extracting": "Extracting", + "embedding": "Indexing", + "summarizing": "Summarizing", + "failed": "Processing failed", + "skipped": "Not processed", + "skippedHint": "No text could be extracted from this file.", + "retry": "Process again" + }, + "notIndexedHint": "No embedding model is configured — materials are not indexed for AI search. An admin can set this up under Admin → AI.", + "uploadRateLimited": "Too many uploads in a short time — please wait a moment.", + "uploadNetworkError": "Network error during upload — check your connection and try again.", + "uploadServerError": "{name}: Server error while saving. Check in the admin area whether file storage is writable.", + "askDocument": "Ask in chat" }, "search": { "modules": "Modules", @@ -878,7 +919,18 @@ "history": "History" }, "errorGeneric": "The request failed. Please try again.", - "errorLimit": "Your monthly AI quota is used up." + "errorLimit": "Your monthly AI quota is used up.", + "errorAuth": "The AI provider rejected the request (API key invalid or expired). Please check the AI settings.", + "sources": "Sources", + "regenerate": "Regenerate answer", + "stop": "Stop", + "modeLabel": "Mode", + "voice": { + "start": "Start voice input", + "stop": "Stop recording", + "empty": "No speech detected.", + "micDenied": "Microphone access was denied." + } }, "learn": { "title": "Learn", @@ -1193,7 +1245,10 @@ "analyzeTitle": "AI analysis", "analyzing": "Analyzing your learning history …", "capped": "session of {shown} — {total} due in total", - "swipeHint": "Swipe right = good, left = again" + "swipeHint": "Swipe right = good, left = again", + "weakTopicsQuiz": "Quiz from weak topics", + "weakTopicsDeck": "Deck from weak topics", + "weakTopicsDeckName": "Weak-topics review" }, "moduleTabs": { "overview": "Overview", diff --git a/package.json b/package.json index a49c90e..a2f59b3 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "study-helper", - "version": "1.0.15", + "version": "1.1.0", "private": true, "scripts": { + "predev": "node scripts/copy-pdf-worker.mjs", "dev": "next dev", + "prebuild": "node scripts/copy-pdf-worker.mjs", "build": "next build", "start": "next start", "worker": "NODE_OPTIONS=--conditions=react-server tsx src/worker.ts", diff --git a/scripts/copy-pdf-worker.mjs b/scripts/copy-pdf-worker.mjs new file mode 100644 index 0000000..b6ad6dd --- /dev/null +++ b/scripts/copy-pdf-worker.mjs @@ -0,0 +1,12 @@ +// Copies the pdf.js worker into public/ so the annotator can load it from a +// stable same-origin URL. Bundler-independent (Turbopack's handling of bare +// specifiers inside `new URL(..., import.meta.url)` is not guaranteed), and +// the copy always matches the installed pdfjs-dist API version. Runs via the +// predev/prebuild npm hooks. +import { copyFile } from "node:fs/promises" +import { createRequire } from "node:module" + +const require = createRequire(import.meta.url) +const src = require.resolve("pdfjs-dist/build/pdf.worker.min.mjs") +await copyFile(src, "public/pdf.worker.min.mjs") +console.log("[copy-pdf-worker] public/pdf.worker.min.mjs updated") diff --git a/src/app/[locale]/(app)/admin/actions.ts b/src/app/[locale]/(app)/admin/actions.ts index c82d6ba..d6d5056 100644 --- a/src/app/[locale]/(app)/admin/actions.ts +++ b/src/app/[locale]/(app)/admin/actions.ts @@ -110,7 +110,24 @@ export async function saveUploads(value: unknown) { export async function saveAiSettings(value: unknown) { await requireAdmin() - await setSetting("ai", aiSettingsSchema.parse(value)) + const { getSetting } = await import("@/lib/settings") + const previous = await getSetting("ai") + const next = aiSettingsSchema.parse(value) + await setSetting("ai", next) + // A changed embedding model orphans every existing chunk (vector search + // filters on the active model ref) — kick off the re-embed backfill so + // search doesn't silently go dark. + if ( + next.defaultEmbeddingModel && + previous?.defaultEmbeddingModel !== next.defaultEmbeddingModel + ) { + try { + const { enqueueReembedMaterials } = await import("@/lib/jobs") + await enqueueReembedMaterials() + } catch (error) { + console.error("[admin] failed to enqueue re-embed after model change", error) + } + } revalidatePath("/", "layout") return { ok: true as const } } @@ -129,3 +146,34 @@ export async function getAnnStatus() { const { getSetting } = await import("@/lib/settings") return (await getSetting("ai.ann")) ?? { status: "idle" as const } } + +/** Fires a one-token test request against a (possibly unsaved) provider config. */ +export async function testAiProvider(value: unknown) { + await requireAdmin() + const { aiProviderSchema } = await import("@/lib/settings") + const provider = aiProviderSchema.parse(value) + const { testProviderConnection } = await import("@/lib/ai/registry") + return testProviderConnection(provider, provider.apiKey) +} + +/** Starts the re-embed backfill for materials embedded with a stale model. */ +export async function startReembed() { + await requireAdmin() + const { enqueueReembedMaterials } = await import("@/lib/jobs") + await enqueueReembedMaterials() + return { ok: true as const } +} + +/** Re-embed progress + live count of stale materials, for the admin UI. */ +export async function getReembedStatus() { + await requireAdmin() + const { getSetting } = await import("@/lib/settings") + const [state, ai] = await Promise.all([getSetting("ai.reembed"), getSetting("ai")]) + const ref = ai?.defaultEmbeddingModel + let staleCount = 0 + if (ref) { + const { countStaleMaterials } = await import("@/lib/ai/reembed") + staleCount = await countStaleMaterials(ref) + } + return { state: state ?? { status: "idle" as const }, staleCount, embeddingModel: ref ?? null } +} diff --git a/src/app/[locale]/(app)/admin/ai/page.tsx b/src/app/[locale]/(app)/admin/ai/page.tsx index 66fcb41..78501ab 100644 --- a/src/app/[locale]/(app)/admin/ai/page.tsx +++ b/src/app/[locale]/(app)/admin/ai/page.tsx @@ -3,39 +3,63 @@ import { getTranslations } from "next-intl/server" import { db } from "@/db" import { aiUsageLog, user } from "@/db/schema" import { requireAdmin } from "@/lib/auth/session" +import { countStaleMaterials } from "@/lib/ai/reembed" import { getSetting } from "@/lib/settings" import { daysAgo } from "@/lib/utils" import { AiSettingsForm } from "@/components/admin/ai-settings-form" import { AnnIndexCard } from "@/components/admin/ann-index-card" +import { ReembedCard } from "@/components/admin/reembed-card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" export default async function AdminAiPage() { await requireAdmin() const t = await getTranslations("admin.ai") const ai = await getSetting("ai") - const ann = await getSetting("ai.ann") + const embeddingRef = ai?.defaultEmbeddingModel const thirtyDaysAgo = daysAgo(30) - const usage = await db - .select({ - userName: user.name, - inputTokens: sum(aiUsageLog.inputTokens).mapWith(Number), - outputTokens: sum(aiUsageLog.outputTokens).mapWith(Number), - }) - .from(aiUsageLog) - .innerJoin(user, eq(aiUsageLog.userId, user.id)) - .where(gte(aiUsageLog.createdAt, thirtyDaysAgo)) - .groupBy(user.id, user.name) - .orderBy(desc(sql`sum(${aiUsageLog.inputTokens} + ${aiUsageLog.outputTokens})`)) + const [ann, reembed, staleCount, usage, usageByFeature] = await Promise.all([ + getSetting("ai.ann"), + getSetting("ai.reembed"), + embeddingRef ? countStaleMaterials(embeddingRef) : Promise.resolve(0), + db + .select({ + userName: user.name, + inputTokens: sum(aiUsageLog.inputTokens).mapWith(Number), + outputTokens: sum(aiUsageLog.outputTokens).mapWith(Number), + }) + .from(aiUsageLog) + .innerJoin(user, eq(aiUsageLog.userId, user.id)) + .where(gte(aiUsageLog.createdAt, thirtyDaysAgo)) + .groupBy(user.id, user.name) + .orderBy(desc(sql`sum(${aiUsageLog.inputTokens} + ${aiUsageLog.outputTokens})`)), + db + .select({ + feature: aiUsageLog.feature, + model: aiUsageLog.model, + calls: sql`count(*)::int`, + inputTokens: sum(aiUsageLog.inputTokens).mapWith(Number), + outputTokens: sum(aiUsageLog.outputTokens).mapWith(Number), + }) + .from(aiUsageLog) + .where(gte(aiUsageLog.createdAt, thirtyDaysAgo)) + .groupBy(aiUsageLog.feature, aiUsageLog.model) + .orderBy(desc(sql`sum(${aiUsageLog.inputTokens} + ${aiUsageLog.outputTokens})`)), + ]) return (
+ @@ -70,6 +94,47 @@ export default async function AdminAiPage() { )} + {usageByFeature.length > 0 && ( + + + {t("usageByFeature")} + + +
+ + + + + + + + + + + + {usageByFeature.map((row) => ( + + + + + + + + ))} + +
{t("usageFeature")}{t("usageModel")}{t("usageCalls")}{t("usageInput")}{t("usageOutput")}
{row.feature} + {row.model} + + {row.calls.toLocaleString()} + + {row.inputTokens.toLocaleString()} + + {row.outputTokens.toLocaleString()} +
+
+
+
+ )}
) } diff --git a/src/app/[locale]/(app)/admin/page.tsx b/src/app/[locale]/(app)/admin/page.tsx index 67cb7d8..f38b3f3 100644 --- a/src/app/[locale]/(app)/admin/page.tsx +++ b/src/app/[locale]/(app)/admin/page.tsx @@ -1,8 +1,10 @@ import { desc } from "drizzle-orm" +import { AlertTriangle } from "lucide-react" import { getTranslations, getFormatter } from "next-intl/server" import { db } from "@/db" import { user } from "@/db/schema" import { requireAdmin } from "@/lib/auth/session" +import { deleteFile, saveFile } from "@/lib/storage" import { UserActions } from "@/components/admin/user-actions" import { Badge } from "@/components/ui/badge" import { @@ -13,13 +15,34 @@ import { CardTitle, } from "@/components/ui/card" +/** Write-probe of the storage backend (same check as /api/health). */ +async function storageWritable(): Promise { + try { + const path = await saveFile("_health", `probe-${crypto.randomUUID()}.txt`, Buffer.from("ok")) + await deleteFile(path) + return true + } catch { + return false + } +} + export default async function AdminUsersPage() { const session = await requireAdmin() const t = await getTranslations("admin.users") const format = await getFormatter() - const users = await db.select().from(user).orderBy(desc(user.createdAt)) + const [users, writable] = await Promise.all([ + db.select().from(user).orderBy(desc(user.createdAt)), + storageWritable(), + ]) return ( + <> + {!writable && ( +
+ +

{t("storageUnwritable")}

+
+ )} {t("title")} @@ -71,5 +94,6 @@ export default async function AdminUsersPage() { + ) } diff --git a/src/app/[locale]/(app)/ai/[conversationId]/page.tsx b/src/app/[locale]/(app)/ai/[conversationId]/page.tsx index de48494..46365c5 100644 --- a/src/app/[locale]/(app)/ai/[conversationId]/page.tsx +++ b/src/app/[locale]/(app)/ai/[conversationId]/page.tsx @@ -53,6 +53,7 @@ export default async function ConversationPage({ title: conversation.title, moduleId: conversation.moduleId, moduleName: conversation.module?.name ?? null, + mode: conversation.mode, }} initialMessages={initialMessages} /> diff --git a/src/app/[locale]/(app)/ai/actions.ts b/src/app/[locale]/(app)/ai/actions.ts index ff72fb3..093fbea 100644 --- a/src/app/[locale]/(app)/ai/actions.ts +++ b/src/app/[locale]/(app)/ai/actions.ts @@ -7,21 +7,123 @@ import { aiConversation } from "@/db/schema" import { requireSession } from "@/lib/auth/session" import { ownModule } from "@/lib/studies/access" -export async function createConversation(moduleId?: string | null, mode?: string) { +export async function createConversation( + moduleId?: string | null, + mode?: string, + materialId?: string | null +) { const session = await requireSession() if (moduleId) await ownModule(moduleId, session.user.id) + let scopedModuleId = moduleId ?? null + if (materialId) { + // "Chat with this document": verify ownership and inherit the module. + const { material } = await import("@/db/schema") + const row = await db.query.material.findFirst({ + where: and(eq(material.id, materialId), eq(material.userId, session.user.id)), + columns: { id: true, moduleId: true }, + }) + if (!row) throw new Error("Not found") + scopedModuleId = scopedModuleId ?? row.moduleId + } const { CHAT_MODES } = await import("@/lib/ai/modes") const safeMode = CHAT_MODES.includes(mode as (typeof CHAT_MODES)[number]) ? (mode as (typeof CHAT_MODES)[number]) : "general" const [created] = await db .insert(aiConversation) - .values({ userId: session.user.id, moduleId: moduleId ?? null, mode: safeMode }) + .values({ + userId: session.user.id, + moduleId: scopedModuleId, + materialId: materialId ?? null, + mode: safeMode, + }) .returning({ id: aiConversation.id }) revalidatePath("/ai") return { ok: true as const, id: created.id } } +/** Changes the tutor mode of an existing conversation. */ +export async function updateConversationMode(conversationId: string, mode: string) { + const session = await requireSession() + const { CHAT_MODES } = await import("@/lib/ai/modes") + if (!CHAT_MODES.includes(mode as (typeof CHAT_MODES)[number])) throw new Error("Unknown mode") + await db + .update(aiConversation) + .set({ mode }) + .where( + and(eq(aiConversation.id, conversationId), eq(aiConversation.userId, session.user.id)) + ) + revalidatePath("/ai") + return { ok: true as const } +} + +/** Whether voice input (speech-to-text) is available for this user. */ +export async function isVoiceInputAvailable(): Promise { + const session = await requireSession() + const { getTranscriptionModel } = await import("@/lib/ai/registry") + return Boolean(await getTranscriptionModel(session.user.id)) +} + +/** Transcribes a short voice recording from the chat input (max 10 MB). */ +export async function transcribeVoiceInput(formData: FormData) { + const session = await requireSession() + const file = formData.get("audio") + if (!(file instanceof Blob)) throw new Error("No audio") + if (file.size > 10 * 1024 * 1024) throw new Error("Recording too large") + const { assertWithinLimit } = await import("@/lib/ai/usage") + await assertWithinLimit(session.user.id) + const { transcribeAudioBuffer } = await import("@/lib/ai/media") + const text = await transcribeAudioBuffer( + new Uint8Array(await file.arrayBuffer()), + session.user.id + ) + return { text } +} + +/** Explains a highlighted passage from one of the user's materials. */ +export async function explainSnippet(materialId: string, snippet: string, context?: string) { + const session = await requireSession() + const cleanSnippet = snippet.trim().slice(0, 4000) + if (!cleanSnippet) throw new Error("Nothing selected") + const { material } = await import("@/db/schema") + const row = await db.query.material.findFirst({ + where: and(eq(material.id, materialId), eq(material.userId, session.user.id)), + columns: { id: true, name: true }, + }) + if (!row) throw new Error("Not found") + const { assertWithinLimit } = await import("@/lib/ai/usage") + await assertWithinLimit(session.user.id) + const { resolveModelForUser, getLanguageModel } = await import("@/lib/ai/registry") + const modelRef = await resolveModelForUser(session.user.id) + if (!modelRef) throw new Error("No AI model configured") + const model = await getLanguageModel(modelRef, session.user.id) + const { generateText } = await import("ai") + const { GEN_PARAMS } = await import("@/lib/ai/params") + const { runAi } = await import("@/lib/ai/run") + const { text } = await runAi( + { + userId: session.user.id, + model: modelRef, + feature: "explain", + entityType: "material", + entityId: row.id, + entityLabel: row.name, + }, + () => + generateText({ + model, + ...GEN_PARAMS, + maxOutputTokens: 800, + prompt: `You are a study tutor. Explain the highlighted passage from the document "${row.name}" clearly and concisely for a university student: what it means, why it matters, and any term worth defining. Use Markdown, max ~150 words, answer in the language of the passage. + +Highlighted passage: +${cleanSnippet} +${context ? `\nSurrounding page text (context only):\n${context.slice(0, 4000)}` : ""}`, + }) + ) + return { explanation: text } +} + /** Load a conversation's messages (used by the floating quick chat). Returns null if not found. */ export async function getConversationMessages(conversationId: string) { const session = await requireSession() @@ -54,6 +156,7 @@ export async function listConversations() { title: c.title, moduleName: c.module?.name ?? null, moduleId: c.moduleId, + mode: c.mode, updatedAt: c.updatedAt.toISOString(), })) } diff --git a/src/app/[locale]/(app)/learn-actions.ts b/src/app/[locale]/(app)/learn-actions.ts index 5697890..b4dc9fb 100644 --- a/src/app/[locale]/(app)/learn-actions.ts +++ b/src/app/[locale]/(app)/learn-actions.ts @@ -1,4 +1,5 @@ "use server" +import { GEN_PARAMS } from "@/lib/ai/params" import { revalidatePath } from "next/cache" import { and, eq } from "drizzle-orm" @@ -237,6 +238,7 @@ export async function generateStudyPlan(input: unknown) { () => generateObject({ model, + ...GEN_PARAMS, schema: generatedPlanSchema, prompt: `Create a realistic study plan for a university student. Today is ${today}. The exam is on ${data.examDate}. @@ -366,6 +368,7 @@ export async function analyzeProgress(moduleId: string) { () => generateText({ model, + ...GEN_PARAMS, prompt: `You are a study coach. Analyze this learning history for one university module and tell the student what to deepen next. Requirements: - Answer in the language of the quiz/flashcard content (German if mixed). @@ -377,5 +380,15 @@ Data (JSON): ${JSON.stringify(data).slice(0, 20000)}`, }) ) - return { ok: true as const, analysis: text } + // Compact weak-topic list (recent wrong questions + most-lapsed cards) so + // the UI can offer one-click "generate a review quiz/deck on my weak spots". + const wrongQuestions = [ + ...new Set( + data.quizzes.flatMap((q) => q.attempts.slice(0, 3).flatMap((a) => a.wrongQuestions)) + ), + ].slice(0, 20) + const lapsedFronts = data.problemFlashcards.slice(0, 10).map((c) => c.front) + const weakTopics = [...wrongQuestions, ...lapsedFronts].join("\n").slice(0, 1500) + + return { ok: true as const, analysis: text, weakTopics } } diff --git a/src/app/[locale]/(app)/materials-actions.ts b/src/app/[locale]/(app)/materials-actions.ts index ede0ec0..7d22f92 100644 --- a/src/app/[locale]/(app)/materials-actions.ts +++ b/src/app/[locale]/(app)/materials-actions.ts @@ -64,6 +64,22 @@ async function ownMaterial(materialId: string, userId: string) { return row } +/** Re-runs extraction/embedding for a file material (idempotent — already + * embedded chunks for the active model are skipped). */ +export async function retryMaterialProcessing(materialId: string) { + const session = await requireSession() + const row = await ownMaterial(materialId, session.user.id) + if (row.kind !== "file") throw new Error("Not a file material") + await db + .update(material) + .set({ extractionStatus: "pending", extractionError: null }) + .where(eq(material.id, materialId)) + const { enqueueEmbedMaterial } = await import("@/lib/jobs") + await enqueueEmbedMaterial(materialId) + revalidatePath("/", "layout") + return { ok: true as const } +} + export async function renameMaterial(materialId: string, name: string) { const session = await requireSession() const clean = name.trim().slice(0, 300) diff --git a/src/app/[locale]/(app)/materials/[id]/page.tsx b/src/app/[locale]/(app)/materials/[id]/page.tsx index a5c4101..952fd4b 100644 --- a/src/app/[locale]/(app)/materials/[id]/page.tsx +++ b/src/app/[locale]/(app)/materials/[id]/page.tsx @@ -6,7 +6,9 @@ import { asc } from "drizzle-orm" import { db } from "@/db" import { material, materialAnnotation } from "@/db/schema" import { requireSession } from "@/lib/auth/session" +import { fileSize } from "@/lib/storage" import { Link } from "@/i18n/navigation" +import { AskDocumentButton } from "@/components/materials/ask-document-button" import { MediaPlayer } from "@/components/materials/media-player" import { PdfAnnotator } from "@/components/materials/pdf-annotator" import { Button } from "@/components/ui/button" @@ -33,6 +35,18 @@ export default async function MaterialViewerPage({ const fileUrl = `/api/materials/${row.id}/file` const mime = row.mimeType ?? "" + // The DB row can outlive the stored file (e.g. uploads lost to a redeploy + // before the storage volume was mounted). Detect that here and explain it, + // instead of every viewer failing with its own cryptic error. + let fileAvailable = Boolean(row.storagePath) + if (fileAvailable) { + try { + await fileSize(row.storagePath!) + } catch { + fileAvailable = false + } + } + const annotations = mime === "application/pdf" ? await db.query.materialAnnotation.findMany({ @@ -52,13 +66,21 @@ export default async function MaterialViewerPage({

{row.name}

{row.module.name}

+ - {mime === "application/pdf" ? ( + {!fileAvailable ? ( + + +

{t("viewer.fileMissing")}

+

{t("viewer.fileMissingHint")}

+
+
+ ) : mime === "application/pdf" ? ( generateObject({ model, + ...GEN_PARAMS, schema: generatedQuizSchema, prompt: `Create a quiz with ${data.count} exam-style questions about: ${query}. ${data.mixed ? "Mix multiple_choice (with exactly 4 plausible options) and free_text questions (about 70/30)." : "Use only multiple_choice questions with exactly 4 plausible options."} @@ -365,11 +367,21 @@ export async function submitAttempt(input: unknown): Promise { () => generateObject({ model, + // Deterministic: a re-submitted identical answer must not flip + // between correct and incorrect. + ...GRADING_PARAMS, schema: z.object({ correct: z.boolean(), feedback: z.string() }), - prompt: `Grade this student answer. Question: "${prompt}" + prompt: `Grade this student answer against the reference. +Question: "${prompt}" Reference answer: "${reference}" Student answer: "${answer}" -Judge leniently on wording but strictly on content. Reply with correct=true/false and one sentence of feedback in the language of the question.`, + +Grading rubric: +- correct=true only if the answer contains the core facts/concepts of the reference; wording, order and minor omissions do not matter. +- correct=false if a core fact is missing, wrong, or contradicted — even when parts are right. +- An empty, off-topic or "I don't know" answer is always false. +- Ignore spelling/grammar entirely. +Reply with correct=true/false and one sentence of feedback in the language of the question.`, }) ) return object diff --git a/src/app/[locale]/(app)/settings/actions.ts b/src/app/[locale]/(app)/settings/actions.ts index 64f9598..4fc8229 100644 --- a/src/app/[locale]/(app)/settings/actions.ts +++ b/src/app/[locale]/(app)/settings/actions.ts @@ -19,7 +19,13 @@ export async function saveUserAiKey(input: unknown) { const { providerId, apiKey } = keySchema.parse(input) const { getSetting } = await import("@/lib/settings") const ai = await getSetting("ai") - if (!ai?.providers.some((p) => p.id === providerId)) throw new Error("Unknown provider") + const provider = ai?.providers.find((p) => p.id === providerId) + if (!provider) throw new Error("Unknown provider") + // Validate the key with a one-token request before storing it — a bad key + // would otherwise only surface later, deep inside a chat stream or job. + const { testProviderConnection } = await import("@/lib/ai/registry") + const test = await testProviderConnection(provider, apiKey) + if (!test.ok) throw new Error(`Key check failed: ${test.error}`) const encrypted = encrypt(apiKey) const existing = await db.query.userAiKey.findFirst({ where: and(eq(userAiKey.userId, session.user.id), eq(userAiKey.providerId, providerId)), diff --git a/src/app/[locale]/(app)/settings/page.tsx b/src/app/[locale]/(app)/settings/page.tsx index fea85ae..e78004a 100644 --- a/src/app/[locale]/(app)/settings/page.tsx +++ b/src/app/[locale]/(app)/settings/page.tsx @@ -19,9 +19,14 @@ import { Link } from "@/i18n/navigation" export default async function SettingsPage() { const session = await requireSession() const t = await getTranslations("settings") - const thirtyDaysAgo = daysAgo(30) + // Calendar-month window — the SAME window the monthly token limit uses + // (usage.ts assertWithinLimit), so the shown numbers match the enforcement. + const monthStart = new Date() + monthStart.setDate(1) + monthStart.setHours(0, 0, 0, 0) + void daysAgo - const [passkeys, ai, userKeys, usage, nPrefs, prefs, availableModels, uploads, storage] = + const [passkeys, ai, userKeys, usage, usageByFeature, nPrefs, prefs, availableModels, uploads, storage] = await Promise.all([ db .select({ id: passkey.id, name: passkey.name, createdAt: passkey.createdAt }) @@ -36,8 +41,19 @@ export default async function SettingsPage() { }) .from(aiUsageLog) .where( - and(eq(aiUsageLog.userId, session.user.id), gte(aiUsageLog.createdAt, thirtyDaysAgo)) + and(eq(aiUsageLog.userId, session.user.id), gte(aiUsageLog.createdAt, monthStart)) ), + db + .select({ + feature: aiUsageLog.feature, + inputTokens: sum(aiUsageLog.inputTokens).mapWith(Number), + outputTokens: sum(aiUsageLog.outputTokens).mapWith(Number), + }) + .from(aiUsageLog) + .where( + and(eq(aiUsageLog.userId, session.user.id), gte(aiUsageLog.createdAt, monthStart)) + ) + .groupBy(aiUsageLog.feature), db.query.notificationPrefs.findFirst({ where: eq(notificationPrefs.userId, session.user.id), }), @@ -54,6 +70,12 @@ export default async function SettingsPage() { })) const totals = usage[0] ?? { inputTokens: 0, outputTokens: 0 } const limit = ai?.monthlyTokenLimitPerUser ?? 0 + const usedTokens = (totals.inputTokens ?? 0) + (totals.outputTokens ?? 0) + const usagePct = limit > 0 ? Math.min(100, Math.round((usedTokens / limit) * 100)) : 0 + const featureRows = usageByFeature + .map((r) => ({ feature: r.feature, tokens: (r.inputTokens ?? 0) + (r.outputTokens ?? 0) })) + .filter((r) => r.tokens > 0) + .sort((a, b) => b.tokens - a.tokens) const quotaMb = uploads?.storageQuotaMbPerUser ?? 0 const quotaBytes = quotaMb * 1024 * 1024 const storagePct = @@ -156,25 +178,54 @@ export default async function SettingsPage() { {t("usage.title")} - -
-

{t("usage.input")}

-

- {(totals.inputTokens ?? 0).toLocaleString()} -

-
-
-

{t("usage.output")}

-

- {(totals.outputTokens ?? 0).toLocaleString()} -

-
-
-

{t("usage.limit")}

-

- {limit === 0 ? t("usage.unlimited") : limit.toLocaleString()} -

+ +
+
+

{t("usage.input")}

+

+ {(totals.inputTokens ?? 0).toLocaleString()} +

+
+
+

{t("usage.output")}

+

+ {(totals.outputTokens ?? 0).toLocaleString()} +

+
+
+

{t("usage.limit")}

+

+ {limit === 0 ? t("usage.unlimited") : limit.toLocaleString()} +

+
+ {limit > 0 && ( +
+
+ {t("usage.utilization")} + {usagePct}% +
+
+
= 100 ? "bg-destructive h-full" : "bg-primary h-full"} + style={{ width: `${usagePct}%` }} + /> +
+
+ )} + {featureRows.length > 0 && ( +
+

{t("usage.byFeature")}

+
    + {featureRows.map((row) => ( +
  • + {row.feature} + {row.tokens.toLocaleString()} +
  • + ))} +
+
+ )} diff --git a/src/app/[locale]/(app)/studies/[programId]/[moduleId]/materials/page.tsx b/src/app/[locale]/(app)/studies/[programId]/[moduleId]/materials/page.tsx index 0008be0..972219b 100644 --- a/src/app/[locale]/(app)/studies/[programId]/[moduleId]/materials/page.tsx +++ b/src/app/[locale]/(app)/studies/[programId]/[moduleId]/materials/page.tsx @@ -2,6 +2,7 @@ import { and, asc, desc, eq } from "drizzle-orm" import { db } from "@/db" import { material, materialFolder } from "@/db/schema" import { requireSession } from "@/lib/auth/session" +import { getSetting } from "@/lib/settings" import { ownModule } from "@/lib/studies/access" import { MaterialsBrowser } from "@/components/materials/materials-browser" @@ -14,7 +15,7 @@ export default async function ModuleMaterialsPage({ const session = await requireSession() const mod = await ownModule(moduleId, session.user.id) - const [materials, folders] = await Promise.all([ + const [materials, folders, ai] = await Promise.all([ db.query.material.findMany({ where: and(eq(material.userId, session.user.id), eq(material.moduleId, moduleId)), orderBy: [desc(material.createdAt)], @@ -23,11 +24,13 @@ export default async function ModuleMaterialsPage({ where: and(eq(materialFolder.userId, session.user.id), eq(materialFolder.moduleId, moduleId)), orderBy: [asc(materialFolder.name)], }), + getSetting("ai"), ]) return ( ({ id: m.id, kind: m.kind, @@ -37,6 +40,8 @@ export default async function ModuleMaterialsPage({ sizeBytes: m.sizeBytes, folderId: m.folderId, createdAt: m.createdAt.toISOString(), + extractionStatus: m.extractionStatus, + extractionError: m.extractionError, }))} folders={folders.map((f) => ({ id: f.id, parentId: f.parentId, name: f.name }))} /> diff --git a/src/app/[locale]/(app)/studies/[programId]/[moduleId]/page.tsx b/src/app/[locale]/(app)/studies/[programId]/[moduleId]/page.tsx index d05465b..e272834 100644 --- a/src/app/[locale]/(app)/studies/[programId]/[moduleId]/page.tsx +++ b/src/app/[locale]/(app)/studies/[programId]/[moduleId]/page.tsx @@ -81,7 +81,9 @@ export default async function ModuleOverviewPage({ return (
- {aiAvailable && } + {aiAvailable && ( + + )} ({ id: m.id, role: m.role, parts: m.parts })) const lastMessage = body.messages.at(-1) - if (lastMessage?.role === "user") { + if (body.trigger === "regenerate-message") { + // Regenerate the last answer: drop the trailing assistant message(s) and + // re-stream from the preceding user turn. Refused when an executed write + // tool is part of that answer — its side effect (deck/quiz/event) already + // exists and would be offered again. + const { isWriteToolPart } = await import("@/lib/ai/chat-history") + const poppedIds: string[] = [] + while (history.length > 0 && history.at(-1)!.role === "assistant") { + const popped = history.pop()! + const executed = popped.parts.some( + (p) => + isWriteToolPart(p) && + p.state === "output-available" && + (p.output as { status?: string } | undefined)?.status === "executed" + ) + if (executed) { + return new Response("Cannot regenerate an answer with executed actions", { status: 409 }) + } + poppedIds.push(popped.id) + } + if (poppedIds.length === 0 || history.at(-1)?.role !== "user") { + return new Response("Nothing to regenerate", { status: 400 }) + } + await db.delete(aiMessage).where(inArray(aiMessage.id, poppedIds)) + await db + .update(aiConversation) + .set({ model: body.model, updatedAt: new Date() }) + .where(eq(aiConversation.id, conversation.id)) + } else if (lastMessage?.role === "user") { // New user turn: keep only text parts and bound their size. const textParts = lastMessage.parts.filter( (p): p is { type: "text"; text: string } => p.type === "text" && typeof p.text === "string" @@ -144,7 +179,8 @@ export async function POST(request: Request) { .returning({ id: aiMessage.id }) history.push({ id: inserted.id, role: "user", parts: textParts }) - // Derive a title from the first user message + // Derive a placeholder title from the first user message; upgraded to an + // AI-written title after the first exchange completes (see onFinish). if (conversation.title === "New conversation") { const text = textParts .map((p) => p.text) @@ -188,16 +224,22 @@ export async function POST(request: Request) { const ragEnabled = Boolean(ai?.defaultEmbeddingModel) const userId = session.user.id const moduleId = conversation.moduleId + const scopedMaterialId = conversation.materialId + // Citation counter shared across all searchMaterials calls of this response + // so [n] indices stay unique within one assistant turn. + let citationIndex = 0 const result = streamText({ model, + ...CHAT_PARAMS, system: buildSystemPrompt( conversation.module?.name, conversation.moduleId, ragEnabled, (conversation.mode as ChatMode) ?? "general", pageContext, - locale + locale, + conversation.material?.name ?? null ), // Incomplete write-tool calls (user typed on without confirming) are // dropped so providers don't reject the dangling tool_use. @@ -215,7 +257,11 @@ export async function POST(request: Request) { query: z.string().describe("Search query in the language of the materials"), }), execute: async ({ query }) => { - const hits = await searchChunks(userId, query, { moduleId, limit: 6 }) + // Document-scoped conversations retrieve from that one material; + // otherwise search the conversation's module (or everything). + const hits = scopedMaterialId + ? await searchChunksInMaterials(userId, query, [scopedMaterialId], { limit: 6 }) + : await searchChunks(userId, query, { moduleId, limit: 6 }) if (hits.length > 0) { const { logAudit } = await import("@/lib/audit") await logAudit({ @@ -228,8 +274,12 @@ export async function POST(request: Request) { conversationId: conversation.id, }) } + // The index lets the model cite [n]; materialId lets the UI + // render the citation as a link to the source material. return hits.map((h) => ({ + index: ++citationIndex, source: h.materialName, + materialId: h.materialId, excerpt: h.content.slice(0, 1500), })) }, @@ -334,12 +384,63 @@ export async function POST(request: Request) { void Promise.resolve(result.consumeStream()).catch(() => {}) return result.toUIMessageStreamResponse({ + // Categorize provider failures for the client (which maps the codes onto + // translated messages) without leaking raw provider/stack details. + onError: (error) => { + console.error("[chat] stream error", error) + const message = error instanceof Error ? error.message : String(error) + if (/api.?key|unauthorized|authentication|401|403/i.test(message)) return "AI_ERROR:auth" + if (/rate.?limit|quota|429|overloaded/i.test(message)) return "AI_ERROR:rate-limit" + return "AI_ERROR:generic" + }, onFinish: async ({ responseMessage }) => { + // A failed stream can produce an empty assistant message — persisting it + // would litter the conversation with blank turns. + const hasContent = responseMessage.parts.some( + (p) => + (p.type === "text" && p.text.trim().length > 0) || + (typeof p.type === "string" && p.type.startsWith("tool-")) + ) + if (!hasContent) return await db.insert(aiMessage).values({ conversationId: conversation.id, role: "assistant", parts: responseMessage.parts, }) + // First exchange of a fresh conversation: replace the truncated + // placeholder title with a short AI-written one (best-effort). + if (conversation.title === "New conversation") { + const firstUser = windowedHistory.find((m) => m.role === "user") + const userText = firstUser?.parts + .filter((p): p is { type: "text"; text: string } => p.type === "text") + .map((p) => p.text) + .join(" ") + const answerText = responseMessage.parts + .filter((p): p is { type: "text"; text: string } => p.type === "text") + .map((p) => p.text) + .join(" ") + if (userText) { + try { + const { generateText } = await import("ai") + const { UTILITY_PARAMS } = await import("@/lib/ai/params") + const { text } = await generateText({ + model, + ...UTILITY_PARAMS, + maxOutputTokens: 30, + prompt: `Write a short title (max 6 words, no quotes, same language as the conversation) for this chat.\nUser: ${userText.slice(0, 500)}\nAssistant: ${answerText.slice(0, 500)}`, + }) + const title = text.trim().replace(/^["'«»\s]+|["'«»\s.]+$/g, "").slice(0, 80) + if (title) { + await db + .update(aiConversation) + .set({ title }) + .where(eq(aiConversation.id, conversation.id)) + } + } catch (error) { + console.error("[chat] title generation failed", error) + } + } + } }, }) } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 1c76ca9..36036b1 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,14 +1,38 @@ import { NextResponse } from "next/server" import { sql } from "drizzle-orm" import { db } from "@/db" +import { deleteFile, saveFile } from "@/lib/storage" export const dynamic = "force-dynamic" +/** + * Verifies the storage backend accepts writes. A read-only uploads volume + * (classic Docker bind-mount ownership problem) is the most common reason + * "uploads don't work" — surface it here instead of as opaque 500s. + */ +async function checkStorage(): Promise { + const probe = `health-${crypto.randomUUID()}.txt` + try { + const path = await saveFile("_health", probe, Buffer.from("ok")) + await deleteFile(path) + return true + } catch (error) { + console.error("[health] storage write check failed", error) + return false + } +} + export async function GET() { + let database = "up" try { await db.execute(sql`select 1`) - return NextResponse.json({ status: "ok", database: "up" }) } catch { - return NextResponse.json({ status: "degraded", database: "down" }, { status: 503 }) + database = "down" } + const storage = (await checkStorage()) ? "up" : "down" + const ok = database === "up" && storage === "up" + return NextResponse.json( + { status: ok ? "ok" : "degraded", database, storage }, + { status: ok ? 200 : 503 } + ) } diff --git a/src/components/admin/ai-settings-form.tsx b/src/components/admin/ai-settings-form.tsx index cef9b8c..5495204 100644 --- a/src/components/admin/ai-settings-form.tsx +++ b/src/components/admin/ai-settings-form.tsx @@ -23,7 +23,7 @@ import { SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" -import { saveAiSettings } from "@/app/[locale]/(app)/admin/actions" +import { saveAiSettings, testAiProvider } from "@/app/[locale]/(app)/admin/actions" import type { AiSettings } from "@/lib/settings" const PROVIDER_TYPES = [ @@ -67,6 +67,7 @@ export function AiSettingsForm({ initial }: { initial: AiSettings }) { ) const [monthlyLimit, setMonthlyLimit] = React.useState(initial.monthlyTokenLimitPerUser) const [useBatchApi, setUseBatchApi] = React.useState(initial.useBatchApi ?? false) + const [rerank, setRerank] = React.useState(initial.rerank ?? false) const modelOptions = providers.flatMap((p) => p.modelsText @@ -83,6 +84,34 @@ export function AiSettingsForm({ initial }: { initial: AiSettings }) { setProviders((list) => list.map((p, i) => (i === index ? { ...p, ...patch } : p))) } + const [testing, setTesting] = React.useState(null) + + /** One-token live request against the (possibly unsaved) provider config. */ + async function testProvider(index: number) { + const p = providers[index] + setTesting(p.id) + try { + const result = await testAiProvider({ + id: p.id, + type: p.type, + name: p.name || p.id, + apiKey: p.apiKey || undefined, + baseUrl: p.baseUrl || undefined, + models: p.modelsText + .split(",") + .map((m) => m.trim()) + .filter(Boolean), + embeddingModel: p.embeddingModel.trim() || undefined, + }) + if (result.ok) toast.success(t("testOk")) + else toast.error(t("testFailed", { error: result.error })) + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + } finally { + setTesting(null) + } + } + async function save() { setPending(true) try { @@ -105,6 +134,7 @@ export function AiSettingsForm({ initial }: { initial: AiSettings }) { defaultEmbeddingModel: defaultEmbeddingModel || undefined, monthlyTokenLimitPerUser: monthlyLimit, useBatchApi, + rerank, }) toast.success(t("saved")) } catch (error) { @@ -199,7 +229,16 @@ export function AiSettingsForm({ initial }: { initial: AiSettings }) { />
-
+
+
+
+
+ + +
+

{t("rerankHint")}

+
diff --git a/src/components/admin/reembed-card.tsx b/src/components/admin/reembed-card.tsx new file mode 100644 index 0000000..b03fce8 --- /dev/null +++ b/src/components/admin/reembed-card.tsx @@ -0,0 +1,117 @@ +"use client" + +import * as React from "react" +import { AlertTriangle, Loader2 } from "lucide-react" +import { useTranslations } from "next-intl" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { getReembedStatus, startReembed } from "@/app/[locale]/(app)/admin/actions" + +type ReembedState = { + status: "idle" | "running" | "done" | "failed" + embeddingModel?: string + total?: number + done?: number + failed?: number + error?: string +} + +/** + * Admin control for the embedding backfill: warns when materials are still + * embedded with a different model than the active one (their vector search is + * dead until re-embedded) and lets the admin (re)start the backfill. Polls + * while the job runs. + */ +export function ReembedCard({ + initialState, + initialStaleCount, + embeddingConfigured, +}: { + initialState: ReembedState + initialStaleCount: number + embeddingConfigured: boolean +}) { + const t = useTranslations("admin.ai") + const [state, setState] = React.useState(initialState) + const [staleCount, setStaleCount] = React.useState(initialStaleCount) + const [pending, setPending] = React.useState(false) + + React.useEffect(() => { + if (state.status !== "running") return + let active = true + const timer = setInterval(async () => { + try { + const s = await getReembedStatus() + if (active) { + setState(s.state as ReembedState) + setStaleCount(s.staleCount) + } + } catch { + // transient — keep polling + } + }, 3000) + return () => { + active = false + clearInterval(timer) + } + }, [state.status]) + + async function onStart() { + setPending(true) + try { + await startReembed() + setState((s) => ({ ...s, status: "running" })) + toast.success(t("reembedStarted")) + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + } finally { + setPending(false) + } + } + + const busy = pending || state.status === "running" + + return ( + + + {t("reembedTitle")} + + +

{t("reembedDescription")}

+ {staleCount > 0 && state.status !== "running" && ( +
+ +

{t("reembedStaleWarning", { count: staleCount })}

+
+ )} +
+ {state.status === "running" ? ( + + + {t("reembedRunning", { done: state.done ?? 0, total: state.total ?? 0 })} + + ) : state.status === "done" ? ( + + {t("reembedDone", { done: state.done ?? 0, failed: state.failed ?? 0 })} + + ) : state.status === "failed" ? ( + + {t("reembedFailed")} + {state.error ? ` — ${state.error}` : ""} + + ) : ( + {t("reembedIdle")} + )} +
+ + {!embeddingConfigured && ( +

{t("annNeedEmbedding")}

+ )} +
+
+ ) +} diff --git a/src/components/ai/chat.tsx b/src/components/ai/chat.tsx index dcd43e5..9a706dd 100644 --- a/src/components/ai/chat.tsx +++ b/src/components/ai/chat.tsx @@ -7,7 +7,17 @@ import { lastAssistantMessageIsCompleteWithToolCalls, type UIMessage, } from "ai" -import { ArrowUp, Check, FileSearch, Loader2, Sparkles, Wrench, X } from "lucide-react" +import { + ArrowUp, + Check, + FileSearch, + Loader2, + RefreshCw, + Sparkles, + Square, + Wrench, + X, +} from "lucide-react" import { useLocale, useTranslations } from "next-intl" import { Link } from "@/i18n/navigation" import { executeAiTool } from "@/app/[locale]/(app)/ai/actions" @@ -15,6 +25,7 @@ import { WRITE_TOOL_NAMES } from "@/lib/ai/tools" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Markdown } from "./markdown" +import { VoiceInputButton } from "./voice-input-button" import { describePageContext, usePageContext } from "./page-context" import { cn } from "@/lib/utils" @@ -38,6 +49,63 @@ type ToolOutput = | { status: "executed"; label: string; href?: string } | { status: "rejected" } +type SourceRef = { index: number; source: string; materialId: string } + +/** Collects the searchMaterials results of a message into deduped source refs. */ +function collectSources(parts: UIMessage["parts"]): SourceRef[] { + const seen = new Map() + for (const part of parts) { + if (part.type !== "tool-searchMaterials") continue + const p = part as { state?: string; output?: unknown } + if (p.state !== "output-available" || !Array.isArray(p.output)) continue + for (const hit of p.output) { + const h = hit as { index?: number; source?: string; materialId?: string } + if (typeof h.index !== "number" || typeof h.materialId !== "string" || !h.source) continue + const existing = seen.get(h.materialId) + if (!existing || h.index < existing.index) { + seen.set(h.materialId, { index: h.index, source: h.source, materialId: h.materialId }) + } + } + } + return [...seen.values()].sort((a, b) => a.index - b.index) +} + +/** Numbered, clickable source chips below an assistant answer. */ +function SourceChips({ parts }: { parts: UIMessage["parts"] }) { + const t = useTranslations("ai") + const sources = collectSources(parts) + if (sources.length === 0) return null + return ( +
+ {t("sources")}: + {sources.map((s) => ( + + [{s.index}] {s.source} + + ))} +
+ ) +} + +/** True when the message contains an executed write tool (side effect exists). */ +function hasExecutedWriteTool(parts: UIMessage["parts"]): boolean { + return parts.some((part) => { + if (!part.type.startsWith("tool-")) return false + if (!WRITE_TOOL_NAMES.includes(part.type.slice(5) as (typeof WRITE_TOOL_NAMES)[number])) { + return false + } + const p = part as { state?: string; output?: unknown } + return ( + p.state === "output-available" && + (p.output as { status?: string } | undefined)?.status === "executed" + ) + }) +} + function ToolCard({ toolName, part, @@ -180,7 +248,7 @@ export function Chat({ }) ) - const { messages, sendMessage, status, error, addToolResult } = useChat({ + const { messages, sendMessage, status, error, addToolResult, stop, regenerate } = useChat({ id: conversationId, messages: initialMessages, transport, @@ -251,7 +319,7 @@ export function Chat({ .join("")}
) : ( -
+
{m.parts.map((part, i) => { if (part.type === "text") { return ( @@ -309,6 +377,19 @@ export function Chat({ } return null })} + + {m.id === messages.at(-1)?.id && + status === "ready" && + !hasExecutedWriteTool(m.parts) && ( + + )}
) )} @@ -320,11 +401,14 @@ export function Chat({ )} {error && (

- {/* Raw error.message can carry transport/provider internals — log - it for debugging, show the user a friendly message. */} - {error.message.toLowerCase().includes("limit") - ? t("errorLimit") - : t("errorGeneric")} + {/* Server categorizes stream errors as AI_ERROR:; anything + else stays generic so provider internals never reach the UI. */} + {error.message === "AI_ERROR:auth" + ? t("errorAuth") + : error.message === "AI_ERROR:rate-limit" || + error.message.toLowerCase().includes("limit") + ? t("errorLimit") + : t("errorGeneric")}

)}
@@ -345,9 +429,22 @@ export function Chat({ rows={2} className="max-h-40 flex-1 resize-none" /> - + setInput((v) => (v ? `${v} ${text}` : text))} + /> + {busy ? ( + // Streaming: the send slot becomes a stop button. Note the server + // deliberately drains the stream for crash-safe persistence, so the + // DB keeps the full answer; regenerate removes it if unwanted. + + ) : ( + + )}
diff --git a/src/components/ai/conversation-panel.tsx b/src/components/ai/conversation-panel.tsx index 7d939eb..dbef83b 100644 --- a/src/components/ai/conversation-panel.tsx +++ b/src/components/ai/conversation-panel.tsx @@ -22,11 +22,13 @@ import { getConversationMessages, listConversations, renameConversation, + updateConversationMode, updateConversationModule, } from "@/app/[locale]/(app)/ai/actions" import { Chat } from "@/components/ai/chat" import { usePageContext } from "@/components/ai/page-context" import { ModuleSelect, type ModuleOption } from "@/components/learn/module-select" +import { CHAT_MODES } from "@/lib/ai/modes" import { Button } from "@/components/ui/button" import { DropdownMenu, @@ -45,6 +47,8 @@ export type ConversationMeta = { title: string moduleName: string | null moduleId: string | null + /** Tutor mode; older callers may omit it — treated as "general". */ + mode?: string } /** @@ -125,6 +129,7 @@ export function ConversationPanel({ title: t("newConversation"), moduleId, moduleName: pageContext.moduleName ?? null, + mode: "general", } setConversations([meta, ...(list ?? conversations)]) setInitialMessages([]) @@ -209,6 +214,16 @@ export function ConversationPanel({ } } + async function onModeChange(mode: string) { + if (!current) return + try { + await updateConversationMode(current.id, mode) + setCurrent({ ...current, mode }) + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + } + } + async function onModuleChange(moduleId: string) { if (!current) return try { @@ -347,13 +362,26 @@ export function ConversationPanel({ onConfirm={onDelete} /> -
+
{t("module")} void onModuleChange(v)} /> + {t("modeLabel")} +
diff --git a/src/components/ai/voice-input-button.tsx b/src/components/ai/voice-input-button.tsx new file mode 100644 index 0000000..7ed3d8b --- /dev/null +++ b/src/components/ai/voice-input-button.tsx @@ -0,0 +1,93 @@ +"use client" + +import * as React from "react" +import { Loader2, Mic, Square } from "lucide-react" +import { useTranslations } from "next-intl" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { isVoiceInputAvailable, transcribeVoiceInput } from "@/app/[locale]/(app)/ai/actions" + +/** + * Push-to-talk voice input: records via MediaRecorder, transcribes server-side + * (Whisper via the configured OpenAI/Groq provider) and hands the text to the + * caller. Renders nothing when the browser can't record or no transcription + * provider is configured. + */ +export function VoiceInputButton({ + disabled, + onTranscript, +}: { + disabled?: boolean + onTranscript: (text: string) => void +}) { + const t = useTranslations("ai.voice") + const [available, setAvailable] = React.useState(false) + const [recording, setRecording] = React.useState(false) + const [transcribing, setTranscribing] = React.useState(false) + const recorderRef = React.useRef(null) + + React.useEffect(() => { + if (typeof MediaRecorder === "undefined" || !navigator.mediaDevices?.getUserMedia) return + isVoiceInputAvailable() + .then(setAvailable) + .catch(() => setAvailable(false)) + }, []) + + React.useEffect(() => { + return () => recorderRef.current?.stream.getTracks().forEach((track) => track.stop()) + }, []) + + async function start() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + const recorder = new MediaRecorder(stream) + const chunks: Blob[] = [] + recorder.ondataavailable = (e) => { + if (e.data.size > 0) chunks.push(e.data) + } + recorder.onstop = async () => { + stream.getTracks().forEach((track) => track.stop()) + setRecording(false) + const blob = new Blob(chunks, { type: recorder.mimeType || "audio/webm" }) + if (blob.size === 0) return + setTranscribing(true) + try { + const formData = new FormData() + formData.append("audio", blob, "voice-input.webm") + const { text } = await transcribeVoiceInput(formData) + if (text) onTranscript(text) + else toast.error(t("empty")) + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + } finally { + setTranscribing(false) + } + } + recorderRef.current = recorder + recorder.start() + setRecording(true) + } catch { + toast.error(t("micDenied")) + } + } + + if (!available) return null + + return ( + + ) +} diff --git a/src/components/learn/analyze-button.tsx b/src/components/learn/analyze-button.tsx index b538eb4..83241f0 100644 --- a/src/components/learn/analyze-button.tsx +++ b/src/components/learn/analyze-button.tsx @@ -1,10 +1,13 @@ "use client" import * as React from "react" -import { Loader2, Sparkles } from "lucide-react" +import { BrainCircuit, Layers, Loader2, Sparkles } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" +import { useRouter } from "@/i18n/navigation" import { analyzeProgress } from "@/app/[locale]/(app)/learn-actions" +import { createDeck, generateCards } from "@/app/[locale]/(app)/deck-actions" +import { generateQuiz } from "@/app/[locale]/(app)/quiz-actions" import { Markdown } from "@/components/ai/markdown" import { Button } from "@/components/ui/button" import { @@ -17,15 +20,22 @@ import { /** "What should I deepen?" — AI analysis over the module's full session history. */ export function AnalyzeButton({ moduleId, + basePath, variant = "outline", }: { moduleId: string + /** Module base path (/studies/[programId]/[moduleId]) for the one-click + * weak-topics generation links; omitting hides those buttons. */ + basePath?: string variant?: "outline" | "default" }) { const t = useTranslations("learnSession") + const router = useRouter() const [open, setOpen] = React.useState(false) const [pending, setPending] = React.useState(false) const [analysis, setAnalysis] = React.useState(null) + const [weakTopics, setWeakTopics] = React.useState("") + const [generating, setGenerating] = React.useState<"quiz" | "deck" | null>(null) async function run() { setOpen(true) @@ -34,6 +44,7 @@ export function AnalyzeButton({ try { const result = await analyzeProgress(moduleId) setAnalysis(result.analysis) + setWeakTopics(result.weakTopics ?? "") } catch (error) { toast.error(error instanceof Error ? error.message : String(error)) setOpen(false) @@ -42,6 +53,30 @@ export function AnalyzeButton({ } } + async function makeQuiz() { + setGenerating("quiz") + try { + const result = await generateQuiz({ moduleId, count: 10, topics: weakTopics, mixed: true }) + router.push(`${basePath}/quizzes/${result.id}`) + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + setGenerating(null) + } + } + + async function makeDeck() { + setGenerating("deck") + try { + const created = await createDeck({ name: t("weakTopicsDeckName"), moduleId }) + await generateCards({ deckId: created.id, count: 15, topics: weakTopics }) + router.push(`${basePath}/decks/${created.id}`) + return + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + setGenerating(null) + } + } + return ( <> + +
+ )} +
) : null} diff --git a/src/components/materials/ask-document-button.tsx b/src/components/materials/ask-document-button.tsx new file mode 100644 index 0000000..4396129 --- /dev/null +++ b/src/components/materials/ask-document-button.tsx @@ -0,0 +1,34 @@ +"use client" + +import * as React from "react" +import { Loader2, MessageSquare } from "lucide-react" +import { useTranslations } from "next-intl" +import { toast } from "sonner" +import { useRouter } from "@/i18n/navigation" +import { Button } from "@/components/ui/button" +import { createConversation } from "@/app/[locale]/(app)/ai/actions" + +/** Starts a document-scoped AI conversation ("chat with this document"). */ +export function AskDocumentButton({ materialId }: { materialId: string }) { + const t = useTranslations("materials") + const router = useRouter() + const [pending, setPending] = React.useState(false) + + async function start() { + setPending(true) + try { + const { id } = await createConversation(null, "general", materialId) + router.push(`/ai/${id}`) + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + setPending(false) + } + } + + return ( + + ) +} diff --git a/src/components/materials/materials-browser.tsx b/src/components/materials/materials-browser.tsx index ea056d6..56d5fa2 100644 --- a/src/components/materials/materials-browser.tsx +++ b/src/components/materials/materials-browser.tsx @@ -32,6 +32,7 @@ import { Music, Pencil, Presentation, + RefreshCw, Trash2, Video, X, @@ -49,6 +50,7 @@ import { moveMaterialToFolder, renameFolder, renameMaterial, + retryMaterialProcessing, } from "@/app/[locale]/(app)/materials-actions" import { ConfirmDeleteDialog } from "@/components/studies/confirm-delete-dialog" import { EntityContextMenu, type ContextMenuAction } from "@/components/entity-context-menu" @@ -68,7 +70,7 @@ import { } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { cn, formatBytes } from "@/lib/utils" -import { readDroppedItems, uploadFiles } from "./upload-client" +import { describeUploadError, readDroppedItems, uploadFiles } from "./upload-client" import { LinkDialog, UploadDialog } from "./upload-dialog" const ROOT = "__root__" @@ -82,6 +84,51 @@ export type MaterialItem = { sizeBytes: number | null folderId: string | null createdAt: string + /** AI extraction/embedding pipeline state (files only). */ + extractionStatus?: string | null + extractionError?: string | null +} + +/** Pipeline states that are still moving — used to poll for fresh status. */ +const ACTIVE_STATUSES = ["pending", "extracting", "embedding", "summarizing"] + +/** + * Compact pipeline-state badge for a file material. "ready" (and links) render + * nothing; active states show a spinner; failed/skipped show why and offer a + * retry via the row menu. + */ +function ProcessingBadge({ item }: { item: MaterialItem }) { + const t = useTranslations("materials.processing") + const status = item.extractionStatus + if (item.kind !== "file" || !status || status === "ready") return null + if (ACTIVE_STATUSES.includes(status)) { + return ( + + + {t(status)} + + ) + } + if (status === "failed") { + return ( + + + {t("failed")} + + ) + } + // skipped + return ( + + {t("skipped")} + + ) } export type FolderNode = { id: string; parentId: string | null; name: string } @@ -235,6 +282,7 @@ function FileItem({ onRename, onMove, onDelete, + onRetry, }: { item: MaterialItem view: "list" | "grid" @@ -244,6 +292,7 @@ function FileItem({ onRename: () => void onMove: () => void onDelete: () => void + onRetry: () => void }) { const t = useTranslations("materials") const tCommon = useTranslations("common") @@ -251,9 +300,16 @@ function FileItem({ const { attributes, listeners, setNodeRef: dragRef, style: dragStyle, isDragging } = useItemDraggable(item.id) + const canRetry = + item.kind === "file" && + (item.extractionStatus === "failed" || item.extractionStatus === "skipped") + const actions: ContextMenuAction[] = [ { label: t("rename"), icon: Pencil, onSelect: onRename }, { label: t("moveTo"), icon: FolderInput, onSelect: onMove }, + ...(canRetry + ? [{ label: t("processing.retry"), icon: RefreshCw, onSelect: onRetry }] + : []), { label: tCommon("delete"), icon: Trash2, destructive: true, onSelect: onDelete, separatorBefore: true }, ] @@ -278,6 +334,9 @@ function FileItem({ {t("rename")} {t("moveTo")} + {canRetry && ( + {t("processing.retry")} + )} {tCommon("delete")} @@ -296,10 +355,11 @@ function FileItem({
{nameEl}
-

+

{formatBytes(item.sizeBytes)} {item.sizeBytes != null && " · "} {format.dateTime(new Date(item.createdAt), { dateStyle: "medium" })} +

{grip} @@ -312,6 +372,7 @@ function FileItem({ {grip} {nameEl} + {formatBytes(item.sizeBytes)} {item.sizeBytes != null && " · "} @@ -389,10 +450,13 @@ export function MaterialsBrowser({ moduleId, materials, folders, + indexingEnabled = true, }: { moduleId: string materials: MaterialItem[] folders: FolderNode[] + /** False when no embedding model is configured — materials won't be searchable. */ + indexingEnabled?: boolean }) { const t = useTranslations("materials") const router = useRouter() @@ -418,6 +482,26 @@ export function MaterialsBrowser({ const saved = window.localStorage.getItem("materials-view-mode") if (saved === "grid" || saved === "list") queueMicrotask(() => setView(saved)) }, []) + + // While any material is still extracting/embedding, refresh periodically so + // the pipeline badges move without a manual reload. + const hasActiveProcessing = materials.some( + (m) => m.extractionStatus && ACTIVE_STATUSES.includes(m.extractionStatus) + ) + React.useEffect(() => { + if (!hasActiveProcessing) return + const id = setInterval(() => router.refresh(), 5000) + return () => clearInterval(id) + }, [hasActiveProcessing, router]) + + async function retryProcessing(materialId: string) { + try { + await retryMaterialProcessing(materialId) + router.refresh() + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)) + } + } function changeView(next: "list" | "grid") { setView(next) window.localStorage.setItem("materials-view-mode", next) @@ -597,7 +681,7 @@ export function MaterialsBrowser({ if (queued > 0) toast.success(t("unpacking")) else toast.success(t("uploaded")) } catch (error) { - toast.error(t("uploadFailed", { error: error instanceof Error ? error.message : String(error) })) + toast.error(t("uploadFailed", { error: describeUploadError(error, t) })) } finally { setUploadingMsg(null) router.refresh() @@ -660,6 +744,11 @@ export function MaterialsBrowser({ return (
+ {!indexingEnabled && materials.some((m) => m.kind === "file") && ( +

+ {t("notIndexedHint")} +

+ )} {/* Stats bar */}
{t("stats.files", { count: totalFiles })} @@ -724,7 +813,7 @@ export function MaterialsBrowser({ setCurrentFolderId(f.id)} onRename={() => { setRenameTarget({ kind: "folder", id: f.id, name: f.name }); setRenameValue(f.name) }} onNewSubfolder={() => { setNewFolderParent(f.id); setNewFolderName("") }} onMove={() => setMoveTarget({ kind: "folder", id: f.id })} onDelete={() => setDeleteTarget({ kind: "folder", id: f.id, label: f.name })} /> ))} {currentFiles.map((item, i) => ( - { toggleSelect(item.id, c); lastIndexRef.current = i }} onRowClick={(e) => onFileRowClick(e, i, item.id)} onRename={() => { setRenameTarget({ kind: "file", id: item.id, name: item.name }); setRenameValue(item.name) }} onMove={() => setMoveTarget({ kind: "file", id: item.id })} onDelete={() => setDeleteTarget({ kind: "file", id: item.id, label: item.name })} /> + { toggleSelect(item.id, c); lastIndexRef.current = i }} onRowClick={(e) => onFileRowClick(e, i, item.id)} onRename={() => { setRenameTarget({ kind: "file", id: item.id, name: item.name }); setRenameValue(item.name) }} onMove={() => setMoveTarget({ kind: "file", id: item.id })} onDelete={() => setDeleteTarget({ kind: "file", id: item.id, label: item.name })} onRetry={() => void retryProcessing(item.id)} /> ))}
) : ( @@ -734,7 +823,7 @@ export function MaterialsBrowser({ ))}
    {currentFiles.map((item, i) => ( - { toggleSelect(item.id, c); lastIndexRef.current = i }} onRowClick={(e) => onFileRowClick(e, i, item.id)} onRename={() => { setRenameTarget({ kind: "file", id: item.id, name: item.name }); setRenameValue(item.name) }} onMove={() => setMoveTarget({ kind: "file", id: item.id })} onDelete={() => setDeleteTarget({ kind: "file", id: item.id, label: item.name })} /> + { toggleSelect(item.id, c); lastIndexRef.current = i }} onRowClick={(e) => onFileRowClick(e, i, item.id)} onRename={() => { setRenameTarget({ kind: "file", id: item.id, name: item.name }); setRenameValue(item.name) }} onMove={() => setMoveTarget({ kind: "file", id: item.id })} onDelete={() => setDeleteTarget({ kind: "file", id: item.id, label: item.name })} onRetry={() => void retryProcessing(item.id)} /> ))}
diff --git a/src/components/materials/pdf-annotator.tsx b/src/components/materials/pdf-annotator.tsx index 9710d2f..52baea9 100644 --- a/src/components/materials/pdf-annotator.tsx +++ b/src/components/materials/pdf-annotator.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { Loader2, Trash2 } from "lucide-react" +import { Loader2, Sparkles, Trash2 } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { @@ -9,7 +9,9 @@ import { deleteAnnotation, updateAnnotationNote, } from "@/app/[locale]/(app)/materials-actions" +import { explainSnippet } from "@/app/[locale]/(app)/ai/actions" import type { AnnotationColor, AnnotationRect } from "@/db/schema/materials" +import { Markdown } from "@/components/ai/markdown" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { cn } from "@/lib/utils" @@ -60,10 +62,10 @@ export function PdfAnnotator({ ;(async () => { try { const pdfjs = await import("pdfjs-dist") - pdfjs.GlobalWorkerOptions.workerSrc = new URL( - "pdfjs-dist/build/pdf.worker.min.mjs", - import.meta.url - ).toString() + // The worker is copied to public/ at build time (scripts/copy-pdf-worker.mjs) + // — a stable same-origin URL that works under every bundler; the version + // query busts caches when pdfjs-dist is upgraded. + pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.mjs?v=${pdfjs.version}` const pdf = await pdfjs.getDocument({ url: fileUrl }).promise if (cancelled) return pdfRef.current = pdf @@ -75,13 +77,26 @@ export function PdfAnnotator({ } if (!cancelled) setPages(infos) } catch (err) { - if (!cancelled) setError(err instanceof Error ? err.message : String(err)) + if (cancelled) return + // Distinguish "the stored file is gone" (e.g. lost to a redeploy before + // uploads were volume-backed) from a real render error — the former + // needs a human-readable explanation, not a pdf.js stack message. + try { + const probe = await fetch(fileUrl, { headers: { Range: "bytes=0-0" } }) + if (probe.status === 404) { + setError(t("fileMissing")) + return + } + } catch { + // probe failed — fall through to the generic error + } + setError(err instanceof Error ? err.message : String(err)) } })() return () => { cancelled = true } - }, [fileUrl]) + }, [fileUrl, t]) async function onCreate(page: number, rect: AnnotationRect) { const temp: PdfAnnotation = { id: `tmp-${crypto.randomUUID()}`, page, rect, color, note: null } @@ -120,6 +135,67 @@ export function PdfAnnotator({ } } + const [explaining, setExplaining] = React.useState(false) + const [explanation, setExplanation] = React.useState(null) + + /** Text of the page region covered by an annotation (pdf.js text layer). */ + async function extractRectText(annotation: PdfAnnotation): Promise<{ + snippet: string + pageText: string + }> { + const pdf = pdfRef.current as { + getPage: (n: number) => Promise<{ + getViewport: (o: { scale: number }) => { width: number; height: number } + getTextContent: () => Promise<{ + items: { str?: string; transform?: number[]; width?: number; height?: number }[] + }> + }> + } | null + if (!pdf) return { snippet: "", pageText: "" } + const page = await pdf.getPage(annotation.page) + const viewport = page.getViewport({ scale: 1 }) + const content = await page.getTextContent() + const parts: string[] = [] + const all: string[] = [] + const r = annotation.rect + for (const item of content.items) { + if (!item.str || !item.transform) continue + all.push(item.str) + // transform[4]/[5] are the item's origin in PDF units (bottom-left based); + // annotation rects are normalized with y from the top. + const cx = (item.transform[4] + (item.width ?? 0) / 2) / viewport.width + const cy = 1 - item.transform[5] / viewport.height + const pad = 0.01 + if ( + cx >= r.x - pad && + cx <= r.x + r.w + pad && + cy >= r.y - pad && + cy <= r.y + r.h + pad + ) { + parts.push(item.str) + } + } + return { snippet: parts.join(" ").trim(), pageText: all.join(" ").trim() } + } + + async function onExplain() { + if (!selected) return + setExplaining(true) + setExplanation(null) + try { + const { snippet, pageText } = await extractRectText(selected) + // A rect over a diagram may cover no text — fall back to the note or page. + const effective = snippet || selected.note || pageText.slice(0, 1000) + if (!effective) throw new Error(t("explainEmpty")) + const result = await explainSnippet(materialId, effective, pageText) + setExplanation(result.explanation) + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)) + } finally { + setExplaining(false) + } + } + if (error) { return

{error}

} @@ -164,6 +240,7 @@ export function PdfAnnotator({ onSelect={(a) => { setSelected(a) setNote(a.note ?? "") + setExplanation(null) }} /> ))} @@ -171,6 +248,11 @@ export function PdfAnnotator({ {selected && (
+ {explanation && ( +
+ {explanation} +
+ )}