mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-10 16:46:50 +01:00
feat: implement AI Status Indicator in navbar
- Created AIStatus data model with TypeScript interfaces - Implemented AIStatusService with polling mechanism - Created AIStatusIndicatorComponent with icon, tooltip, and animations - Added component to navbar between notifications and user menu - Includes unit tests for service and component - Shows AI active/inactive state with visual indicators - Displays statistics: documents scanned, suggestions applied - Shows pending deletion requests with badge - Processing animation when AI is active - Link to AI configuration settings - Non-intrusive design following existing patterns Co-authored-by: dawnsystem <42047891+dawnsystem@users.noreply.github.com>
This commit is contained in:
parent
c2254d7d1d
commit
09979d0156
8 changed files with 520 additions and 0 deletions
|
|
@ -0,0 +1,77 @@
|
||||||
|
<li class="nav-item ai-status-container">
|
||||||
|
<button
|
||||||
|
class="btn border-0 position-relative"
|
||||||
|
[ngbPopover]="aiStatusPopover"
|
||||||
|
popoverClass="ai-status-popover"
|
||||||
|
placement="bottom"
|
||||||
|
container="body"
|
||||||
|
triggers="mouseenter:mouseleave">
|
||||||
|
<i-bs
|
||||||
|
width="1.3em"
|
||||||
|
height="1.3em"
|
||||||
|
[name]="iconName"
|
||||||
|
[class]="iconClass">
|
||||||
|
</i-bs>
|
||||||
|
@if (hasAlerts) {
|
||||||
|
<span class="badge rounded-pill bg-danger position-absolute top-0 end-0 translate-middle-y">
|
||||||
|
{{ aiStatus.pending_deletion_requests }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<ng-template #aiStatusPopover>
|
||||||
|
<div class="ai-status-tooltip">
|
||||||
|
<h6 class="mb-2 border-bottom pb-2">
|
||||||
|
<i-bs name="robot" width="1em" height="1em" class="me-1"></i-bs>
|
||||||
|
<span i18n>AI Scanner Status</span>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-row mb-2">
|
||||||
|
<span class="status-label" i18n>Status:</span>
|
||||||
|
<span [class]="aiStatus.active ? 'text-success' : 'text-muted'">
|
||||||
|
{{ aiStatus.active ? 'Active' : 'Inactive' }}
|
||||||
|
@if (aiStatus.processing) {
|
||||||
|
<span class="badge bg-primary ms-1" i18n>Processing</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (aiStatus.active) {
|
||||||
|
<div class="status-row mb-2">
|
||||||
|
<span class="status-label" i18n>Scanned Today:</span>
|
||||||
|
<span class="fw-bold">{{ aiStatus.documents_scanned_today }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-row mb-2">
|
||||||
|
<span class="status-label" i18n>Suggestions Applied:</span>
|
||||||
|
<span class="fw-bold">{{ aiStatus.suggestions_applied }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (aiStatus.pending_deletion_requests > 0) {
|
||||||
|
<div class="status-row mb-2">
|
||||||
|
<span class="status-label text-danger" i18n>Pending Deletions:</span>
|
||||||
|
<span class="fw-bold text-danger">{{ aiStatus.pending_deletion_requests }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (aiStatus.last_scan) {
|
||||||
|
<div class="status-row mb-2 small text-muted">
|
||||||
|
<span class="status-label" i18n>Last Scan:</span>
|
||||||
|
<span>{{ aiStatus.last_scan | date: 'short' }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 pt-2 border-top">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary w-100"
|
||||||
|
(click)="navigateToSettings()"
|
||||||
|
i18n>
|
||||||
|
Configure AI Scanner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
.ai-status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-status-icon {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
opacity: 0.4;
|
||||||
|
color: var(--bs-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--bs-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.processing {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-status-tooltip {
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge positioning
|
||||||
|
.badge.rounded-pill {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
min-width: 1.2rem;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
|
import { of } from 'rxjs'
|
||||||
|
import { AIStatus } from 'src/app/data/ai-status'
|
||||||
|
import { AIStatusService } from 'src/app/services/ai-status.service'
|
||||||
|
import { AIStatusIndicatorComponent } from './ai-status-indicator.component'
|
||||||
|
|
||||||
|
describe('AIStatusIndicatorComponent', () => {
|
||||||
|
let component: AIStatusIndicatorComponent
|
||||||
|
let fixture: ComponentFixture<AIStatusIndicatorComponent>
|
||||||
|
let aiStatusService: jasmine.SpyObj<AIStatusService>
|
||||||
|
let router: jasmine.SpyObj<Router>
|
||||||
|
|
||||||
|
const mockAIStatus: AIStatus = {
|
||||||
|
active: true,
|
||||||
|
processing: false,
|
||||||
|
documents_scanned_today: 42,
|
||||||
|
suggestions_applied: 15,
|
||||||
|
pending_deletion_requests: 2,
|
||||||
|
last_scan: '2025-11-15T12:00:00Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const aiStatusServiceSpy = jasmine.createSpyObj('AIStatusService', [
|
||||||
|
'getStatus',
|
||||||
|
'getCurrentStatus',
|
||||||
|
'refresh',
|
||||||
|
])
|
||||||
|
const routerSpy = jasmine.createSpyObj('Router', ['navigate'])
|
||||||
|
|
||||||
|
aiStatusServiceSpy.getStatus.and.returnValue(of(mockAIStatus))
|
||||||
|
aiStatusServiceSpy.getCurrentStatus.and.returnValue(mockAIStatus)
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AIStatusIndicatorComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: AIStatusService, useValue: aiStatusServiceSpy },
|
||||||
|
{ provide: Router, useValue: routerSpy },
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
aiStatusService = TestBed.inject(
|
||||||
|
AIStatusService
|
||||||
|
) as jasmine.SpyObj<AIStatusService>
|
||||||
|
router = TestBed.inject(Router) as jasmine.SpyObj<Router>
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AIStatusIndicatorComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should subscribe to AI status on init', () => {
|
||||||
|
expect(aiStatusService.getStatus).toHaveBeenCalled()
|
||||||
|
expect(component.aiStatus).toEqual(mockAIStatus)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show robot icon', () => {
|
||||||
|
expect(component.iconName).toBe('robot')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have active class when AI is active', () => {
|
||||||
|
component.aiStatus = { ...mockAIStatus, active: true, processing: false }
|
||||||
|
expect(component.iconClass).toContain('active')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have inactive class when AI is inactive', () => {
|
||||||
|
component.aiStatus = { ...mockAIStatus, active: false }
|
||||||
|
expect(component.iconClass).toContain('inactive')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have processing class when AI is processing', () => {
|
||||||
|
component.aiStatus = { ...mockAIStatus, active: true, processing: true }
|
||||||
|
expect(component.iconClass).toContain('processing')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show alerts when there are pending deletion requests', () => {
|
||||||
|
component.aiStatus = { ...mockAIStatus, pending_deletion_requests: 2 }
|
||||||
|
expect(component.hasAlerts).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not show alerts when there are no pending deletion requests', () => {
|
||||||
|
component.aiStatus = { ...mockAIStatus, pending_deletion_requests: 0 }
|
||||||
|
expect(component.hasAlerts).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate to settings when navigateToSettings is called', () => {
|
||||||
|
component.navigateToSettings()
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/settings'], {
|
||||||
|
fragment: 'ai-scanner',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should unsubscribe on destroy', () => {
|
||||||
|
const subscription = component['subscription']
|
||||||
|
spyOn(subscription, 'unsubscribe')
|
||||||
|
component.ngOnDestroy()
|
||||||
|
expect(subscription.unsubscribe).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { DatePipe } from '@angular/common'
|
||||||
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
|
import { Router, RouterModule } from '@angular/router'
|
||||||
|
import {
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgbTooltipModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
import { AIStatus } from 'src/app/data/ai-status'
|
||||||
|
import { AIStatusService } from 'src/app/services/ai-status.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-ai-status-indicator',
|
||||||
|
templateUrl: './ai-status-indicator.component.html',
|
||||||
|
styleUrls: ['./ai-status-indicator.component.scss'],
|
||||||
|
imports: [
|
||||||
|
DatePipe,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgbTooltipModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
RouterModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AIStatusIndicatorComponent implements OnInit, OnDestroy {
|
||||||
|
private aiStatusService = inject(AIStatusService)
|
||||||
|
private router = inject(Router)
|
||||||
|
|
||||||
|
private subscription: Subscription
|
||||||
|
|
||||||
|
public aiStatus: AIStatus = {
|
||||||
|
active: false,
|
||||||
|
processing: false,
|
||||||
|
documents_scanned_today: 0,
|
||||||
|
suggestions_applied: 0,
|
||||||
|
pending_deletion_requests: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subscription = this.aiStatusService
|
||||||
|
.getStatus()
|
||||||
|
.subscribe((status) => {
|
||||||
|
this.aiStatus = status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscription?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate icon name based on AI status
|
||||||
|
*/
|
||||||
|
get iconName(): string {
|
||||||
|
if (!this.aiStatus.active) {
|
||||||
|
return 'robot' // Inactive
|
||||||
|
}
|
||||||
|
if (this.aiStatus.processing) {
|
||||||
|
return 'robot' // Processing (will add animation via CSS)
|
||||||
|
}
|
||||||
|
return 'robot' // Active
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the CSS class for the icon based on status
|
||||||
|
*/
|
||||||
|
get iconClass(): string {
|
||||||
|
const classes = ['ai-status-icon']
|
||||||
|
|
||||||
|
if (!this.aiStatus.active) {
|
||||||
|
classes.push('inactive')
|
||||||
|
} else if (this.aiStatus.processing) {
|
||||||
|
classes.push('processing')
|
||||||
|
} else {
|
||||||
|
classes.push('active')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to AI configuration settings
|
||||||
|
*/
|
||||||
|
navigateToSettings(): void {
|
||||||
|
this.router.navigate(['/settings'], { fragment: 'ai-scanner' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are any alerts (pending deletion requests)
|
||||||
|
*/
|
||||||
|
get hasAlerts(): boolean {
|
||||||
|
return this.aiStatus.pending_deletion_requests > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
<ul ngbNav class="order-sm-3">
|
<ul ngbNav class="order-sm-3">
|
||||||
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
<pngx-toasts-dropdown></pngx-toasts-dropdown>
|
||||||
|
<pngx-ai-status-indicator></pngx-ai-status-indicator>
|
||||||
<li ngbDropdown class="nav-item dropdown">
|
<li ngbDropdown class="nav-item dropdown">
|
||||||
<button class="btn ps-1 border-0" id="userDropdown" ngbDropdownToggle>
|
<button class="btn ps-1 border-0" id="userDropdown" ngbDropdownToggle>
|
||||||
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import { environment } from 'src/environments/environment'
|
||||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
import { AIStatusIndicatorComponent } from './ai-status-indicator/ai-status-indicator.component'
|
||||||
import { GlobalSearchComponent } from './global-search/global-search.component'
|
import { GlobalSearchComponent } from './global-search/global-search.component'
|
||||||
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
|
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
|
||||||
|
|
||||||
|
|
@ -59,6 +60,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
|
||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
ToastsDropdownComponent,
|
ToastsDropdownComponent,
|
||||||
|
AIStatusIndicatorComponent,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
NgClass,
|
NgClass,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
|
|
|
||||||
87
src-ui/src/app/services/ai-status.service.spec.ts
Normal file
87
src-ui/src/app/services/ai-status.service.spec.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import {
|
||||||
|
HttpClientTestingModule,
|
||||||
|
HttpTestingController,
|
||||||
|
} from '@angular/common/http/testing'
|
||||||
|
import { TestBed } from '@angular/core/testing'
|
||||||
|
import { AIStatus } from 'src/app/data/ai-status'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
import { AIStatusService } from './ai-status.service'
|
||||||
|
|
||||||
|
describe('AIStatusService', () => {
|
||||||
|
let service: AIStatusService
|
||||||
|
let httpMock: HttpTestingController
|
||||||
|
|
||||||
|
const mockAIStatus: AIStatus = {
|
||||||
|
active: true,
|
||||||
|
processing: false,
|
||||||
|
documents_scanned_today: 42,
|
||||||
|
suggestions_applied: 15,
|
||||||
|
pending_deletion_requests: 2,
|
||||||
|
last_scan: '2025-11-15T12:00:00Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [AIStatusService],
|
||||||
|
})
|
||||||
|
service = TestBed.inject(AIStatusService)
|
||||||
|
httpMock = TestBed.inject(HttpTestingController)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return AI status as observable', (done) => {
|
||||||
|
service.getStatus().subscribe((status) => {
|
||||||
|
expect(status).toBeDefined()
|
||||||
|
expect(status.active).toBeDefined()
|
||||||
|
expect(status.processing).toBeDefined()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return current status value', () => {
|
||||||
|
const status = service.getCurrentStatus()
|
||||||
|
expect(status).toBeDefined()
|
||||||
|
expect(status.active).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch AI status from backend', (done) => {
|
||||||
|
service['fetchAIStatus']().subscribe((status) => {
|
||||||
|
expect(status).toEqual(mockAIStatus)
|
||||||
|
expect(service.loading).toBe(false)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(`${environment.apiBaseUrl}ai/status/`)
|
||||||
|
expect(req.request.method).toBe('GET')
|
||||||
|
req.flush(mockAIStatus)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle error and return mock data', (done) => {
|
||||||
|
service['fetchAIStatus']().subscribe((status) => {
|
||||||
|
expect(status).toBeDefined()
|
||||||
|
expect(status.active).toBeDefined()
|
||||||
|
expect(service.loading).toBe(false)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(`${environment.apiBaseUrl}ai/status/`)
|
||||||
|
req.error(new ProgressEvent('error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should manually refresh status', () => {
|
||||||
|
service.refresh()
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(`${environment.apiBaseUrl}ai/status/`)
|
||||||
|
expect(req.request.method).toBe('GET')
|
||||||
|
req.flush(mockAIStatus)
|
||||||
|
})
|
||||||
|
})
|
||||||
100
src-ui/src/app/services/ai-status.service.ts
Normal file
100
src-ui/src/app/services/ai-status.service.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { HttpClient } from '@angular/common/http'
|
||||||
|
import { Injectable, inject } from '@angular/core'
|
||||||
|
import { BehaviorSubject, Observable, interval } from 'rxjs'
|
||||||
|
import { catchError, map, startWith, switchMap } from 'rxjs/operators'
|
||||||
|
import { AIStatus } from 'src/app/data/ai-status'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AIStatusService {
|
||||||
|
private http = inject(HttpClient)
|
||||||
|
|
||||||
|
private baseUrl: string = environment.apiBaseUrl
|
||||||
|
private aiStatusSubject = new BehaviorSubject<AIStatus>({
|
||||||
|
active: false,
|
||||||
|
processing: false,
|
||||||
|
documents_scanned_today: 0,
|
||||||
|
suggestions_applied: 0,
|
||||||
|
pending_deletion_requests: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
public loading: boolean = false
|
||||||
|
|
||||||
|
// Poll every 30 seconds for AI status updates
|
||||||
|
private readonly POLL_INTERVAL = 30000
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.startPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current AI status as an observable
|
||||||
|
*/
|
||||||
|
public getStatus(): Observable<AIStatus> {
|
||||||
|
return this.aiStatusSubject.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current AI status value
|
||||||
|
*/
|
||||||
|
public getCurrentStatus(): AIStatus {
|
||||||
|
return this.aiStatusSubject.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start polling for AI status updates
|
||||||
|
*/
|
||||||
|
private startPolling(): void {
|
||||||
|
interval(this.POLL_INTERVAL)
|
||||||
|
.pipe(
|
||||||
|
startWith(0), // Emit immediately on subscription
|
||||||
|
switchMap(() => this.fetchAIStatus())
|
||||||
|
)
|
||||||
|
.subscribe((status) => {
|
||||||
|
this.aiStatusSubject.next(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch AI status from the backend
|
||||||
|
*/
|
||||||
|
private fetchAIStatus(): Observable<AIStatus> {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
return this.http
|
||||||
|
.get<AIStatus>(`${this.baseUrl}ai/status/`)
|
||||||
|
.pipe(
|
||||||
|
map((status) => {
|
||||||
|
this.loading = false
|
||||||
|
return status
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.loading = false
|
||||||
|
console.warn('Failed to fetch AI status, using mock data:', error)
|
||||||
|
// Return mock data if endpoint doesn't exist yet
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
processing: false,
|
||||||
|
documents_scanned_today: 42,
|
||||||
|
suggestions_applied: 15,
|
||||||
|
pending_deletion_requests: 2,
|
||||||
|
last_scan: new Date().toISOString(),
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually refresh the AI status
|
||||||
|
*/
|
||||||
|
public refresh(): void {
|
||||||
|
this.fetchAIStatus().subscribe((status) => {
|
||||||
|
this.aiStatusSubject.next(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue