diff --git a/talent-management/public/data/menu.json b/talent-management/public/data/menu.json
index 09c557b..3ef2220 100644
--- a/talent-management/public/data/menu.json
+++ b/talent-management/public/data/menu.json
@@ -89,6 +89,12 @@
}
}
]
+ },
+ {
+ "route": "ai-chat",
+ "name": "aiChat",
+ "type": "link",
+ "icon": "smart_toy"
}
]
}
diff --git a/talent-management/public/i18n/en-US.json b/talent-management/public/i18n/en-US.json
index 0bf0f26..c46b7d3 100644
--- a/talent-management/public/i18n/en-US.json
+++ b/talent-management/public/i18n/en-US.json
@@ -13,6 +13,7 @@
"salaryRanges": "Salary Ranges",
"salaryRanges.salaryRangeList": "List",
"salaryRanges.addSalaryRange": "Create",
+ "aiChat": "AI Assistant",
"design": "Design",
"design.colors": "Color System",
"design.icons": "Material Icons",
diff --git a/talent-management/src/app/app.routes.ts b/talent-management/src/app/app.routes.ts
index 615a840..bcabc90 100644
--- a/talent-management/src/app/app.routes.ts
+++ b/talent-management/src/app/app.routes.ts
@@ -23,6 +23,7 @@ import { SalaryRangeDetailComponent } from './routes/salary-ranges/salary-range-
import { SalaryRangeFormComponent } from './routes/salary-ranges/salary-range-form.component';
import { ProfileOverviewComponent } from './routes/profile/profile-overview.component';
import { ProfileSettingsComponent } from './routes/profile/profile-settings.component';
+import { AiChatComponent } from './routes/ai-chat/ai-chat.component';
export const routes: Routes = [
{
@@ -57,6 +58,7 @@ export const routes: Routes = [
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
],
},
+ { path: 'ai-chat', component: AiChatComponent },
{ path: '403', component: Error403 },
{ path: '404', component: Error404 },
{ path: '500', component: Error500 },
diff --git a/talent-management/src/app/routes/ai-chat/ai-chat.component.html b/talent-management/src/app/routes/ai-chat/ai-chat.component.html
new file mode 100644
index 0000000..5c3e12d
--- /dev/null
+++ b/talent-management/src/app/routes/ai-chat/ai-chat.component.html
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
info
+
+
AI features are disabled.
+
+ To enable AI, set aiEnabled: true in
+ src/environments/environment.ts and
+ "AiEnabled": true in the API's appsettings.json.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ chat
+ General Chat
+
+
+
+
+
+ AI Assistant
+ Ask anything — general knowledge, writing help, code questions
+
+
+
+
+
+ 0">
+
+
{{ msg.role === 'user' ? 'person' : 'smart_toy' }}
+
{{ msg.content }}
+
+
+
+
+
+
chat_bubble_outline
+
Start a conversation
+
+
+
+
+
+ Thinking…
+
+
+
+
+ error_outline
+ {{ chatError }}
+
+
+
+
+
+
+
+ Message
+
+
+
+
+
+
+
+
+
+
+
+ analytics
+ HR Insights
+
+
+
+
+
+ HR AI Assistant
+ Ask about your live workforce data — headcount, departments, recent hires
+
+
+
+
+
+
+
Try asking:
+
+
+
+
+
+
+
+
+
+ 0">
+
+
{{ msg.role === 'user' ? 'person' : 'analytics' }}
+
+
{{ msg.content }}
+
{{ msg.executionTimeMs }}ms
+
+
+
+
+
+
+
+ Fetching live data and reasoning…
+
+
+
+
+ error_outline
+ {{ hrError }}
+
+
+
+
+
+
+
+ Question
+
+
+
+
+
+
+
+
+
+
diff --git a/talent-management/src/app/routes/ai-chat/ai-chat.component.scss b/talent-management/src/app/routes/ai-chat/ai-chat.component.scss
new file mode 100644
index 0000000..e3f0467
--- /dev/null
+++ b/talent-management/src/app/routes/ai-chat/ai-chat.component.scss
@@ -0,0 +1,262 @@
+.ai-disabled-banner {
+ padding: 16px;
+
+ .disabled-card {
+ max-width: 720px;
+ margin: 0 auto;
+ border-left: 4px solid #2196f3;
+
+ mat-card-content {
+ padding: 20px;
+ }
+
+ .disabled-content {
+ display: flex;
+ align-items: flex-start;
+ gap: 16px;
+
+ mat-icon {
+ font-size: 28px;
+ width: 28px;
+ height: 28px;
+ color: #2196f3;
+ flex-shrink: 0;
+ margin-top: 2px;
+ }
+
+ strong {
+ font-size: 16px;
+ }
+
+ p {
+ margin: 8px 0 0;
+ color: rgba(0, 0, 0, 0.6);
+ font-size: 14px;
+ line-height: 1.5;
+ }
+
+ code {
+ background: rgba(0, 0, 0, 0.06);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 13px;
+ }
+ }
+ }
+}
+
+.chat-container {
+ padding: 16px;
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.tab-icon {
+ margin-right: 6px;
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ vertical-align: middle;
+}
+
+.tab-content {
+ padding-top: 16px;
+}
+
+.chat-card {
+ mat-card-header {
+ display: flex;
+ align-items: flex-start;
+ padding: 16px 16px 0;
+ margin-bottom: 0;
+
+ .mat-mdc-card-header-text {
+ flex: 1;
+ }
+
+ .header-actions {
+ margin-left: auto;
+ }
+ }
+
+ mat-card-content {
+ padding: 16px;
+ min-height: 320px;
+ max-height: 480px;
+ overflow-y: auto;
+ }
+}
+
+// Messages
+.message-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.message {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+
+ .avatar-icon {
+ font-size: 22px;
+ width: 22px;
+ height: 22px;
+ flex-shrink: 0;
+ margin-top: 4px;
+ }
+
+ .bubble {
+ padding: 10px 14px;
+ border-radius: 12px;
+ font-size: 14px;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-width: 100%;
+ }
+
+ .bubble-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ .exec-time {
+ font-size: 11px;
+ color: rgba(0, 0, 0, 0.38);
+ padding-left: 4px;
+ }
+ }
+
+ &.user-message {
+ flex-direction: row-reverse;
+
+ .avatar-icon {
+ color: #3f51b5;
+ }
+
+ .bubble {
+ background: #e8eaf6;
+ color: rgba(0, 0, 0, 0.87);
+ }
+ }
+
+ &.assistant-message {
+ .avatar-icon {
+ color: #4caf50;
+ }
+
+ .bubble {
+ background: #f5f5f5;
+ color: rgba(0, 0, 0, 0.87);
+ }
+ }
+}
+
+// Empty state
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 240px;
+ color: rgba(0, 0, 0, 0.38);
+
+ mat-icon {
+ font-size: 48px;
+ width: 48px;
+ height: 48px;
+ margin-bottom: 12px;
+ }
+
+ p {
+ margin: 0;
+ font-size: 14px;
+ }
+}
+
+// Suggestions
+.suggestions {
+ padding-bottom: 16px;
+
+ .suggestions-label {
+ font-size: 13px;
+ color: rgba(0, 0, 0, 0.54);
+ margin: 0 0 10px;
+ }
+
+ .suggestion-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+
+ button {
+ font-size: 13px;
+ height: 32px;
+ }
+ }
+}
+
+// Loading
+.loading-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 0;
+ color: rgba(0, 0, 0, 0.54);
+ font-size: 14px;
+}
+
+// Error
+.error-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 12px;
+ border-radius: 6px;
+ background: #fff3e0;
+ color: #e65100;
+ font-size: 14px;
+ margin-top: 8px;
+
+ mat-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ }
+}
+
+// Input area
+.input-area {
+ padding: 12px 16px;
+ display: flex;
+ gap: 12px;
+ align-items: flex-end;
+
+ .message-input {
+ flex: 1;
+ margin-bottom: 0;
+ }
+}
+
+@media (max-width: 600px) {
+ .chat-container {
+ padding: 8px;
+ }
+
+ .input-area {
+ flex-direction: column;
+ align-items: stretch;
+
+ button {
+ width: 100%;
+ }
+ }
+
+ .suggestion-list {
+ flex-direction: column;
+ }
+}
diff --git a/talent-management/src/app/routes/ai-chat/ai-chat.component.ts b/talent-management/src/app/routes/ai-chat/ai-chat.component.ts
new file mode 100644
index 0000000..5094a29
--- /dev/null
+++ b/talent-management/src/app/routes/ai-chat/ai-chat.component.ts
@@ -0,0 +1,145 @@
+import { Component, OnDestroy, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatCardModule } from '@angular/material/card';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatDividerModule } from '@angular/material/divider';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { PageHeader } from '@shared';
+import { AiService } from '../../services/api/ai.service';
+import { environment } from '../../../environments/environment';
+
+export interface ChatMessage {
+ role: 'user' | 'assistant';
+ content: string;
+ executionTimeMs?: number;
+}
+
+@Component({
+ selector: 'app-ai-chat',
+ standalone: true,
+ templateUrl: './ai-chat.component.html',
+ styleUrl: './ai-chat.component.scss',
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatCardModule,
+ MatIconModule,
+ MatButtonModule,
+ MatInputModule,
+ MatFormFieldModule,
+ MatProgressSpinnerModule,
+ MatTabsModule,
+ MatDividerModule,
+ PageHeader,
+ ],
+})
+export class AiChatComponent implements OnDestroy {
+ private aiService = inject(AiService);
+ private destroy$ = new Subject();
+
+ aiEnabled = environment.aiEnabled;
+
+ // General chat state
+ chatMessages: ChatMessage[] = [];
+ chatInput = '';
+ chatLoading = false;
+ chatError = '';
+
+ // HR insight state
+ hrMessages: ChatMessage[] = [];
+ hrInput = '';
+ hrLoading = false;
+ hrError = '';
+
+ sendChat(): void {
+ const message = this.chatInput.trim();
+ if (!message || this.chatLoading) return;
+
+ this.chatMessages.push({ role: 'user', content: message });
+ this.chatInput = '';
+ this.chatLoading = true;
+ this.chatError = '';
+
+ this.aiService
+ .chat(message)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: response => {
+ this.chatMessages.push({ role: 'assistant', content: response.reply });
+ this.chatLoading = false;
+ },
+ error: err => {
+ this.chatError = err?.error?.detail ?? 'Failed to get a response. Is the API running with AiEnabled: true?';
+ this.chatLoading = false;
+ },
+ });
+ }
+
+ sendHrInsight(): void {
+ const question = this.hrInput.trim();
+ if (!question || this.hrLoading) return;
+
+ this.hrMessages.push({ role: 'user', content: question });
+ this.hrInput = '';
+ this.hrLoading = true;
+ this.hrError = '';
+
+ this.aiService
+ .hrInsight(question)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: result => {
+ if (result.succeeded && result.data) {
+ this.hrMessages.push({
+ role: 'assistant',
+ content: result.data.answer,
+ executionTimeMs: result.data.executionTimeMs,
+ });
+ } else {
+ this.hrError = result.message ?? 'Unexpected response from HR insights endpoint.';
+ }
+ this.hrLoading = false;
+ },
+ error: err => {
+ this.hrError = err?.error?.detail ?? 'Failed to get HR insights. Is the API running with AiEnabled: true?';
+ this.hrLoading = false;
+ },
+ });
+ }
+
+ onChatKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ this.sendChat();
+ }
+ }
+
+ onHrKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ this.sendHrInsight();
+ }
+ }
+
+ clearChat(): void {
+ this.chatMessages = [];
+ this.chatError = '';
+ }
+
+ clearHr(): void {
+ this.hrMessages = [];
+ this.hrError = '';
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+}
diff --git a/talent-management/src/app/services/api/ai.service.ts b/talent-management/src/app/services/api/ai.service.ts
new file mode 100644
index 0000000..6313062
--- /dev/null
+++ b/talent-management/src/app/services/api/ai.service.ts
@@ -0,0 +1,54 @@
+import { Injectable, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { environment } from '../../../environments/environment';
+
+export interface AiChatResponse {
+ reply: string;
+}
+
+export interface HrInsightDto {
+ question: string;
+ answer: string;
+ executionTimeMs: number;
+}
+
+export interface HrInsightResult {
+ succeeded: boolean;
+ data: HrInsightDto;
+ message?: string;
+}
+
+/**
+ * AI API Service
+ * Calls /api/v1/ai/chat and /api/v1/ai/hr-insight endpoints.
+ * Requires AiEnabled feature flag to be true in the .NET API.
+ */
+@Injectable({
+ providedIn: 'root',
+})
+export class AiService {
+ private http = inject(HttpClient);
+ private apiUrl = environment.apiUrl;
+
+ /**
+ * Send a message to the general-purpose AI assistant.
+ * Calls POST /api/v1/ai/chat
+ */
+ chat(message: string, systemPrompt?: string): Observable {
+ return this.http.post(`${this.apiUrl}/ai/chat`, {
+ message,
+ systemPrompt,
+ });
+ }
+
+ /**
+ * Ask the HR AI assistant a data-aware question.
+ * Calls POST /api/v1/ai/hr-insight — the backend injects live workforce metrics.
+ */
+ hrInsight(question: string): Observable {
+ return this.http.post(`${this.apiUrl}/ai/hr-insight`, {
+ question,
+ });
+ }
+}
diff --git a/talent-management/src/app/services/api/index.ts b/talent-management/src/app/services/api/index.ts
index 6387bfe..011796d 100644
--- a/talent-management/src/app/services/api/index.ts
+++ b/talent-management/src/app/services/api/index.ts
@@ -1,3 +1,4 @@
+export * from './ai.service';
export * from './base-api.service';
export * from './dashboard.service';
export * from './department.service';
diff --git a/talent-management/src/environments/environment.prod.ts b/talent-management/src/environments/environment.prod.ts
index 6805681..e88046f 100644
--- a/talent-management/src/environments/environment.prod.ts
+++ b/talent-management/src/environments/environment.prod.ts
@@ -13,4 +13,5 @@ export const environment = {
// Feature Flags
allowAnonymousAccess: true,
+ aiEnabled: false,
};
diff --git a/talent-management/src/environments/environment.ts b/talent-management/src/environments/environment.ts
index 23d083b..7c97f13 100644
--- a/talent-management/src/environments/environment.ts
+++ b/talent-management/src/environments/environment.ts
@@ -17,6 +17,7 @@ export const environment = {
// Feature Flags
allowAnonymousAccess: true,
+ aiEnabled: false,
};
/*