Spaces:
Building
Building
import { Component, OnInit } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { FormsModule } from '@angular/forms'; | |
import { MatCardModule } from '@angular/material/card'; | |
import { MatFormFieldModule } from '@angular/material/form-field'; | |
import { MatSelectModule } from '@angular/material/select'; | |
import { MatButtonModule } from '@angular/material/button'; | |
import { MatIconModule } from '@angular/material/icon'; | |
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | |
import { MatExpansionModule } from '@angular/material/expansion'; | |
import { MatTableModule } from '@angular/material/table'; | |
import { MatChipsModule } from '@angular/material/chips'; | |
import { MatDividerModule } from '@angular/material/divider'; | |
import { ApiService } from '../../services/api.service'; | |
import { MatSnackBar } from '@angular/material/snack-bar'; | |
interface SparkResponse { | |
type: string; | |
timestamp: Date; | |
request?: any; | |
response?: any; | |
error?: string; | |
} | |
interface SparkProject { | |
project_name: string; | |
version: number; | |
enabled: boolean; | |
status: string; | |
last_accessed: string; | |
base_model: string; | |
has_adapter: boolean; | |
} | |
({ | |
selector: 'app-spark', | |
standalone: true, | |
imports: [ | |
CommonModule, | |
FormsModule, | |
MatCardModule, | |
MatFormFieldModule, | |
MatSelectModule, | |
MatButtonModule, | |
MatIconModule, | |
MatProgressSpinnerModule, | |
MatExpansionModule, | |
MatTableModule, | |
MatChipsModule, | |
MatDividerModule | |
], | |
template: ` | |
<div class="spark-container"> | |
<mat-card> | |
<mat-card-header> | |
<mat-card-title> | |
<mat-icon>flash_on</mat-icon> | |
Spark Integration | |
</mat-card-title> | |
<mat-card-subtitle> | |
Manage Spark LLM service integration | |
</mat-card-subtitle> | |
</mat-card-header> | |
<mat-card-content> | |
<mat-form-field appearance="outline" class="project-select"> | |
<mat-label>Select Project</mat-label> | |
<mat-select [(ngModel)]="selectedProject" (selectionChange)="onProjectChange()"> | |
<mat-option *ngFor="let project of projects" [value]="project.name"> | |
{{ project.name }} {{ project.caption ? '- ' + project.caption : '' }} | |
</mat-option> | |
</mat-select> | |
<mat-icon matPrefix>folder</mat-icon> | |
</mat-form-field> | |
<div class="action-buttons"> | |
<button mat-raised-button color="primary" | |
(click)="projectStartup()" | |
[disabled]="!selectedProject || loading"> | |
<mat-icon>rocket_launch</mat-icon> | |
Project Startup | |
</button> | |
<button mat-raised-button | |
(click)="getProjectStatus()" | |
[disabled]="!selectedProject || loading"> | |
<mat-icon>info</mat-icon> | |
Get Project Status | |
</button> | |
<button mat-raised-button color="accent" | |
(click)="enableProject()" | |
[disabled]="!selectedProject || loading"> | |
<mat-icon>power</mat-icon> | |
Enable Project | |
</button> | |
<button mat-raised-button | |
(click)="disableProject()" | |
[disabled]="!selectedProject || loading"> | |
<mat-icon>power_off</mat-icon> | |
Disable Project | |
</button> | |
<button mat-raised-button color="warn" | |
(click)="deleteProject()" | |
[disabled]="!selectedProject || loading"> | |
<mat-icon>delete</mat-icon> | |
Delete Project | |
</button> | |
</div> | |
@if (loading) { | |
<div class="loading-indicator"> | |
<mat-spinner diameter="40"></mat-spinner> | |
<p>Processing request...</p> | |
</div> | |
} | |
@if (responses.length > 0) { | |
<mat-divider class="section-divider"></mat-divider> | |
<h3>Response History</h3> | |
<div class="response-list"> | |
@for (response of responses; track response.timestamp) { | |
<mat-expansion-panel [expanded]="$index === 0"> | |
<mat-expansion-panel-header> | |
<mat-panel-title> | |
<mat-chip [class]="response.error ? 'error-chip' : 'success-chip'"> | |
{{ response.type }} | |
</mat-chip> | |
<span class="timestamp">{{ response.timestamp | date:'HH:mm:ss' }}</span> | |
</mat-panel-title> | |
</mat-expansion-panel-header> | |
@if (response.request) { | |
<div class="response-section"> | |
<h4>Request:</h4> | |
<pre class="json-display">{{ response.request | json }}</pre> | |
</div> | |
} | |
@if (response.response) { | |
<div class="response-section"> | |
<h4>Response:</h4> | |
@if (response.type === 'Get Project Status' && response.response.projects) { | |
<table mat-table [dataSource]="response.response.projects" class="projects-table"> | |
<ng-container matColumnDef="project_name"> | |
<th mat-header-cell *matHeaderCellDef>Project</th> | |
<td mat-cell *matCellDef="let project">{{ project.project_name }}</td> | |
</ng-container> | |
<ng-container matColumnDef="version"> | |
<th mat-header-cell *matHeaderCellDef>Version</th> | |
<td mat-cell *matCellDef="let project">v{{ project.version }}</td> | |
</ng-container> | |
<ng-container matColumnDef="status"> | |
<th mat-header-cell *matHeaderCellDef>Status</th> | |
<td mat-cell *matCellDef="let project"> | |
<mat-chip [class]="getStatusClass(project.status)"> | |
{{ project.status }} | |
</mat-chip> | |
</td> | |
</ng-container> | |
<ng-container matColumnDef="enabled"> | |
<th mat-header-cell *matHeaderCellDef>Enabled</th> | |
<td mat-cell *matCellDef="let project"> | |
<mat-icon [color]="project.enabled ? 'primary' : ''"> | |
{{ project.enabled ? 'check_circle' : 'cancel' }} | |
</mat-icon> | |
</td> | |
</ng-container> | |
<ng-container matColumnDef="base_model"> | |
<th mat-header-cell *matHeaderCellDef>Base Model</th> | |
<td mat-cell *matCellDef="let project" class="model-cell"> | |
{{ project.base_model }} | |
</td> | |
</ng-container> | |
<ng-container matColumnDef="last_accessed"> | |
<th mat-header-cell *matHeaderCellDef>Last Accessed</th> | |
<td mat-cell *matCellDef="let project">{{ project.last_accessed }}</td> | |
</ng-container> | |
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | |
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> | |
</table> | |
} @else { | |
<pre class="json-display">{{ response.response | json }}</pre> | |
} | |
</div> | |
} | |
@if (response.error) { | |
<div class="response-section error"> | |
<h4>Error:</h4> | |
<pre class="json-display error-text">{{ response.error }}</pre> | |
</div> | |
} | |
</mat-expansion-panel> | |
} | |
</div> | |
} | |
</mat-card-content> | |
</mat-card> | |
</div> | |
`, | |
styles: [` | |
.spark-container { | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
mat-card-header { | |
margin-bottom: 24px; | |
mat-card-title { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
font-size: 24px; | |
mat-icon { | |
font-size: 28px; | |
width: 28px; | |
height: 28px; | |
} | |
} | |
} | |
.project-select { | |
width: 100%; | |
max-width: 400px; | |
margin-bottom: 24px; | |
} | |
.action-buttons { | |
display: flex; | |
gap: 16px; | |
flex-wrap: wrap; | |
margin-bottom: 24px; | |
button { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
} | |
.loading-indicator { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 16px; | |
padding: 32px; | |
p { | |
color: #666; | |
font-size: 14px; | |
} | |
} | |
.section-divider { | |
margin: 32px 0; | |
} | |
.response-list { | |
margin-top: 16px; | |
mat-expansion-panel { | |
margin-bottom: 16px; | |
} | |
mat-panel-title { | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
.timestamp { | |
margin-left: auto; | |
color: #666; | |
font-size: 14px; | |
} | |
} | |
} | |
.response-section { | |
margin: 16px 0; | |
h4 { | |
margin-bottom: 8px; | |
color: #666; | |
} | |
&.error { | |
h4 { | |
color: #f44336; | |
} | |
} | |
} | |
.json-display { | |
background-color: #f5f5f5; | |
padding: 16px; | |
border-radius: 4px; | |
font-family: 'Consolas', 'Monaco', monospace; | |
font-size: 13px; | |
overflow-x: auto; | |
white-space: pre-wrap; | |
word-break: break-word; | |
&.error-text { | |
background-color: #ffebee; | |
color: #c62828; | |
} | |
} | |
.projects-table { | |
width: 100%; | |
background: #fafafa; | |
.model-cell { | |
font-size: 12px; | |
max-width: 200px; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
} | |
mat-chip { | |
font-size: 12px; | |
min-height: 24px; | |
padding: 4px 12px; | |
&.success-chip { | |
background-color: #4caf50; | |
color: white; | |
} | |
&.error-chip { | |
background-color: #f44336; | |
color: white; | |
} | |
} | |
::ng-deep { | |
.mat-mdc-progress-spinner { | |
--mdc-circular-progress-active-indicator-color: #3f51b5; | |
} | |
} | |
`] | |
}) | |
export class SparkComponent implements OnInit { | |
projects: any[] = []; | |
selectedProject: string = ''; | |
loading = false; | |
responses: SparkResponse[] = []; | |
displayedColumns: string[] = ['project_name', 'version', 'status', 'enabled', 'base_model', 'last_accessed']; | |
constructor( | |
private apiService: ApiService, | |
private snackBar: MatSnackBar | |
) {} | |
ngOnInit() { | |
this.loadProjects(); | |
} | |
loadProjects() { | |
this.apiService.getProjects().subscribe({ | |
next: (projects) => { | |
this.projects = projects.filter((p: any) => p.enabled && !p.deleted); | |
}, | |
error: (err) => { | |
this.snackBar.open('Failed to load projects', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
}); | |
} | |
onProjectChange() { | |
// Clear previous responses when project changes | |
this.responses = []; | |
} | |
private addResponse(type: string, request?: any, response?: any, error?: string) { | |
this.responses.unshift({ | |
type, | |
timestamp: new Date(), | |
request, | |
response, | |
error | |
}); | |
// Keep only last 10 responses | |
if (this.responses.length > 10) { | |
this.responses.pop(); | |
} | |
} | |
projectStartup() { | |
if (!this.selectedProject) return; | |
this.loading = true; | |
const request = { project_name: this.selectedProject }; | |
this.apiService.sparkStartup(this.selectedProject).subscribe({ | |
next: (response) => { | |
this.addResponse('Project Startup', request, response); | |
this.snackBar.open(response.message || 'Startup initiated', 'Close', { | |
duration: 3000 | |
}); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.addResponse('Project Startup', request, null, err.error?.detail || err.message); | |
this.snackBar.open(err.error?.detail || 'Startup failed', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.loading = false; | |
} | |
}); | |
} | |
getProjectStatus() { | |
this.loading = true; | |
this.apiService.sparkGetProjects().subscribe({ | |
next: (response) => { | |
this.addResponse('Get Project Status', null, response); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.addResponse('Get Project Status', null, null, err.error?.detail || err.message); | |
this.snackBar.open(err.error?.detail || 'Failed to get status', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.loading = false; | |
} | |
}); | |
} | |
enableProject() { | |
if (!this.selectedProject) return; | |
this.loading = true; | |
const request = { project_name: this.selectedProject }; | |
this.apiService.sparkEnableProject(this.selectedProject).subscribe({ | |
next: (response) => { | |
this.addResponse('Enable Project', request, response); | |
this.snackBar.open(response.message || 'Project enabled', 'Close', { | |
duration: 3000 | |
}); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.addResponse('Enable Project', request, null, err.error?.detail || err.message); | |
this.snackBar.open(err.error?.detail || 'Enable failed', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.loading = false; | |
} | |
}); | |
} | |
disableProject() { | |
if (!this.selectedProject) return; | |
this.loading = true; | |
const request = { project_name: this.selectedProject }; | |
this.apiService.sparkDisableProject(this.selectedProject).subscribe({ | |
next: (response) => { | |
this.addResponse('Disable Project', request, response); | |
this.snackBar.open(response.message || 'Project disabled', 'Close', { | |
duration: 3000 | |
}); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.addResponse('Disable Project', request, null, err.error?.detail || err.message); | |
this.snackBar.open(err.error?.detail || 'Disable failed', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.loading = false; | |
} | |
}); | |
} | |
deleteProject() { | |
if (!this.selectedProject) return; | |
if (!confirm(`Are you sure you want to delete "${this.selectedProject}" from Spark?`)) { | |
return; | |
} | |
this.loading = true; | |
const request = { project_name: this.selectedProject }; | |
this.apiService.sparkDeleteProject(this.selectedProject).subscribe({ | |
next: (response) => { | |
this.addResponse('Delete Project', request, response); | |
this.snackBar.open(response.message || 'Project deleted', 'Close', { | |
duration: 3000 | |
}); | |
this.loading = false; | |
this.selectedProject = ''; | |
}, | |
error: (err) => { | |
this.addResponse('Delete Project', request, null, err.error?.detail || err.message); | |
this.snackBar.open(err.error?.detail || 'Delete failed', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.loading = false; | |
} | |
}); | |
} | |
getStatusClass(status: string): string { | |
switch (status) { | |
case 'ready': | |
return 'status-ready'; | |
case 'loading': | |
return 'status-loading'; | |
case 'error': | |
return 'status-error'; | |
case 'unloaded': | |
return 'status-unloaded'; | |
default: | |
return ''; | |
} | |
} | |
trackByTimestamp(index: number, response: SparkResponse): Date { | |
return response.timestamp; | |
} | |
} |