mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
4.6 KiB
143 lines
4.6 KiB
<!-- Floating trigger button -->
|
|
<button
|
|
class="ai-fab"
|
|
[class.ai-fab--open]="isOpen"
|
|
(click)="togglePanel()"
|
|
aria-label="Open Portfolio Assistant"
|
|
>
|
|
<span class="ai-fab__icon">🤖</span>
|
|
<span class="ai-fab__label" [class.ai-fab__label--hidden]="isOpen">Ask AI</span>
|
|
</button>
|
|
|
|
<!-- Slide-in panel -->
|
|
<div class="ai-panel" [class.ai-panel--visible]="isOpen" role="complementary" aria-label="Portfolio Assistant">
|
|
|
|
<!-- Panel header -->
|
|
<div class="ai-panel__header">
|
|
<div class="ai-panel__header-left">
|
|
<span class="ai-panel__avatar">🤖</span>
|
|
<div>
|
|
<span class="ai-panel__title">Portfolio Assistant</span>
|
|
<span class="ai-panel__subtitle">Powered by Claude</span>
|
|
</div>
|
|
</div>
|
|
<button class="ai-panel__close" (click)="closePanel()" aria-label="Close">✕</button>
|
|
</div>
|
|
|
|
<!-- Success banner -->
|
|
@if (successBanner) {
|
|
<div class="ai-banner ai-banner--success">{{ successBanner }}</div>
|
|
}
|
|
|
|
<!-- Messages area -->
|
|
<div class="ai-messages" #messagesContainer>
|
|
@for (msg of messages; track $index) {
|
|
<div class="ai-message" [class.ai-message--user]="msg.role === 'user'" [class.ai-message--assistant]="msg.role === 'assistant'">
|
|
|
|
<!-- Bubble -->
|
|
<div class="ai-bubble" [innerHTML]="msg.content | aiMarkdown"></div>
|
|
|
|
<!-- Assistant metadata row -->
|
|
@if (msg.role === 'assistant' && msg.confidence !== undefined) {
|
|
<div class="ai-meta">
|
|
|
|
<!-- Low confidence warning -->
|
|
@if (msg.confidence < 0.6) {
|
|
<div class="ai-meta__warning">⚠️ Low confidence — some data may be incomplete</div>
|
|
}
|
|
|
|
<div class="ai-meta__row">
|
|
<!-- Confidence badge -->
|
|
<span class="ai-badge" [class]="confidenceClass(msg.confidence)">
|
|
{{ confidenceLabel(msg.confidence) }} ({{ (msg.confidence * 100).toFixed(0) }}%)
|
|
</span>
|
|
|
|
<!-- Tools used chips -->
|
|
@for (tool of msg.toolsUsed; track tool) {
|
|
<span class="ai-chip">{{ tool }}</span>
|
|
}
|
|
|
|
<!-- Latency -->
|
|
<span class="ai-meta__latency">{{ msg.latency?.toFixed(1) }}s</span>
|
|
|
|
<!-- Feedback -->
|
|
<div class="ai-feedback">
|
|
<button
|
|
class="ai-feedback__btn"
|
|
[class.ai-feedback__btn--active-up]="msg.feedbackGiven === 1"
|
|
[disabled]="msg.feedbackGiven !== null"
|
|
(click)="giveFeedback($index, 1)"
|
|
aria-label="Thumbs up"
|
|
>👍</button>
|
|
<button
|
|
class="ai-feedback__btn"
|
|
[class.ai-feedback__btn--active-down]="msg.feedbackGiven === -1"
|
|
[disabled]="msg.feedbackGiven !== null"
|
|
(click)="giveFeedback($index, -1)"
|
|
aria-label="Thumbs down"
|
|
>👎</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
}
|
|
|
|
</div>
|
|
}
|
|
|
|
<!-- Typing indicator -->
|
|
@if (isThinking) {
|
|
<div class="ai-message ai-message--assistant">
|
|
<div class="ai-bubble ai-bubble--typing">
|
|
<span class="ai-dot"></span>
|
|
<span class="ai-dot"></span>
|
|
<span class="ai-dot"></span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Confirmation buttons (shown when awaiting yes/no) -->
|
|
@if (awaitingConfirmation && !isThinking) {
|
|
<div class="ai-confirm-bar">
|
|
<span class="ai-confirm-bar__label">Confirm this transaction?</span>
|
|
<button class="ai-confirm-bar__btn ai-confirm-bar__btn--yes" (click)="confirmWrite()">✅ Confirm</button>
|
|
<button class="ai-confirm-bar__btn ai-confirm-bar__btn--no" (click)="cancelWrite()">❌ Cancel</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Input area -->
|
|
@if (!awaitingConfirmation || isThinking) {
|
|
<div class="ai-input-area">
|
|
<textarea
|
|
#inputField
|
|
class="ai-input"
|
|
rows="1"
|
|
placeholder="Ask about your portfolio..."
|
|
[disabled]="isThinking"
|
|
[(ngModel)]="inputValue"
|
|
(keydown)="onKeydown($event)"
|
|
></textarea>
|
|
<button
|
|
class="ai-send"
|
|
[disabled]="isThinking || !inputValue.trim()"
|
|
(click)="sendMessage()"
|
|
aria-label="Send"
|
|
>
|
|
@if (isThinking) {
|
|
<span class="ai-send__spinner"></span>
|
|
} @else {
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
</svg>
|
|
}
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
</div>
|
|
|
|
<!-- Backdrop (mobile) -->
|
|
@if (isOpen) {
|
|
<div class="ai-backdrop" (click)="closePanel()"></div>
|
|
}
|
|
|