diff --git a/agent/scripts/seed_portfolio.py b/agent/scripts/seed_portfolio.py
new file mode 100644
index 000000000..5e4e3de4d
--- /dev/null
+++ b/agent/scripts/seed_portfolio.py
@@ -0,0 +1,87 @@
+"""
+seed_portfolio.py — Seed a Ghostfolio account with demo holdings.
+
+Usage:
+ python scripts/seed_portfolio.py
+
+Environment variables:
+ GHOSTFOLIO_BASE_URL (default: http://localhost:3333)
+ GHOSTFOLIO_BEARER_TOKEN (required)
+"""
+
+import asyncio
+import os
+import sys
+
+import httpx
+
+GHOSTFOLIO_URL = os.getenv("GHOSTFOLIO_BASE_URL", "http://localhost:3333")
+TOKEN = os.getenv("GHOSTFOLIO_BEARER_TOKEN", "")
+
+
+async def seed() -> None:
+ if not TOKEN:
+ print("ERROR: GHOSTFOLIO_BEARER_TOKEN is not set.", file=sys.stderr)
+ sys.exit(1)
+
+ holdings = [
+ {
+ "symbol": "AAPL",
+ "quantity": 10,
+ "unitPrice": 150.00,
+ "date": "2024-01-15T00:00:00.000Z",
+ "type": "BUY",
+ "currency": "USD",
+ "dataSource": "YAHOO",
+ "fee": 4.95,
+ },
+ {
+ "symbol": "MSFT",
+ "quantity": 5,
+ "unitPrice": 300.00,
+ "date": "2024-03-10T00:00:00.000Z",
+ "type": "BUY",
+ "currency": "USD",
+ "dataSource": "YAHOO",
+ "fee": 4.95,
+ },
+ {
+ "symbol": "NVDA",
+ "quantity": 3,
+ "unitPrice": 400.00,
+ "date": "2024-06-01T00:00:00.000Z",
+ "type": "BUY",
+ "currency": "USD",
+ "dataSource": "YAHOO",
+ "fee": 4.95,
+ },
+ {
+ "symbol": "TSLA",
+ "quantity": 2,
+ "unitPrice": 200.00,
+ "date": "2024-09-20T00:00:00.000Z",
+ "type": "BUY",
+ "currency": "USD",
+ "dataSource": "YAHOO",
+ "fee": 4.95,
+ },
+ ]
+
+ async with httpx.AsyncClient(timeout=30) as client:
+ resp = await client.post(
+ f"{GHOSTFOLIO_URL}/api/v1/import",
+ headers={"Authorization": f"Bearer {TOKEN}"},
+ json={"activities": holdings},
+ )
+ print(f"Seed result: {resp.status_code}")
+ if resp.status_code == 201:
+ print(f"SUCCESS — imported {len(holdings)} transactions.")
+ for h in holdings:
+ print(f" ✓ {h['type']} {h['quantity']} {h['symbol']} @ ${h['unitPrice']}")
+ else:
+ print(f"RESPONSE: {resp.text}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ asyncio.run(seed())
diff --git a/apps/client/src/app/components/ai-chat/ai-chat.component.html b/apps/client/src/app/components/ai-chat/ai-chat.component.html
index 67a03a4dc..6888f00fd 100644
--- a/apps/client/src/app/components/ai-chat/ai-chat.component.html
+++ b/apps/client/src/app/components/ai-chat/ai-chat.component.html
@@ -1,17 +1,23 @@
-
-
+
-
+
@@ -34,8 +52,18 @@
Your portfolio is empty. Load demo data to try the AI?
-
-
+
+
}
@@ -44,26 +72,32 @@
}
-
+
@for (msg of messages; track $index) {
-
-
+
@if (msg.role === 'assistant' && msg.confidence !== undefined) {
}
-
}
@@ -111,12 +151,76 @@
}
+
+ @if (showSuggestions && !isThinking) {
+
+
+
+
+
+
+
+
+ @if (enableRealEstate) {
+
+
+
+
+
+ }
+
+ }
+
@if (awaitingConfirmation && !isThinking) {
Confirm this transaction?
-
-
+
+
}
@@ -126,29 +230,28 @@
}
-
diff --git a/apps/client/src/app/components/ai-chat/ai-chat.component.scss b/apps/client/src/app/components/ai-chat/ai-chat.component.scss
index 8c5d4373d..c49130df5 100644
--- a/apps/client/src/app/components/ai-chat/ai-chat.component.scss
+++ b/apps/client/src/app/components/ai-chat/ai-chat.component.scss
@@ -21,7 +21,9 @@
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
- transition: transform 0.2s ease, box-shadow 0.2s ease;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
white-space: nowrap;
&:hover {
@@ -123,6 +125,32 @@
letter-spacing: 0.03em;
}
+.ai-panel__header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.ai-clear-btn {
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ border-radius: 0.4rem;
+ color: rgba(255, 255, 255, 0.85);
+ cursor: pointer;
+ font-size: 0.7rem;
+ font-weight: 500;
+ padding: 0.25rem 0.55rem;
+ letter-spacing: 0.02em;
+ transition:
+ background 0.15s ease,
+ color 0.15s ease;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.22);
+ color: #fff;
+ }
+}
+
.ai-panel__close {
background: rgba(255, 255, 255, 0.15);
border: none;
@@ -194,7 +222,9 @@
cursor: pointer;
transition: opacity 0.15s;
- &:hover { opacity: 0.85; }
+ &:hover {
+ opacity: 0.85;
+ }
&--primary {
background: #2563eb;
@@ -322,7 +352,9 @@
}
@keyframes typingBounce {
- 0%, 60%, 100% {
+ 0%,
+ 60%,
+ 100% {
transform: translateY(0);
opacity: 0.4;
}
@@ -427,7 +459,9 @@
padding: 0.1rem;
border-radius: 0.25rem;
opacity: 0.5;
- transition: opacity 0.15s ease, background 0.15s ease;
+ transition:
+ opacity 0.15s ease,
+ background 0.15s ease;
&:hover:not([disabled]) {
opacity: 1;
@@ -486,7 +520,9 @@
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
- transition: transform 0.15s ease, opacity 0.15s ease;
+ transition:
+ transform 0.15s ease,
+ opacity 0.15s ease;
&:hover {
transform: translateY(-1px);
@@ -509,6 +545,92 @@
}
}
+// ---------------------------------------------------------------------------
+// Suggestion chips
+// ---------------------------------------------------------------------------
+
+.ai-suggestions {
+ padding: 0.5rem 0.75rem 0.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ flex-shrink: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
+
+ :host-context(.theme-dark) & {
+ border-top-color: rgba(255, 255, 255, 0.06);
+ }
+
+ &__row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+
+ &--realestate {
+ padding-top: 0.1rem;
+ }
+ }
+}
+
+.ai-suggestion-chip {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.3rem 0.65rem;
+ border-radius: 1.5rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background 0.15s ease,
+ transform 0.1s ease,
+ opacity 0.15s ease;
+ border: 1px solid rgba(99, 102, 241, 0.3);
+ background: rgba(99, 102, 241, 0.08);
+ color: #4f46e5;
+ white-space: nowrap;
+
+ :host-context(.theme-dark) & {
+ background: rgba(99, 102, 241, 0.15);
+ color: #a5b4fc;
+ border-color: rgba(99, 102, 241, 0.35);
+ }
+
+ &:hover {
+ background: rgba(99, 102, 241, 0.16);
+ transform: translateY(-1px);
+
+ :host-context(.theme-dark) & {
+ background: rgba(99, 102, 241, 0.28);
+ }
+ }
+
+ &:active {
+ transform: translateY(0);
+ opacity: 0.85;
+ }
+
+ // Real estate row chips — slightly warmer tint to distinguish from portfolio
+ &--re {
+ border-color: rgba(16, 185, 129, 0.3);
+ background: rgba(16, 185, 129, 0.07);
+ color: #047857;
+
+ :host-context(.theme-dark) & {
+ background: rgba(16, 185, 129, 0.15);
+ color: #6ee7b7;
+ border-color: rgba(16, 185, 129, 0.35);
+ }
+
+ &:hover {
+ background: rgba(16, 185, 129, 0.15);
+
+ :host-context(.theme-dark) & {
+ background: rgba(16, 185, 129, 0.25);
+ }
+ }
+ }
+}
+
// ---------------------------------------------------------------------------
// Input area
// ---------------------------------------------------------------------------
@@ -578,7 +700,9 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
- transition: transform 0.15s ease, opacity 0.15s ease;
+ transition:
+ transform 0.15s ease,
+ opacity 0.15s ease;
&:hover:not([disabled]) {
transform: scale(1.05);
diff --git a/apps/client/src/app/components/ai-chat/ai-chat.component.ts b/apps/client/src/app/components/ai-chat/ai-chat.component.ts
index 4555f5a5d..deb482476 100644
--- a/apps/client/src/app/components/ai-chat/ai-chat.component.ts
+++ b/apps/client/src/app/components/ai-chat/ai-chat.component.ts
@@ -1,3 +1,10 @@
+import { AiChatService } from '@ghostfolio/client/services/ai-chat.service';
+import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
+import { GfEnvironment } from '@ghostfolio/ui/environment';
+import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
+
+import { CommonModule } from '@angular/common';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@@ -5,14 +12,11 @@ import {
ElementRef,
Inject,
OnDestroy,
+ OnInit,
ViewChild
} from '@angular/core';
-import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { HttpClient, HttpClientModule } from '@angular/common/http';
-import { GfEnvironment } from '@ghostfolio/ui/environment';
-import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
-import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
+import { Subscription } from 'rxjs';
import { AiMarkdownPipe } from './ai-markdown.pipe';
@@ -35,6 +39,8 @@ interface AgentResponse {
latency_seconds: number;
}
+const HISTORY_KEY = 'portfolioAssistantHistory';
+
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, HttpClientModule, AiMarkdownPipe],
@@ -42,7 +48,7 @@ interface AgentResponse {
styleUrls: ['./ai-chat.component.scss'],
templateUrl: './ai-chat.component.html'
})
-export class GfAiChatComponent implements OnDestroy {
+export class GfAiChatComponent implements OnInit, OnDestroy {
@ViewChild('messagesContainer') private messagesContainer: ElementRef;
public isOpen = false;
@@ -52,6 +58,7 @@ export class GfAiChatComponent implements OnDestroy {
public successBanner = '';
public showSeedBanner = false;
public isSeeding = false;
+ public enableRealEstate: boolean;
// Write confirmation state
private pendingWrite: Record
| null = null;
@@ -60,29 +67,119 @@ export class GfAiChatComponent implements OnDestroy {
private readonly AGENT_URL: string;
private readonly FEEDBACK_URL: string;
private readonly SEED_URL: string;
+ private aiChatSubscription: Subscription;
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private http: HttpClient,
private tokenStorageService: TokenStorageService,
+ private aiChatService: AiChatService,
@Inject(GF_ENVIRONMENT) environment: GfEnvironment
) {
const base = (environment.agentUrl ?? '/agent').replace(/\/$/, '');
this.AGENT_URL = `${base}/chat`;
this.FEEDBACK_URL = `${base}/feedback`;
this.SEED_URL = `${base}/seed`;
+ this.enableRealEstate = environment.enableRealEstate ?? false;
+ }
+
+ public ngOnInit(): void {
+ const saved = sessionStorage.getItem(HISTORY_KEY);
+ if (saved) {
+ try {
+ this.messages = JSON.parse(saved);
+ } catch {
+ this.messages = [];
+ }
+ }
+
+ // Listen for external open-with-query events (e.g. from Real Estate nav item)
+ this.aiChatSubscription = this.aiChatService.openWithQuery.subscribe(
+ (query) => {
+ if (!this.isOpen) {
+ this.openPanel();
+ }
+ // Small delay so the panel transition completes before firing the query
+ setTimeout(() => {
+ this.doSend(query);
+ this.changeDetectorRef.markForCheck();
+ }, 150);
+ }
+ );
+ }
+
+ public ngOnDestroy(): void {
+ this.aiChatSubscription?.unsubscribe();
+ }
+
+ // ---------------------------------------------------------------------------
+ // Welcome message (changes with real-estate flag)
+ // ---------------------------------------------------------------------------
+
+ public get welcomeMessage(): string {
+ if (this.enableRealEstate) {
+ return (
+ "Hello! I'm your Portfolio Assistant, powered by Claude. " +
+ 'Ask me about your portfolio performance, transactions, or tax estimates — ' +
+ 'or explore housing markets and compare neighborhoods. ' +
+ 'Use the chips below to get started.'
+ );
+ }
+ return (
+ "Hello! I'm your Portfolio Assistant. " +
+ 'Ask me about your portfolio performance, transactions, tax estimates, ' +
+ 'or use commands like "buy 5 shares of AAPL" to record transactions.'
+ );
+ }
+
+ // ---------------------------------------------------------------------------
+ // History management
+ // ---------------------------------------------------------------------------
+
+ private saveHistory(): void {
+ sessionStorage.setItem(HISTORY_KEY, JSON.stringify(this.messages));
+ }
+
+ public clearHistory(): void {
+ this.messages = [{ role: 'assistant', content: this.welcomeMessage }];
+ sessionStorage.removeItem(HISTORY_KEY);
+ this.awaitingConfirmation = false;
+ this.pendingWrite = null;
+ this.successBanner = '';
+ this.showSeedBanner = false;
+ this.changeDetectorRef.markForCheck();
+ }
+
+ // ---------------------------------------------------------------------------
+ // Suggestion chips
+ // ---------------------------------------------------------------------------
+
+ public get showSuggestions(): boolean {
+ return this.messages.length <= 1;
}
- public ngOnDestroy() {}
+ public clickChip(text: string): void {
+ this.inputValue = text;
+ this.sendMessage();
+ }
+
+ // ---------------------------------------------------------------------------
+ // Panel open / close
+ // ---------------------------------------------------------------------------
+
+ private openPanel(): void {
+ this.isOpen = true;
+ if (this.messages.length === 0) {
+ this.messages.push({ role: 'assistant', content: this.welcomeMessage });
+ }
+ this.changeDetectorRef.markForCheck();
+ setTimeout(() => this.scrollToBottom(), 50);
+ }
public togglePanel(): void {
this.isOpen = !this.isOpen;
if (this.isOpen && this.messages.length === 0) {
- this.messages.push({
- role: 'assistant',
- content:
- 'Hello! I\'m your Portfolio Assistant. Ask me about your portfolio performance, transactions, tax estimates, or use commands like "buy 5 shares of AAPL" to record transactions.'
- });
+ this.messages.push({ role: 'assistant', content: this.welcomeMessage });
}
this.changeDetectorRef.markForCheck();
if (this.isOpen) {
@@ -95,6 +192,10 @@ export class GfAiChatComponent implements OnDestroy {
this.changeDetectorRef.markForCheck();
}
+ // ---------------------------------------------------------------------------
+ // Messaging
+ // ---------------------------------------------------------------------------
+
public onKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
@@ -142,9 +243,6 @@ export class GfAiChatComponent implements OnDestroy {
body.pending_write = this.pendingWrite;
}
- // Send the logged-in user's token so the agent uses their own data.
- // When not logged in, the field is omitted and the agent falls back to
- // the shared env-var token (useful for demo/unauthenticated access).
const userToken = this.tokenStorageService.getToken();
if (userToken) {
body.bearer_token = userToken;
@@ -170,8 +268,15 @@ export class GfAiChatComponent implements OnDestroy {
this.awaitingConfirmation = data.awaiting_confirmation;
this.pendingWrite = data.pending_write;
- // Detect an empty portfolio and offer to seed demo data
- const emptyPortfolioHints = ['0 holdings', '0 positions', 'no holdings', 'no positions', 'empty portfolio', 'no transactions', '0.00 (0.0%)'];
+ const emptyPortfolioHints = [
+ '0 holdings',
+ '0 positions',
+ 'no holdings',
+ 'no positions',
+ 'empty portfolio',
+ 'no transactions',
+ '0.00 (0.0%)'
+ ];
const isEmptyPortfolio = emptyPortfolioHints.some((hint) =>
data.response.toLowerCase().includes(hint)
);
@@ -188,6 +293,7 @@ export class GfAiChatComponent implements OnDestroy {
}
this.isThinking = false;
+ this.saveHistory();
this.changeDetectorRef.markForCheck();
this.scrollToBottom();
},
@@ -199,12 +305,17 @@ export class GfAiChatComponent implements OnDestroy {
this.isThinking = false;
this.awaitingConfirmation = false;
this.pendingWrite = null;
+ this.saveHistory();
this.changeDetectorRef.markForCheck();
this.scrollToBottom();
}
});
}
+ // ---------------------------------------------------------------------------
+ // Seed portfolio
+ // ---------------------------------------------------------------------------
+
public seedPortfolio(): void {
this.isSeeding = true;
this.showSeedBanner = false;
@@ -216,40 +327,51 @@ export class GfAiChatComponent implements OnDestroy {
body.bearer_token = userToken;
}
- this.http.post<{ success: boolean; message: string }>(this.SEED_URL, body).subscribe({
- next: (data) => {
- this.isSeeding = false;
- if (data.success) {
+ this.http
+ .post<{ success: boolean; message: string }>(this.SEED_URL, body)
+ .subscribe({
+ next: (data) => {
+ this.isSeeding = false;
+ if (data.success) {
+ this.messages.push({
+ role: 'assistant',
+ content: `🌱 **Demo portfolio loaded!** I've added 18 transactions across AAPL, MSFT, NVDA, GOOGL, AMZN, and VTI spanning 2021–2024. Try asking "how is my portfolio doing?" to see your analysis.`
+ });
+ } else {
+ this.messages.push({
+ role: 'assistant',
+ content: '⚠️ Could not load demo data. Please try again.'
+ });
+ }
+ this.saveHistory();
+ this.changeDetectorRef.markForCheck();
+ this.scrollToBottom();
+ },
+ error: () => {
+ this.isSeeding = false;
this.messages.push({
role: 'assistant',
- content: `🌱 **Demo portfolio loaded!** I've added 18 transactions across AAPL, MSFT, NVDA, GOOGL, AMZN, and VTI spanning 2021–2024. Try asking "how is my portfolio doing?" to see your analysis.`
+ content:
+ '⚠️ Could not reach the seeding endpoint. Make sure the agent is running.'
});
- } else {
- this.messages.push({ role: 'assistant', content: '⚠️ Could not load demo data. Please try again.' });
+ this.saveHistory();
+ this.changeDetectorRef.markForCheck();
}
- this.changeDetectorRef.markForCheck();
- this.scrollToBottom();
- },
- error: () => {
- this.isSeeding = false;
- this.messages.push({ role: 'assistant', content: '⚠️ Could not reach the seeding endpoint. Make sure the agent is running.' });
- this.changeDetectorRef.markForCheck();
- }
- });
+ });
}
- public giveFeedback(
- msgIndex: number,
- rating: 1 | -1
- ): void {
+ // ---------------------------------------------------------------------------
+ // Feedback
+ // ---------------------------------------------------------------------------
+
+ public giveFeedback(msgIndex: number, rating: 1 | -1): void {
const msg = this.messages[msgIndex];
if (!msg || msg.feedbackGiven !== null) {
return;
}
msg.feedbackGiven = rating;
- const userQuery =
- msgIndex > 0 ? this.messages[msgIndex - 1].content : '';
+ const userQuery = msgIndex > 0 ? this.messages[msgIndex - 1].content : '';
this.http
.post(this.FEEDBACK_URL, {
@@ -262,6 +384,10 @@ export class GfAiChatComponent implements OnDestroy {
this.changeDetectorRef.markForCheck();
}
+ // ---------------------------------------------------------------------------
+ // Confidence helpers
+ // ---------------------------------------------------------------------------
+
public confidenceLabel(score: number): string {
if (score >= 0.8) {
return 'High';
@@ -282,6 +408,10 @@ export class GfAiChatComponent implements OnDestroy {
return 'confidence-low';
}
+ // ---------------------------------------------------------------------------
+ // Scroll
+ // ---------------------------------------------------------------------------
+
private scrollToBottom(): void {
setTimeout(() => {
if (this.messagesContainer?.nativeElement) {
diff --git a/apps/client/src/app/components/ai-chat/ai-markdown.pipe.ts b/apps/client/src/app/components/ai-chat/ai-markdown.pipe.ts
index b714c53ac..456b2f8d1 100644
--- a/apps/client/src/app/components/ai-chat/ai-markdown.pipe.ts
+++ b/apps/client/src/app/components/ai-chat/ai-markdown.pipe.ts
@@ -31,7 +31,7 @@ export class AiMarkdownPipe implements PipeTransform {
// Horizontal rule ---
.replace(/^---+$/gm, '
')
// Bullet lines starting with "- " or "* "
- .replace(/^[*\-] (.+)$/gm, '$1')
+ .replace(/^[*-] (.+)$/gm, '$1')
// Wrap consecutive in
.replace(/(- .*<\/li>(\n|$))+/g, (block) => ``)
// Newlines →
(except inside )
diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html
index 501119b31..61e157e30 100644
--- a/apps/client/src/app/components/header/header.component.html
+++ b/apps/client/src/app/components/header/header.component.html
@@ -326,6 +326,15 @@
>About Ghostfolio
+ @if (enableRealEstate) {
+
+ }
@@ -409,6 +418,18 @@
>
}
+ @if (enableRealEstate && user) {
+
+
+
+ }
();
public constructor(
+ private aiChatService: AiChatService,
private dataService: DataService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
@@ -142,8 +148,10 @@ export class GfHeaderComponent implements OnChanges {
private router: Router,
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService,
- private userService: UserService
+ private userService: UserService,
+ @Inject(GF_ENVIRONMENT) environment: GfEnvironment
) {
+ this.enableRealEstate = environment.enableRealEstate ?? false;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
@@ -283,6 +291,12 @@ export class GfHeaderComponent implements OnChanges {
this.signOut.next();
}
+ public openRealEstateChat(): void {
+ this.aiChatService.openChat(
+ 'Compare Austin vs Denver for housing affordability'
+ );
+ }
+
public openLoginDialog() {
const dialogRef = this.dialog.open<
GfLoginWithAccessTokenDialogComponent,
diff --git a/apps/client/src/app/services/ai-chat.service.ts b/apps/client/src/app/services/ai-chat.service.ts
new file mode 100644
index 000000000..2f6f0118f
--- /dev/null
+++ b/apps/client/src/app/services/ai-chat.service.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+import { Observable, Subject } from 'rxjs';
+
+/**
+ * Broadcast service that lets any component open the AI chat panel
+ * with an optional pre-filled query that auto-submits immediately.
+ * Used by the Real Estate header nav item (and any future trigger points).
+ */
+@Injectable({ providedIn: 'root' })
+export class AiChatService {
+ private openWithQuery$ = new Subject();
+
+ public get openWithQuery(): Observable {
+ return this.openWithQuery$.asObservable();
+ }
+
+ public openChat(query: string): void {
+ this.openWithQuery$.next(query);
+ }
+}
diff --git a/apps/client/src/environments/environment.prod.ts b/apps/client/src/environments/environment.prod.ts
index c22d8b3d3..20e1481e4 100644
--- a/apps/client/src/environments/environment.prod.ts
+++ b/apps/client/src/environments/environment.prod.ts
@@ -2,6 +2,7 @@ import type { GfEnvironment } from '@ghostfolio/ui/environment';
export const environment: GfEnvironment = {
agentUrl: 'https://ghostfolio-agent-production.up.railway.app',
+ enableRealEstate: true,
lastPublish: '{BUILD_TIMESTAMP}',
production: true
};
diff --git a/apps/client/src/environments/environment.ts b/apps/client/src/environments/environment.ts
index c53be4d8f..47bb8e3df 100644
--- a/apps/client/src/environments/environment.ts
+++ b/apps/client/src/environments/environment.ts
@@ -6,6 +6,7 @@ import type { GfEnvironment } from '@ghostfolio/ui/environment';
export const environment: GfEnvironment = {
agentUrl: '/agent',
+ enableRealEstate: true,
lastPublish: null,
production: false
};
diff --git a/libs/ui/src/lib/environment/environment.interface.ts b/libs/ui/src/lib/environment/environment.interface.ts
index db7be1b29..84e85a6ac 100644
--- a/libs/ui/src/lib/environment/environment.interface.ts
+++ b/libs/ui/src/lib/environment/environment.interface.ts
@@ -1,5 +1,6 @@
export interface GfEnvironment {
agentUrl?: string;
+ enableRealEstate?: boolean;
lastPublish: string | null;
production: boolean;
}