Skip to content

Commit dd06c03

Browse files
authored
[eval] Eval Viewer Quick Reactions and Comments Dashboard (#8639)
## What Added Thumbs Up and Thumbs Down reaction toggles for immediate evaluation rating in the Breadboard Eval Viewer's Topology inspector, and established a grouped "All Comments" modal to aggregate visual notes by section. ## Why Improves evaluation velocity by allowing raters to visually endorse ("Good") or flag ("Bad") individual Node Configurations, Dimensions, and Transcript events via mutually-exclusive singleton reaction toggles stored cleanly as `.notes.json` sidecar files. ## Changes - **Backend & Types (`types.ts`, `filesystem.ts`)**: Expanded `UserNote` to accommodate optional `.reaction` fields. Added dedicated `.notes.json` read/write capabilities to `FileSystemEvalBackend`. - **UI Components (`notes-container.ts`, `bgl-viewer.ts`)**: Built `<ui-notes-container>` delivering distinct hover-light-up reactivity, composed bubbling for event delegation, and real-time active state styles. Incorporated a dashboard for grouping non-reaction notes by context. - **State Orchestration (`inspector.ts`)**: Engineered `add-note` and `delete-note` custom event handlers in `A2UIEvalInspector` enforcing singleton and mutually-exclusive reaction conditions dynamically on state updates. ## Testing - Navigate to the Eval Viewer's Topology mode. - Click Thumbs Up or Thumbs Down on any configuration item; verify the corresponding button lights up and replaces opposite reactions. - Click the lit reaction button again to toggle it OFF. - Open `.notes.json` to confirm identical state persistence on disk.
1 parent 15e4bd6 commit dd06c03

5 files changed

Lines changed: 237 additions & 60 deletions

File tree

packages/visual-editor/eval/viewer/src/filesystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export class FileSystemEvalBackend {
262262
const fileBlob = await (descriptor as FileSystemFileHandle).getFile();
263263
const text = await fileBlob.text();
264264
const parsed = JSON.parse(text) as RunNotes;
265-
const count = parsed?.notes?.length || 0;
265+
const count = (parsed?.notes || []).filter((n) => !n.reaction).length;
266266
const baseName = name.replace(/\.notes\.json$/, "");
267267
notesCountMap.set(baseName, count);
268268
} catch {

packages/visual-editor/eval/viewer/src/inspector.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -947,34 +947,71 @@ export class A2UIEvalInspector extends SignalWatcher(LitElement) {
947947
</section>`;
948948
}
949949

950-
async #handleAddNote(e: CustomEvent<{ location: NoteLocation; text: string }>) {
951-
if (!this.selectedFilePath) return;
950+
async #handleAddNote(e: CustomEvent<{ location: NoteLocation; text: string; reaction?: "good" | "bad" }>) {
951+
if (!this.selectedFilePath) {
952+
console.warn("Inspector: No selectedFilePath available!");
953+
return;
954+
}
952955

953956
const newNote: UserNote = {
954957
id: crypto.randomUUID(),
955958
location: e.detail.location,
956959
text: e.detail.text,
957960
timestamp: new Date().toISOString(),
961+
reaction: e.detail.reaction,
958962
};
959963

960-
const updatedNotes = [...this.notes, newNote];
964+
let updatedNotes = [...this.notes];
965+
966+
if (e.detail.reaction) {
967+
// Enforce Singleton & Mutually Exclusive: Filter out any existing reaction note for this location.
968+
updatedNotes = updatedNotes.filter((n) => {
969+
const isSameLoc = (locA: NoteLocation, locB: NoteLocation) => {
970+
if (locA.type !== locB.type) return false;
971+
if (locA.type === "node-config" && locB.type === "node-config") {
972+
return locA.nodeId === locB.nodeId && locA.fieldName === locB.fieldName;
973+
}
974+
if (locA.type === "rater" && locB.type === "rater") {
975+
return locA.dimension === locB.dimension && locA.fieldName === locB.fieldName;
976+
}
977+
if (locA.type === "transcript" && locB.type === "transcript") {
978+
return locA.turn === locB.turn && locA.eventIndex === locB.eventIndex && locA.fieldName === locB.fieldName;
979+
}
980+
return false;
981+
};
982+
983+
const hasReaction = n.reaction !== undefined;
984+
const isSame = isSameLoc(n.location, e.detail.location);
985+
986+
// Keep the note if it's NOT a reaction note at the same location.
987+
return !(hasReaction && isSame);
988+
});
989+
}
990+
991+
updatedNotes.push(newNote);
992+
961993
const result = await this.#fileSystem.writeNotes(this.selectedFilePath, { notes: updatedNotes });
962994
if (ok(result)) {
963995
this.notes = updatedNotes;
964-
this.#updateSidebarNoteCount(this.selectedFilePath, updatedNotes.length);
996+
const nonReactionCount = updatedNotes.filter((n) => !n.reaction).length;
997+
this.#updateSidebarNoteCount(this.selectedFilePath, nonReactionCount);
965998
} else {
966999
console.warn("Failed to save note:", result.$error);
9671000
}
9681001
}
9691002

9701003
async #handleDeleteNote(e: CustomEvent<{ noteId: string }>) {
971-
if (!this.selectedFilePath) return;
1004+
if (!this.selectedFilePath) {
1005+
console.warn("Inspector: No selectedFilePath available!");
1006+
return;
1007+
}
9721008

9731009
const updatedNotes = this.notes.filter((n) => n.id !== e.detail.noteId);
9741010
const result = await this.#fileSystem.writeNotes(this.selectedFilePath, { notes: updatedNotes });
9751011
if (ok(result)) {
9761012
this.notes = updatedNotes;
977-
this.#updateSidebarNoteCount(this.selectedFilePath, updatedNotes.length);
1013+
const nonReactionCount = updatedNotes.filter((n) => !n.reaction).length;
1014+
this.#updateSidebarNoteCount(this.selectedFilePath, nonReactionCount);
9781015
} else {
9791016
console.warn("Failed to delete note:", result.$error);
9801017
}

packages/visual-editor/eval/viewer/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type UserNote = {
1414
location: NoteLocation;
1515
text: string;
1616
timestamp: string;
17+
reaction?: "good" | "bad";
1718
};
1819

1920
export type RunNotes = {

packages/visual-editor/eval/viewer/src/ui/bgl-viewer.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,8 @@ class BGLViewer extends LitElement {
989989
}
990990

991991
#renderAllNotesModal() {
992-
if (!this.notes || this.notes.length === 0) {
992+
const nonReactionNotes = (this.notes || []).filter((n) => !n.reaction);
993+
if (nonReactionNotes.length === 0) {
993994
return html`<dialog ${ref(this.#allNotesDialogRef)}>
994995
<form method="dialog">
995996
<div id="dialog-header">
@@ -1006,7 +1007,7 @@ class BGLViewer extends LitElement {
10061007
}
10071008

10081009
const groups = new Map<string, UserNote[]>();
1009-
for (const note of this.notes) {
1010+
for (const note of nonReactionNotes) {
10101011
const section = this.#getSectionTitle(note.location);
10111012
if (!groups.has(section)) {
10121013
groups.set(section, []);
@@ -1017,7 +1018,7 @@ class BGLViewer extends LitElement {
10171018
return html`<dialog ${ref(this.#allNotesDialogRef)}>
10181019
<form method="dialog">
10191020
<div id="dialog-header">
1020-
<h2>All Comments (${this.notes.length})</h2>
1021+
<h2>All Comments (${nonReactionNotes.length})</h2>
10211022
<button type="submit" aria-label="Close">
10221023
<span class="g-icon filled round">close</span>
10231024
</button>
@@ -1036,7 +1037,14 @@ class BGLViewer extends LitElement {
10361037
<span style="font-weight: 600; color: var(--light-dark-n-20);">${fieldRef}</span>
10371038
<span>${new Date(note.timestamp).toLocaleString()}</span>
10381039
</div>
1039-
<div style="white-space: pre-wrap;">${note.text}</div>
1040+
<div style="display: flex; align-items: center; gap: var(--bb-grid-size-2);">
1041+
${note.reaction ? html`<span
1042+
class="g-icon round"
1043+
style="color: ${note.reaction === 'good' ? '#34a853' : '#ea4335'}; font-size: 16px; flex-shrink: 0;"
1044+
title=${note.reaction === 'good' ? 'Marked as Good' : 'Marked as Bad'}
1045+
>${note.reaction === 'good' ? 'thumb_up' : 'thumb_down'}</span>` : nothing}
1046+
<div style="white-space: pre-wrap;">${note.text}</div>
1047+
</div>
10401048
</div>`;
10411049
})}
10421050
</div>
@@ -1076,9 +1084,9 @@ class BGLViewer extends LitElement {
10761084
</div>
10771085
<button @click=${() => this.#showRaterModal()}>Show Details</button>
10781086
<button
1079-
style="margin-top: var(--bb-grid-size); ${this.notes && this.notes.length > 0 ? '' : 'opacity: 0.5;'}"
1087+
style="margin-top: var(--bb-grid-size); ${(this.notes || []).filter((n) => !n.reaction).length > 0 ? '' : 'opacity: 0.5;'}"
10801088
@click=${() => this.#showAllNotesModal()}
1081-
>All Comments (${this.notes?.length || 0})</button>
1089+
>All Comments (${(this.notes || []).filter((n) => !n.reaction).length})</button>
10821090
</div>` : nothing}
10831091
</div>
10841092
<div

0 commit comments

Comments
 (0)