Spaces:
Paused
Paused
| 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; | |
| } | |
| } |