fix(ai): wire up streaming generate dialog (event names + JSON extract + loading state)
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.
This commit is contained in:
parent
740c921e12
commit
efdf902b80
2 changed files with 25 additions and 8 deletions
|
|
@ -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, () => {
|
|||
|
||||
<div v-if="status !== 'idle'" class="grid gap-2">
|
||||
<Label class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.ai.generate.preview_label') }}</Label>
|
||||
<div class="min-h-[120px] whitespace-pre-wrap rounded-lg border-2 border-foreground bg-card px-3 py-2 text-sm font-medium text-foreground shadow-2xs">{{ text || '...' }}</div>
|
||||
<div class="min-h-[120px] whitespace-pre-wrap rounded-lg border-2 border-foreground bg-card px-3 py-2 text-sm font-medium text-foreground shadow-2xs">{{ previewText || '...' }}</div>
|
||||
<p v-if="status === 'failed'" class="text-xs font-semibold text-rose-700">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
v-if="status === 'idle'"
|
||||
:disabled="! prompt.trim() || dispatching"
|
||||
v-if="status === 'idle' || status === 'streaming'"
|
||||
:loading="dispatching || status === 'streaming'"
|
||||
:disabled="! prompt.trim()"
|
||||
@click="startGeneration"
|
||||
>
|
||||
{{ $t('posts.ai.generate.start') }}
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ export const useAiStream = () => {
|
|||
subscribedName = channelName;
|
||||
|
||||
echo().private(channelName)
|
||||
.listen('.TextDelta', (e: TextDeltaEvent) => {
|
||||
.listen('.text_delta', (e: TextDeltaEvent) => {
|
||||
text.value += e.delta ?? '';
|
||||
})
|
||||
.listen('.StreamEnd', () => {
|
||||
.listen('.stream_end', () => {
|
||||
status.value = 'completed';
|
||||
})
|
||||
.listen('.Error', (e: ErrorEvent) => {
|
||||
.listen('.error', (e: ErrorEvent) => {
|
||||
status.value = 'failed';
|
||||
errorMessage.value = e?.message ?? 'AI generation failed';
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue