Feature/extend AI prompt API by mode (#4395)
* Extend AI prompt API by mode * Update changelog
This commit is contained in:
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a _Copy portfolio data to clipboard for AI prompt_ action to the analysis page (experimental)
|
||||||
|
|
||||||
## 2.144.0 - 2025-03-06
|
## 2.144.0 - 2025-03-06
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { AiPromptResponse } from '@ghostfolio/common/interfaces';
|
import { AiPromptResponse } from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, Param, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
@ -21,11 +21,14 @@ export class AiController {
|
|||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('prompt')
|
@Get('prompt/:mode')
|
||||||
@HasPermission(permissions.readAiPrompt)
|
@HasPermission(permissions.readAiPrompt)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async getPrompt(): Promise<AiPromptResponse> {
|
public async getPrompt(
|
||||||
|
@Param('mode') mode: AiPromptMode
|
||||||
|
): Promise<AiPromptResponse> {
|
||||||
const prompt = await this.aiService.getPrompt({
|
const prompt = await this.aiService.getPrompt({
|
||||||
|
mode,
|
||||||
impersonationId: undefined,
|
impersonationId: undefined,
|
||||||
languageCode:
|
languageCode:
|
||||||
this.request.user.Settings.settings.language ?? DEFAULT_LANGUAGE_CODE,
|
this.request.user.Settings.settings.language ?? DEFAULT_LANGUAGE_CODE,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
|
import type { AiPromptMode } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@ -9,11 +10,13 @@ export class AiService {
|
|||||||
public async getPrompt({
|
public async getPrompt({
|
||||||
impersonationId,
|
impersonationId,
|
||||||
languageCode,
|
languageCode,
|
||||||
|
mode,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
|
mode: AiPromptMode;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
@ -43,6 +46,10 @@ export class AiService {
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (mode === 'portfolio') {
|
||||||
|
return holdingsTable.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`,
|
`You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`,
|
||||||
...holdingsTable,
|
...holdingsTable,
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { GroupBy } from '@ghostfolio/common/types';
|
import type { AiPromptMode, GroupBy } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
import { Clipboard } from '@angular/cdk/clipboard';
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
@ -142,9 +142,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
this.fetchDividendsAndInvestments();
|
this.fetchDividendsAndInvestments();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCopyPromptToClipboard() {
|
public onCopyPromptToClipboard(mode: AiPromptMode) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPrompt()
|
.fetchPrompt(mode)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ prompt }) => {
|
.subscribe(({ prompt }) => {
|
||||||
this.clipboard.copy(prompt);
|
this.clipboard.copy(prompt);
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<button
|
<button
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[disabled]="!hasPermissionToReadAiPrompt"
|
[disabled]="!hasPermissionToReadAiPrompt"
|
||||||
(click)="onCopyPromptToClipboard()"
|
(click)="onCopyPromptToClipboard('portfolio')"
|
||||||
>
|
>
|
||||||
<span class="align-items-center d-flex">
|
<span class="align-items-center d-flex">
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
@ -24,7 +24,25 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<ion-icon class="mr-2" name="copy-outline" />
|
<ion-icon class="mr-2" name="copy-outline" />
|
||||||
}
|
}
|
||||||
<ng-container i18n>Copy AI prompt to clipboard</ng-container>
|
<ng-container i18n
|
||||||
|
>Copy portfolio data to clipboard for AI prompt</ng-container
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="!hasPermissionToReadAiPrompt"
|
||||||
|
(click)="onCopyPromptToClipboard('analysis')"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="mr-2" />
|
||||||
|
} @else {
|
||||||
|
<ion-icon class="mr-2" name="copy-outline" />
|
||||||
|
}
|
||||||
|
<ng-container i18n
|
||||||
|
>Copy AI prompt to clipboard for analysis</ng-container
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -46,7 +46,12 @@ import {
|
|||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import { AccountWithValue, DateRange, GroupBy } from '@ghostfolio/common/types';
|
import type {
|
||||||
|
AccountWithValue,
|
||||||
|
AiPromptMode,
|
||||||
|
DateRange,
|
||||||
|
GroupBy
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
@ -650,8 +655,8 @@ export class DataService {
|
|||||||
return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
|
return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPrompt() {
|
public fetchPrompt(mode: AiPromptMode) {
|
||||||
return this.http.get<AiPromptResponse>('/api/v1/ai/prompt');
|
return this.http.get<AiPromptResponse>(`/api/v1/ai/prompt/${mode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPublicPortfolio(aAccessId: string) {
|
public fetchPublicPortfolio(aAccessId: string) {
|
||||||
|
1
libs/common/src/lib/types/ai-prompt-mode.type.ts
Normal file
1
libs/common/src/lib/types/ai-prompt-mode.type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type AiPromptMode = 'analysis' | 'portfolio';
|
@ -2,6 +2,7 @@ import type { AccessType } from './access-type.type';
|
|||||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||||
import type { AccountWithPlatform } from './account-with-platform.type';
|
import type { AccountWithPlatform } from './account-with-platform.type';
|
||||||
import type { AccountWithValue } from './account-with-value.type';
|
import type { AccountWithValue } from './account-with-value.type';
|
||||||
|
import type { AiPromptMode } from './ai-prompt-mode.type';
|
||||||
import type { BenchmarkTrend } from './benchmark-trend.type';
|
import type { BenchmarkTrend } from './benchmark-trend.type';
|
||||||
import type { ColorScheme } from './color-scheme.type';
|
import type { ColorScheme } from './color-scheme.type';
|
||||||
import type { DateRange } from './date-range.type';
|
import type { DateRange } from './date-range.type';
|
||||||
@ -24,6 +25,7 @@ export type {
|
|||||||
AccessWithGranteeUser,
|
AccessWithGranteeUser,
|
||||||
AccountWithPlatform,
|
AccountWithPlatform,
|
||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
|
AiPromptMode,
|
||||||
BenchmarkTrend,
|
BenchmarkTrend,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
DateRange,
|
DateRange,
|
||||||
|
Reference in New Issue
Block a user