web-app/src/app/layout/basic/basic.component.ts (257 lines of code) (raw):
import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, SettingsService, User } from '@delon/theme';
import { LayoutDefaultOptions } from '@delon/theme/layout-default';
import { environment } from '@env/environment';
import { Observable, Subject, of } from 'rxjs';
import { delay, tap, finalize, catchError, takeUntil } from 'rxjs/operators';
import { CONSTANTS } from '../../shared/constants';
import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service';
@Component({
selector: 'layout-basic',
template: `
<layout-default [options]="options" [nav]="navTpl" [content]="contentTpl" [customError]="null">
<layout-default-header-item direction="left">
<a layout-default-header-item-trigger href="//github.com/apache/hertzbeat" target="_blank">
<i nz-icon nzType="github"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="left" hidden="pc">
<div layout-default-header-item-trigger (click)="searchToggleStatus = !searchToggleStatus">
<i nz-icon nzType="search"></i>
</div>
</layout-default-header-item>
<layout-default-header-item direction="middle">
<header-search class="alain-default__search" [toggleChange]="searchToggleStatus"></header-search>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<header-notify></header-notify>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<a layout-default-header-item-trigger routerLink="/passport/lock">
<i nz-icon nzType="lock"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzTrigger="click" nzPlacement="bottomRight">
<i nz-icon nzType="setting"></i>
</div>
<nz-dropdown-menu #settingsMenu="nzDropdownMenu">
<div nz-menu style="width: 200px;">
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu-item routerLink="/setting/labels">
<i nz-icon nzType="tag" class="mr-sm"></i>
<span style="margin-left: 4px">{{ 'menu.advanced.labels' | i18n }}</span>
</div>
<div nz-menu-item>
<header-i18n></header-i18n>
</div>
</div>
</nz-dropdown-menu>
</layout-default-header-item>
<layout-default-header-item direction="right">
<header-user></header-user>
</layout-default-header-item>
<ng-template #navTpl>
<layout-default-nav class="d-block py-lg" openStrictly="true"></layout-default-nav>
</ng-template>
<ng-template #contentTpl>
<router-outlet></router-outlet>
</ng-template>
</layout-default>
<global-footer>
<div style="margin-top: 30px">
Apache HertzBeat (incubating) {{ version }}<br />
Copyright © {{ currentYear }}
<a href="https://hertzbeat.apache.org" target="_blank">Apache HertzBeat</a>
<br />
Licensed under the Apache License, Version 2.0
</div>
</global-footer>
<setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
<!-- AI Chatbot -->
<div class="ai-chatbot-container">
<div class="ai-chatbot-button" (click)="toggleChatbot()" *ngIf="!isChatbotOpen">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="28"
height="28"
fill="white"
style="min-width:28px; min-height:28px;"
>
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
/>
</svg>
</div>
<div class="ai-chatbot-window" *ngIf="isChatbotOpen" [class.maximized]="isChatbotMaximized">
<div class="chatbot-header">
<div class="chatbot-title">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="20"
height="20"
fill="white"
style="margin-right: 6px; vertical-align: middle;"
>
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
/>
</svg>
AI 助手
</div>
<div class="chatbot-controls">
<span class="control-item" (click)="toggleMaximize()" title="{{ isChatbotMaximized ? '还原' : '最大化' }}">
<i nz-icon [nzType]="isChatbotMaximized ? 'fullscreen-exit' : 'fullscreen'" nzTheme="outline"></i>
</span>
<span class="control-item" (click)="toggleChatbot()" title="关闭">
<i nz-icon nzType="close" nzTheme="outline"></i>
</span>
</div>
</div>
<div class="chatbot-messages" #chatMessagesContainer>
<div
*ngFor="let message of chatMessages"
[class.user-message]="message.isUser"
[class.bot-message]="!message.isUser"
class="message"
>
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.timestamp | date : 'HH:mm' }}</div>
</div>
<div *ngIf="currentBotMessage && isLoading" class="bot-message streaming-message">
<div class="message-content">{{ currentBotMessage.content }}</div>
<div class="message-time">{{ currentBotMessage.timestamp | date : 'HH:mm' }}</div>
</div>
<div *ngIf="isLoading && !currentBotMessage" class="bot-message loading-message">
<nz-spin nzSimple></nz-spin>
</div>
</div>
<div class="chatbot-input">
<input nz-input placeholder="请输入问题..." [(ngModel)]="currentMessage" (keyup.enter)="sendMessage()" [disabled]="isLoading" />
<button nz-button nzType="primary" [disabled]="!currentMessage.trim() || isLoading" (click)="sendMessage()"> 发送 </button>
</div>
</div>
</div>
`,
styleUrls: ['./basic.component.less']
})
export class LayoutBasicComponent implements OnInit, OnDestroy {
options: LayoutDefaultOptions = {
logoExpanded: `./assets/brand_white.svg`,
logoCollapsed: `./assets/logo.svg`
};
avatar: string = `./assets/img/avatar.svg`;
searchToggleStatus = false;
showSettingDrawer = !environment.production;
version = CONSTANTS.VERSION;
currentYear = new Date().getFullYear();
get user(): User {
return this.settings.user;
}
get role(): string {
let userTmp = this.settings.user;
if (userTmp == undefined || userTmp.role == undefined) {
return this.i18nSvc.fanyi('app.role.admin');
} else {
let roles: string[] = JSON.parse(userTmp.role);
return roles.length > 0 ? roles[0] : '';
}
}
// AI Chatbot related properties
isChatbotOpen = false;
isChatbotMaximized = false;
chatMessages: ChatMessage[] = [];
currentMessage = '';
isLoading = false;
currentBotMessage: ChatMessage | null = null;
// For subscription cleanup
private destroy$ = new Subject<void>();
constructor(
private settings: SettingsService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private aiBotService: AiBotService
) {}
ngOnInit(): void {
// Initialize welcome message
this.chatMessages.push({
content: '你好!我是AI助手,有什么可以帮助你的吗?',
isUser: false,
timestamp: new Date()
});
console.log('AI Chatbot initialization completed');
}
ngOnDestroy(): void {
// Cancel all subscriptions when component is destroyed
this.destroy$.next();
this.destroy$.complete();
}
toggleChatbot(): void {
this.isChatbotOpen = !this.isChatbotOpen;
if (!this.isChatbotOpen) {
setTimeout(() => {
this.isChatbotMaximized = false;
}, 300);
} else {
// Scroll to bottom when window opens
setTimeout(() => this.scrollToBottom(), 100);
}
console.log('Toggle chatbot status:', this.isChatbotOpen ? 'open' : 'closed');
}
toggleMaximize(): void {
setTimeout(() => {
this.isChatbotMaximized = !this.isChatbotMaximized;
console.log('Chat window maximize status:', this.isChatbotMaximized ? 'maximized' : 'normal');
}, 10);
}
sendMessage(): void {
if (!this.currentMessage.trim() || this.isLoading) return;
// Add user message
this.chatMessages.push({
content: this.currentMessage,
isUser: true,
timestamp: new Date()
});
const userMessage = this.currentMessage;
this.currentMessage = '';
this.isLoading = true;
this.currentBotMessage = null;
// Ensure scrolling to bottom after message display
setTimeout(() => this.scrollToBottom(), 100);
// Call AI service to get response
this.aiBotService
.sendMessage(userMessage)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isLoading = false;
// If there is a current message, add it to chat history
if (this.currentBotMessage) {
this.chatMessages.push({ ...this.currentBotMessage });
this.currentBotMessage = null;
}
// Ensure scrolling to bottom after message display
setTimeout(() => this.scrollToBottom(), 100);
})
)
.subscribe({
next: response => {
console.log('Received AI response update:', response);
// Update currently receiving message
this.currentBotMessage = response;
// Scroll to bottom in real-time
this.scrollToBottom();
},
error: error => {
console.error('AI response error:', error);
// Add error message
this.chatMessages.push({
content: '抱歉,连接AI助手时出现问题,请稍后再试。',
isUser: false,
timestamp: new Date()
});
}
});
}
// Scroll to bottom of messages
private scrollToBottom(): void {
try {
const chatMessages = document.querySelector('.chatbot-messages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
} catch (err) {
console.error('Failed to scroll to bottom:', err);
}
}
}