flare / flare-ui /src /app /dialogs /version-compare-dialog /version-compare-dialog.component.ts
ciyidogan's picture
Upload 118 files
9f79da5 verified
import { Component, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatChipsModule } from '@angular/material/chips';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatDividerModule } from '@angular/material/divider';
import { MatListModule } from '@angular/material/list';
import { FormsModule } from '@angular/forms';
import { Version } from '../../services/api.service';
interface Difference {
field: string;
label: string;
v1Value: any;
v2Value: any;
type: 'added' | 'removed' | 'modified' | 'unchanged';
}
@Component({
selector: 'app-version-compare-dialog',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatDialogModule,
MatSelectModule,
MatButtonModule,
MatIconModule,
MatChipsModule,
MatExpansionModule,
MatDividerModule,
MatListModule
],
template: `
<h2 mat-dialog-title>Compare Versions</h2>
<mat-dialog-content>
<div class="compare-container">
<!-- Version Selectors -->
<div class="version-selectors">
<mat-form-field appearance="outline">
<mat-label>Version 1</mat-label>
<mat-select [(value)]="version1" (selectionChange)="compareVersions()">
<mat-option *ngFor="let v of versions" [value]="v">
Version {{ v.no }} - {{ v.caption }}
<span class="published-marker" *ngIf="v.published">(Published)</span>
</mat-option>
</mat-select>
</mat-form-field>
<mat-icon class="compare-icon">compare_arrows</mat-icon>
<mat-form-field appearance="outline">
<mat-label>Version 2</mat-label>
<mat-select [(value)]="version2" (selectionChange)="compareVersions()">
<mat-option *ngFor="let v of versions" [value]="v">
Version {{ v.no }} - {{ v.caption }}
<span class="published-marker" *ngIf="v.published">(Published)</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Comparison Results -->
<div class="comparison-results" *ngIf="differences.length > 0">
<!-- Summary -->
<div class="summary-chips">
<mat-chip-listbox>
<mat-chip-option selected>
<mat-icon>add_circle</mat-icon>
{{ addedCount }} Added
</mat-chip-option>
<mat-chip-option selected color="warn">
<mat-icon>remove_circle</mat-icon>
{{ removedCount }} Removed
</mat-chip-option>
<mat-chip-option selected color="accent">
<mat-icon>edit</mat-icon>
{{ modifiedCount }} Modified
</mat-chip-option>
</mat-chip-listbox>
</div>
<!-- General Differences -->
<mat-expansion-panel [expanded]="hasGeneralDifferences">
<mat-expansion-panel-header>
<mat-panel-title>
General Configuration
</mat-panel-title>
<mat-panel-description>
{{ generalDifferences.length }} differences
</mat-panel-description>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let diff of generalDifferences">
<mat-icon matListItemIcon [class]="'diff-' + diff.type">
{{ getDiffIcon(diff.type) }}
</mat-icon>
<div matListItemTitle>{{ diff.label }}</div>
<div matListItemLine class="diff-values">
<span class="old-value" *ngIf="diff.type !== 'added'">{{ formatValue(diff.v1Value) }}</span>
<mat-icon *ngIf="diff.type === 'modified'">arrow_forward</mat-icon>
<span class="new-value" *ngIf="diff.type !== 'removed'">{{ formatValue(diff.v2Value) }}</span>
</div>
</mat-list-item>
</mat-list>
</mat-expansion-panel>
<!-- LLM Differences -->
<mat-expansion-panel [expanded]="hasLLMDifferences">
<mat-expansion-panel-header>
<mat-panel-title>
LLM Configuration
</mat-panel-title>
<mat-panel-description>
{{ llmDifferences.length }} differences
</mat-panel-description>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let diff of llmDifferences">
<mat-icon matListItemIcon [class]="'diff-' + diff.type">
{{ getDiffIcon(diff.type) }}
</mat-icon>
<div matListItemTitle>{{ diff.label }}</div>
<div matListItemLine class="diff-values">
<span class="old-value" *ngIf="diff.type !== 'added'">{{ formatValue(diff.v1Value) }}</span>
<mat-icon *ngIf="diff.type === 'modified'">arrow_forward</mat-icon>
<span class="new-value" *ngIf="diff.type !== 'removed'">{{ formatValue(diff.v2Value) }}</span>
</div>
</mat-list-item>
</mat-list>
</mat-expansion-panel>
<!-- Intent Differences -->
<mat-expansion-panel [expanded]="hasIntentDifferences">
<mat-expansion-panel-header>
<mat-panel-title>
Intents
</mat-panel-title>
<mat-panel-description>
{{ intentDifferences.added.length + intentDifferences.removed.length + intentDifferences.modified.length }} differences
</mat-panel-description>
</mat-expansion-panel-header>
<div class="intents-comparison">
<!-- Added Intents -->
<div class="intent-group" *ngIf="intentDifferences.added.length > 0">
<h4><mat-icon>add_circle</mat-icon> Added Intents</h4>
<mat-list>
<mat-list-item *ngFor="let intent of intentDifferences.added">
<mat-icon matListItemIcon class="diff-added">add</mat-icon>
<div matListItemTitle>{{ intent.name }}</div>
<div matListItemLine>{{ intent.caption || 'No description' }}</div>
</mat-list-item>
</mat-list>
</div>
<!-- Removed Intents -->
<div class="intent-group" *ngIf="intentDifferences.removed.length > 0">
<h4><mat-icon>remove_circle</mat-icon> Removed Intents</h4>
<mat-list>
<mat-list-item *ngFor="let intent of intentDifferences.removed">
<mat-icon matListItemIcon class="diff-removed">remove</mat-icon>
<div matListItemTitle>{{ intent.name }}</div>
<div matListItemLine>{{ intent.caption || 'No description' }}</div>
</mat-list-item>
</mat-list>
</div>
<!-- Modified Intents -->
<div class="intent-group" *ngIf="intentDifferences.modified.length > 0">
<h4><mat-icon>edit</mat-icon> Modified Intents</h4>
<mat-expansion-panel *ngFor="let intent of intentDifferences.modified">
<mat-expansion-panel-header>
<mat-panel-title>{{ intent.name }}</mat-panel-title>
<mat-panel-description>{{ intent.changes.length }} changes</mat-panel-description>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let change of intent.changes">
<mat-icon matListItemIcon [class]="'diff-' + change.type">
{{ getDiffIcon(change.type) }}
</mat-icon>
<div matListItemTitle>{{ change.label }}</div>
<div matListItemLine class="diff-values">
<span class="old-value" *ngIf="change.type !== 'added'">{{ formatValue(change.v1Value) }}</span>
<mat-icon *ngIf="change.type === 'modified'">arrow_forward</mat-icon>
<span class="new-value" *ngIf="change.type !== 'removed'">{{ formatValue(change.v2Value) }}</span>
</div>
</mat-list-item>
</mat-list>
</mat-expansion-panel>
</div>
</div>
</mat-expansion-panel>
</div>
<!-- No Selection State -->
<div class="empty-state" *ngIf="!version1 || !version2">
<mat-icon>compare</mat-icon>
<p>Select two versions to compare</p>
</div>
<!-- Same Version State -->
<div class="empty-state" *ngIf="version1 && version2 && version1.no === version2.no">
<mat-icon>info</mat-icon>
<p>Please select different versions to compare</p>
</div>
<!-- No Differences State -->
<div class="empty-state" *ngIf="version1 && version2 && version1.no !== version2.no && differences.length === 0">
<mat-icon>check_circle</mat-icon>
<p>These versions are identical</p>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">Close</button>
</mat-dialog-actions>
`,
styles: [`
.compare-container {
min-width: 800px;
max-width: 1000px;
}
.version-selectors {
display: flex;
gap: 24px;
align-items: center;
justify-content: center;
margin-bottom: 32px;
mat-form-field {
flex: 1;
max-width: 350px;
}
.compare-icon {
font-size: 32px;
width: 32px;
height: 32px;
color: #666;
}
.published-marker {
color: #4caf50;
font-weight: 500;
margin-left: 8px;
}
}
.summary-chips {
margin-bottom: 24px;
display: flex;
justify-content: center;
mat-chip {
margin: 0 4px;
mat-icon {
margin-right: 4px;
}
}
}
.comparison-results {
mat-expansion-panel {
margin-bottom: 16px;
}
}
.diff-values {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
.old-value {
color: #d32f2f;
text-decoration: line-through;
}
.new-value {
color: #388e3c;
font-weight: 500;
}
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: #666;
}
}
.diff-added {
color: #388e3c;
}
.diff-removed {
color: #d32f2f;
}
.diff-modified {
color: #1976d2;
}
.intents-comparison {
.intent-group {
margin-bottom: 24px;
h4 {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: #666;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
mat-expansion-panel {
margin-bottom: 8px;
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
mat-icon {
font-size: 64px;
width: 64px;
height: 64px;
color: #e0e0e0;
margin-bottom: 16px;
}
p {
color: #666;
font-size: 16px;
}
}
`]
})
export default class VersionCompareDialogComponent {
versions: Version[];
version1: Version | null = null;
version2: Version | null = null;
differences: Difference[] = [];
generalDifferences: Difference[] = [];
llmDifferences: Difference[] = [];
intentDifferences = {
added: [] as any[],
removed: [] as any[],
modified: [] as any[]
};
constructor(
public dialogRef: MatDialogRef<VersionCompareDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { versions: Version[], selectedVersion?: Version }
) {
this.versions = data.versions;
// Pre-select versions
if (data.selectedVersion) {
this.version1 = data.selectedVersion;
// Select the next most recent version as version2
const otherVersions = this.versions.filter(v => v.no !== data.selectedVersion!.no);
if (otherVersions.length > 0) {
this.version2 = otherVersions[0];
this.compareVersions();
}
}
}
get addedCount(): number {
return this.differences.filter(d => d.type === 'added').length + this.intentDifferences.added.length;
}
get removedCount(): number {
return this.differences.filter(d => d.type === 'removed').length + this.intentDifferences.removed.length;
}
get modifiedCount(): number {
return this.differences.filter(d => d.type === 'modified').length + this.intentDifferences.modified.length;
}
get hasGeneralDifferences(): boolean {
return this.generalDifferences.length > 0;
}
get hasLLMDifferences(): boolean {
return this.llmDifferences.length > 0;
}
get hasIntentDifferences(): boolean {
return this.intentDifferences.added.length > 0 ||
this.intentDifferences.removed.length > 0 ||
this.intentDifferences.modified.length > 0;
}
compareVersions() {
if (!this.version1 || !this.version2 || this.version1.no === this.version2.no) {
this.differences = [];
this.generalDifferences = [];
this.llmDifferences = [];
this.intentDifferences = { added: [], removed: [], modified: [] };
return;
}
this.differences = [];
// Compare general fields
this.compareField('caption', 'Caption', this.version1.caption, this.version2.caption);
this.compareField('general_prompt', 'General Prompt',
(this.version1 as any).general_prompt,
(this.version2 as any).general_prompt);
this.compareField('published', 'Published Status', this.version1.published, this.version2.published);
// Compare LLM configuration
if (this.version1.llm && this.version2.llm) {
this.compareField('llm.repo_id', 'Model Repository',
this.version1.llm.repo_id,
this.version2.llm.repo_id);
this.compareField('llm.use_fine_tune', 'Use Fine-Tune',
this.version1.llm.use_fine_tune,
this.version2.llm.use_fine_tune);
if (this.version1.llm.use_fine_tune || this.version2.llm.use_fine_tune) {
this.compareField('llm.fine_tune_zip', 'Fine-Tune ZIP',
this.version1.llm.fine_tune_zip,
this.version2.llm.fine_tune_zip);
}
// Compare generation config
const gc1 = this.version1.llm.generation_config;
const gc2 = this.version2.llm.generation_config;
if (gc1 && gc2) {
this.compareField('llm.generation_config.max_new_tokens', 'Max Tokens',
gc1.max_new_tokens, gc2.max_new_tokens);
this.compareField('llm.generation_config.temperature', 'Temperature',
gc1.temperature, gc2.temperature);
this.compareField('llm.generation_config.top_p', 'Top P',
gc1.top_p, gc2.top_p);
this.compareField('llm.generation_config.repetition_penalty', 'Repetition Penalty',
gc1.repetition_penalty, gc2.repetition_penalty);
}
}
// Compare intents
this.compareIntents();
// Categorize differences
this.generalDifferences = this.differences.filter(d =>
!d.field.startsWith('llm.') && !d.field.startsWith('intent.'));
this.llmDifferences = this.differences.filter(d => d.field.startsWith('llm.'));
}
private compareField(field: string, label: string, v1Value: any, v2Value: any) {
if (v1Value === v2Value) {
return;
}
let type: 'added' | 'removed' | 'modified';
if (v1Value === undefined || v1Value === null || v1Value === '') {
type = 'added';
} else if (v2Value === undefined || v2Value === null || v2Value === '') {
type = 'removed';
} else {
type = 'modified';
}
this.differences.push({
field,
label,
v1Value,
v2Value,
type
});
}
private compareIntents() {
const intents1 = this.version1?.intents || [];
const intents2 = this.version2?.intents || [];
const intents1Map = new Map(intents1.map(i => [i.name, i]));
const intents2Map = new Map(intents2.map(i => [i.name, i]));
// Find added intents
this.intentDifferences.added = intents2.filter(i => !intents1Map.has(i.name));
// Find removed intents
this.intentDifferences.removed = intents1.filter(i => !intents2Map.has(i.name));
// Find modified intents
this.intentDifferences.modified = [];
for (const [name, intent1] of intents1Map) {
const intent2 = intents2Map.get(name);
if (intent2) {
const changes = this.compareIntentDetails(intent1, intent2);
if (changes.length > 0) {
this.intentDifferences.modified.push({
name,
changes
});
}
}
}
}
private compareIntentDetails(intent1: any, intent2: any): Difference[] {
const changes: Difference[] = [];
// Compare basic fields
if (intent1.caption !== intent2.caption) {
changes.push({
field: `intent.${intent1.name}.caption`,
label: 'Caption',
v1Value: intent1.caption,
v2Value: intent2.caption,
type: 'modified'
});
}
if (intent1.detection_prompt !== intent2.detection_prompt) {
changes.push({
field: `intent.${intent1.name}.detection_prompt`,
label: 'Detection Prompt',
v1Value: intent1.detection_prompt,
v2Value: intent2.detection_prompt,
type: 'modified'
});
}
if (intent1.action !== intent2.action) {
changes.push({
field: `intent.${intent1.name}.action`,
label: 'API Action',
v1Value: intent1.action,
v2Value: intent2.action,
type: 'modified'
});
}
// Compare examples
const examples1 = intent1.examples || [];
const examples2 = intent2.examples || [];
if (JSON.stringify(examples1) !== JSON.stringify(examples2)) {
changes.push({
field: `intent.${intent1.name}.examples`,
label: 'Examples',
v1Value: `${examples1.length} examples`,
v2Value: `${examples2.length} examples`,
type: 'modified'
});
}
// Compare parameters
const params1 = intent1.parameters || [];
const params2 = intent2.parameters || [];
if (JSON.stringify(params1) !== JSON.stringify(params2)) {
changes.push({
field: `intent.${intent1.name}.parameters`,
label: 'Parameters',
v1Value: `${params1.length} parameters`,
v2Value: `${params2.length} parameters`,
type: 'modified'
});
}
return changes;
}
getDiffIcon(type: string): string {
switch (type) {
case 'added': return 'add_circle';
case 'removed': return 'remove_circle';
case 'modified': return 'edit';
default: return 'circle';
}
}
formatValue(value: any): string {
if (value === null || value === undefined) return 'Not set';
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
if (typeof value === 'string' && value.length > 100) {
return value.substring(0, 100) + '...';
}
return String(value);
}
close() {
this.dialogRef.close();
}
}