Spaces:
Building
Building
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'; | |
} | |
({ | |
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>, | |
public data: { versions: Version[], selectedVersion?: Version } (MAT_DIALOG_DATA) | |
) { | |
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(); | |
} | |
} |