Skip to content

feat: dispatch custom DOM events for AI generation success/failure#180

Merged
jjroelofs merged 6 commits into1.xfrom
jur/1.x/4414-dispatch-ai-generation-events
Mar 27, 2026
Merged

feat: dispatch custom DOM events for AI generation success/failure#180
jjroelofs merged 6 commits into1.xfrom
jur/1.x/4414-dispatch-ai-generation-events

Conversation

@jjroelofs
Copy link
Copy Markdown
Contributor

Summary

  • Dispatch dxpr:ai:generation:success CustomEvent on document after successful AI generation
  • Dispatch dxpr:ai:generation:failure CustomEvent on document after failed AI generation
  • Events include: model, promptLength, generationDurationMs, outputLength (success) / errorType, errorCode (failure)
  • No hard dependency on dxpr_builder — events are standard DOM CustomEvents that any listener can consume

Relates to dxpr/dxpr_builder#4414

Test plan

  • Generate AI content in CKEditor → verify dxpr:ai:generation:success event dispatched on document
  • Trigger AI generation failure → verify dxpr:ai:generation:failure event dispatched
  • Verify no errors when no event listeners are attached

Jurriaan Roelofs added 2 commits March 26, 2026 21:29
Emit dxpr:ai:generation:success and dxpr:ai:generation:failure
CustomEvents on document after AI generation completes. Events include
model, promptLength, generationDurationMs, and error details. This
enables external analytics integration without hard dependencies.

Relates to dxpr/dxpr_builder#4414
@jjroelofs
Copy link
Copy Markdown
Contributor Author

Follow-up update pushed: 7601d7b

What changed:

  • Fix success event metrics to emit real outputLength (previously always 0 due to non-existent helper).
  • Add outputWordCount to success event detail for better parity with builder analytics contracts.
  • Include responseModel in success detail.
  • Remove duplicate processCompleted() call in streaming flow.

Warning: please include this latest commit in any release/update paired with dxpr/dxpr_builder#4427; otherwise CKEditor success analytics will under-report output metrics (notably output_length).

@jjroelofs
Copy link
Copy Markdown
Contributor Author

Review finding (P2): Canceled CKEditor generations can be emitted as success.

File: src/aiagentservice.ts:347

dxpr:ai:generation:success is dispatched unconditionally after handleStreamingResponse returns. Cancellation sets abortGeneration and calls llm.stop(this.stream); if that stops the stream cleanly (no throw), control reaches the success dispatch and records a false success instead of cancel/failure.

Suggested fix: add an abortGeneration guard before dispatching success (or propagate cancellation state).

@jjroelofs
Copy link
Copy Markdown
Contributor Author

Addressed the cancellation/success finding in src/aiagentservice.ts.

What changed:

  • Reset abortGeneration at generation start.
  • Added guard before success event dispatch to return when generation was canceled.

Commit: 223a080

This prevents canceled runs from emitting dxpr:ai:generation:success.

Also confirmed current PR checks are green:

  • Run Tests
  • translations-collect

Copy link
Copy Markdown
Contributor Author

@jjroelofs jjroelofs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Looks clean overall. A few notes:

generatedContent redundant intermediate assignment

In handleStreamingResponse, generatedContent = contentBuffer is assigned inside updateContent() (~line 407) on every content flush, but it's also assigned in the finally block (~line 447). The finally assignment is the correct/authoritative one since contentBuffer may have new data after the last flush. The intermediate assignment inside updateContent is now redundant — could be removed for clarity.

getOutputMetrics creates a temporary DOM element

The getOutputMetrics method (aiagentservice.ts:53-56) creates a <div>, sets innerHTML, and extracts textContent for word counting. This is fine since it's only called once at the end of generation (not per-chunk). Just noting the approach — it's simple and correct.

Good: double processCompleted removed

The duplicate this.processContentHelper.processCompleted(blockID) that used to run after the streaming finally block is correctly removed. The one inside finally is sufficient.

Good: cancel guard

The if (this.abortGeneration) return; check before dispatching the success event (aiagentservice.ts:349-351) correctly prevents a success event from firing when the user canceled mid-stream. The this.abortGeneration = false reset at the top of run() ensures clean state for each generation.


Severity Item
Low Redundant generatedContent = contentBuffer inside updateContent()
Cancel guard, double processCompleted fix, abort reset

@jjroelofs
Copy link
Copy Markdown
Contributor Author

Follow-up update pushed in eb314a1.

What changed:

  • Removed redundant intermediate generatedContent = contentBuffer assignment inside streaming updateContent().
  • Final metrics assignment remains in the finally block, which is the authoritative point for outputLength/outputWordCount calculation.

Earlier fixes remain in place:

  • Cancel guard prevents canceled runs from emitting success (223a080).
  • Success payload includes output metrics + responseModel (7601d7b).

Warning: include these latest plugin commits in any release/update paired with dxpr/dxpr_builder#4427, otherwise analytics parity between builder and CKEditor paths will drift.

Copy link
Copy Markdown
Contributor Author

@jjroelofs jjroelofs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (commit eb314a1)

chore: remove redundant streaming metrics assignment — Addressed

The redundant generatedContent = contentBuffer inside updateContent() was removed as suggested. The finally block assignment is now the sole place where generatedContent is set. Good.

Minor: accidental indentation change

The updateContent closure picked up an extra level of indentation (3 tabs → function body at 4 tabs, was 2 → 3). Cosmetic only but inconsistent with the surrounding code:

-		const updateContent = async () => {
-			if ( contentBuffer ) {
+			const updateContent = async () => {
+				if ( contentBuffer ) {

Worth a quick fix.

Overall

LGTM — all review items addressed.

@jjroelofs
Copy link
Copy Markdown
Contributor Author

Addressed the follow-up cosmetic note in 2f4b9a3.

  • Normalized indentation for updateContent in handleStreamingResponse to match surrounding style.
  • No behavior change.
  • Verified build: npm run ts:build

At this point all follow-up review items are addressed.

Copy link
Copy Markdown
Contributor Author

@jjroelofs jjroelofs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (commit 2f4b9a3)

style: normalize streaming updateContent indentation

Fixes the indentation I flagged — but only partially. updateInterval was re-indented to match updateContent, but now both are at 3 tabs instead of the expected 2:

		let generatedContent = '';          // 2 tabs ✓
			const updateInterval = ...;     // 3 tabs ✗
			const updateContent = ...;      // 3 tabs ✗

These local variables are inside handleStreamingResponse which is indented at 2 tabs. They should be at 2 tabs as well.

@jjroelofs
Copy link
Copy Markdown
Contributor Author

You were right — it was still over-indented.

I pushed 14da4cf to fully normalize tab depth in handleStreamingResponse:

  • method body statements at 2 tabs
  • nested block statements at 3+ tabs

Verified with visible-tab check and npm run ts:build.

@jjroelofs jjroelofs merged commit a21a8bd into 1.x Mar 27, 2026
2 checks passed
@jjroelofs jjroelofs deleted the jur/1.x/4414-dispatch-ai-generation-events branch March 27, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant