diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..4a854a2f Binary files /dev/null and b/.DS_Store differ diff --git a/package-lock.json b/package-lock.json index aadf9dea..52ce2f30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@viz-js/viz": "^3.12.0", "codemirror": "^6.0.2", "mermaid": "^11.14.0", - "ngx-json-viewer": "^3.2.1", "ngx-markdown": "^21.0.1", "ngx-vflow": "^1.16.4", "rxjs": "~7.8.0", @@ -34,6 +33,7 @@ "uuidv7": "^1.0.2", "vanilla-jsoneditor": "^3.6.0", "yaml": "^2.8.0", + "zod": "^4.4.3", "zone.js": "~0.15.0" }, "devDependencies": { @@ -88,6 +88,15 @@ "zod-to-json-schema": "^3.25.1" } }, + "node_modules/@a2ui/web_core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@algolia/abtesting": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", @@ -13124,15 +13133,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ngx-json-viewer": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ngx-json-viewer/-/ngx-json-viewer-3.2.1.tgz", - "integrity": "sha512-TTHtXsrBX+IXPqqAIsxklHPqSNmyGeQaziFZbCDJq1PnPOQmTrEHfwNrzN3LnWGhf7UxeM1cK0njegVPChwEcg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - } - }, "node_modules/ngx-markdown": { "version": "21.1.0", "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-21.1.0.tgz", @@ -17494,9 +17494,9 @@ "license": "MIT" }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 4244bf46..74d58df5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@viz-js/viz": "^3.12.0", "codemirror": "^6.0.2", "mermaid": "^11.14.0", - "ngx-json-viewer": "^3.2.1", "ngx-markdown": "^21.0.1", "ngx-vflow": "^1.16.4", "rxjs": "~7.8.0", @@ -39,6 +38,7 @@ "uuidv7": "^1.0.2", "vanilla-jsoneditor": "^3.6.0", "yaml": "^2.8.0", + "zod": "^4.4.3", "zone.js": "~0.15.0" }, "devDependencies": { diff --git a/src/app/components/add-item-dialog/add-item-dialog.component.ts b/src/app/components/add-item-dialog/add-item-dialog.component.ts index b27a9039..b8300046 100644 --- a/src/app/components/add-item-dialog/add-item-dialog.component.ts +++ b/src/app/components/add-item-dialog/add-item-dialog.component.ts @@ -118,7 +118,7 @@ export class AddItemDialogComponent { const allTabAgents = new Map(); YamlUtils.generateYamlFile(rootAgent, formData, trimmedName, allTabAgents); - this.agentService.agentBuildTmp(formData).subscribe((success) => { + this.agentService.agentBuildTmp(trimmedName, formData).subscribe((success) => { if (success) { this.router.navigate(['/'], { queryParams: { app: trimmedName, mode: 'builder' } diff --git a/src/app/components/builder-assistant/builder-assistant.component.ts b/src/app/components/builder-assistant/builder-assistant.component.ts index 615ddc51..4fa4805f 100644 --- a/src/app/components/builder-assistant/builder-assistant.component.ts +++ b/src/app/components/builder-assistant/builder-assistant.component.ts @@ -277,7 +277,7 @@ export class BuilderAssistantComponent implements OnInit, AfterViewChecked { YamlUtils.generateYamlFile(rootAgent, formData, appName, tabAgents); - this.agentService.agentBuildTmp(formData).subscribe((success) => { + this.agentService.agentBuildTmp(appName, formData).subscribe((success) => { if (success) { console.log("save to tmp") } else { diff --git a/src/app/components/builder-tabs/builder-tabs.component.ts b/src/app/components/builder-tabs/builder-tabs.component.ts index fbf6608a..48bf86c0 100644 --- a/src/app/components/builder-tabs/builder-tabs.component.ts +++ b/src/app/components/builder-tabs/builder-tabs.component.ts @@ -774,9 +774,9 @@ export class BuilderTabsComponent { YamlUtils.generateYamlFile(rootAgent, formData, appName, tabAgents); - this.agentService.agentBuildTmp(formData).subscribe((success) => { + this.agentService.agentBuildTmp(appName, formData).subscribe((success) => { if (success) { - this.agentService.agentBuild(formData).subscribe((success) => { + this.agentService.agentBuild(appName, formData).subscribe((success) => { if (success) { this.router.navigate(['/'], { queryParams: { app: appName } diff --git a/src/app/components/canvas/canvas.component.ts b/src/app/components/canvas/canvas.component.ts index 0cb5466d..0a88b718 100644 --- a/src/app/components/canvas/canvas.component.ts +++ b/src/app/components/canvas/canvas.component.ts @@ -1229,7 +1229,7 @@ export class CanvasComponent implements AfterViewInit, OnInit, OnChanges { const agentToolBoards = this.agentToolBoards(); YamlUtils.generateYamlFile(rootAgent, formData, appName, agentToolBoards); - this.agentService.agentBuild(formData).subscribe((success) => { + this.agentService.agentBuild(appName, formData).subscribe((success) => { if (success) { this.router .navigate(["/"], { diff --git a/src/app/components/chat-panel/chat-panel.component.html b/src/app/components/chat-panel/chat-panel.component.html index d9c7e656..53b8127a 100644 --- a/src/app/components/chat-panel/chat-panel.component.html +++ b/src/app/components/chat-panel/chat-panel.component.html @@ -58,7 +58,7 @@

Evaluation Result

@if (uiEvents.length === 0 && agentReadme) {
+ [ngComponentOutletInputs]="{text: agentReadme, thought: false, isReadme: true}">
} @for (item of displayItems; track item; let i = $index) { diff --git a/src/app/components/chat-panel/chat-panel.component.ts b/src/app/components/chat-panel/chat-panel.component.ts index 207f54c6..2222fea3 100644 --- a/src/app/components/chat-panel/chat-panel.component.ts +++ b/src/app/components/chat-panel/chat-panel.component.ts @@ -33,7 +33,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatTabsModule } from '@angular/material/tabs'; import { MatSelectModule } from '@angular/material/select'; -import { NgxJsonViewerModule } from 'ngx-json-viewer'; +import { CustomJsonViewerComponent } from '../custom-json-viewer/custom-json-viewer.component'; import { EMPTY, merge, NEVER, of, Subject } from 'rxjs'; import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; @@ -42,6 +42,7 @@ import { isComputerUseResponse, isVisibleComputerUseClick } from '../../core/mod import type { EvalCase } from '../../core/models/Eval'; import { FunctionCall, FunctionResponse } from '../../core/models/types'; import { UiEvent } from '../../core/models/UiEvent'; +import { Span } from '../../core/models/Trace'; import { AGENT_SERVICE } from '../../core/services/interfaces/agent'; import { FEATURE_FLAG_SERVICE } from '../../core/services/interfaces/feature-flag'; import { SAFE_VALUES_SERVICE } from '../../core/services/interfaces/safevalues'; @@ -110,7 +111,6 @@ export type DisplayItem = { MatMenuModule, MatProgressSpinnerModule, MatSlideToggleModule, - NgxJsonViewerModule, MatTooltipModule, MatButtonToggleModule, MatTabsModule, @@ -126,7 +126,7 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { sessionName = input(''); @Input() uiEvents: UiEvent[] = []; @Input() showBranches: boolean = false; - @Input() traceData: any[] = []; + @Input() traceData: Span[] = []; @Input() isChatMode: boolean = true; @Input() evalCase: EvalCase | null = null; @Input() isEvalEditMode: boolean = false; @@ -147,7 +147,7 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { @Input() sessionId: string = ''; @Input() viewMode: 'events' | 'traces' = 'events'; @Input() shouldShowEvent?: (uiEvent: UiEvent) => boolean; - spansByInvocationId = new Map(); + spansByInvocationId = new Map(); displayItems: DisplayItem[] = []; eventsScrollTop = -1; tracesScrollTop = -1; @@ -234,13 +234,6 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { evalCaseResult = input(null); showEvalSummary = input(false); - - - - - - - constructor() { effect(() => { const sessionName = this.sessionName(); @@ -346,7 +339,7 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { if (changes['viewMode']) { const prevMode = changes['viewMode'].previousValue; const currentMode = changes['viewMode'].currentValue; - + if (this.scrollContainer?.nativeElement) { if (prevMode === 'events') { this.eventsScrollTop = this.scrollContainer.nativeElement.scrollTop; @@ -354,7 +347,7 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { this.tracesScrollTop = this.scrollContainer.nativeElement.scrollTop; } } - + setTimeout(() => { if (this.scrollContainer?.nativeElement) { if (currentMode === 'events' && this.eventsScrollTop !== -1) { @@ -465,29 +458,25 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { } rebuildTrace() { - const invocTraces = this.traceData.reduce((map: any, item: any) => { - const key = item.trace_id; + const invocTraces = this.traceData.reduce((map: Map, item: Span) => { + const key = String(item.trace_id); const group = map.get(key); if (group) { group.push(item); - group.sort((a: any, b: any) => a.start_time - b.start_time); + group.sort((a: Span, b: Span) => a.start_time - b.start_time); } else { map.set(key, [item]); } return map; - }, new Map()); + }, new Map()); - this.spansByInvocationId = new Map(); + this.spansByInvocationId = new Map(); for (const [key, group] of invocTraces) { - let invocId = group.find( - (item: any) => item.attributes !== undefined && 'gcp.vertex.agent.invocation_id' in item.attributes - )?.attributes['gcp.vertex.agent.invocation_id']; + let invocId = group.find(s => s.attrInvocationId !== undefined)?.attrInvocationId; // Fallback 1: Use associated_event_ids if (!invocId) { - const associatedEventIds = group.find( - (item: any) => item.attributes !== undefined && 'gcp.vertex.agent.associated_event_ids' in item.attributes - )?.attributes['gcp.vertex.agent.associated_event_ids']; + const associatedEventIds = group.find(s => s.attrAssociatedEventIds !== undefined)?.attrAssociatedEventIds; if (associatedEventIds && associatedEventIds.length > 0) { invocId = associatedEventIds[0]; } @@ -499,7 +488,7 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit { } if (invocId) { - this.spansByInvocationId.set(invocId, group); + this.spansByInvocationId.set(String(invocId), group); } } } diff --git a/src/app/components/chat/chat.component.spec.ts b/src/app/components/chat/chat.component.spec.ts index 301a8d2a..34d48920 100644 --- a/src/app/components/chat/chat.component.spec.ts +++ b/src/app/components/chat/chat.component.spec.ts @@ -633,7 +633,7 @@ describe('ChatComponent', () => { it('should call getTrace', () => { expect(mockEventService.getTrace) - .toHaveBeenCalledWith(SESSION_1_ID); + .toHaveBeenCalledWith(component.appName, SESSION_1_ID); }); describe('canEdit', () => { diff --git a/src/app/components/chat/chat.component.ts b/src/app/components/chat/chat.component.ts index e2fc1d2d..fd5f7fc2 100644 --- a/src/app/components/chat/chat.component.ts +++ b/src/app/components/chat/chat.component.ts @@ -36,7 +36,7 @@ import { MatTooltip } from '@angular/material/tooltip'; import { MatToolbar } from '@angular/material/toolbar'; import { SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; -import { NgxJsonViewerModule } from 'ngx-json-viewer'; +import { CustomJsonViewerComponent } from '../custom-json-viewer/custom-json-viewer.component'; import { combineLatest, firstValueFrom, Observable, of, Subscription } from 'rxjs'; import { catchError, distinctUntilChanged, filter, first, map, shareReplay, startWith, switchMap, take, tap } from 'rxjs/operators'; @@ -87,6 +87,7 @@ import { FormatMetricNamePipe } from '../eval-tab/format-metric-name.pipe'; import { ChatMessagesInjectionToken } from './chat.component.i18n'; import { SidePanelMessagesInjectionToken } from '../side-panel/side-panel.component.i18n'; +import { Span, OPERATION_GENERATE_CONTENT, SpanIo } from '../../core/models/Trace'; const ROOT_AGENT = 'root_agent'; /** Query parameter for pre-filling user input. */ @@ -154,7 +155,6 @@ const BIDI_STREAMING_RESTART_WARNING = FormsModule, ReactiveFormsModule, MatIcon, - NgxJsonViewerModule, MatButton, MatIconButton, MatMenuModule, @@ -206,7 +206,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { private readonly traceService = inject(TRACE_SERVICE); protected readonly uiStateService = inject(UI_STATE_SERVICE); protected readonly agentBuilderService = inject(AGENT_BUILDER_SERVICE); - protected readonly themeService = inject(THEME_SERVICE, {optional: true}); + protected readonly themeService = inject(THEME_SERVICE, { optional: true }); protected readonly logoComponent: Type | null = inject(LOGO_COMPONENT, { optional: true, }); @@ -535,7 +535,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { sessionHasUsedBidi = new Set(); eventData = new Map(); - traceData: any[] = []; + traceData: Span[] = []; renderedEventGraph: SafeHtml | undefined; rawSvgString: string | null = null; agentGraphData = signal(null); @@ -555,8 +555,6 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { selectedMessageIndex: number | undefined = undefined; llmRequest: any = undefined; llmResponse: any = undefined; - llmRequestKey = 'gcp.vertex.agent.llm_request'; - llmResponseKey = 'gcp.vertex.agent.llm_response'; getMediaTypeFromMimetype = getMediaTypeFromMimetype; @@ -1333,7 +1331,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { if (existingIndex >= 0) { const existingEvent = events[existingIndex]; - + // Preserve functionResponses and functionCalls if not present in new event if (!uiEvent.functionResponses || uiEvent.functionResponses.length === 0) { uiEvent.functionResponses = existingEvent.functionResponses; @@ -1555,8 +1553,6 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { } } - - private formatBase64Data(data: string, mimeType: string) { const fixedBase64Data = fixBase64String(data); return `data:${mimeType};base64,${fixedBase64Data}`; @@ -1701,14 +1697,14 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { next: (res: any) => { let mimeType = res.mimeType; let data = res.data; - + if (!mimeType || !data) { if (res.inlineData) { mimeType = res.inlineData.mimeType; data = res.inlineData.data; } } - + if (!mimeType && !data && res.text) { mimeType = 'text/plain'; try { @@ -1719,7 +1715,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { return; } } - + if (!mimeType || !data) { this.handleArtifactFetchFailure(uiEvent, artifactId, versionId, { message: 'Invalid response data: missing mimeType or data or text' }); return; @@ -2104,8 +2100,8 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { private loadTraceData() { if (!this.sessionId) return; this.uiStateService.setIsEventRequestResponseLoading(true); - this.eventService.getTrace(this.sessionId) - .pipe(first(), catchError((err) => { console.error('[DEBUG] getTrace error:', err); return of([]); })) + this.eventService.getTrace(this.appName, this.sessionId) + .pipe(first(), catchError((err) => { console.error('[DEBUG] getTrace error:', err); return of([] as Span[]); })) .subscribe(res => { this.traceData = res; this.traceService.setEventData(this.eventData); @@ -3656,30 +3652,30 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.selectedEvent) return; - const matchingSpan = this.traceData?.find( - (span: any) => span?.attributes?.['gcp.vertex.agent.event_id'] === this.selectedEvent.id && span?.name === 'call_llm' - ); + const io = this.findSpanIoForSelectedEvent(); + if (io === undefined) return; + + // The downstream JSON viewer renders whatever shape comes in, so we + // pass the discriminated {@link SpanIo} payload through verbatim. + this.llmRequest = io.inputs; + this.llmResponse = io.outputs; + } - if (matchingSpan) { - const requestStr = matchingSpan.attributes?.[this.llmRequestKey]; - const responseStr = matchingSpan.attributes?.[this.llmResponseKey]; + private findSpanIoForSelectedEvent(): SpanIo | undefined { + const eventId = this.selectedEvent?.id; + if (eventId === undefined) return undefined; - if (requestStr) { - try { - this.llmRequest = typeof requestStr === 'string' ? JSON.parse(requestStr) : requestStr; - } catch (e) { - console.warn('Failed to parse LLM request', e); - } - } + const generateContentSpan = this.traceData?.find( + (span) => + span.attrOperationName === OPERATION_GENERATE_CONTENT + && span.attrEventId === eventId + ); + if (generateContentSpan?.io !== undefined) return generateContentSpan.io; - if (responseStr) { - try { - this.llmResponse = typeof responseStr === 'string' ? JSON.parse(responseStr) : responseStr; - } catch (e) { - console.warn('Failed to parse LLM response', e); - } - } - } + const legacySpan = this.traceData?.find( + (span) => span.attrEventId === eventId && span.name === 'call_llm', + ); + return legacySpan?.io; } deleteSession(session: string) { diff --git a/src/app/components/content-bubble/content-bubble.component.html b/src/app/components/content-bubble/content-bubble.component.html index 118b79d2..9df472e4 100644 --- a/src/app/components/content-bubble/content-bubble.component.html +++ b/src/app/components/content-bubble/content-bubble.component.html @@ -185,11 +185,11 @@ @if (uiEvent.actualInvocationToolUses) {
{{ i18n.actualToolUsesLabel }}
- +
{{ i18n.expectedToolUsesLabel }}
- +
} @else if (uiEvent.actualFinalResponse) {
@@ -213,17 +213,17 @@
} } @else if (type === 'output') { - + > } @else if (type === 'error') { - + > } @else if (type === 'transcription') { @if (role === 'user' && uiEvent.event.inputTranscription) { {{ uiEvent.event.inputTranscription.text }} diff --git a/src/app/components/content-bubble/content-bubble.component.scss b/src/app/components/content-bubble/content-bubble.component.scss index 67663139..5e9c7ab4 100644 --- a/src/app/components/content-bubble/content-bubble.component.scss +++ b/src/app/components/content-bubble/content-bubble.component.scss @@ -1,5 +1,6 @@ :host { - display: contents; /* Ensure the wrapper doesn't break flex layouts of the parent */ + display: contents; + /* Ensure the wrapper doesn't break flex layouts of the parent */ } .content-bubble { @@ -89,6 +90,7 @@ transition: transform 0.2s ease-in-out; } + .generated-image-container { max-width: 400px; margin-top: 8px; @@ -123,7 +125,8 @@ padding: 5px 12px !important; max-width: 90% !important; } + .role-user { max-width: 90% !important; } -} +} \ No newline at end of file diff --git a/src/app/components/content-bubble/content-bubble.component.ts b/src/app/components/content-bubble/content-bubble.component.ts index 59bf364d..de945aa0 100644 --- a/src/app/components/content-bubble/content-bubble.component.ts +++ b/src/app/components/content-bubble/content-bubble.component.ts @@ -21,7 +21,7 @@ import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; import {MatIconModule} from '@angular/material/icon'; import {MatTooltipModule} from '@angular/material/tooltip'; -import {NgxJsonViewerModule} from 'ngx-json-viewer'; +import {CustomJsonViewerComponent} from '../custom-json-viewer/custom-json-viewer.component'; import {UiEvent} from '../../core/models/UiEvent'; import {SAFE_VALUES_SERVICE} from '../../core/services/interfaces/safevalues'; @@ -42,7 +42,7 @@ import { ARTIFACT_SERVICE } from '../../core/services/interfaces/artifact'; FormsModule, MatIconModule, MatTooltipModule, - NgxJsonViewerModule, + CustomJsonViewerComponent, A2uiCanvasComponent, AudioPlayerComponent, JsonTooltipDirective, diff --git a/src/app/components/custom-json-viewer/custom-json-viewer.component.html b/src/app/components/custom-json-viewer/custom-json-viewer.component.html new file mode 100644 index 00000000..d892e236 --- /dev/null +++ b/src/app/components/custom-json-viewer/custom-json-viewer.component.html @@ -0,0 +1,65 @@ +
+
+ @if (isExpandable() && depth > 0) { + + } + + @if (key !== undefined) { + {{ key }} + : + @if (showMarkdown && hasLineBreaks(json)) { + + } +   + } + + @if (isExpandable()) { + + @if (isArray(json)) { + [ + @if (!isExpanded) { + ... + ] + } + } @else { + @if (!isExpanded) { + ... + } + } + + } @else { + + @if (isString(json)) { + "{{ json }}" + } @else if (isNumber(json)) { + {{ json }} + } @else if (isBoolean(json)) { + {{ json }} + } @else if (isNull(json)) { + null + } @else if (isUndefined(json)) { + undefined + } + + } +
+ + @if (isExpandable() && isExpanded) { +
+ @if (isArray(json)) { + @for (item of json; track $index; let last = $last) { + + } + } @else { + @for (k of getKeys(json); track k; let last = $last) { + + } + } + @if (isArray(json)) { +
]
+ } +
+ } +
diff --git a/src/app/components/custom-json-viewer/custom-json-viewer.component.scss b/src/app/components/custom-json-viewer/custom-json-viewer.component.scss new file mode 100644 index 00000000..9ae57503 --- /dev/null +++ b/src/app/components/custom-json-viewer/custom-json-viewer.component.scss @@ -0,0 +1,154 @@ +:host { + display: block; + font-family: var(--ngx-json-font-family, monospace); + font-size: var(--ngx-json-font-size, 13px); + line-height: 1.4; +} + +.segment { + margin: 2px 0; + display: block; +} + +.segment-header { + display: flex; + align-items: flex-start; + flex-wrap: wrap; +} + +.segment-toggler { + cursor: pointer; + display: inline-block; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 0 5px 6px; + border-color: transparent transparent transparent var(--mat-sys-outline); + margin-right: 8px; + margin-top: 4px; + transition: transform 0.15s ease; + + &.expanded { + transform: rotate(90deg); + } + + &:hover { + border-left-color: var(--mat-sys-primary); + } +} + +.segment-key { + color: var(--mat-sys-primary); + font-weight: normal; + cursor: pointer; +} + +.segment-separator { + color: var(--mat-sys-on-surface); +} + +.segment-space { + display: inline-block; + width: 4px; + user-select: none; +} + +.segment-value { + color: var(--mat-sys-on-surface); +} + +.bracket { + color: var(--mat-sys-outline); + font-weight: normal; +} + +.collapsed-summary { + color: var(--mat-sys-on-surface-variant); + font-size: 11px; + margin: 0 4px; +} + +.segment-children { + margin-left: 12px; + padding-left: 4px; + + &.root-children { + margin-left: 0; + padding-left: 0; + } +} + +.close-bracket { + display: block; +} + +/* MD Button styling */ +.md-btn { + border: none; + outline: none; + cursor: pointer; + font-family: 'Roboto', sans-serif; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.5px; + color: var(--mat-sys-primary); + background-color: var(--mat-sys-primary-container); + border-radius: 4px; + padding: 2px 6px; + margin-left: 4px; + margin-right: 2px; + display: inline-flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease, background-color 0.2s ease, color 0.2s ease, transform 0.2s ease; + height: 16px; + + &:hover { + opacity: 1 !important; + transform: scale(1.05); + background-color: var(--mat-sys-primary); + color: var(--mat-sys-on-primary); + } +} + +.segment-header:hover { + .md-btn { + opacity: 0.5; + visibility: visible; + } +} + +/* Type highlighting classes */ +.segment-type-string { + color: var(--ngx-json-string, #FF6B6B); + + .value-string { + white-space: pre-wrap; + word-break: break-word; + } +} + +.segment-type-number { + color: var(--mat-sys-error); +} + +.segment-type-boolean { + color: var(--mat-sys-secondary); +} + +.segment-type-null, .segment-type-undefined { + color: var(--mat-sys-outline); + font-style: italic; +} + +/* Dialog styling customization */ +::ng-deep .custom-md-dialog { + .mat-mdc-dialog-container { + border-radius: 12px !important; + border: 1px solid var(--mat-sys-outline-variant); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3) !important; + background-color: var(--mat-sys-surface-container-high) !important; + } +} diff --git a/src/app/components/custom-json-viewer/custom-json-viewer.component.ts b/src/app/components/custom-json-viewer/custom-json-viewer.component.ts new file mode 100644 index 00000000..487f9869 --- /dev/null +++ b/src/app/components/custom-json-viewer/custom-json-viewer.component.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MarkdownComponent } from '../markdown/markdown.component'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-markdown-preview-dialog', + standalone: true, + imports: [ + CommonModule, + MatDialogModule, + MatIcon, + MatIconButton, + MarkdownComponent + ], + template: ` +
+

+ article + Markdown Preview - {{ data.key }} +

+ +
+ + + + `, + styles: [` + .md-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px 8px; + border-bottom: 1px solid var(--mat-sys-outline-variant); + } + .md-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 1.25rem; + font-weight: 500; + color: var(--mat-sys-on-surface); + } + .title-icon { + color: var(--mat-sys-primary); + } + .close-button { + color: var(--mat-sys-on-surface-variant); + } + .md-dialog-content { + padding: 24px; + min-width: 500px; + max-width: 80vw; + max-height: 70vh; + overflow-y: auto; + background-color: var(--mat-sys-surface-container-high); + color: var(--mat-sys-on-surface); + } + `] +}) +export class MarkdownPreviewDialogComponent { + dialogRef = inject(MatDialogRef); + data = inject(MAT_DIALOG_DATA) as { key: string; value: string }; + + close(): void { + this.dialogRef.close(); + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-custom-json-viewer', + templateUrl: './custom-json-viewer.component.html', + styleUrls: ['./custom-json-viewer.component.scss'], + standalone: true, + imports: [ + CommonModule, + MatTooltip, + MatDialogModule, + ], +}) +export class CustomJsonViewerComponent implements OnInit { + @Input() json: any; + @Input() key: string | number | undefined; + @Input() expanded = true; + @Input() depth = 0; + @Input() showMarkdown = false; + + private readonly dialog = inject(MatDialog); + + isExpanded = true; + + ngOnInit() { + this.isExpanded = this.expanded; + } + + isExpandable(): boolean { + return this.json !== null && typeof this.json === 'object'; + } + + isObject(val: any): boolean { + return val !== null && typeof val === 'object' && !Array.isArray(val); + } + + isArray(val: any): boolean { + return Array.isArray(val); + } + + isString(val: any): boolean { + return typeof val === 'string'; + } + + hasLineBreaks(val: any): boolean { + return typeof val === 'string' && val.includes('\n'); + } + + isNumber(val: any): boolean { + return typeof val === 'number'; + } + + isBoolean(val: any): boolean { + return typeof val === 'boolean'; + } + + isNull(val: any): boolean { + return val === null; + } + + isUndefined(val: any): boolean { + return val === undefined; + } + + getKeys(val: any): string[] { + if (!val) return []; + return Object.keys(val); + } + + getTypeClass(val: any): string { + if (this.isString(val)) return 'segment-type-string'; + if (this.isNumber(val)) return 'segment-type-number'; + if (this.isBoolean(val)) return 'segment-type-boolean'; + if (this.isNull(val)) return 'segment-type-null'; + return 'segment-type-undefined'; + } + + toggleExpand(event: Event): void { + event.stopPropagation(); + this.isExpanded = !this.isExpanded; + } + + openMarkdownDialog(key: string | number, value: string, event: Event): void { + event.stopPropagation(); + this.dialog.open(MarkdownPreviewDialogComponent, { + data: { key: key.toString(), value }, + width: '800px', + maxWidth: '90vw', + panelClass: 'custom-md-dialog' + }); + } +} diff --git a/src/app/components/event-content/event-content.component.html b/src/app/components/event-content/event-content.component.html index 77fadba4..5376b6f7 100644 --- a/src/app/components/event-content/event-content.component.html +++ b/src/app/components/event-content/event-content.component.html @@ -47,7 +47,7 @@ @for (functionCall of uiEvent.functionCalls; track functionCall.id) { diff --git a/src/app/components/event-content/event-content.component.scss b/src/app/components/event-content/event-content.component.scss index 96326aef..c5eddcee 100644 --- a/src/app/components/event-content/event-content.component.scss +++ b/src/app/components/event-content/event-content.component.scss @@ -4,7 +4,7 @@ width: 100%; } -app-content-bubble + app-content-bubble { +app-content-bubble+app-content-bubble { margin-top: 5px; } @@ -47,6 +47,7 @@ app-content-bubble + app-content-bubble { width: 100%; } + .function-response-chip-container { display: inline-flex; align-items: center; @@ -79,4 +80,4 @@ app-content-bubble + app-content-bubble { .function-response-chip-container:hover .menu-trigger-btn { visibility: visible; -} +} \ No newline at end of file diff --git a/src/app/components/event-content/event-content.component.ts b/src/app/components/event-content/event-content.component.ts index f9316b23..699cb760 100644 --- a/src/app/components/event-content/event-content.component.ts +++ b/src/app/components/event-content/event-content.component.ts @@ -15,27 +15,27 @@ * limitations under the License. */ -import {CommonModule, NgClass} from '@angular/common'; -import {Component, EventEmitter, Input, Output, inject} from '@angular/core'; -import {MatButtonModule} from '@angular/material/button'; -import {MatIconModule} from '@angular/material/icon'; -import {MatTooltipModule} from '@angular/material/tooltip'; -import {MatDialog} from '@angular/material/dialog'; -import {MatMenuModule} from '@angular/material/menu'; -import {FunctionResponse} from '../../core/models/types'; -import {EditJsonDialogComponent} from '../edit-json-dialog/edit-json-dialog.component'; - -import {AgentRunRequest} from '../../core/models/AgentRunRequest'; -import {isComputerUseResponse, isVisibleComputerUseClick} from '../../core/models/ComputerUse'; -import type {EvalCase} from '../../core/models/Eval'; -import {UiEvent} from '../../core/models/UiEvent'; -import {WorkflowGraphTooltipDirective} from '../../directives/workflow-graph-tooltip.directive'; -import {JsonTooltipDirective} from '../../directives/html-tooltip.directive'; -import {ComputerActionComponent} from '../computer-action/computer-action.component'; -import {HoverInfoButtonComponent} from '../hover-info-button/hover-info-button.component'; -import {LongRunningResponseComponent} from '../long-running-response/long-running-response'; -import {ChatPanelMessagesInjectionToken} from '../chat-panel/chat-panel.component.i18n'; -import {ContentBubbleComponent} from '../content-bubble/content-bubble.component'; +import { CommonModule, NgClass } from '@angular/common'; +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDialog } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; +import { FunctionCall, FunctionResponse } from '../../core/models/types'; +import { EditJsonDialogComponent } from '../edit-json-dialog/edit-json-dialog.component'; + +import { AgentRunRequest } from '../../core/models/AgentRunRequest'; +import { isComputerUseResponse, isVisibleComputerUseClick } from '../../core/models/ComputerUse'; +import type { EvalCase } from '../../core/models/Eval'; +import { UiEvent } from '../../core/models/UiEvent'; +import { WorkflowGraphTooltipDirective } from '../../directives/workflow-graph-tooltip.directive'; +import { JsonTooltipDirective } from '../../directives/html-tooltip.directive'; +import { ComputerActionComponent } from '../computer-action/computer-action.component'; +import { HoverInfoButtonComponent } from '../hover-info-button/hover-info-button.component'; +import { LongRunningResponseComponent } from '../long-running-response/long-running-response'; +import { ChatPanelMessagesInjectionToken } from '../chat-panel/chat-panel.component.i18n'; +import { ContentBubbleComponent } from '../content-bubble/content-bubble.component'; @Component({ selector: 'app-event-content', @@ -58,39 +58,39 @@ import {ContentBubbleComponent} from '../content-bubble/content-bubble.component ], }) export class EventContentComponent { - @Input({required: true}) uiEvent!: UiEvent; - @Input({required: true}) index!: number; + @Input({ required: true }) uiEvent!: UiEvent; + @Input({ required: true }) index!: number; @Input() uiEvents: UiEvent[] = []; - + @Input() appName: string = ''; @Input() userId: string = ''; @Input() sessionId: string = ''; @Input() sessionName: string = ''; - + @Input() evalCase: EvalCase | null = null; @Input() isEvalEditMode: boolean = false; @Input() isEvalCaseEditing: boolean = false; @Input() isEditFunctionArgsEnabled: boolean = false; @Input() userEditEvalCaseMessage: string = ''; - + @Input() agentGraphData: any = null; @Input() allWorkflowNodes: any = null; - @Output() readonly handleKeydown = new EventEmitter<{event: KeyboardEvent, message: any}>(); + @Output() readonly handleKeydown = new EventEmitter<{ event: KeyboardEvent, message: any }>(); @Output() readonly cancelEditMessage = new EventEmitter(); @Output() readonly saveEditMessage = new EventEmitter(); @Output() readonly userEditEvalCaseMessageChange = new EventEmitter(); - - @Output() readonly openViewImageDialog = new EventEmitter<{images: string[], currentIndex: number, urls?: string[], coordinates?: ({x: number, y: number} | null)[]}>(); - @Output() readonly openBase64InNewTab = new EventEmitter<{data: string, mimeType: string}>(); - + + @Output() readonly openViewImageDialog = new EventEmitter<{ images: string[], currentIndex: number, urls?: string[], coordinates?: ({ x: number, y: number } | null)[] }>(); + @Output() readonly openBase64InNewTab = new EventEmitter<{ data: string, mimeType: string }>(); + @Output() readonly editEvalCaseMessage = new EventEmitter(); - @Output() readonly deleteEvalCaseMessage = new EventEmitter<{message: any, index: number}>(); + @Output() readonly deleteEvalCaseMessage = new EventEmitter<{ message: any, index: number }>(); @Output() readonly editFunctionArgs = new EventEmitter(); - + @Output() readonly clickEvent = new EventEmitter(); @Output() readonly longRunningResponseComplete = new EventEmitter(); - @Output() readonly agentStateClick = new EventEmitter<{event: Event, index: number}>(); + @Output() readonly agentStateClick = new EventEmitter<{ event: Event, index: number }>(); protected readonly i18n = inject(ChatPanelMessagesInjectionToken); private readonly dialog = inject(MatDialog); @@ -98,13 +98,69 @@ export class EventContentComponent { readonly Object = Object; readonly String = String; + getFunctionCallButtonText(functionCall: FunctionCall): string { + let args: any = functionCall.args; + if (args && typeof args === 'string') { + try { + args = JSON.parse(args); + } catch { + // ignore parsing error, treat as raw string + } + } + if (args && typeof args === 'object') { + const specialFuncArgMap: Record = { + 'EditFile': 'path', + 'WriteFile': 'path', + }; + if (functionCall.name in specialFuncArgMap) { + const argKey = specialFuncArgMap[functionCall.name]; + if (argKey in args) { + const valueStr = this.formatPythonValue(args[argKey]); + const hasMore = Object.keys(args).length > 1; + return `${functionCall.name}(${valueStr}${hasMore ? ', …' : ''})`; + } + } + + const keys = Object.keys(args); + if (keys.length === 1) { + const value = args[keys[0]]; + const valueStr = this.formatPythonValue(value); + return `${functionCall.name}(${valueStr})`; + } else if (keys.length === 0) { + return `${functionCall.name}()`; + } + } else if (!args) { + return `${functionCall.name}()`; + } + return functionCall.name; + } + + private formatPythonValue(value: any): string { + if (value === null || value === undefined) { + return 'None'; + } + if (typeof value === 'boolean') { + return value ? 'True' : 'False'; + } + if (typeof value === 'string') { + return `"${value}"`; + } + if (typeof value === 'object') { + return JSON.stringify(value) + .replace(/\btrue\b/g, 'True') + .replace(/\bfalse\b/g, 'False') + .replace(/\bnull\b/g, 'None'); + } + return String(value); + } + shouldShowMessageCard(message: any): boolean { return !!( - message.text || message.attachments || message.inlineData || - message.executableCode || message.codeExecutionResult || - message.a2uiData || message.renderedContent || message.isLoading || - (message.failedMetric && message.evalStatus === 2) || - message.event?.content?.parts?.some((part: any) => part.fileData)); + message.text || message.attachments || message.inlineData || + message.executableCode || message.codeExecutionResult || + message.a2uiData || message.renderedContent || message.isLoading || + (message.failedMetric && message.evalStatus === 2) || + message.event?.content?.parts?.some((part: any) => part.fileData)); } isComputerUseClick(input: any): boolean { @@ -122,7 +178,7 @@ export class EventContentComponent { getFilteredStateDelta(stateDelta: any): any { if (!stateDelta) return null; - const filtered = {...stateDelta}; + const filtered = { ...stateDelta }; delete filtered['__llm_request_key__']; return filtered; } @@ -155,7 +211,7 @@ export class EventContentComponent { if (!callId) { return false; } - return this.uiEvents.some(event => + return this.uiEvents.some(event => event.functionResponses?.some(response => response.id === callId && (response.response as any)?.status !== 'pending') ); } @@ -163,7 +219,7 @@ export class EventContentComponent { openSendAnotherResponseDialog(functionResponse: FunctionResponse) { let functionCallEventId = ''; const callId = functionResponse.id; - + if (callId) { for (const event of this.uiEvents) { if (event.functionCalls) { @@ -256,6 +312,6 @@ export class EventContentComponent { onImageClick(clickedImage: string) { const images = this.getAllImages(); const currentIndex = images.indexOf(clickedImage); - this.openViewImageDialog.emit({images, currentIndex}); + this.openViewImageDialog.emit({ images, currentIndex }); } } diff --git a/src/app/components/event-tab/event-tab.component.html b/src/app/components/event-tab/event-tab.component.html index 352ccfde..9356b690 100644 --- a/src/app/components/event-tab/event-tab.component.html +++ b/src/app/components/event-tab/event-tab.component.html @@ -82,7 +82,7 @@ @if (selectedEvent()!.nodeInfo!['outputFor']) {
- + @@ -109,7 +109,7 @@ @if (isObject($any(selectedEvent()!.actions)[key])) {
- + @@ -130,7 +130,7 @@ {{ fc?.name }}
- + @@ -171,7 +171,7 @@
}
- + @@ -258,7 +258,7 @@ } @if (selectedDetailTab === 'raw') {
- + @@ -275,7 +275,7 @@
Old Value
@if (isObject(change.oldValue)) { - + } @else { {{ change.oldValue }} } @@ -285,7 +285,7 @@
New Value
@if (isObject(change.newValue)) { - + } @else { {{ change.newValue }} } @@ -398,7 +398,7 @@
Select an LLM response to see request details.
} @else {
- + @@ -414,7 +414,7 @@
Select an LLM response to see response details.
} @else {
- + @@ -423,4 +423,4 @@ }
-
\ No newline at end of file +
diff --git a/src/app/components/event-tab/event-tab.component.spec.ts b/src/app/components/event-tab/event-tab.component.spec.ts index a8a25689..b1f76d45 100644 --- a/src/app/components/event-tab/event-tab.component.spec.ts +++ b/src/app/components/event-tab/event-tab.component.spec.ts @@ -23,7 +23,7 @@ import {MatDialog} from '@angular/material/dialog'; import {MatListHarness} from '@angular/material/list/testing'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {Span} from '../../core/models/Trace'; +import {Span, SpanValidator} from '../../core/models/Trace'; import {FEATURE_FLAG_SERVICE} from '../../core/services/interfaces/feature-flag'; import {TRACE_SERVICE} from '../../core/services/interfaces/trace'; import {UI_STATE_SERVICE} from '../../core/services/interfaces/ui-state'; @@ -34,8 +34,21 @@ import {MockUiStateService} from '../../core/services/testing/mock-ui-state.serv import {EventTabComponent} from './event-tab.component'; import {TraceChartComponent} from './trace-chart/trace-chart.component'; +/** + * Helper that builds a `Span` (optionally with `children`) by routing the + * envelope through `SpanValidator` so promoted `attr*` fields are + * populated and the raw `attributes` bag is dropped. + */ +function makeSpan(raw: unknown, children: Span[] = []): Span { + const result = SpanValidator.safeParse(raw); + if (!result.success) { + throw new Error(`Failed to build test span: ${result.error.message}`); + } + return children.length > 0 ? {...result.data, children} : result.data; +} + const MOCK_TRACE_DATA: Span[] = [ - { + makeSpan({ name: 'agent.act', start_time: 1733084700000000000, end_time: 1733084760000000000, @@ -47,41 +60,44 @@ const MOCK_TRACE_DATA: Span[] = [ 'gcp.vertex.agent.llm_request': '{"contents":[{"role":"user","parts":[{"text":"Hello"}]},{"role":"agent","parts":[{"text":"Hi. What can I help you with?"}]},{"role":"user","parts":[{"text":"I need help with my project."}]}]}', }, - }, - { - name: 'tool.invoke', - start_time: 1733084705000000000, - end_time: 1733084755000000000, - span_id: 'span-2', - parent_span_id: 'span-1', - trace_id: 'trace-1', - attributes: { - 'tool_name': 'project_helper', - }, - children: [ + }), + makeSpan( { - name: 'sub-tool-1.invoke', - start_time: 1733084710000000000, - end_time: 1733084750000000000, - span_id: 'span-3', - parent_span_id: 'span-2', + name: 'tool.invoke', + start_time: 1733084705000000000, + end_time: 1733084755000000000, + span_id: 'span-2', + parent_span_id: 'span-1', trace_id: 'trace-1', attributes: { - 'sub_tool_name': 'sub_project_helper_1', + 'tool_name': 'project_helper', }, - children: [ + }, + [makeSpan( { - name: 'sub-tool-2.invoke', - start_time: 1733084715000000000, - end_time: 1733084745000000000, - span_id: 'span-4', - parent_span_id: 'span-3', + name: 'sub-tool-1.invoke', + start_time: 1733084710000000000, + end_time: 1733084750000000000, + span_id: 'span-3', + parent_span_id: 'span-2', trace_id: 'trace-1', attributes: { - 'sub_tool_name': 'sub_project_helper_2', + 'sub_tool_name': 'sub_project_helper_1', }, - children: [ + }, + [makeSpan( { + name: 'sub-tool-2.invoke', + start_time: 1733084715000000000, + end_time: 1733084745000000000, + span_id: 'span-4', + parent_span_id: 'span-3', + trace_id: 'trace-1', + attributes: { + 'sub_tool_name': 'sub_project_helper_2', + }, + }, + [makeSpan({ name: 'sub-tool-3.invoke', start_time: 1733084720000000000, end_time: 1733084740000000000, @@ -91,15 +107,8 @@ const MOCK_TRACE_DATA: Span[] = [ attributes: { 'sub_tool_name': 'sub_project_helper_3', }, - children: [], - }, - ], - }, - ], - }, - ], - } -] as Span[]; + })])])]), +]; const MOCK_EVENTS_MAP = new Map([ ['event1', {title: 'Event 1 Title'}], diff --git a/src/app/components/event-tab/event-tab.component.ts b/src/app/components/event-tab/event-tab.component.ts index 379728c7..d3398c99 100644 --- a/src/app/components/event-tab/event-tab.component.ts +++ b/src/app/components/event-tab/event-tab.component.ts @@ -25,15 +25,21 @@ import {MatProgressSpinner} from '@angular/material/progress-spinner'; import {MatTooltip} from '@angular/material/tooltip'; import {MatMenuModule, MatMenuTrigger} from '@angular/material/menu'; import {type SafeHtml} from '@angular/platform-browser'; -import {NgxJsonViewerModule} from 'ngx-json-viewer'; +import {CustomJsonViewerComponent} from '../custom-json-viewer/custom-json-viewer.component'; import {InfoTable} from '../info-table/info-table'; import {Event, Part} from '../../core/models/types'; import {UI_STATE_SERVICE} from '../../core/services/interfaces/ui-state'; import {SidePanelMessagesInjectionToken} from '../side-panel/side-panel.component.i18n'; -import {SpanNode} from '../../core/models/Trace'; +import {Span} from '../../core/models/Trace'; import {TRACE_SERVICE} from '../../core/services/interfaces/trace'; import {addSvgNodeHoverEffects} from '../../utils/svg-interaction.utils'; +export type SpanNode = Span & { + children: SpanNode[]; + depth: number; + duration: number; + id: string; // Using span_id as string ID +}; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -52,7 +58,7 @@ import {addSvgNodeHoverEffects} from '../../utils/svg-interaction.utils'; MatProgressSpinner, MatTooltip, MatMenuModule, - NgxJsonViewerModule, + CustomJsonViewerComponent, InfoTable, ], }) @@ -66,7 +72,7 @@ export class EventTabComponent { readonly rawSvgString = input(null); readonly llmRequest = input(); readonly llmResponse = input(); - readonly traceData = input([]); + readonly traceData = input([]); readonly appName = input(''); readonly selectedEventGraphPath = input(''); readonly hasSubWorkflows = input(false); @@ -161,10 +167,10 @@ export class EventTabComponent { readonly associatedSpans = computed(() => { const ev = this.selectedEvent(); if (!ev || !ev.id) return []; - + const allSpans = this.traceData(); if (!allSpans) return []; - + const flatten = (arr: any[]): any[] => { let result: any[] = []; for (const item of arr) { @@ -175,9 +181,9 @@ export class EventTabComponent { } return result; }; - + const flatSpans = flatten(allSpans); - return flatSpans.filter(s => s.attributes && s.attributes['gcp.vertex.agent.event_id'] === ev.id); + return flatSpans.filter(s => s.attrEventId === ev.id); }); readonly sessionUsageMetadata = computed(() => { @@ -207,11 +213,11 @@ export class EventTabComponent { }); private _selectedDetailTab: 'event' | 'raw' | 'request' | 'response' | 'graph' | 'metadata' | 'state' = 'event'; - + get selectedDetailTab() { return this._selectedDetailTab; } - + set selectedDetailTab(tab: 'event' | 'raw' | 'request' | 'response' | 'graph' | 'metadata' | 'state') { this._selectedDetailTab = tab; window.localStorage.setItem('adk-event-tab-selected-tab', tab); @@ -311,7 +317,7 @@ export class EventTabComponent { effect(() => { const force = this.forceGraphTab(); const event = this.selectedEvent(); - + if (force && !prevForceGraphTab) { this.selectedDetailTab = this.graphsAvailable() ? 'graph' : 'event'; } @@ -354,7 +360,7 @@ export class EventTabComponent { if (targetInvocationId) { allEvents = allEvents.filter(ev => ev.invocationId === targetInvocationId); } - + const travelsForNode: any[][] = []; let currentTravel: any[] = []; let lastNodeName = ''; @@ -381,7 +387,7 @@ export class EventTabComponent { } else { evGraphPath = segments.slice(1, -1).join('/'); } - + if (evGraphPath === this.selectedEventGraphPath()) { const fullSegments = np.split('/'); const fullEvNodeName = fullSegments[fullSegments.length - 1]; @@ -394,7 +400,7 @@ export class EventTabComponent { lastNodeName = currentName; currentTravel = []; } - + if (currentName === nodeName) { currentTravel.push(ev); } diff --git a/src/app/components/event-tab/invoc-id.pipe.ts b/src/app/components/event-tab/invoc-id.pipe.ts index 237f7818..6eb10377 100644 --- a/src/app/components/event-tab/invoc-id.pipe.ts +++ b/src/app/components/event-tab/invoc-id.pipe.ts @@ -27,10 +27,11 @@ export class InvocIdPipe implements PipeTransform { if (!spans) { return undefined; } - return spans.find( - (item) => - item.attributes !== undefined && - 'gcp.vertex.agent.invocation_id' in item.attributes, - )?.attributes['gcp.vertex.agent.invocation_id']; + for (const span of spans) { + if (span.attrInvocationId !== undefined) { + return span.attrInvocationId; + } + } + return undefined; } } diff --git a/src/app/components/json-tooltip/json-tooltip.component.ts b/src/app/components/json-tooltip/json-tooltip.component.ts index 517a4eef..fe9f391b 100644 --- a/src/app/components/json-tooltip/json-tooltip.component.ts +++ b/src/app/components/json-tooltip/json-tooltip.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { NgxJsonViewerModule } from 'ngx-json-viewer'; +import { CustomJsonViewerComponent } from '../custom-json-viewer/custom-json-viewer.component'; @Component({ changeDetection: ChangeDetectionStrategy.Default, @@ -26,7 +26,7 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer';
{{ title }}
}
- +
`, @@ -64,14 +64,14 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer'; background: inherit; z-index: 1; } - ngx-json-viewer { + app-custom-json-viewer { display: block; height: auto !important; min-width: 0; } `], standalone: true, - imports: [NgxJsonViewerModule], + imports: [CustomJsonViewerComponent], }) export class JsonTooltipComponent { @Input() title: string = ''; diff --git a/src/app/components/long-running-response/long-running-response.html b/src/app/components/long-running-response/long-running-response.html index 1d8ac910..30410605 100644 --- a/src/app/components/long-running-response/long-running-response.html +++ b/src/app/components/long-running-response/long-running-response.html @@ -44,7 +44,7 @@
Payload
- +