open($request); if ($session instanceof JsonResponse) { return $session; } $document = $this->input($request); if (is_string($document)) { return $this->validationError(['body' => [$document]]); } $state = $this->state($session); if ($session->run_status === 'running') { return $this->error(__('The assistant is writing; wait for it to finish.', 'maze-graph'), 409); } if ((int) $request->input('rev', -1) !== $state['rev']) { return $this->error(__('A newer copy of this document exists. Reload to see it.', 'maze-graph'), 409, ['rev' => [(string) $state['rev']]]); } if ($state['working'] === $document) { return $this->noStore($this->ok(['rev' => $state['rev'], 'capped' => false])); } if ($state['rev'] >= StudioState::MAX_REVISIONS) { return $this->noStore($this->ok(['rev' => $state['rev'], 'capped' => true])); } $this->store->append($session->id, Transcript::event('studio_revision', $document)); return $this->noStore($this->ok(['rev' => $state['rev'] + 1, 'capped' => false])); } public function outline(Request $request, ModelGateway $gateway): JsonResponse { $session = $this->open($request); if ($session instanceof JsonResponse) { return $session; } $instruction = Redactor::cleanText((string) $request->input('instruction', '')); if (mb_strlen($instruction) > 1000) { return $this->validationError(['instruction' => [__('Use at most 1,000 characters.', 'maze-graph')]]); } $refused = $this->refusal($gateway); if ($refused !== null) { return $refused; } $started = $this->runs->start($session->id, DraftingPrompts::lead($this->state($session), 'plan', true) . DraftingPrompts::newOutline($instruction), $this->userId(), $this->clientId($request)); if ($started === null) { return $this->busy(); } $this->store->append($session->id, Transcript::event('studio_expect', ['kind' => 'outline'])); return $this->reply($session, $started); } public function saveOutline(Request $request): JsonResponse { $session = $this->open($request); if ($session instanceof JsonResponse) { return $session; } if ($session->run_status === 'running') { return $this->error(__('The assistant is writing; wait for it to finish.', 'maze-graph'), 409); } $sections = StudioState::cleanOutline(is_array($request->input('sections')) ? $request->input('sections') : []); if (is_string($sections)) { return $this->validationError(['sections' => [match ($sections) { 'heading_too_long' => __('A heading can have at most 200 characters.', 'maze-graph'), 'too_many' => sprintf( /* translators: %d: maximum number of sections. */ __('An outline can have at most %d sections.', 'maze-graph'), StudioState::MAX_SECTIONS ), default => __('Add at least one section with a heading.', 'maze-graph'), }]]); } $this->store->append($session->id, Transcript::event('studio_outline', ['sections' => $sections, 'approved' => $request->input('approved') === true])); return $this->noStore($this->ok($this->document($session))); } public function section(Request $request, ModelGateway $gateway): JsonResponse { $session = $this->open($request); if ($session instanceof JsonResponse) { return $session; } $state = $this->state($session); $key = (string) $request->route('key'); $section = null; foreach ($state['outline']['sections'] ?? [] as $candidate) { if ($candidate['key'] === $key) { $section = $candidate; } } if ($section === null) { return $this->validationError(['key' => [__('That section is not in the outline.', 'maze-graph')]]); } $instruction = Redactor::cleanText((string) $request->input('instruction', '')); if (mb_strlen($instruction) > 500) { return $this->validationError(['instruction' => [__('Use at most 500 characters.', 'maze-graph')]]); } $refused = $this->refusal($gateway); if ($refused !== null) { return $refused; } $started = $this->runs->start($session->id, DraftingPrompts::lead($state, 'write', true) . DraftingPrompts::section($state['outline']['sections'], $section, $instruction), $this->userId(), $this->clientId($request)); if ($started === null) { return $this->busy(); } $this->store->append($session->id, Transcript::event('studio_expect', ['kind' => 'section', 'key' => $key, 'heading' => $section['heading']])); return $this->reply($session, $started); } public function resolve(Request $request): JsonResponse { $session = $this->open($request); if ($session instanceof JsonResponse) { return $session; } if ($session->run_status === 'running') { return $this->error(__('The assistant is writing; wait for it to finish.', 'maze-graph'), 409); } $action = (string) $request->input('action', ''); if (!in_array($action, ['accept', 'discard'], true)) { return $this->validationError(['action' => [__('Choose accept or discard.', 'maze-graph')]]); } $state = $this->state($session); if ($state['proposal'] === null) { return $this->error(__('There is no rewrite waiting.', 'maze-graph'), 409); } $this->store->append($session->id, Transcript::event('studio_resolve', ['action' => $action])); if ($action === 'accept') { $this->store->append($session->id, Transcript::event('studio_revision', $state['proposal'] + ['source' => 'rewrite'])); } return $this->noStore($this->ok($this->document($session))); } /** * The document's session, or the reply that says why not. */ protected function open(Request $request): Session|JsonResponse { if (!$this->on()) { return $this->off(); } return $this->session($request) ?? $this->missing(); } }