This commit is contained in:
Bastian Wagner
2026-06-26 10:13:31 +02:00
parent 9078da9f66
commit 671fd62ad8
7 changed files with 852 additions and 0 deletions

View File

@@ -57,6 +57,20 @@
Template
</button>
<button
mat-stroked-button
type="button"
[disabled]="cleaningList() || !onlineStatus.online()"
(click)="applyCleanup()"
>
@if (cleaningList()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">auto_fix_high</mat-icon>
}
AI Cleanup
</button>
<button mat-stroked-button type="button" (click)="showEditor() ? cancelEditing() : startEditing()">
<mat-icon aria-hidden="true">{{ showEditor() ? 'close' : 'edit' }}</mat-icon>
{{ showEditor() ? 'Abbrechen' : 'Bearbeiten' }}

View File

@@ -20,6 +20,7 @@ import { OnboardingService } from '../../onboarding/onboarding.service';
import { ConfirmDeleteListDialogComponent } from '../confirm-delete-list-dialog/confirm-delete-list-dialog.component';
import { OnlineStatusService } from '../../offline/online-status.service';
import {
ApplyListCleanupResponse,
ListItemSuggestion,
ListRealtimeEvent,
UserList,
@@ -66,6 +67,7 @@ export class ListDetailComponent implements OnInit {
protected readonly saving = signal(false);
protected readonly creatingWithAi = signal(false);
protected readonly creatingTemplate = signal(false);
protected readonly cleaningList = signal(false);
protected readonly deletingList = signal(false);
protected readonly editing = signal(false);
protected readonly addingItem = signal(false);
@@ -342,6 +344,32 @@ export class ListDetailComponent implements OnInit {
});
}
protected applyCleanup(): void {
const listId = this.currentListId();
if (!listId || this.cleaningList()) {
return;
}
this.cleaningList.set(true);
this.listsService
.applyCleanup(listId)
.pipe(finalize(() => this.cleaningList.set(false)))
.subscribe({
next: (response) => {
this.setList(response.list, !this.showEditor());
this.itemSuggestions.set([]);
this.suggestionsLoaded.set(false);
this.snackBar.open(this.cleanupSummary(response), 'OK', {
duration: 4000,
});
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 });
},
});
}
protected addSuggestion(suggestion: ListItemSuggestion): void {
const listId = this.currentListId();
@@ -615,4 +643,23 @@ export class ListDetailComponent implements OnInit {
private suggestionKey(value: string): string {
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}
private cleanupSummary(response: ApplyListCleanupResponse): string {
const parts = [
response.summary.listUpdated ? 'Details aktualisiert' : null,
response.summary.itemsUpdated > 0
? `${response.summary.itemsUpdated} Items verbessert`
: null,
response.summary.duplicatesDeleted > 0
? `${response.summary.duplicatesDeleted} Duplikate entfernt`
: null,
response.summary.itemsAdded > 0
? `${response.summary.itemsAdded} Items hinzugefuegt`
: null,
].filter((part): part is string => part !== null);
return parts.length > 0
? `Cleanup angewendet: ${parts.join(', ')}.`
: 'Cleanup geprueft: keine sinnvollen Aenderungen gefunden.';
}
}

View File

@@ -79,6 +79,53 @@ export interface CreateListWithItemSuggestionsResponse {
suggestions: ListItemSuggestion[];
}
export interface ListCleanupListUpdate {
name?: string;
description?: string;
reason?: string;
}
export interface ListCleanupItemUpdate {
itemId: string;
title?: string;
notes?: string;
quantity?: number;
required?: boolean;
reason?: string;
}
export interface ListCleanupDuplicateDeletion {
itemId: string;
keptItemId: string;
reason?: string;
}
export interface ListCleanupItemAddition extends ListItemSuggestion {
reason?: string;
}
export interface ListCleanupSuggestion {
listUpdate?: ListCleanupListUpdate;
itemUpdates: ListCleanupItemUpdate[];
duplicateDeletions: ListCleanupDuplicateDeletion[];
itemAdditions: ListCleanupItemAddition[];
}
export interface ListCleanupSuggestionsResponse {
cleanup: ListCleanupSuggestion;
}
export interface ApplyListCleanupResponse {
list: UserList;
cleanup: ListCleanupSuggestion;
summary: {
listUpdated: boolean;
itemsUpdated: number;
duplicatesDeleted: number;
itemsAdded: number;
};
}
export interface UpdateListItemRequest {
title?: string;
notes?: string;

View File

@@ -4,8 +4,10 @@ import { Observable } from 'rxjs';
import { of, tap, throwError } from 'rxjs';
import {
AddListItemRequest,
ApplyListCleanupResponse,
CreateListRequest,
CreateListWithItemSuggestionsResponse,
ListCleanupSuggestionsResponse,
ListItemSuggestionsResponse,
UpdateListItemRequest,
UpdateListRequest,
@@ -173,6 +175,34 @@ export class ListsService {
);
}
suggestCleanup(listId: string): Observable<ListCleanupSuggestionsResponse> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('AI Cleanup ist nur online verfuegbar.'),
);
}
return this.http.post<ListCleanupSuggestionsResponse>(
`${this.apiUrl}/${listId}/cleanup-suggestions`,
{},
);
}
applyCleanup(listId: string): Observable<ApplyListCleanupResponse> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('AI Cleanup ist nur online verfuegbar.'),
);
}
return this.http
.post<ApplyListCleanupResponse>(
`${this.apiUrl}/${listId}/apply-cleanup`,
{},
)
.pipe(tap((response) => this.upsertCachedList(response.list)));
}
createTemplateFromList(listId: string): Observable<ListTemplate> {
if (!this.onlineStatus.online()) {
return throwError(