Browse Source

Feature/Ghostfolio AI in user settings (#8142)

* Persist Ghostfolio AI in user settings

* Update changelog
pull/8147/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
d910fd40fc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/endpoints/ai/ai.controller.spec.ts
  3. 2
      apps/api/src/app/endpoints/ai/ai.controller.ts
  4. 4
      apps/api/src/app/user/user.controller.ts
  5. 7
      apps/api/src/app/user/user.service.ts
  6. 22
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  7. 19
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  8. 18
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  9. 3
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  10. 4
      libs/common/src/lib/dtos/update-user-setting.dto.ts
  11. 1
      libs/common/src/lib/helper.ts
  12. 1
      libs/common/src/lib/interfaces/user-settings.interface.ts
  13. 2
      libs/common/src/lib/permissions.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added an option to enable _Ghostfolio AI_ in the user settings of the user account page (experimental)
- Added an icon to indicate the client of the last request in the users table of the admin control panel - Added an icon to indicate the client of the last request in the users table of the admin control panel
### Changed ### Changed

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

@ -95,7 +95,7 @@ describe('AiController', () => {
describe('getActivitiesFromFile', () => { describe('getActivitiesFromFile', () => {
it('Requires the permission and limits the requests of each user per week', () => { it('Requires the permission and limits the requests of each user per week', () => {
expect(getMetadataOfMethod<string>(HAS_PERMISSION_KEY)).toBe( expect(getMetadataOfMethod<string>(HAS_PERMISSION_KEY)).toBe(
permissions.importActivitiesWithAi permissions.enableGhostfolioAi
); );
const guards = getMetadataOfMethod<unknown[]>(GUARDS_METADATA); 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 * 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. * timeout gives the status 400 with a message for the caller as well.
*/ */
@HasPermission(permissions.importActivitiesWithAi) @HasPermission(permissions.enableGhostfolioAi)
@HttpCode(StatusCodes.OK) @HttpCode(StatusCodes.OK)
@Post('activities') @Post('activities')
@Throttle({ @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({ return this.userService.updateUserSetting({
emitPortfolioChangedEvent, emitPortfolioChangedEvent,
userSettings, userSettings,

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

@ -469,6 +469,9 @@ export class UserService {
// Reset holdings view mode // Reset holdings view mode
user.settings.settings.holdingsViewMode = undefined; user.settings.settings.holdingsViewMode = undefined;
// Reset Ghostfolio AI
user.settings.settings.isGhostfolioAiEnabled = undefined;
} else if (user.subscription?.type === SubscriptionType.Premium) { } else if (user.subscription?.type === SubscriptionType.Premium) {
if (!hasRole(user, Role.DEMO)) { if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.createApiKey); currentPermissions.push(permissions.createApiKey);
@ -477,7 +480,7 @@ export class UserService {
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
if ((user.settings.settings as UserSettings).isExperimentalFeatures) { if ((user.settings.settings as UserSettings).isExperimentalFeatures) {
currentPermissions.push(permissions.importActivitiesWithAi); currentPermissions.push(permissions.enableGhostfolioAi);
} }
} }
@ -527,9 +530,9 @@ export class UserService {
if (isReadOnlyMode) { if (isReadOnlyMode) {
currentPermissions = currentPermissions.filter((permission) => { currentPermissions = currentPermissions.filter((permission) => {
return !( return !(
permission === permissions.enableGhostfolioAi ||
permission.startsWith('create') || permission.startsWith('create') ||
permission.startsWith('delete') || permission.startsWith('delete') ||
permission.startsWith('import') ||
permission.startsWith('update') permission.startsWith('update')
); );
}); });

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 hasImpersonationId: boolean;
protected hasPermissionToDeleteOwnUser: boolean; protected hasPermissionToDeleteOwnUser: boolean;
protected hasPermissionToEnableGhostfolioAi: boolean;
protected hasPermissionToRequestOwnUserDeletion: boolean; protected hasPermissionToRequestOwnUserDeletion: boolean;
protected hasPermissionToUpdateOwnAccessToken: boolean; protected hasPermissionToUpdateOwnAccessToken: boolean;
protected hasPermissionToUpdateViewMode: boolean; protected hasPermissionToUpdateViewMode: boolean;
@ -190,6 +191,11 @@ export class GfUserAccountSettingsComponent implements OnInit {
permissions.deleteOwnUser permissions.deleteOwnUser
); );
this.hasPermissionToEnableGhostfolioAi = hasPermission(
this.user.permissions,
permissions.enableGhostfolioAi
);
this.hasPermissionToRequestOwnUserDeletion = hasPermission( this.hasPermissionToRequestOwnUserDeletion = hasPermission(
this.user.permissions, this.user.permissions,
permissions.requestOwnUserDeletion 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) { protected onRestrictedViewChange(aEvent: MatSlideToggleChange) {
this.dataService this.dataService
.putUserSetting({ isRestrictedView: aEvent.checked }) .putUserSetting({ isRestrictedView: aEvent.checked })

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

@ -290,6 +290,25 @@
/> />
</div> </div>
</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="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50"> <div class="pr-1 w-50">
Ghostfolio <ng-container i18n>User ID</ng-container> 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 dialogTitle = $localize`Import Activities`;
protected errorMessages: string[] = []; 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 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, this.data.user?.permissions,
permissions.importActivitiesWithAi permissions.enableGhostfolioAi
); );
protected holdings: PortfolioPosition[] = []; protected holdings: PortfolioPosition[] = [];
protected importStep: ImportStep = ImportStep.UPLOAD_FILE; 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 isLoading = false;
protected isMaxActivitiesOfGhostfolioAiReached = false; protected isMaxActivitiesOfGhostfolioAiReached = false;
protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES; protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES;
@ -261,6 +268,11 @@ export class GfImportActivitiesDialogComponent {
protected onGhostfolioAiChange({ checked }: MatCheckboxChange) { protected onGhostfolioAiChange({ checked }: MatCheckboxChange) {
this.isGhostfolioAiEnabled = checked; this.isGhostfolioAiEnabled = checked;
this.dataService
.putUserSetting({ isGhostfolioAiEnabled: checked })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
} }
protected onImportStepChange(event: StepperSelectionEvent) { protected onImportStepChange(event: StepperSelectionEvent) {

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save