/** * TASK-0126 — AI page-generation endpoint (SSE). * * POST /workspaces/:workspaceId/ai/generate (body `{ prompt, parentId? }`, * header `Idempotency-Key?`) * * Route style mirrors `rag.controller.ts` (explicit `@Inject`, `ZodValidate` * body, `requireUser`, `@Res()` passthrough — the service owns the socket; * pre-stream failures throw → global filter JSON envelope). Frames: `tool` * (per-step progress), then a terminal `done` (with `pageId` + `created`), * `cancelled`, or `error` (invalid plan / step failure / timeout / budget). */ import { Body, Controller, Inject, Param, Post, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import { AiGenerateService } from "./generate.service.js"; import { ZodValidate } from "../../../common/pipes/zod-validation.pipe.js"; import { requireActor } from "../../../common/types.js"; import { SkipIdempotency } from "../../../common/middleware/idempotency.js"; import { AiGenerateDto, type AiGenerateInput } from "./generate.dto.js"; @Controller() export class AiGenerateController { constructor(@Inject(AiGenerateService) private readonly generateService: AiGenerateService) {} /** * Generate a page (SSE). Pre-stream: 404 (non-member workspace, fail-closed), * 403 (no EDIT), 402 (budget), 429 (shared ai.calls cap), 400/422 (validation). * First frames after the headers are the per-step `tool` progress events; the * stream ends with a terminal `done` (the created `pageId` + `created` counts) * or an in-band `error` / `cancelled`. * * The `Idempotency-Key` header memoizes a cancelled run's created page so a * retried request does not double-create (the 24 h idempotency contract). */ @Post("workspaces/:workspaceId/ai/generate") @SkipIdempotency() generatePage( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiGenerateDto)) body: AiGenerateInput, @Req() req: Request, @Res() res: Response, ) { const keyHeader = req.header("idempotency-key"); const idempotencyKey = keyHeader && keyHeader.trim() ? keyHeader.trim() : undefined; return this.generateService.generate(workspaceId, requireActor(req), body, idempotencyKey, req, res); } }