diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..b02d5aad4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +patches/*.patch whitespace=-space-before-tab diff --git a/drizzle-postgres/0003_glorious_wolf_cub.sql b/drizzle-postgres/0003_glorious_wolf_cub.sql new file mode 100644 index 000000000..47db548b8 --- /dev/null +++ b/drizzle-postgres/0003_glorious_wolf_cub.sql @@ -0,0 +1,13 @@ +CREATE TABLE "workspace_item_user_states" ( + "user_id" text NOT NULL, + "item_id" text NOT NULL, + "state" jsonb NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "workspace_item_user_states_user_id_item_id_pk" PRIMARY KEY("user_id","item_id") +); +--> statement-breakpoint +ALTER TABLE "workspace_items" DROP CONSTRAINT "workspace_items_type_check";--> statement-breakpoint +ALTER TABLE "workspace_item_user_states" ADD CONSTRAINT "workspace_item_user_states_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspace_item_user_states" ADD CONSTRAINT "workspace_item_user_states_item_id_workspace_items_id_fk" FOREIGN KEY ("item_id") REFERENCES "public"."workspace_items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "workspace_item_user_states_item_id_idx" ON "workspace_item_user_states" USING btree ("item_id");--> statement-breakpoint +ALTER TABLE "workspace_items" ADD CONSTRAINT "workspace_items_type_check" CHECK ("workspace_items"."type" in ('folder', 'document', 'flashcard', 'file')); diff --git a/drizzle-postgres/meta/0003_snapshot.json b/drizzle-postgres/meta/0003_snapshot.json new file mode 100644 index 000000000..e5cfe8275 --- /dev/null +++ b/drizzle-postgres/meta/0003_snapshot.json @@ -0,0 +1,2478 @@ +{ + "id": "25aa9ff5-8b2a-4423-8d78-053fd64107c5", + "prevId": "2b13358a-fc28-4689-a006-e4dd005e868e", + "version": "7", + "dialect": "postgresql", + "tables": { + "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 with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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 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": { + "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.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_id_idx": { + "name": "oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_consent_user_id_idx": { + "name": "oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_id_idx": { + "name": "oauth_refresh_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit": { + "name": "rate_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_key_unique": { + "name": "rate_limit_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "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()" + }, + "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 + } + }, + "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.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 + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": {}, + "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 with time zone", + "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": { + "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.workspace_file_assets": { + "name": "workspace_file_assets", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_object_key": { + "name": "source_object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "preview_object_key": { + "name": "preview_object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview_size_bytes": { + "name": "preview_size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_assets_item_id_workspace_items_id_fk": { + "name": "workspace_file_assets_item_id_workspace_items_id_fk", + "tableFrom": "workspace_file_assets", + "tableTo": "workspace_items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_assets_source_object_key_unique": { + "name": "workspace_file_assets_source_object_key_unique", + "nullsNotDistinct": false, + "columns": [ + "source_object_key" + ] + }, + "workspace_file_assets_preview_object_key_unique": { + "name": "workspace_file_assets_preview_object_key_unique", + "nullsNotDistinct": false, + "columns": [ + "preview_object_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invites": { + "name": "workspace_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_invites_token_unique": { + "name": "workspace_invites_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_pending_link_per_role": { + "name": "workspace_invites_pending_link_per_role", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_invites\".\"type\" = 'link' and \"workspace_invites\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_pending_email_per_workspace": { + "name": "workspace_invites_pending_email_per_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_invites\".\"type\" = 'email' and \"workspace_invites\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_workspace_id_idx": { + "name": "workspace_invites_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_created_by_user_id_idx": { + "name": "workspace_invites_created_by_user_id_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_invites_workspace_id_workspaces_id_fk": { + "name": "workspace_invites_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_invites", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invites_created_by_user_id_user_id_fk": { + "name": "workspace_invites_created_by_user_id_user_id_fk", + "tableFrom": "workspace_invites", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_invites_role_check": { + "name": "workspace_invites_role_check", + "value": "\"workspace_invites\".\"role\" in ('owner', 'admin', 'editor', 'viewer')" + }, + "workspace_invites_type_check": { + "name": "workspace_invites_type_check", + "value": "\"workspace_invites\".\"type\" in ('email', 'link')" + }, + "workspace_invites_status_check": { + "name": "workspace_invites_status_check", + "value": "\"workspace_invites\".\"status\" in ('pending', 'accepted', 'revoked', 'expired')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_item_contents": { + "name": "workspace_item_contents", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_item_contents_item_id_workspace_items_id_fk": { + "name": "workspace_item_contents_item_id_workspace_items_id_fk", + "tableFrom": "workspace_item_contents", + "tableTo": "workspace_items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_item_extractions": { + "name": "workspace_item_extractions", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_mode": { + "name": "provider_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_item_extractions_status_idx": { + "name": "workspace_item_extractions_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_extractions_item_fk": { + "name": "workspace_item_extractions_item_fk", + "tableFrom": "workspace_item_extractions", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "item_id" + ], + "columnsTo": [ + "workspace_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_item_extractions_status_check": { + "name": "workspace_item_extractions_status_check", + "value": "\"workspace_item_extractions\".\"status\" in ('processing', 'ready', 'failed')" + }, + "workspace_item_extractions_tier_check": { + "name": "workspace_item_extractions_tier_check", + "value": "\"workspace_item_extractions\".\"tier\" is null or \"workspace_item_extractions\".\"tier\" in ('fast', 'enhanced')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_item_pages": { + "name": "workspace_item_pages", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_number": { + "name": "page_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "markdown_bytes": { + "name": "markdown_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_item_pages_item_id_workspace_items_id_fk": { + "name": "workspace_item_pages_item_id_workspace_items_id_fk", + "tableFrom": "workspace_item_pages", + "tableTo": "workspace_items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_item_pages_item_id_page_number_pk": { + "name": "workspace_item_pages_item_id_page_number_pk", + "columns": [ + "item_id", + "page_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_item_pages_number_check": { + "name": "workspace_item_pages_number_check", + "value": "\"workspace_item_pages\".\"page_number\" > 0" + }, + "workspace_item_pages_bytes_check": { + "name": "workspace_item_pages_bytes_check", + "value": "\"workspace_item_pages\".\"markdown_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_item_relations": { + "name": "workspace_item_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_item_id": { + "name": "from_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_item_id": { + "name": "to_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_item_relations_unique": { + "name": "workspace_item_relations_unique", + "columns": [ + { + "expression": "from_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "note", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_item_relations_from_idx": { + "name": "workspace_item_relations_from_idx", + "columns": [ + { + "expression": "from_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_item_relations_to_idx": { + "name": "workspace_item_relations_to_idx", + "columns": [ + { + "expression": "to_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_relations_from_fk": { + "name": "workspace_item_relations_from_fk", + "tableFrom": "workspace_item_relations", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "from_item_id" + ], + "columnsTo": [ + "workspace_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_item_relations_to_fk": { + "name": "workspace_item_relations_to_fk", + "tableFrom": "workspace_item_relations", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "to_item_id" + ], + "columnsTo": [ + "workspace_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_item_relations_kind_check": { + "name": "workspace_item_relations_kind_check", + "value": "\"workspace_item_relations\".\"kind\" in ('derived_from', 'references')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_item_user_states": { + "name": "workspace_item_user_states", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_item_user_states_item_id_idx": { + "name": "workspace_item_user_states_item_id_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_user_states_user_id_user_id_fk": { + "name": "workspace_item_user_states_user_id_user_id_fk", + "tableFrom": "workspace_item_user_states", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_item_user_states_item_id_workspace_items_id_fk": { + "name": "workspace_item_user_states_item_id_workspace_items_id_fk", + "tableFrom": "workspace_item_user_states", + "tableTo": "workspace_items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_item_user_states_user_id_item_id_pk": { + "name": "workspace_item_user_states_user_id_item_id_pk", + "columns": [ + "user_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_items": { + "name": "workspace_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_key": { + "name": "name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "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": { + "workspace_items_tree_idx": { + "name": "workspace_items_tree_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_items_type_idx": { + "name": "workspace_items_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_items_root_name_unique": { + "name": "workspace_items_root_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_items\".\"parent_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_items_parent_name_unique": { + "name": "workspace_items_parent_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_items\".\"parent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_items_workspace_id_workspaces_id_fk": { + "name": "workspace_items_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_items", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_items_parent_fk": { + "name": "workspace_items_parent_fk", + "tableFrom": "workspace_items", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "parent_id" + ], + "columnsTo": [ + "workspace_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_items_workspace_id_id_unique": { + "name": "workspace_items_workspace_id_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_items_type_check": { + "name": "workspace_items_type_check", + "value": "\"workspace_items\".\"type\" in ('folder', 'document', 'flashcard', 'file')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "last_opened_at": { + "name": "last_opened_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_members_workspace_user_unique": { + "name": "workspace_members_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_members_user_id_idx": { + "name": "workspace_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_members_user_last_opened_at_idx": { + "name": "workspace_members_user_last_opened_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_members_workspace_id_workspaces_id_fk": { + "name": "workspace_members_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_user_id_user_id_fk": { + "name": "workspace_members_user_id_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_members_role_check": { + "name": "workspace_members_role_check", + "value": "\"workspace_members\".\"role\" in ('owner', 'admin', 'editor', 'viewer')" + } + }, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owner_id": { + "name": "owner_id", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspaces_owner_id_idx": { + "name": "workspaces_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_archived_at_idx": { + "name": "workspaces_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_owner_id_user_id_fk": { + "name": "workspaces_owner_id_user_id_fk", + "tableFrom": "workspaces", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle-postgres/meta/_journal.json b/drizzle-postgres/meta/_journal.json index 29d845db7..a7d5371d6 100644 --- a/drizzle-postgres/meta/_journal.json +++ b/drizzle-postgres/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1786634339603, "tag": "0002_wild_kat_farrell", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1786643511996, + "tag": "0003_glorious_wolf_cub", + "breakpoints": true } ] } \ No newline at end of file diff --git a/patches/agents@0.19.0.patch b/patches/agents@0.19.0.patch index 262a64a88..957d5f6cc 100644 --- a/patches/agents@0.19.0.patch +++ b/patches/agents@0.19.0.patch @@ -11,7 +11,7 @@ index 8d5b57034f927298d207d8a2bcbac927614780f5..9cf49a4471b9da862ddde11b818fd1ee } /** * For a single message, resolve its ID by matching toolCallId against server state. -@@ -1948,17 +1948,21 @@ function assistantContentKey(message, sanitize) { +@@ -1948,23 +1948,27 @@ function assistantContentKey(message, sanitize) { return JSON.stringify(sanitized.parts); } function mergeServerToolOutputs(incoming, serverMessages) { @@ -36,6 +36,15 @@ index 8d5b57034f927298d207d8a2bcbac927614780f5..9cf49a4471b9da862ddde11b818fd1ee let hasChanges = false; const updatedParts = msg.parts.map((part) => { const record = part; +- if ("toolCallId" in record && "state" in record && (record.state === "input-available" || record.state === "approval-requested" || record.state === "approval-responded") && serverResolvedParts.has(record.toolCallId)) { +- hasChanges = true; +- const server = serverResolvedParts.get(record.toolCallId); ++ if ("toolCallId" in record && "state" in record && (record.state === "input-available" || record.state === "approval-requested" || record.state === "approval-responded") && serverResolvedParts.has(record.toolCallId)) { ++ hasChanges = true; ++ const server = serverResolvedParts.get(record.toolCallId); + const merged = { + ...part, + state: server.state @@ -1999,7 +2003,22 @@ function reconcileAssistantIds(incoming, serverMessages, sanitize) { } return incoming.map((incomingMessage, incomingIdx) => { @@ -71,7 +80,7 @@ index 8d5b57034f927298d207d8a2bcbac927614780f5..9cf49a4471b9da862ddde11b818fd1ee for (const msg of messages) { if (msg.role !== "assistant") continue; diff --git a/dist/chat/react.js b/dist/chat/react.js -index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2623a2b43 100644 +index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..44059d63d10b5f1ae192eb7a6e63f0df61ddf49e 100644 --- a/dist/chat/react.js +++ b/dist/chat/react.js @@ -253,7 +253,7 @@ var WebSocketChatTransport = class { @@ -83,23 +92,79 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 }; agent.addEventListener("message", onMessage, { signal: abortController.signal }); agent.addEventListener("close", onClose, { signal: abortController.signal }); -@@ -967,6 +967,7 @@ function useAgentChat(options) { +@@ -716,9 +716,35 @@ function prependMissingHydratedMessages(hydratedMessages, currentMessages) { + const missingHydratedMessages = hydratedMessages.filter((message) => !currentMessageIds.has(message.id)); + if (missingHydratedMessages.length === 0) return currentMessages; + return [...missingHydratedMessages, ...currentMessages]; + } ++function continuationPartsOverlap(baseline, current) { ++ if (baseline.type !== current.type) return false; ++ if ("toolCallId" in baseline || "toolCallId" in current) return baseline.toolCallId === current.toolCallId; ++ if ("id" in baseline && "id" in current && typeof baseline.id === "string" && typeof current.id === "string") return baseline.id === current.id; ++ if ((baseline.type === "text" || baseline.type === "reasoning") && "text" in baseline && "text" in current) return current.text.startsWith(baseline.text); ++ return JSON.stringify(baseline) === JSON.stringify(current); ++} ++function preserveContinuationBaseline(messages, baselineMessages) { ++ const baselineAssistant = findLastAssistantMessage(baselineMessages)?.message; ++ if (!baselineAssistant) return messages; ++ const assistantIndex = messages.findIndex((message) => message.id === baselineAssistant.id); ++ if (assistantIndex < 0) return messages; ++ const assistant = messages[assistantIndex]; ++ let overlapLength = Math.min(baselineAssistant.parts.length, assistant.parts.length); ++ while (overlapLength > 0 && !baselineAssistant.parts.slice(-overlapLength).every((part, index) => continuationPartsOverlap(part, assistant.parts[index]))) overlapLength--; ++ const missingParts = baselineAssistant.parts.slice(0, baselineAssistant.parts.length - overlapLength); ++ const missingMetadata = assistant.metadata == null && baselineAssistant.metadata != null; ++ if (missingParts.length === 0 && !missingMetadata) return messages; ++ const next = [...messages]; ++ next[assistantIndex] = { ++ ...assistant, ++ parts: [...structuredClone(missingParts), ...assistant.parts], ++ ...missingMetadata ? { metadata: structuredClone(baselineAssistant.metadata) } : {} ++ }; ++ return next; ++} + /** + * React hook for building AI chat interfaces using an Agent + * @param options Chat options including the agent connection + * @returns Chat interface controls and state with added clearHistory method + */ +@@ -967,6 +990,25 @@ function useAgentChat(options) { const [clientToolResults, setClientToolResults] = useState(/* @__PURE__ */ new Map()); const messagesRef = useRef(chatMessages); messagesRef.current = chatMessages; + const continuationStartMessagesRef = useRef(/* @__PURE__ */ new Map()); ++ const transportContinuationRequestIdRef = useRef(null); ++ const completedTransportContinuationRequestIdRef = useRef(null); ++ useEffect(() => { ++ const requestId = transportContinuationRequestIdRef.current; ++ if (!requestId) return; ++ const baseline = continuationStartMessagesRef.current.get(requestId); ++ if (!baseline) return; ++ const next = preserveContinuationBaseline(chatMessages, baseline); ++ if (next !== chatMessages) { ++ setMessages(next); ++ return; ++ } ++ if (completedTransportContinuationRequestIdRef.current === requestId) { ++ continuationStartMessagesRef.current.delete(requestId); ++ transportContinuationRequestIdRef.current = null; ++ completedTransportContinuationRequestIdRef.current = null; ++ } ++ }, [chatMessages, setMessages, status]); const initialMessagesRef = useRef(initialMessages); initialMessagesRef.current = initialMessages; const seededInitialMessagesKeyRef = useRef(null); -@@ -1088,6 +1089,7 @@ function useAgentChat(options) { +@@ -1088,6 +1130,9 @@ function useAgentChat(options) { pendingReplayResumeRequestIdsRef.current.clear(); fallbackAckedResumeRequestIdsRef.current.clear(); replayHydratedAssistantMessageIdsRef.current.clear(); + continuationStartMessagesRef.current.clear(); ++ transportContinuationRequestIdRef.current = null; ++ completedTransportContinuationRequestIdRef.current = null; protectedStreamingAssistantRef.current = null; }, [ markInitialMessagesSeeded, -@@ -1268,6 +1270,16 @@ function useAgentChat(options) { +@@ -1268,6 +1313,20 @@ function useAgentChat(options) { const [isRecovering, setIsRecovering] = useState(false); useEffect(() => { const localResponseIds = localResponseMessageIdsRef.current; @@ -107,6 +172,10 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 + const captureContinuationStart = (requestId) => { + if (!continuationStartMessages.has(requestId)) continuationStartMessages.set(requestId, structuredClone(messagesRef.current)); + }; ++ const completeContinuation = (requestId) => { ++ if (transportContinuationRequestIdRef.current === requestId) completedTransportContinuationRequestIdRef.current = requestId; ++ else continuationStartMessages.delete(requestId); ++ }; + const rewindContinuation = (requestId) => { + const baseline = continuationStartMessages.get(requestId); + if (!baseline) return; @@ -116,20 +185,25 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 /** * Unified message handler that parses JSON once and dispatches based on type. * Avoids duplicate parsing overhead from separate listeners. -@@ -1322,6 +1334,7 @@ function useAgentChat(options) { +@@ -1322,6 +1381,9 @@ function useAgentChat(options) { break; case "cf_agent_stream_resume_none": if (customTransport.handleStreamResumeNone(data) && data.reason === STREAM_RESUME_NONE_REASONS.IDLE && typeof data.probeId === "string") { + continuationStartMessages.clear(); ++ transportContinuationRequestIdRef.current = null; ++ completedTransportContinuationRequestIdRef.current = null; const result = transition(streamStateRef.current, { type: "clear" }); streamStateRef.current = result.state; setIsServerStreaming(result.isStreaming); -@@ -1337,9 +1350,14 @@ function useAgentChat(options) { +@@ -1337,9 +1399,17 @@ function useAgentChat(options) { if (!isEarlyToolContinuation) return; } if (!resumingToolContinuationRef.current) pendingReplayResumeRequestIdsRef.current.add(data.id); + if (customTransport.isAwaitingResume()) { + // AI SDK snapshots the last message as soon as this resolves. ++ captureContinuationStart(data.id); ++ transportContinuationRequestIdRef.current = data.id; ++ completedTransportContinuationRequestIdRef.current = null; + rewindContinuation(data.id); + } if (customTransport.handleStreamResuming(data)) return; @@ -139,7 +213,7 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 if (isEarlyToolContinuation) { pendingToolContinuationRef.current = false; observedToolContinuationRequestIdRef.current = data.id; -@@ -1369,6 +1387,7 @@ function useAgentChat(options) { +@@ -1369,6 +1439,7 @@ function useAgentChat(options) { const chunkData = JSON.parse(data.body); if (chunkData.type === "start" && typeof chunkData.messageId === "string") { localResponseIds.set(data.id, chunkData.messageId); @@ -147,7 +221,7 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 if (!data.continuation) { if (protectedStreamingAssistantRef.current?.assistantId !== chunkData.messageId) { const msgs = messagesRef.current; -@@ -1386,8 +1405,8 @@ function useAgentChat(options) { +@@ -1386,8 +1457,8 @@ function useAgentChat(options) { } } } catch {} @@ -158,11 +232,11 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 if (streamStateRef.current.status === "observing" && streamStateRef.current.streamId === data.id) { streamStateRef.current = { status: "idle" }; setIsServerStreaming(false); -@@ -1397,14 +1416,30 @@ function useAgentChat(options) { +@@ -1397,14 +1468,30 @@ function useAgentChat(options) { localResponseIds.delete(data.id); localRequestIdsRef.current.delete(data.id); fallbackAckedResumeRequestIdsRef.current.delete(data.id); -+ continuationStartMessages.delete(data.id); ++ completeContinuation(data.id); if (observedToolContinuationRequestIdRef.current === data.id) resetToolContinuation(); } return; @@ -173,7 +247,7 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 + pendingReplayResumeRequestIdsRef.current.delete(data.id); + customTransport.handleServerTurnCompleted(data.id); + fallbackAckedResumeRequestIdsRef.current.delete(data.id); -+ continuationStartMessages.delete(data.id); ++ completeContinuation(data.id); + setIsRecovering(false); + if (streamStateRef.current.status === "idle" || streamStateRef.current.streamId === data.id) { + const result = transition(streamStateRef.current, { type: "clear" }); @@ -189,14 +263,24 @@ index 51c24ed62b48d43a64a093b5d08e6ccb3aafcbf9..f9fa50a098030d9e99a48bcfe261d9e2 if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id && typeof chunkData.messageId === "string" && chunkData.type === "start") { pendingReplayResumeRequestIdsRef.current.delete(data.id); resetMatchingHydratedAssistantForReplay(chunkData.messageId); -@@ -1417,6 +1452,7 @@ function useAgentChat(options) { +@@ -1417,6 +1504,7 @@ function useAgentChat(options) { if (data.done) { customTransport.handleServerTurnCompleted(data.id); fallbackAckedResumeRequestIdsRef.current.delete(data.id); -+ continuationStartMessages.delete(data.id); ++ completeContinuation(data.id); setIsRecovering(false); } const completedObservedToolContinuation = data.done && observedToolContinuationRequestIdRef.current === data.id; +@@ -1493,7 +1585,9 @@ function useAgentChat(options) { + streamStateRef.current = { status: "idle" }; + setIsServerStreaming(false); + setIsRecovering(false); ++ transportContinuationRequestIdRef.current = null; ++ completedTransportContinuationRequestIdRef.current = null; + protectedStreamingAssistantRef.current = null; + localResponseIds.clear(); + customTransport.resetResumeState(); + invalidateResumeGeneration(); diff --git a/dist/wire-types-CU9rLoeS.js b/dist/wire-types-CU9rLoeS.js index a0f027305b8594ea5cec3edcbf4273cf878319e5..d565dbb7c92d0ac7ca0e4a14c51a2820834a07d0 100644 --- a/dist/wire-types-CU9rLoeS.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 353b1a4ac..86d74b571 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ overrides: patchedDependencies: '@cloudflare/think@0.15.0': 1325490ec006d39225ccf20fdf92d9c0a193a58c0427100f2b4b3c1b95a82576 '@embedpdf/plugin-zoom@2.15.0': 7bdd44c58d2eec07af954b81e11e168032c753d87e339354df3b5974505330a9 - agents@0.19.0: b4925f176922f992a0781ffa357da123a7e05876e924b4ff009585baa101bf4d + agents@0.19.0: 74127fdd44b0a88cbe724996b67baba9c5d32a1c23a2ecc78bbba1d604ff984c importers: @@ -43,7 +43,7 @@ importers: version: 0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.64(zod@4.4.3))(zod@4.4.3) '@cloudflare/think': specifier: ^0.15.0 - version: 0.15.0(patch_hash=1325490ec006d39225ccf20fdf92d9c0a193a58c0427100f2b4b3c1b95a82576)(@ai-sdk/provider@4.0.7)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(agents@0.19.0(patch_hash=b4925f176922f992a0781ffa357da123a7e05876e924b4ff009585baa101bf4d)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.64(zod@4.4.3))(react@19.2.8)(zod@4.4.3) + version: 0.15.0(patch_hash=1325490ec006d39225ccf20fdf92d9c0a193a58c0427100f2b4b3c1b95a82576)(@ai-sdk/provider@4.0.7)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(agents@0.19.0(patch_hash=74127fdd44b0a88cbe724996b67baba9c5d32a1c23a2ecc78bbba1d604ff984c)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.64(zod@4.4.3))(react@19.2.8)(zod@4.4.3) '@content-collections/core': specifier: ^0.15.2 version: 0.15.2(typescript@7.0.2) @@ -214,7 +214,7 @@ importers: version: 8.18.0 agents: specifier: ^0.19.0 - version: 0.19.0(patch_hash=b4925f176922f992a0781ffa357da123a7e05876e924b4ff009585baa101bf4d)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) + version: 0.19.0(patch_hash=74127fdd44b0a88cbe724996b67baba9c5d32a1c23a2ecc78bbba1d604ff984c)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) ai: specifier: ^7.0.64 version: 7.0.64(zod@4.4.3) @@ -10138,13 +10138,13 @@ snapshots: - ai - zod - '@cloudflare/think@0.15.0(patch_hash=1325490ec006d39225ccf20fdf92d9c0a193a58c0427100f2b4b3c1b95a82576)(@ai-sdk/provider@4.0.7)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(agents@0.19.0(patch_hash=b4925f176922f992a0781ffa357da123a7e05876e924b4ff009585baa101bf4d)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.64(zod@4.4.3))(react@19.2.8)(zod@4.4.3)': + '@cloudflare/think@0.15.0(patch_hash=1325490ec006d39225ccf20fdf92d9c0a193a58c0427100f2b4b3c1b95a82576)(@ai-sdk/provider@4.0.7)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(agents@0.19.0(patch_hash=74127fdd44b0a88cbe724996b67baba9c5d32a1c23a2ecc78bbba1d604ff984c)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.64(zod@4.4.3))(react@19.2.8)(zod@4.4.3)': dependencies: '@ai-sdk/anthropic': 4.0.21(zod@4.4.3) '@ai-sdk/openai': 4.0.20(zod@4.4.3) '@cloudflare/codemode': 0.5.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.64(zod@4.4.3))(zod@4.4.3) '@cloudflare/shell': 0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.64(zod@4.4.3))(zod@4.4.3) - agents: 0.19.0(patch_hash=b4925f176922f992a0781ffa357da123a7e05876e924b4ff009585baa101bf4d)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) + agents: 0.19.0(patch_hash=74127fdd44b0a88cbe724996b67baba9c5d32a1c23a2ecc78bbba1d604ff984c)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) ai: 7.0.64(zod@4.4.3) aywson: 0.0.16 chat: 4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3) @@ -13568,7 +13568,7 @@ snapshots: prompts: 2.4.2 yaml: 2.9.0 - agents@0.19.0(patch_hash=b4925f176922f992a0781ffa357da123a7e05876e924b4ff009585baa101bf4d)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3): + agents@0.19.0(patch_hash=74127fdd44b0a88cbe724996b67baba9c5d32a1c23a2ecc78bbba1d604ff984c)(@ai-sdk/react@4.0.67(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260812.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.9(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(ai@7.0.64(zod@4.4.3))(chat@4.31.0(ai@7.0.64(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3): dependencies: '@babel/plugin-proposal-decorators': 8.0.2(@babel/core@7.29.7) '@cfworker/json-schema': 4.1.1 diff --git a/src/db/schema.ts b/src/db/schema.ts index 0ccdd65f8..ffc550e61 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -14,11 +14,11 @@ import { unique, uniqueIndex, } from "drizzle-orm/pg-core"; +import { WORKSPACE_ITEM_TYPES } from "#/features/workspaces/workspace-item-registry"; const WORKSPACE_ROLES = ["owner", "admin", "editor", "viewer"] as const; const WORKSPACE_INVITE_TYPES = ["email", "link"] as const; const WORKSPACE_INVITE_STATUSES = ["pending", "accepted", "revoked", "expired"] as const; -const WORKSPACE_ITEM_TYPES = ["folder", "document", "file"] as const; const WORKSPACE_RELATION_KINDS = ["derived_from", "references"] as const; const WORKSPACE_EXTRACTION_STATUSES = ["processing", "ready", "failed"] as const; const WORKSPACE_EXTRACTION_TIERS = ["fast", "enhanced"] as const; @@ -297,6 +297,24 @@ export const workspaceItemContents = pgTable("workspace_item_contents", { content: text("content").notNull(), }); +export const workspaceItemUserStates = pgTable( + "workspace_item_user_states", + { + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + itemId: text("item_id") + .notNull() + .references(() => workspaceItems.id, { onDelete: "cascade" }), + state: jsonb("state").$type>().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + primaryKey({ columns: [table.userId, table.itemId] }), + index("workspace_item_user_states_item_id_idx").on(table.itemId), + ], +); + export const workspaceFileAssets = pgTable("workspace_file_assets", { itemId: text("item_id") .primaryKey() diff --git a/src/features/workspaces/ai/ai-thread-orchestration-contract.ts b/src/features/workspaces/ai/ai-thread-orchestration-contract.ts index 11cccd3c7..d9d499ac2 100644 --- a/src/features/workspaces/ai/ai-thread-orchestration-contract.ts +++ b/src/features/workspaces/ai/ai-thread-orchestration-contract.ts @@ -267,7 +267,8 @@ function getDocumentEditAction(call: z.output const receiptId = getDocumentEditReceiptMetadata(call.result); const lineChanges = asRecord(result.lineChanges); - return itemId && path && applied > 0 && receiptId + const isDocument = result.itemType === "document"; + return isDocument && itemId && path && applied > 0 && receiptId ? { itemId, kind: "document-edit" as const, diff --git a/src/features/workspaces/ai/ai-thread-orchestration.worker.test.ts b/src/features/workspaces/ai/ai-thread-orchestration.worker.test.ts index c6749f8cc..134d43fa5 100644 --- a/src/features/workspaces/ai/ai-thread-orchestration.worker.test.ts +++ b/src/features/workspaces/ai/ai-thread-orchestration.worker.test.ts @@ -143,6 +143,7 @@ describe("AI thread orchestration", () => { applied: 1, failed: [], itemId: "document-1", + itemType: "document", path: "/Notes", __thinkexUi: { documentEditReceiptId: "receipt-secret" }, }, @@ -170,6 +171,34 @@ describe("AI thread orchestration", () => { expect(JSON.stringify(telemetryOutput)).not.toContain("receipt-secret"); }); + it("does not turn flashcard edits into document review actions", () => { + const output = normalizeAIThreadOrchestrationOutput({ + status: "completed", + executionId: "execution-flashcard-edit", + result: null, + calls: [ + { + seq: 1, + connector: "tools", + method: "workspace_edit_item", + state: "applied", + requiresApproval: false, + args: { path: "/Biology", type: "flashcard" }, + result: { + applied: 1, + failed: [], + itemId: "flashcard-1", + itemType: "flashcard", + path: "/Biology", + __thinkexUi: { documentEditReceiptId: "should-not-be-used" }, + }, + }, + ], + }); + + expect(output.calls[0]).not.toHaveProperty("action"); + }); + it("fails closed when a completed runtime result contains a malformed child call", () => { const output = normalizeAIThreadOrchestrationOutput({ status: "completed", diff --git a/src/features/workspaces/ai/ai-thread-tool-ui-metadata.test.ts b/src/features/workspaces/ai/ai-thread-tool-ui-metadata.test.ts new file mode 100644 index 000000000..716423e5f --- /dev/null +++ b/src/features/workspaces/ai/ai-thread-tool-ui-metadata.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { attachDocumentEditReceiptMetadata } from "#/features/workspaces/ai/ai-thread-tool-ui-metadata"; + +describe("AI thread edit metadata", () => { + it("adds review metadata only to document edits", () => { + const document = attachDocumentEditReceiptMetadata( + { applied: 1, itemType: "document" }, + "receipt-1", + ); + const flashcards = attachDocumentEditReceiptMetadata( + { applied: 1, itemType: "flashcard" }, + "receipt-2", + ); + + expect(document).toMatchObject({ + __thinkexUi: { documentEditReceiptId: "receipt-1" }, + }); + expect(flashcards).not.toHaveProperty("__thinkexUi"); + }); +}); diff --git a/src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts b/src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts index 69a01d732..add4c4b47 100644 --- a/src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts +++ b/src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts @@ -7,7 +7,7 @@ interface AIThreadToolUiMetadata { } export function attachDocumentEditReceiptMetadata(output: unknown, receiptId: string) { - if (!isRecord(output)) { + if (!isRecord(output) || output.itemType !== "document") { return output; } diff --git a/src/features/workspaces/ai/workspace-tool-result-adapters.ts b/src/features/workspaces/ai/workspace-tool-result-adapters.ts index 0e029feb5..7d99d493f 100644 --- a/src/features/workspaces/ai/workspace-tool-result-adapters.ts +++ b/src/features/workspaces/ai/workspace-tool-result-adapters.ts @@ -49,7 +49,7 @@ const workspaceCreateItemsResultAdapter = defineWorkspaceToolResultAdapter({ z.object({ itemId: z.string(), path: z.string(), - type: z.enum(["document", "folder"]), + type: z.enum(["document", "flashcard", "folder"]), }), ), references: z.array(workspaceReferenceRecordSchema), diff --git a/src/features/workspaces/components/WorkspaceContent.tsx b/src/features/workspaces/components/WorkspaceContent.tsx index 96df09153..0982e6ccc 100644 --- a/src/features/workspaces/components/WorkspaceContent.tsx +++ b/src/features/workspaces/components/WorkspaceContent.tsx @@ -10,6 +10,7 @@ import { EmptyTitle, } from "#/components/ui/empty"; import { DocumentEditorSurface } from "#/features/workspaces/components/document-editor/DocumentEditorSurface"; +import { FlashcardViewer } from "#/features/workspaces/components/flashcards/FlashcardViewer"; import { WorkspaceClipboardIntakeDialog } from "#/features/workspaces/components/WorkspaceClipboardIntakeDialog"; import WorkspaceClickableEmptyState from "#/features/workspaces/components/WorkspaceClickableEmptyState"; import { useWorkspaceClipboardIntake } from "#/features/workspaces/components/useWorkspaceClipboardIntake"; @@ -42,7 +43,6 @@ import { type WorkspaceItem, type WorkspaceItemType, type WorkspaceSummary, - getWorkspaceItemContentKind, } from "#/features/workspaces/contracts"; import { getWorkspaceItemDisplay } from "#/features/workspaces/model/item-display"; import { @@ -500,7 +500,7 @@ function WorkspaceItemView({ }) { const viewCapabilities = useWorkspaceViewCapabilities(); - if (getWorkspaceItemContentKind(item.type) === "document") { + if (item.type === "document") { return ( ; + } + const { Icon: ItemIcon, iconClassName, surfaceClassName } = getWorkspaceItemDisplay(item); const itemViewContent = (
diff --git a/src/features/workspaces/components/WorkspaceCreateMenu.tsx b/src/features/workspaces/components/WorkspaceCreateMenu.tsx index 0e979d149..ebe5196af 100644 --- a/src/features/workspaces/components/WorkspaceCreateMenu.tsx +++ b/src/features/workspaces/components/WorkspaceCreateMenu.tsx @@ -22,10 +22,7 @@ import { WorkspaceViewerRoleBadge, } from "#/features/workspaces/components/workspace-viewer-ui"; import type { WorkspaceItemType } from "#/features/workspaces/contracts"; -import { - workspaceItemAcquisitionActions, - workspaceItemPrimaryCreateActions, -} from "#/features/workspaces/model/item-display"; +import { workspaceCreateMenuActionGroups } from "#/features/workspaces/model/item-display"; interface WorkspaceCreateMenuProps { parentId: string | null; @@ -96,24 +93,16 @@ function getWorkspaceCreateMenuActions({ }: WorkspaceCreateMenuProps & { onUploadFile: (parentId: string | null) => void; }) { - return [ - ...workspaceItemPrimaryCreateActions.map(({ type, label, Icon, iconClassName }) => ({ + return workspaceCreateMenuActionGroups.flatMap((group, index) => [ + ...(index > 0 ? [{ kind: "separator" as const, id: `create-${group.id}` }] : []), + ...group.actions.map((action) => ({ kind: "item" as const, - id: type, - label, - leading: , - onSelect: () => onCreateItem({ type, parentId }), + id: action.kind === "item" ? action.type : action.id, + label: action.label, + leading: , + ...(action.kind === "item" + ? { onSelect: () => onCreateItem({ type: action.type, parentId }) } + : { onSelect: () => onUploadFile(parentId) }), })), - ...workspaceItemAcquisitionActions.map( - ({ id, label, description, Icon, iconClassName, disabled }) => ({ - kind: "item" as const, - id, - label, - trailing: description, - disabled, - leading: , - onSelect: id === "upload-file" ? () => onUploadFile(parentId) : undefined, - }), - ), - ]; + ]); } diff --git a/src/features/workspaces/components/WorkspaceImageViewer.tsx b/src/features/workspaces/components/WorkspaceImageViewer.tsx index ab9ec7d2e..2240c93de 100644 --- a/src/features/workspaces/components/WorkspaceImageViewer.tsx +++ b/src/features/workspaces/components/WorkspaceImageViewer.tsx @@ -1,11 +1,12 @@ -import { useCallback, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { Spinner } from "#/components/ui/spinner"; import { useWorkspaceImageViewerTransform } from "#/features/workspaces/components/use-workspace-image-viewer-transform"; import { WorkspaceCaptureShortcuts, WorkspaceCaptureViewerFrame, } from "#/features/workspaces/components/WorkspaceCaptureChrome"; -import { useFileItemToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; +import { WorkspaceFileToolbar } from "#/features/workspaces/components/WorkspaceFileToolbar"; +import { useWorkspaceItemToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; import { WorkspaceImageRegionCaptureOverlay } from "#/features/workspaces/components/WorkspaceRegionCaptureOverlay"; import { useWorkspaceViewCapabilities } from "#/features/workspaces/components/workspace-view-policy"; import { renderImageRegionCapture } from "#/features/workspaces/components/workspace-image-capture"; @@ -66,17 +67,19 @@ function WorkspaceImageViewerContent({ isCaptureActive: captureActive, }); - useFileItemToolbar({ - capture: enableFileCapture - ? { - isActive: captureActive, - onToggle: toggleCapture, + const toolbar = useMemo( + () => ( + + ), + [captureActive, enableFileCapture, fileUrl, item.name, toggleCapture], + ); + useWorkspaceItemToolbar(viewInstanceId, toolbar); const handleImageLoad = useCallback(() => { setStatus("ready"); diff --git a/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx b/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx index c32f44c16..05acd2f2b 100644 --- a/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx +++ b/src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx @@ -1,4 +1,3 @@ -import type { Editor } from "@tiptap/react"; import { createContext, type Dispatch, @@ -6,33 +5,16 @@ import { type SetStateAction, use, useEffect, + useMemo, useState, } from "react"; import { TooltipProvider } from "#/components/ui/tooltip"; -import { DocumentToolbar } from "#/features/workspaces/components/document-editor/DocumentToolbar"; -import { WorkspaceFileToolbar } from "#/features/workspaces/components/WorkspaceFileToolbar"; -type WorkspaceItemToolbarRegistration = - | { - canEdit: boolean; - documentPath: string; - editor: Editor | null; - itemId: string; - kind: "document"; - slotId: string; - workspaceId: string; - } - | { - capture?: { - isActive: boolean; - onToggle: () => void; - }; - fileName: string; - fileUrl: string; - kind: "file"; - slotId: string; - }; +interface WorkspaceItemToolbarRegistration { + content: ReactNode; + slotId: string; +} interface WorkspaceItemToolbarContextValue { registrationsBySlotId: Record; @@ -53,141 +35,29 @@ export function WorkspaceItemToolbarProvider({ children }: { children: ReactNode ); } -export function useDocumentEditorToolbar({ - canEdit, - documentPath, - editor, - itemId, - slotId, - workspaceId, -}: { - canEdit: boolean; - documentPath: string; - editor: Editor | null; - itemId: string; - slotId: string; - workspaceId: string; -}) { - const context = use(WorkspaceItemToolbarContext); - const setRegistration = context?.setRegistration; - - useEffect(() => { - if (!setRegistration) { - return; - } - - const registration = { - canEdit, - documentPath, - editor, - itemId, - kind: "document" as const, - slotId, - workspaceId, - }; - setRegistration((current) => { - const existing = current[slotId]; - if ( - existing?.kind === "document" && - existing.canEdit === canEdit && - existing.documentPath === documentPath && - existing.editor === editor && - existing.itemId === itemId && - existing.slotId === slotId && - existing.workspaceId === workspaceId - ) { - return current; - } - - return { - ...current, - [slotId]: registration, - }; - }); - - return () => { - setRegistration((current) => { - if (current[slotId] !== registration) { - return current; - } - - const next = { ...current }; - delete next[slotId]; - - return next; - }); - }; - }, [canEdit, documentPath, editor, itemId, slotId, workspaceId, setRegistration]); -} - -export function useFileItemToolbar({ - capture, - fileName, - fileUrl, - slotId, -}: { - capture?: { - isActive: boolean; - onToggle: () => void; - }; - fileName: string; - fileUrl: string; - slotId: string; -}) { +export function useWorkspaceItemToolbar(slotId: string, content: ReactNode) { + const registration = useMemo(() => ({ content, slotId }), [content, slotId]); const context = use(WorkspaceItemToolbarContext); const setRegistration = context?.setRegistration; - const captureIsActive = capture?.isActive; - const captureOnToggle = capture?.onToggle; useEffect(() => { - if (!setRegistration) { - return; - } + if (!setRegistration) return; - const registeredCapture = captureOnToggle - ? { - isActive: Boolean(captureIsActive), - onToggle: captureOnToggle, - } - : undefined; - const registration = { - capture: registeredCapture, - fileName, - fileUrl, - kind: "file" as const, - slotId, - }; - setRegistration((current) => { - const existing = current[slotId]; - if ( - existing?.kind === "file" && - existing.fileName === fileName && - existing.fileUrl === fileUrl && - existing.capture?.isActive === registeredCapture?.isActive && - existing.capture?.onToggle === registeredCapture?.onToggle - ) { - return current; - } - - return { - ...current, - [slotId]: registration, - }; - }); + setRegistration((current) => + current[registration.slotId] === registration + ? current + : { ...current, [registration.slotId]: registration }, + ); return () => { setRegistration((current) => { - if (current[slotId] !== registration) { - return current; - } - + if (current[registration.slotId] !== registration) return current; const next = { ...current }; - delete next[slotId]; - + delete next[registration.slotId]; return next; }); }; - }, [captureIsActive, captureOnToggle, fileName, fileUrl, slotId, setRegistration]); + }, [registration, setRegistration]); } export function WorkspaceItemToolbarSlot({ @@ -200,29 +70,11 @@ export function WorkspaceItemToolbarSlot({ ? context?.registrationsBySlotId[activeToolbarSlotId] : null; - if (!activeToolbarSlotId || !registration) { - return null; - } + if (!registration) return null; return (
- - {registration.kind === "document" ? ( - - ) : ( - - )} - + {registration.content}
); } diff --git a/src/features/workspaces/components/WorkspaceLayout.tsx b/src/features/workspaces/components/WorkspaceLayout.tsx index 6ba7c40c3..2475fe0fc 100644 --- a/src/features/workspaces/components/WorkspaceLayout.tsx +++ b/src/features/workspaces/components/WorkspaceLayout.tsx @@ -1,9 +1,10 @@ import { useQueryClient } from "@tanstack/react-query"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { workspacePageQueryKey } from "#/features/workspaces/cache-keys"; import { applyWorkspacePageDeltaToCache } from "#/features/workspaces/cache-page"; import AiChatPanel from "#/features/workspaces/components/AiChatPanel"; import WorkspaceChatLayout from "#/features/workspaces/components/WorkspaceChatLayout"; +import { CreateFlashcardsDialog } from "#/features/workspaces/components/flashcards/CreateFlashcardsDialog"; import WorkspaceContextBar from "#/features/workspaces/components/WorkspaceContextBar"; import { hasActiveWorkspaceCapture } from "#/features/workspaces/components/WorkspaceCaptureChrome"; import WorkspaceDragProvider from "#/features/workspaces/components/WorkspaceDragProvider"; @@ -32,15 +33,19 @@ import type { WorkspaceLocation } from "#/features/workspaces/locations/workspac import { WorkspaceLocationProvider } from "#/features/workspaces/locations/workspace-location-context"; import { DocumentEditReviewProvider } from "#/features/workspaces/documents/document-edit-review-context"; import { isWorkspaceItemView } from "#/features/workspaces/model/view"; +import { getWorkspaceItemPath } from "#/features/workspaces/model/tree"; import { workspaceItemRequiresHeavyViewerRuntime } from "#/features/workspaces/model/workspace-file"; -import { getWorkspaceMobileChatSurfaceMode } from "#/features/workspaces/model/workspace-ui"; +import { + getActiveWorkspaceViewInstanceId, + getWorkspaceMobileChatSurfaceMode, +} from "#/features/workspaces/model/workspace-ui"; import { useWorkspaceNavigation } from "#/features/workspaces/navigation/useWorkspaceNavigation"; import { useWorkspaceRealtime } from "#/features/workspaces/realtime/use-workspace-presence"; import { useWorkspacePersistedStoresHydrated } from "#/features/workspaces/state/persisted-store-hydration"; import { useWorkspaceAiComposerDraftQuotes } from "#/features/workspaces/state/workspace-ai-composer-draft-store"; import { useWorkspaceSelectionItemIds } from "#/features/workspaces/state/workspace-selection-store"; import { - useWorkspaceItemViewStates, + useWorkspaceItemViewStatesByViewInstance, useWorkspaceUiSession, useWorkspaceUiStore, } from "#/features/workspaces/state/workspace-ui-store"; @@ -68,9 +73,10 @@ export function WorkspaceShell({ const queryClient = useQueryClient(); const createWorkspaceItemMutation = useCreateWorkspaceItemMutation(); const moveWorkspaceItemsMutation = useMoveWorkspaceItemsMutation(); + const [flashcardParentId, setFlashcardParentId] = useState(); const persistedStoresHydrated = useWorkspacePersistedStoresHydrated(); const ensureWorkspaceUiSession = useWorkspaceUiStore((state) => state.ensureWorkspaceSession); - const itemViewStatesByItemId = useWorkspaceItemViewStates(workspace.id); + const itemViewStatesByViewInstanceId = useWorkspaceItemViewStatesByViewInstance(workspace.id); const selectedQuotes = useWorkspaceAiComposerDraftQuotes(workspace.id); const setChatSurfaceMode = useWorkspaceUiStore((state) => state.setChatSurfaceMode); const toggleChatPanel = useWorkspaceUiStore((state) => state.toggleChatPanel); @@ -132,6 +138,11 @@ export function WorkspaceShell({ return; } + if (input.type === "flashcard") { + setFlashcardParentId(input.parentId); + return; + } + createWorkspaceItemMutation.mutate({ id: crypto.randomUUID(), workspaceId: workspace.id, @@ -178,7 +189,7 @@ export function WorkspaceShell({ const aiContextScope = { activeItem: isWorkspaceItemView(activeItem) ? activeItem : undefined, activeTabId: activeTab.id, - itemViewStatesByItemId, + itemViewStatesByViewInstanceId, itemsById, presentation, selectedItemIds, @@ -193,7 +204,7 @@ export function WorkspaceShell({ workspace={workspace} activeItem={activeItem} itemsById={itemsById} - toolbarSlotId={activeTab.id} + toolbarSlotId={getActiveWorkspaceViewInstanceId(presentation, activeTab.id)} onCreateItem={createWorkspaceItem} onCloseItemView={isWorkspaceItemView(activeItem) ? closeItemView : undefined} onNavigateToRoot={openWorkspaceRoot} @@ -290,6 +301,11 @@ export function WorkspaceShell({ ); + const flashcardParent = flashcardParentId ? itemsById.get(flashcardParentId) : undefined; + const flashcardDialogOpen = flashcardParentId === null || flashcardParent !== undefined; + const flashcardParentPath = flashcardParent + ? getWorkspaceItemPath(flashcardParent, itemsById) + : "/"; return ( @@ -298,6 +314,14 @@ export function WorkspaceShell({ {workspaceInteractionContent} + { + if (!open) setFlashcardParentId(undefined); + }} + /> diff --git a/src/features/workspaces/components/WorkspacePdfViewer.tsx b/src/features/workspaces/components/WorkspacePdfViewer.tsx index a0e897905..8b9210a68 100644 --- a/src/features/workspaces/components/WorkspacePdfViewer.tsx +++ b/src/features/workspaces/components/WorkspacePdfViewer.tsx @@ -38,13 +38,14 @@ import { import { TilingLayer, TilingPluginPackage } from "@embedpdf/plugin-tiling/react"; import { Viewport, ViewportPluginPackage } from "@embedpdf/plugin-viewport/react"; import { ZoomGestureWrapper, ZoomMode, ZoomPluginPackage } from "@embedpdf/plugin-zoom/react"; -import { type ReactNode, useCallback, useEffect, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { Spinner } from "#/components/ui/spinner"; import { WorkspaceCaptureShortcuts, WorkspaceCaptureViewerFrame, } from "#/features/workspaces/components/WorkspaceCaptureChrome"; -import { useFileItemToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; +import { WorkspaceFileToolbar } from "#/features/workspaces/components/WorkspaceFileToolbar"; +import { useWorkspaceItemToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; import { useWorkspacePaneHotkey, useWorkspacePaneRuntime, @@ -127,17 +128,19 @@ export default function WorkspacePdfViewer({ setIsCaptureActive((current) => !current); }, []); - useFileItemToolbar({ - capture: enableFileCapture - ? { - isActive: captureActive, - onToggle: toggleCapture, + const toolbar = useMemo( + () => ( + + ), + [captureActive, enableFileCapture, fileUrl, item.name, toggleCapture], + ); + useWorkspaceItemToolbar(viewInstanceId, toolbar); return (
@@ -315,6 +318,7 @@ function WorkspacePdfDocumentLoader({ itemId={itemId} onCaptureModeExit={onCaptureModeExit} onCaptureModeToggle={onCaptureModeToggle} + viewInstanceId={viewInstanceId} workspaceId={workspaceId} {...props} /> @@ -335,6 +339,7 @@ function WorkspacePdfDocumentContent({ itemId, onCaptureModeExit, onCaptureModeToggle, + viewInstanceId, workspaceId, }: { documentId: string; @@ -348,6 +353,7 @@ function WorkspacePdfDocumentContent({ itemId: string; onCaptureModeExit: () => void; onCaptureModeToggle: () => void; + viewInstanceId: string; workspaceId: string; }) { const [selectionPoint, setSelectionPoint] = useState(null); @@ -391,6 +397,7 @@ function WorkspacePdfDocumentContent({ @@ -540,10 +547,12 @@ function WorkspacePdfPage({ function WorkspacePdfItemViewStateReporter({ documentId, itemId, + viewInstanceId, workspaceId, }: { documentId: string; itemId: string; + viewInstanceId: string; workspaceId: string; }) { const { @@ -553,18 +562,17 @@ function WorkspacePdfItemViewStateReporter({ const setItemViewState = useWorkspaceUiStore((state) => state.setItemViewState); useEffect(() => { - setItemViewState(workspaceId, { - kind: "pdf-page", + setItemViewState(workspaceId, viewInstanceId, { itemId, - pageNumber: currentPage, + label: `p. ${currentPage}`, }); - }, [currentPage, itemId, setItemViewState, workspaceId]); + }, [currentPage, itemId, setItemViewState, viewInstanceId, workspaceId]); useEffect(() => { return () => { - clearItemViewState(workspaceId, itemId); + clearItemViewState(workspaceId, viewInstanceId); }; - }, [clearItemViewState, itemId, workspaceId]); + }, [clearItemViewState, viewInstanceId, workspaceId]); return null; } diff --git a/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx b/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx index 630d0c2ab..c3431078e 100644 --- a/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx @@ -1,5 +1,5 @@ import { Mic, Paperclip } from "lucide-react"; -import { type SetStateAction, useCallback, useEffect, useRef } from "react"; +import { type SetStateAction, useCallback, useRef } from "react"; import { type AttachmentsContext, @@ -41,7 +41,6 @@ import { useWorkspaceAiComposerDraftFiles, useWorkspaceAiComposerDraftStore, useWorkspaceAiComposerDraftText, - useWorkspaceAiComposerFocusRequest, } from "#/features/workspaces/state/workspace-ai-composer-draft-store"; import { cn } from "#/lib/utils"; @@ -98,8 +97,6 @@ export default function AiChatPromptInput({ (value: SetStateAction) => setDraftText(activeThreadId, value), [activeThreadId, setDraftText], ); - const focusRequest = useWorkspaceAiComposerFocusRequest(activeThreadId); - const clearFocusRequest = useWorkspaceAiComposerDraftStore((state) => state.clearFocusRequest); const dictation = useAiChatDictation({ input, setInput }); const draftFiles = useWorkspaceAiComposerDraftFiles(activeThreadId); const attachmentsReady = @@ -130,26 +127,6 @@ export default function AiChatPromptInput({ setInput, textareaRef, }); - useEffect(() => { - if (focusRequest === 0) { - return; - } - - const frame = requestAnimationFrame(() => { - const textarea = textareaRef.current; - if (!textarea) { - return; - } - - textarea.focus(); - const caret = textarea.value.length; - textarea.setSelectionRange(caret, caret); - clearFocusRequest(activeThreadId, focusRequest); - }); - - return () => cancelAnimationFrame(frame); - }, [activeThreadId, clearFocusRequest, focusRequest]); - const attachments: Omit = { add: addFiles, composerReady: canType, diff --git a/src/features/workspaces/components/ai-chat/AiChatThreadView.tsx b/src/features/workspaces/components/ai-chat/AiChatThreadView.tsx index 7a66db476..11db88254 100644 --- a/src/features/workspaces/components/ai-chat/AiChatThreadView.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatThreadView.tsx @@ -1,5 +1,5 @@ import { generateId } from "ai"; -import { useEffect, useState } from "react"; +import { useEffect, useEffectEvent, useState } from "react"; import type { PromptInputMessage } from "#/features/workspaces/components/ai-chat/ai-chat-prompt-input"; import type { AIThreadSummary } from "#/features/workspaces/ai/user-ai-agents"; @@ -12,9 +12,13 @@ import type { AiChatSendMessage, } from "#/features/workspaces/components/ai-chat/types"; import { useWorkspaceAiChat } from "#/features/workspaces/components/ai-chat/useWorkspaceAiChat"; +import { useWorkspaceAiAllowance } from "#/features/workspaces/ai/use-workspace-ai-allowance"; import type { WorkspaceAiContextScope } from "#/features/workspaces/model/workspace-ai-context-types"; import { buildWorkspaceAiContextSnapshot } from "#/features/workspaces/model/workspace-ai-context-snapshot"; -import { useWorkspaceAiComposerDraftStore } from "#/features/workspaces/state/workspace-ai-composer-draft-store"; +import { + useWorkspaceAiComposerDraftStore, + useWorkspaceAiDirectPrompt, +} from "#/features/workspaces/state/workspace-ai-composer-draft-store"; export default function AiChatThreadView({ context, @@ -47,6 +51,10 @@ export default function AiChatThreadView({ const clearDraftArtifacts = useWorkspaceAiComposerDraftStore( (state) => state.clearDraftArtifacts, ); + const directPrompt = useWorkspaceAiDirectPrompt(threadId); + const setDraftText = useWorkspaceAiComposerDraftStore((state) => state.setText); + const takeDirectPrompt = useWorkspaceAiComposerDraftStore((state) => state.takeDirectPrompt); + const { isBlocked } = useWorkspaceAiAllowance(modelId); useEffect(() => { onRecoveringChange?.(presentation.isRecovering); @@ -71,7 +79,7 @@ export default function AiChatThreadView({ } }; - const sendMessage = (message: PromptInputMessage) => { + const sendMessage = (message: PromptInputMessage, clearDraft = true) => { const chatMessage = getChatMessageFromPrompt(message, generateId()); if (!chatMessage) { @@ -84,8 +92,35 @@ export default function AiChatThreadView({ }, }); setSentMessageAnimationId(chatMessage.id); - clearDraftArtifacts(context.workspaceId, threadId); + if (clearDraft) clearDraftArtifacts(context.workspaceId, threadId); }; + const sendDirectPrompt = useEffectEvent((text: string) => { + sendMessage({ files: [], text }, false); + }); + useEffect(() => { + if (!directPrompt) return; + if (isBlocked || connectionError) { + const text = takeDirectPrompt(threadId, directPrompt.id); + if (text) { + queueMicrotask(() => + setDraftText(threadId, (current) => (current.trim() ? `${current}\n\n${text}` : text)), + ); + } + return; + } + if (!canSend || inputStatus !== "ready") return; + const text = takeDirectPrompt(threadId, directPrompt.id); + if (text) queueMicrotask(() => sendDirectPrompt(text)); + }, [ + canSend, + connectionError, + directPrompt, + inputStatus, + isBlocked, + setDraftText, + takeDirectPrompt, + threadId, + ]); return (
@@ -109,7 +144,7 @@ export default function AiChatThreadView({ modelId={modelId} status={inputStatus} onModelChange={onModelChange} - onSubmit={sendMessage} + onSubmit={(message) => sendMessage(message)} onStop={() => { stopChatAndBrowser(); }} diff --git a/src/features/workspaces/components/ai-chat/agents-use-agent-chat.test.tsx b/src/features/workspaces/components/ai-chat/agents-use-agent-chat.test.tsx index 1fa071a68..5d001654f 100644 --- a/src/features/workspaces/components/ai-chat/agents-use-agent-chat.test.tsx +++ b/src/features/workspaces/components/ai-chat/agents-use-agent-chat.test.tsx @@ -120,7 +120,9 @@ function continuationBaseline() { }, { id: "assistant-1", + metadata: { phase: "before-continuation" }, parts: [ + { text: "already ", type: "text" }, { input: { query: "topic" }, output: { result: "found" }, @@ -313,8 +315,11 @@ describe("agents useAgentChat observer", () => { dispatchContinuation(target, "continuation-1", "text-1"); }); await vi.waitFor(() => { - expect(assistantText(chat()?.messages)).toBe("already streamed"); + expect(assistantText(chat()?.messages)).toBe("already already streamed"); expect(assistantHasTool(chat()?.messages)).toBe(true); + expect(lastAssistant(chat()?.messages)?.metadata).toEqual({ + phase: "before-continuation", + }); }); await act(async () => { @@ -339,7 +344,42 @@ describe("agents useAgentChat observer", () => { }); await vi.waitFor(() => { - expect(assistantText(chat()?.messages)).toBe("already streamed"); + expect(assistantText(chat()?.messages)).toBe("already already streamed"); + }); + }); + + it("keeps a text-only baseline when a resumed text part starts", async () => { + const { agent, sentFrames, target } = createFakeAgent(); + const baseline = continuationBaseline(); + baseline[1].parts.splice(1); + const chat = await mountChat(agent, baseline); + + await vi.waitFor(() => { + expect(countFrames(sentFrames, "cf_agent_stream_resume_request")).toBe(1); + }); + await act(async () => { + dispatch(target, { + id: "continuation-1", + type: "cf_agent_stream_resuming", + }); + }); + await vi.waitFor(() => { + expect(countFrames(sentFrames, "cf_agent_stream_resume_ack")).toBe(1); + }); + + await act(async () => { + dispatchChunk(target, "continuation-1", { + messageId: "assistant-1", + type: "start", + }); + dispatchChunk(target, "continuation-1", { + id: "text-1", + type: "text-start", + }); + }); + + await vi.waitFor(() => { + expect(assistantText(chat()?.messages)).toBe("already "); }); }); @@ -365,7 +405,7 @@ describe("agents useAgentChat observer", () => { dispatchContinuation(target, "continuation-2", "text-2"); }); await vi.waitFor(() => { - expect(assistantText(chat()?.messages)).toBe("already streamed"); + expect(assistantText(chat()?.messages)).toBe("already already streamed"); expect(assistantHasTool(chat()?.messages)).toBe(true); }); @@ -388,7 +428,7 @@ describe("agents useAgentChat observer", () => { }); await vi.waitFor(() => { - expect(assistantText(chat()?.messages)).toBe("already streamed"); + expect(assistantText(chat()?.messages)).toBe("already already streamed"); }); }); }); diff --git a/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.test.ts b/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.test.ts new file mode 100644 index 000000000..8d6df945f --- /dev/null +++ b/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { getAiChatDocumentEditGroups } from "#/features/workspaces/components/ai-chat/ai-chat-document-edit-actions"; +import type { AiChatRenderablePart } from "#/features/workspaces/components/ai-chat/ai-chat-display-state"; + +describe("AI chat document edit actions", () => { + it("does not offer document review for an applied flashcard edit", () => { + expect( + getAiChatDocumentEditGroups([ + editPart("flashcard", "call-flashcard"), + editPart("document", "call-document"), + ]), + ).toEqual([ + { + itemId: "item-document", + lineChanges: { added: 2, removed: 1 }, + path: "/Document", + receiptIds: ["call-document"], + }, + ]); + }); +}); + +function editPart(itemType: "document" | "flashcard", toolCallId: string) { + return { + input: {}, + output: { + applied: 1, + itemId: `item-${itemType}`, + itemType, + lineChanges: { added: 2, removed: 1 }, + path: itemType === "document" ? "/Document" : "/Flashcards", + }, + state: "output-available", + toolCallId, + type: "tool-workspace_edit_item", + } as AiChatRenderablePart; +} diff --git a/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts b/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts index bb67e51fb..59dbea208 100644 --- a/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts +++ b/src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts @@ -44,10 +44,12 @@ export function getAiChatDocumentEditGroups( const path = typeof output.path === "string" ? output.path : null; const itemId = typeof output.itemId === "string" ? output.itemId : null; const applied = typeof output.applied === "number" ? output.applied : 0; - if (itemId && path && applied > 0) { + const lineChanges = readLineChanges(output.lineChanges); + const isDocument = output.itemType === "document"; + if (isDocument && itemId && path && applied > 0) { addToGroup(groupsByItemId, seenReceiptIds, { itemId, - lineChanges: readLineChanges(output.lineChanges), + lineChanges, path, receiptId: part.toolCallId, }); diff --git a/src/features/workspaces/components/ai-chat/ai-chat-tool-receipts.ts b/src/features/workspaces/components/ai-chat/ai-chat-tool-receipts.ts index 8a2eeeceb..7f605c3e9 100644 --- a/src/features/workspaces/components/ai-chat/ai-chat-tool-receipts.ts +++ b/src/features/workspaces/components/ai-chat/ai-chat-tool-receipts.ts @@ -166,8 +166,10 @@ export function getFinishedToolReceipt(input: { return summarizeWorkspaceBatch(input.output, { failureVerb: "create", successVerb: "Created", - typeFromItem: (item) => - getString(asRecord(item).type) === "folder" ? "folder" : "document", + typeFromItem: (item) => { + const type = getString(asRecord(item).type); + return type === "folder" ? "folder" : type === "flashcard" ? "flashcard set" : "document"; + }, }); case "workspace_delete_items": return summarizeWorkspaceBatch(input.output, { diff --git a/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx b/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx index 238136206..f1bb20dbd 100644 --- a/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx +++ b/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx @@ -2,13 +2,14 @@ import Collaboration from "@tiptap/extension-collaboration"; import CollaborationCaret from "@tiptap/extension-collaboration-caret"; import { EditorContent, useEditor } from "@tiptap/react"; import { useQuery } from "@tanstack/react-query"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Skeleton } from "#/components/ui/skeleton"; -import { stageComposerPrompt } from "#/features/workspaces/composer/workspace-composer-actions"; +import { sendComposerPrompt } from "#/features/workspaces/composer/workspace-composer-actions"; import { DocumentAskSelectionMenu } from "#/features/workspaces/components/document-editor/DocumentAskSelectionMenu"; +import { DocumentToolbar } from "#/features/workspaces/components/document-editor/DocumentToolbar"; import { DocumentWordCount } from "#/features/workspaces/components/document-editor/DocumentWordCount"; -import { useDocumentEditorToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; +import { useWorkspaceItemToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot"; import { useWorkspacePaneRuntime } from "#/features/workspaces/components/WorkspacePaneRuntime"; import { useWorkspaceMutationAccess } from "#/features/workspaces/components/workspace-mutation-access"; import { DocumentEditReviewExtension } from "#/features/workspaces/documents/document-edit-review-extension"; @@ -107,14 +108,19 @@ function DocumentEditorInstance({ }, }); - useDocumentEditorToolbar({ - canEdit: capabilities.canMutateContent, - documentPath, - editor: capabilities.canMutateContent ? editor : null, - itemId: item.id, - slotId: viewInstanceId, - workspaceId, - }); + const toolbar = useMemo( + () => ( + + ), + [capabilities.canMutateContent, documentPath, editor, item.id, workspaceId], + ); + useWorkspaceItemToolbar(viewInstanceId, toolbar); useDocumentEditReviewOverlay({ canEdit: capabilities.canMutateContent, editor, @@ -135,7 +141,7 @@ function DocumentEditorInstance({ onAskAiToFix={ capabilities.canMutateContent ? (error) => - stageComposerPrompt( + sendComposerPrompt( workspaceId, `A widget in ${documentPath} hit this error. Please fix it:\n\n${error}`, ) diff --git a/src/features/workspaces/components/flashcards/CreateFlashcardsDialog.tsx b/src/features/workspaces/components/flashcards/CreateFlashcardsDialog.tsx new file mode 100644 index 000000000..b113d21f7 --- /dev/null +++ b/src/features/workspaces/components/flashcards/CreateFlashcardsDialog.tsx @@ -0,0 +1,98 @@ +import { Layers3 } from "lucide-react"; +import { useId } from "react"; + +import { Button } from "#/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "#/components/ui/dialog"; +import { Field, FieldGroup, FieldLabel } from "#/components/ui/field"; +import { NativeSelect, NativeSelectOption } from "#/components/ui/native-select"; +import { Textarea } from "#/components/ui/textarea"; +import { sendComposerPrompt } from "#/features/workspaces/composer/workspace-composer-actions"; + +export function CreateFlashcardsDialog({ + open, + parentPath, + workspaceId, + onOpenChange, +}: { + open: boolean; + parentPath: string; + workspaceId: string; + onOpenChange: (open: boolean) => void; +}) { + const topicId = useId(); + const countId = useId(); + + return ( + + {open ? ( + +
{ + const rawTopic = formData.get("topic"); + const topic = typeof rawTopic === "string" ? rawTopic.trim() : ""; + const count = Number(formData.get("count")); + if (!topic || !Number.isInteger(count)) return; + if ( + !sendComposerPrompt( + workspaceId, + `Create a flashcard set with exactly ${count} cards ${describeFlashcardLocation(parentPath)}. Cover: ${topic}`, + ) + ) + return; + onOpenChange(false); + }} + > + + + + AI will create the set in your current chat. + + + + What should the cards cover? +