Browse Source

feat(ai-chat): add auto-seed banner for empty portfolios

When the agent returns an empty-portfolio response (e.g. after a new
Google OAuth sign-in), a blue banner appears offering to load 18 demo
activities. Clicking 'Load demo data' calls POST /agent/seed with the
user's own bearer token so the seeded data belongs to their account.
Also adds the /seed URL construction in the component constructor.

Co-authored-by: Cursor <cursoragent@cursor.com>
pull/6453/head
Priyanka Punukollu 1 month ago
parent
commit
8d66e75bea
  1. 14
      apps/client/src/app/components/ai-chat/ai-chat.component.html
  2. 45
      apps/client/src/app/components/ai-chat/ai-chat.component.scss
  3. 46
      apps/client/src/app/components/ai-chat/ai-chat.component.ts

14
apps/client/src/app/components/ai-chat/ai-chat.component.html

@ -29,6 +29,20 @@
<div class="ai-banner ai-banner--success">{{ successBanner }}</div>
}
<!-- Empty portfolio seed banner -->
@if (showSeedBanner && !isSeeding) {
<div class="ai-banner ai-banner--seed">
<span>Your portfolio is empty. Load demo data to try the AI?</span>
<div class="ai-banner__actions">
<button class="ai-banner__btn ai-banner__btn--primary" (click)="seedPortfolio()">🌱 Load demo data</button>
<button class="ai-banner__btn ai-banner__btn--dismiss" (click)="showSeedBanner = false">Dismiss</button>
</div>
</div>
}
@if (isSeeding) {
<div class="ai-banner ai-banner--seed">⏳ Loading demo portfolio…</div>
}
<!-- Messages area -->
<div class="ai-messages" #messagesContainer>
@for (msg of messages; track $index) {

45
apps/client/src/app/components/ai-chat/ai-chat.component.scss

@ -159,6 +159,51 @@
color: #a7f3d0;
}
}
&--seed {
background: #eff6ff;
color: #1e40af;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
font-weight: 500;
:host-context(.theme-dark) & {
background: #1e3a5f;
color: #bfdbfe;
}
}
&__actions {
display: flex;
gap: 0.4rem;
flex-shrink: 0;
}
&__btn {
border: none;
border-radius: 6px;
padding: 0.25rem 0.65rem;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
&:hover { opacity: 0.85; }
&--primary {
background: #2563eb;
color: #fff;
}
&--dismiss {
background: transparent;
color: inherit;
opacity: 0.6;
}
}
}
@keyframes slideDown {

46
apps/client/src/app/components/ai-chat/ai-chat.component.ts

@ -50,6 +50,8 @@ export class GfAiChatComponent implements OnDestroy {
public inputValue = '';
public messages: ChatMessage[] = [];
public successBanner = '';
public showSeedBanner = false;
public isSeeding = false;
// Write confirmation state
private pendingWrite: Record<string, unknown> | null = null;
@ -57,6 +59,7 @@ export class GfAiChatComponent implements OnDestroy {
private readonly AGENT_URL: string;
private readonly FEEDBACK_URL: string;
private readonly SEED_URL: string;
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@ -67,6 +70,7 @@ export class GfAiChatComponent implements OnDestroy {
const base = (environment.agentUrl ?? '/agent').replace(/\/$/, '');
this.AGENT_URL = `${base}/chat`;
this.FEEDBACK_URL = `${base}/feedback`;
this.SEED_URL = `${base}/seed`;
}
public ngOnDestroy() {}
@ -166,6 +170,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 isEmptyPortfolio = emptyPortfolioHints.some((hint) =>
data.response.toLowerCase().includes(hint)
);
if (isEmptyPortfolio && !this.showSeedBanner) {
this.showSeedBanner = true;
}
if (isWriteSuccess) {
this.successBanner = '✅ Transaction recorded successfully';
setTimeout(() => {
@ -192,6 +205,39 @@ export class GfAiChatComponent implements OnDestroy {
});
}
public seedPortfolio(): void {
this.isSeeding = true;
this.showSeedBanner = false;
this.changeDetectorRef.markForCheck();
const body: { bearer_token?: string } = {};
const userToken = this.tokenStorageService.getToken();
if (userToken) {
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.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.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

Loading…
Cancel
Save