trypost/app/Http/Controllers/App/PostAiGenerateController.php

43 lines
1.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Http\Controllers\App;
use App\Http\Requests\App\Ai\GeneratePostContentRequest;
use App\Jobs\Ai\StreamPostContent;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Response;
class PostAiGenerateController extends Controller
{
public function generate(GeneratePostContentRequest $request, Post $post): JsonResponse
{
$workspace = $request->user()->currentWorkspace;
$this->authorize('update', $post);
$gate = Gate::inspect('useAi', $workspace->account);
if ($gate->denied()) {
return response()->json(['message' => $gate->message()], Response::HTTP_PAYMENT_REQUIRED);
}
fix: subscribe to AI generation channel before dispatching the job (#269) * fix: subscribe to AI generation channel before dispatching the job AiGenerateDialog posted the generate request and only subscribed to the broadcast channel once the response came back. Reverb's plain Channel (vendor/laravel/reverb/.../Channels/Channel.php) delivers broadcasts by iterating the in-memory connection list at send time only — no history, no replay. If the queued job started streaming text_delta/stream_end events before the private-channel subscribe handshake finished, those events were gone for good and the dialog hung on 'streaming' forever. Generate the generation_id client-side, await channel subscription confirmation (Echo's channel.subscribed(), backed by pusher:subscription_succeeded) before sending the generate request, and have the backend use the client-supplied id instead of minting its own. Related to #218. * fix: handle subscribe failure, dialog-close race, and i18n hardcoded strings Code review on the previous commit surfaced real gaps: - useAiStream only handled the subscribe-success path (.subscribed()). A definitive pusher:subscription_error (expired session, CSRF mismatch) fell through to the 5s timeout and was treated as success, reproducing the exact #218 hang via a different trigger. subscribe() now returns a boolean: true on confirm-or-ambiguous-timeout (proceed optimistically), false only on an explicit error (don't dispatch work nothing will ever hear). - AiGenerateDialog now bails without dispatching if the dialog is closed while awaiting subscription confirmation (up to 5s), and unsubscribes on a post-subscribe dispatch failure so stale listeners can't flip status to 'completed' after an error was already shown. - Extracted aiGenerationChannel() so the frontend has one place to update if the channel format changes, and cross-referenced the two remaining backend copies (PostAiGenerateController, StreamPostContent). - Replaced the two hardcoded English fallback error strings with i18n keys (posts.ai.generate.errors.*) across all 16 locales — this app only ships translated copy. - Simplified subscribe(): dropped a redundant "already settled" guard and the clearTimeout bookkeeping — Promise.resolve() already no-ops after the first call, so there was nothing to guard. * fix: subscribe before dispatch in AI post creation and image regeneration flows Extracts subscribePrivateChannel as a shared helper and applies the same subscribe-before-dispatch fix from the post edit flow (issue #218) to the other two flows that stream over private channels. Post creation wizard moves the StreamPostCreation dispatch out of AiPostWizard.vue and into Loading.vue, so the channel is subscribed before the job that broadcasts on it is dispatched — previously a full page navigation happened between dispatch and subscribe with no timeout fallback, so a lost event hung the page forever. * refactor: trim explanatory comments added in the subscribe-before-dispatch fix Rationale lives in the commit history, not scattered inline comments. * refactor: trim remaining explanatory comments from the subscribe-before-dispatch fix Same cleanup as the previous commit, applied to the files from the original post-edit-flow fix. * fix: dedup post-creation dispatch, surface field-level AI errors, harden edge cases Uses ShouldBeUnique on StreamPostCreation (scoped per-user) instead of an ad-hoc cache guard, matching the pattern already used by PublishToSocialPlatform and VerifyUpcomingPostConnections. Field-level validation errors (prompt/instruction) now render inline via InputError, matching how every other form in the app surfaces them, instead of a generic status banner. Also fixes a 422 response missing the `errors` key that Inertia's client silently swallows, adds error handling around the wizard's navigation to the loading page, and extracts the duplicated error-message parsing into a shared helper.
2026-08-10 19:06:37 +00:00
$generationId = $request->string('generation_id')->toString();
StreamPostContent::dispatch(
workspaceId: $workspace->id,
userId: $request->user()->id,
generationId: $generationId,
prompt: $request->string('prompt')->toString(),
currentContent: $request->input('current_content'),
);
return response()->json([
'generation_id' => $generationId,
'channel' => "user.{$request->user()->id}.ai-gen.{$generationId}",
], Response::HTTP_ACCEPTED);
}
}