Browse Source

Persist Ghostfolio AI in user settings

pull/8142/head
Thomas Kaul 4 days ago
parent
commit
c8ca7ff525
  1. 2
      apps/api/src/app/endpoints/ai/ai.controller.spec.ts
  2. 2
      apps/api/src/app/endpoints/ai/ai.controller.ts
  3. 4
      apps/api/src/app/user/user.controller.ts
  4. 5
      apps/api/src/app/user/user.service.ts
  5. 22
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  6. 19
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  7. 18
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  8. 3
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  9. 4
      libs/common/src/lib/dtos/update-user-setting.dto.ts
  10. 1
      libs/common/src/lib/helper.ts
  11. 1
      libs/common/src/lib/interfaces/user-settings.interface.ts
  12. 2
      libs/common/src/lib/permissions.ts

2
apps/api/src/app/endpoints/ai/ai.controller.spec.ts

@ -95,7 +95,7 @@ describe('AiController', () => {
describe('getActivitiesFromFile', () => {
it('Requires the permission and limits the requests of each user per week', () => {
expect(getMetadataOfMethod<string>(HAS_PERMISSION_KEY)).toBe(
permissions.importActivitiesWithAi
permissions.enableGhostfolioAi
);
const guards = getMetadataOfMethod<unknown[]>(GUARDS_METADATA);

2
apps/api/src/app/endpoints/ai/ai.controller.ts

@ -73,7 +73,7 @@ export class AiController {
* written for the caller gives the status 400 with this message, the
* timeout gives the status 400 with a message for the caller as well.
*/
@HasPermission(permissions.importActivitiesWithAi)
@HasPermission(permissions.enableGhostfolioAi)
@HttpCode(StatusCodes.OK)
@Post('activities')
@Throttle({

4
apps/api/src/app/user/user.controller.ts

@ -240,6 +240,10 @@ export class UserController {
}
}
if (!userSettings.isExperimentalFeatures) {
delete userSettings.isGhostfolioAiEnabled;
}
return this.userService.updateUserSetting({
emitPortfolioChangedEvent,
userSettings,

5
apps/api/src/app/user/user.service.ts

@ -469,6 +469,9 @@ export class UserService {
// Reset holdings view mode
user.settings.settings.holdingsViewMode = undefined;
// Reset Ghostfolio AI
user.settings.settings.isGhostfolioAiEnabled = undefined;
} else if (user.subscription?.type === SubscriptionType.Premium) {
if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.createApiKey);
@ -477,7 +480,7 @@ export class UserService {
currentPermissions.push(permissions.reportDataGlitch);
if ((user.settings.settings as UserSettings).isExperimentalFeatures) {
currentPermissions.push(permissions.importActivitiesWithAi);
currentPermissions.push(permissions.enableGhostfolioAi);
}
}

22
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -93,6 +93,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
});
protected hasImpersonationId: boolean;
protected hasPermissionToDeleteOwnUser: boolean;
protected hasPermissionToEnableGhostfolioAi: boolean;
protected hasPermissionToRequestOwnUserDeletion: boolean;
protected hasPermissionToUpdateOwnAccessToken: boolean;
protected hasPermissionToUpdateViewMode: boolean;
@ -190,6 +191,11 @@ export class GfUserAccountSettingsComponent implements OnInit {
permissions.deleteOwnUser
);
this.hasPermissionToEnableGhostfolioAi = hasPermission(
this.user.permissions,
permissions.enableGhostfolioAi
);
this.hasPermissionToRequestOwnUserDeletion = hasPermission(
this.user.permissions,
permissions.requestOwnUserDeletion
@ -339,6 +345,22 @@ export class GfUserAccountSettingsComponent implements OnInit {
});
}
protected onGhostfolioAiChange(aEvent: MatSlideToggleChange) {
this.dataService
.putUserSetting({ isGhostfolioAiEnabled: aEvent.checked })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
protected onRestrictedViewChange(aEvent: MatSlideToggleChange) {
this.dataService
.putUserSetting({ isRestrictedView: aEvent.checked })

19
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -290,6 +290,25 @@
/>
</div>
</div>
@if (deviceType() !== 'mobile' && hasPermissionToEnableGhostfolioAi) {
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50">
<div>Ghostfolio AI</div>
<div class="hint-text text-muted" i18n>
Data processing by a third-party AI provider
</div>
</div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
hideIcon="true"
[checked]="user.settings.isGhostfolioAiEnabled"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onGhostfolioAiChange($event)"
/>
</div>
</div>
}
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50">
Ghostfolio <ng-container i18n>User ID</ng-container>

18
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -124,13 +124,20 @@ export class GfImportActivitiesDialogComponent {
protected dialogTitle = $localize`Import Activities`;
protected errorMessages: string[] = [];
protected readonly ghostfolioAiTooltip = $localize`The content of your file will be processed by a third-party AI provider. Please remove any personal data before uploading.`;
protected readonly hasPermissionToImportActivitiesWithAi = hasPermission(
protected readonly hasPermissionToEnableGhostfolioAi = hasPermission(
this.data.user?.permissions,
permissions.importActivitiesWithAi
permissions.enableGhostfolioAi
);
protected holdings: PortfolioPosition[] = [];
protected importStep: ImportStep = ImportStep.UPLOAD_FILE;
protected isGhostfolioAiEnabled = false;
protected isGhostfolioAiEnabled =
this.data.deviceType !== 'mobile' &&
this.hasPermissionToEnableGhostfolioAi &&
this.data.user?.settings?.isGhostfolioAiEnabled === true;
protected isLoading = false;
protected isMaxActivitiesOfGhostfolioAiReached = false;
protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES;
@ -261,6 +268,11 @@ export class GfImportActivitiesDialogComponent {
protected onGhostfolioAiChange({ checked }: MatCheckboxChange) {
this.isGhostfolioAiEnabled = checked;
this.dataService
.putUserSetting({ isGhostfolioAiEnabled: checked })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
}
protected onImportStepChange(event: StepperSelectionEvent) {

3
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -125,8 +125,7 @@
</small>
</p>
@if (
data.deviceType !== 'mobile' &&
hasPermissionToImportActivitiesWithAi
data.deviceType !== 'mobile' && hasPermissionToEnableGhostfolioAi
) {
<div class="align-items-center d-flex mt-4">
<mat-checkbox

4
libs/common/src/lib/dtos/update-user-setting.dto.ts

@ -84,6 +84,10 @@ export class UpdateUserSettingDto {
@IsOptional()
isExperimentalFeatures?: boolean;
@IsBoolean()
@IsOptional()
isGhostfolioAiEnabled?: boolean;
@IsBoolean()
@IsOptional()
isRestrictedView?: boolean;

1
libs/common/src/lib/helper.ts

@ -86,6 +86,7 @@ const USER_SETTINGS_KEYS_OF_AUTHENTICATED_USER: (keyof UserSettings)[] = [
'filters.tags',
'holdingsViewMode',
'isExperimentalFeatures',
'isGhostfolioAiEnabled',
'isRestrictedView',
'language',
'locale',

1
libs/common/src/lib/interfaces/user-settings.interface.ts

@ -22,6 +22,7 @@ export interface UserSettings {
'filters.tags'?: string[];
holdingsViewMode?: HoldingsViewMode;
isExperimentalFeatures?: boolean;
isGhostfolioAiEnabled?: boolean;
isRestrictedView?: boolean;
language?: string;
locale?: string;

2
libs/common/src/lib/permissions.ts

@ -39,6 +39,7 @@ export const permissions = {
enableAuthToken: 'enableAuthToken',
enableDataProviderGhostfolio: 'enableDataProviderGhostfolio',
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
enableGhostfolioAi: 'enableGhostfolioAi',
enableImport: 'enableImport',
enableMcp: 'enableMcp',
enableBlog: 'enableBlog',
@ -47,7 +48,6 @@ export const permissions = {
enableSubscriptionInterstitial: 'enableSubscriptionInterstitial',
enableSystemMessage: 'enableSystemMessage',
impersonateAllUsers: 'impersonateAllUsers',
importActivitiesWithAi: 'importActivitiesWithAi',
readAiPrompt: 'readAiPrompt',
readMarketData: 'readMarketData',
readMarketDataOfMarkets: 'readMarketDataOfMarkets',

Loading…
Cancel
Save