Conversation
⚡ A2UI Composer PR PreviewYour automated preview is successfully live:
|
There was a problem hiding this comment.
Code Review
This pull request adds support for streaming and displaying the model's thought process ('thinking' blocks) and introduces the ability to abort active prompt streams. Key changes include UI updates for the thinking blocks, a toggleable stop/send button, and backend integration using AbortController to handle stream cancellation. The review feedback suggests simplifying the AutoScrollDirective by using an input setter instead of ngOnChanges, cleaning up several inline type imports in favor of top-level imports, and using a more robust object property check for AbortError in the standalone client.
| export class AutoScrollDirective implements OnChanges { | ||
| @Input('a2uiComposerAutoScroll') autoScroll: string | undefined = ''; | ||
| private readonly el = inject(ElementRef); | ||
|
|
||
| ngOnChanges() { | ||
| requestAnimationFrame(() => { | ||
| this.el.nativeElement.scrollTop = this.el.nativeElement.scrollHeight; | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Using an input setter is more idiomatic in Angular for simple input-triggered side effects. It allows you to remove the OnChanges interface implementation and the ngOnChanges lifecycle hook entirely, simplifying the class structure.
export class AutoScrollDirective {
private readonly el = inject(ElementRef);
@Input('a2uiComposerAutoScroll') set autoScroll(value: string | undefined) {
requestAnimationFrame(() => {
this.el.nativeElement.scrollTop = this.el.nativeElement.scrollHeight;
});
}
}There was a problem hiding this comment.
...but please use the input signal, and not the @Input directive.
| ]; | ||
| } | ||
|
|
||
| private activeStreamResponse?: import('../llm-client/llm-client').LlmStreamResponse; |
There was a problem hiding this comment.
Avoid using inline imports for types that can be imported at the top of the file. Please add LlmStreamResponse to the existing imports from ../llm-client/llm-client at the top of the file.
| private activeStreamResponse?: import('../llm-client/llm-client').LlmStreamResponse; | |
| private activeStreamResponse?: LlmStreamResponse; |
| const config: import('@google/genai').GenerateContentConfig = systemInstruction | ||
| ? {systemInstruction} | ||
| : {}; |
There was a problem hiding this comment.
Avoid using inline imports for types that can be imported at the top of the file. Please add GenerateContentConfig to the imports from @google/genai at the top of the file.
| const config: import('@google/genai').GenerateContentConfig = systemInstruction | |
| ? {systemInstruction} | |
| : {}; | |
| const config: GenerateContentConfig = systemInstruction | |
| ? {systemInstruction} | |
| : {}; |
| const responseStream = await ai.models.generateContentStream(params); | ||
|
|
||
| const buffer: string[] = []; | ||
| const buffer: import('./llm-client').LlmStreamChunk[] = []; |
There was a problem hiding this comment.
| } catch (err: unknown) { | ||
| if (err instanceof Error && err.name === 'AbortError') { | ||
| isDone = true; |
There was a problem hiding this comment.
The instanceof Error check can sometimes be fragile or fail in certain environments (e.g., cross-realm/iframe environments or specific test runners) when dealing with DOMException or custom abort objects. Using a robust object property check (similar to the one used in chat-coordinator.ts) is safer and more consistent.
| } catch (err: unknown) { | |
| if (err instanceof Error && err.name === 'AbortError') { | |
| isDone = true; | |
| } catch (err: unknown) { | |
| if (err && typeof err === 'object' && 'name' in err && err.name === 'AbortError') { | |
| isDone = true; |
| const contentStream: AsyncIterable<import('./llm-client').LlmStreamChunk> = { | ||
| [Symbol.asyncIterator]() { | ||
| let localBufferIndex = 0; | ||
| return { | ||
| async next(): Promise<IteratorResult<string>> { | ||
| async next(): Promise<IteratorResult<import('./llm-client').LlmStreamChunk>> { |
There was a problem hiding this comment.
Avoid using inline imports for types that can be imported at the top of the file. Please use the top-level LlmStreamChunk import here as well.
| const contentStream: AsyncIterable<import('./llm-client').LlmStreamChunk> = { | |
| [Symbol.asyncIterator]() { | |
| let localBufferIndex = 0; | |
| return { | |
| async next(): Promise<IteratorResult<string>> { | |
| async next(): Promise<IteratorResult<import('./llm-client').LlmStreamChunk>> { | |
| const contentStream: AsyncIterable<LlmStreamChunk> = { | |
| [Symbol.asyncIterator]() { | |
| let localBufferIndex = 0; | |
| return { | |
| async next(): Promise<IteratorResult<LlmStreamChunk>> { |
| export class AutoScrollDirective implements OnChanges { | ||
| @Input('a2uiComposerAutoScroll') autoScroll: string | undefined = ''; | ||
| private readonly el = inject(ElementRef); | ||
|
|
||
| ngOnChanges() { | ||
| requestAnimationFrame(() => { | ||
| this.el.nativeElement.scrollTop = this.el.nativeElement.scrollHeight; | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
...but please use the input signal, and not the @Input directive.
|
|
||
| .thinking-header { | ||
| padding: 6px 10px; | ||
| font-weight: 500; |
There was a problem hiding this comment.
Pretty sure there's a material token that we should use here: font: var(--mat-sys-label-medium);
Note that this also removes the font-size property above (which is a good thing).
| /** | ||
| * Aborts the currently active prompt stream. | ||
| */ | ||
| protected abortPrompt(): void { |
There was a problem hiding this comment.
Bit of a nit, but I recommend cancelPrompt instead of abortPropmpt.
| /** | ||
| * Aborts the currently active streaming request if there is one. | ||
| */ | ||
| abortActiveStream(): void { |
There was a problem hiding this comment.
I recommend changing all usages of abort to cancel
| expect(streamResponse.complete).toBeDefined(); | ||
|
|
||
| const collectedChunks: string[] = []; | ||
| const collectedChunks: import('./llm-client').LlmStreamChunk[] = []; |
There was a problem hiding this comment.
Here too (and below), don't use inline imports.
| readonly thinking?: string; | ||
| } | ||
|
|
||
| export interface LlmStreamChunk { |
There was a problem hiding this comment.
Do we need both the LlmStreamChunk and LlmResponse since they're the same?
If we want to know if something is the complete response or just a chunk, maybe a boolean flag to differentiate between the cases would be simpler?
| notifyListeners(); | ||
| } catch (err) { | ||
| } catch (err: unknown) { | ||
| if (err instanceof Error && err.name === 'AbortError') { |
There was a problem hiding this comment.
Checking the name seems fragile.
At the very least, use a const here and where the Error is thrown from.
https://screencast.googleplex.com/cast/NTQ4OTk3NjE2OTk4ODA5Nnw1MzUwM2MyNS04YQ