From efdf902b8047e2e327a672f04fdecf985f230220 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 11 May 2026 21:42:50 -0300 Subject: [PATCH] fix(ai): wire up streaming generate dialog (event names + JSON extract + loading state) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline "Generate post with AI" dialog never showed any output, even after the queue job finished. Three independent issues stacked: 1. Event name mismatch in useAiStream. Listeners were CamelCased (.TextDelta, .StreamEnd, .Error) but laravel/ai broadcasts events with snake_case names via StreamEvent::type() (text_delta, stream_end, error). Echo never matched, the listener never fired, status stuck on 'streaming' forever. Renamed to snake_case. 2. The streamer agent shares the structured-output prompt template with PostContentGenerator, so the stream emits a JSON object like {"content":"...","image_title":"...","image_keywords":[...]}, not plain caption text. Added a previewText computed in AiGenerateDialog that JSON.parse's the accumulated text and grabs .content. Mid-stream the parse fails, so the preview stays empty (showing the existing '...' placeholder) until stream_end produces a complete JSON. 3. The Generate button used v-if="status === 'idle'" — it disappeared the instant generation started, leaving the user with no feedback. Swapped to v-if="status === 'idle' || status === 'streaming'" and used the Button component's built-in :loading prop so it stays visible with a spinner inside while the agent runs. --- .../components/posts/ai/AiGenerateDialog.vue | 27 +++++++++++++++---- resources/js/composables/echo/useAiStream.ts | 6 ++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/resources/js/components/posts/ai/AiGenerateDialog.vue b/resources/js/components/posts/ai/AiGenerateDialog.vue index 788190c1..bd3a1f6a 100644 --- a/resources/js/components/posts/ai/AiGenerateDialog.vue +++ b/resources/js/components/posts/ai/AiGenerateDialog.vue @@ -45,8 +45,24 @@ const startGeneration = async () => { } }; +// The streamer agent shares the structured-output prompt template with +// PostContentGenerator, so what comes through the stream is a JSON object +// like {"content": "...", "image_title": "...", ...}. We only want the +// content field — and only once the JSON is complete (mid-stream the parse +// fails and we show nothing, avoiding the typewriter-of-JSON effect). +const previewText = computed(() => { + if (! text.value) return ''; + try { + const parsed = JSON.parse(text.value); + if (parsed && typeof parsed.content === 'string') return parsed.content; + } catch { + // Mid-stream the JSON is incomplete — leave preview empty. + } + return ''; +}); + const apply = () => { - emit('apply', text.value); + emit('apply', previewText.value); open.value = false; }; @@ -56,7 +72,7 @@ const retry = () => { startGeneration(); }; -const canApply = computed(() => status.value === 'completed' && text.value.trim().length > 0); +const canApply = computed(() => status.value === 'completed' && previewText.value.trim().length > 0); const canRetry = computed(() => status.value === 'completed' || status.value === 'failed'); watch(open, () => { @@ -88,15 +104,16 @@ watch(open, () => {
-
{{ text || '...' }}
+
{{ previewText || '...' }}

{{ errorMessage }}