diff --git a/.gitignore b/.gitignore
index f0f90ede6fc42c9399b234a3876c46d99ff4035d..068190e077f033ed9af38f9908c71cccef0e05ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,3 +54,4 @@ src/assets/config/config.theming.json
/src/assets/config/theming.scss
/.angular
/.tmp/
+/proxy.conf.json
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 2853e74fab0d04af2741935f687aaffd9f02cbf1..6b8bb30b360e2031d1aa585bcd3fbb244c2384a4 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -5,6 +5,7 @@ import { MetricsComponent } from './landing/metrics/metrics.component';
import { PortalsComponent } from './landing/portals/portals.component';
import { PageNotFoundComponent } from './shared/components/page-not-found/page-not-found.component';
import { RedirectComponent } from './shared/components/redirect/redirect.component';
+import { AssistantComponent } from './p_chat/assistant/assistant.component';
import { LandingComponent } from './landing/landing.component';
const routes: Routes = [
@@ -27,6 +28,8 @@ const routes: Routes = [
{ path: 'products', component: PortalsComponent},
{ path: 'products', loadChildren: () => import('./app-products.module').then(m => m.AppProductsModule)},
+ { path: 'assistant', loadChildren: () => import('./shared.module').then(m => m.SharedModule)},
+
{ path: '**', redirectTo: '404'},
{ path: '404', component: PageNotFoundComponent}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index b18301408c0ff2a0f37f2e9fb2e15dc36c6081dd..c9f6e3d60c77af2022ca720c96f4aa0c3eca9c9b 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -50,7 +50,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
-import {DragDropModule} from '@angular/cdk/drag-drop';
+import { DragDropModule } from '@angular/cdk/drag-drop';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -92,11 +92,11 @@ import { CdkTableModule } from "@angular/cdk/table";
registerLocaleData(enGB);
export function initializeApp(bootstrap: BootstrapService) {
- return () => bootstrap.loadConfig()
+ return () => bootstrap.loadConfig()
}
export function initializeAppTheme(bootstrap: BootstrapService) {
- return () => bootstrap.loadConfig()
+ return () => bootstrap.loadConfig()
}
@NgModule({
@@ -106,35 +106,37 @@ export function initializeAppTheme(bootstrap: BootstrapService) {
FooterComponent,
ContactComponent,
MetricsComponent,
- LandingComponent,
+ LandingComponent,
PortalsComponent,
CloneGstTemplateComponent,
PageNotFoundComponent,
RedirectComponent,
- DoughnutchartComponent,
- StatusCardComponent,
- PortalCardComponent,
- SkeletonComponent,
+ DoughnutchartComponent,
+ StatusCardComponent,
+ PortalCardComponent,
+ SkeletonComponent,
],
imports: [
- BrowserModule,
- AppRoutingModule,
- HttpClientModule,
- BrowserAnimationsModule,
- CollapseModule.forRoot(),
- BsDropdownModule.forRoot(),
- FormsModule,
- ReactiveFormsModule,
- NgProgressModule,
- NgProgressHttpModule,
- MatInputModule,
- MatDialogModule,
- MatCheckboxModule,
- OAuthModule.forRoot(),
- ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }),
- NgChartsModule,
- CdkTableModule
-],
+ BrowserModule,
+ AppRoutingModule,
+ HttpClientModule,
+ BrowserAnimationsModule,
+ CollapseModule.forRoot(),
+ BsDropdownModule.forRoot(),
+ FormsModule,
+ ReactiveFormsModule,
+ NgProgressModule,
+ NgProgressHttpModule,
+ MatInputModule,
+ MatDialogModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatTooltipModule,
+ OAuthModule.forRoot(),
+ ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }),
+ NgChartsModule,
+ CdkTableModule
+ ],
providers: [
AppService,
AuthService,
diff --git a/src/app/p_chat/assistant/assistant.component.html b/src/app/p_chat/assistant/assistant.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..9a4d044d3ad84b3b359f2af4aef79dc491f0c3b5
--- /dev/null
+++ b/src/app/p_chat/assistant/assistant.component.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ message.timestamp | date:'shortTime' }}
+
+
+
+
+
+
+
+
+
+
+
OpenSlice AI is thinking...
+
+
+
+
+
+
+ Type your message
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/p_chat/assistant/assistant.component.scss b/src/app/p_chat/assistant/assistant.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..97904a594a938a2388c9f800c1584f4b5aad9239
--- /dev/null
+++ b/src/app/p_chat/assistant/assistant.component.scss
@@ -0,0 +1,293 @@
+.large-header {
+ padding: 16px 20px !important;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+
+ .header-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ .brand-logo-container {
+ height: 52px; // This matches the combined height of Title (1.5rem) + Subtitle + Gap
+ width: 52px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ // background: #f8f9fa;
+ border-radius: 12px;
+ overflow: hidden;
+ flex-shrink: 0;
+
+ .brand-logo {
+ height: 100%;
+ width: 100%;
+ object-fit: contain; // Ensures logo doesn't distort
+ // padding: 4px; // Tiny padding so logo doesn't touch the container edges
+ }
+ }
+
+ .title-group {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ line-height: 1.2;
+
+ .gradient-title {
+ font-weight: 800 !important;
+ letter-spacing: -0.5px;
+ font-size: 1.25rem !important;
+ // background: linear-gradient(90deg, #3f51b5, #7b1fa2);
+ background: linear-gradient(90deg, #00AFBB, #C8D400);
+
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ margin: 0;
+ }
+
+ .status-subtitle {
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+ font-size: 0.75rem;
+ color: #666;
+
+ .status-dot {
+ height: 7px;
+ width: 7px;
+ background-color: #4caf50;
+ border-radius: 50%;
+ margin-right: 6px;
+ animation: pulse 2s infinite;
+ }
+ }
+ }
+}
+
+::ng-deep .large-header .mat-card-header-text {
+ margin: 0 !important;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7);
+ }
+
+ 70% {
+ transform: scale(1);
+ box-shadow: 0 0 0 6px rgba(76, 175, 80, 0);
+ }
+
+ 100% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
+ }
+}
+
+
+.chat-container {
+ // max-width: 1200px;
+ margin: 20px auto;
+ height: 75vh; // Use viewport height for better responsiveness
+ min-height: 400px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.chat-history {
+ flex-grow: 1;
+ overflow-y: auto;
+ padding: 16px;
+ background-color: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ scroll-behavior: smooth;
+
+ /* Custom Scrollbar for a cleaner look */
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #ccc;
+ border-radius: 10px;
+ }
+}
+
+.user-row {
+ display: flex;
+ justify-content: flex-end;
+
+ .message-bubble {
+ background-color: #3f51b5; // Material Primary
+ color: white;
+ border-radius: 15px 15px 0 15px;
+ }
+}
+
+.ai-row {
+ display: flex;
+ justify-content: flex-start;
+
+ .message-bubble {
+ background-color: #e0e0e0;
+ color: rgba(0, 0, 0, 0.87);
+ border-radius: 15px 15px 15px 0;
+ }
+}
+
+.ai-loading-bubble {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ background-color: #f0f2f5 !important;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ padding: 10px 16px !important;
+
+ .loading-text {
+ font-size: 0.85rem;
+ font-style: italic;
+ color: #5c6bc0;
+ font-weight: 500;
+ }
+}
+
+.typing-indicator {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ span {
+ height: 6px;
+ width: 6px;
+ background: linear-gradient(135deg, #3f51b5 0%, #7b1fa2 100%);
+ border-radius: 50%;
+ display: block;
+ opacity: 0.4;
+ animation: typing 1s infinite;
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ }
+ }
+}
+
+@keyframes typing {
+
+ 0%,
+ 100% {
+ transform: translateY(0);
+ opacity: 0.4;
+ }
+
+ 50% {
+ transform: translateY(-5px);
+ opacity: 1;
+ }
+}
+
+.message-bubble {
+ padding: 12px 16px;
+ max-width: 85%;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ animation: fadeIn 0.3s ease-in-out;
+
+ // white-space: pre-wrap;
+ word-wrap: break-word; // Standard support
+ overflow-wrap: break-word; // Modern support
+
+ // Target elements generated by the markdown parser
+ ::ng-deep markdown {
+ p {
+ margin-bottom: 8px;
+ white-space: initial;
+ }
+
+ p:last-child {
+ margin-bottom: 0 !important;
+ }
+
+ code {
+ background-color: rgba(0, 0, 0, 0.1);
+ padding: 2px 4px;
+ border-radius: 4px;
+ font-family: monospace;
+ }
+
+ ul,
+ ol {
+ padding-left: 20px;
+ margin: 8px 0;
+ }
+
+ blockquote {
+ border-left: 3px solid #ccc;
+ margin: 8px 0;
+ padding-left: 10px;
+ font-style: italic;
+ }
+
+ pre {
+ background-color: #2d2d2d;
+ color: #ccc;
+ padding: 12px;
+ border-radius: 8px;
+ overflow-x: auto;
+ margin: 10px 0;
+ }
+
+ img {
+ max-width: 100%;
+ border-radius: 8px;
+ }
+ }
+
+}
+
+.timestamp {
+ display: block;
+ font-size: 0.7rem;
+ margin-top: 4px;
+ opacity: 0.7;
+ text-align: right;
+}
+
+// Ensure the user bubble markdown text stays white
+.user-row .message-bubble ::ng-deep markdown {
+ color: white;
+
+ code {
+ background-color: rgba(255, 255, 255, 0.2);
+ }
+}
+
+
+.chat-input-area {
+ padding: 8px 16px !important;
+
+ .full-width {
+ width: 100%;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
\ No newline at end of file
diff --git a/src/app/p_chat/assistant/assistant.component.spec.ts b/src/app/p_chat/assistant/assistant.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f04738852a963e24d038a865434c54f360bb4bf1
--- /dev/null
+++ b/src/app/p_chat/assistant/assistant.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AssistantComponent } from './assistant.component';
+
+describe('AssistantComponent', () => {
+ let component: AssistantComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ AssistantComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AssistantComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/p_chat/assistant/assistant.component.ts b/src/app/p_chat/assistant/assistant.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e28f9a1b169619d1b63e70866e358f149e7909b
--- /dev/null
+++ b/src/app/p_chat/assistant/assistant.component.ts
@@ -0,0 +1,110 @@
+import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AssistantService } from './services/assistant.service';
+import { Subscription } from 'rxjs';
+import { FormsModule } from '@angular/forms';
+import { AuthService } from 'src/app/shared/services/auth.service';
+
+@Component({
+ selector: 'app-assistant',
+ templateUrl: './assistant.component.html',
+ styleUrls: ['./assistant.component.scss']
+})
+export class AssistantComponent implements OnInit {
+
+
+ @ViewChild('chatHistory') private chatHistoryContainer!: ElementRef;
+ messages: ChatMessage[] = [];
+ userInput: string = '';
+ isLoading: boolean = false;
+
+ private chatSubscription?: Subscription;
+
+ constructor(
+ private assistantService: AssistantService,
+ private authService: AuthService
+ ) { }
+
+ ngOnInit(): void {
+ this.messages = [
+ { text: `Hello, ${this.authService.portalUserJWT.preferred_username}! How can I assist you today?`, sender: 'ai', timestamp: new Date() }
+ ];
+ }
+
+ sendMessage(): void {
+ const currentInput = this.userInput.trim();
+ if (!currentInput) return;
+
+ this.messages.push({
+ text: currentInput,
+ sender: 'user',
+ timestamp: new Date()
+ });
+
+ this.userInput = ''; // Clear input
+ this.isLoading = true; // Show loading indicator
+
+ // Trigger scroll after DOM updates from user message
+ this.scheduleScroll();
+
+ // Send to backend and get AI response
+ this.chatSubscription = this.assistantService.sendMessage(currentInput).subscribe({
+ next: (response) => {
+ const cleanMessage = response.answer.trim()
+
+ this.messages.push({
+ text: cleanMessage,
+ sender: 'ai',
+ timestamp: new Date()
+ });
+ this.isLoading = false;
+ this.scheduleScroll();
+ },
+ error: (err) => {
+ console.error('Error getting AI response:', err);
+ this.messages.push({
+ text: 'Oops! Something went wrong. Please try again.',
+ sender: 'ai',
+ timestamp: new Date()
+ });
+ this.isLoading = false;
+ this.scheduleScroll();
+ }
+ });
+ }
+
+ handleEnter(event: any): void {
+ // If user presses Enter WITHOUT Shift, send the message
+ if (!event.shiftKey) {
+ event.preventDefault(); // Prevents adding a new line to the textarea
+ this.sendMessage();
+ }
+}
+
+ private scheduleScroll(): void {
+ // Replaces the 'effect' logic by manually scheduling a scroll check
+ setTimeout(() => this.scrollToBottom(), 0);
+ }
+
+ private scrollToBottom(): void {
+ if (this.chatHistoryContainer) {
+ const element = this.chatHistoryContainer.nativeElement;
+ element.scrollTop = element.scrollHeight;
+ }
+ }
+
+ ngOnDestroy(): void {
+ // Clean up subscription when component is destroyed
+ if (this.chatSubscription) {
+ this.chatSubscription.unsubscribe();
+ }
+ }
+
+}
+
+
+export interface ChatMessage {
+ text: string;
+ sender: 'user' | 'ai'; // To differentiate who sent the message
+ timestamp: Date;
+}
\ No newline at end of file
diff --git a/src/app/p_chat/assistant/services/assistant.service.ts b/src/app/p_chat/assistant/services/assistant.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be2fe45d5c6ab63163db79f22c28ffa8efd42606
--- /dev/null
+++ b/src/app/p_chat/assistant/services/assistant.service.ts
@@ -0,0 +1,20 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/internal/Observable';
+import { BootstrapService } from 'src/app/bootstrap/bootstrap.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AssistantService {
+ private assistantURL = this.bootstrapService.getConfig().ASSISTANT_URL;
+
+ constructor(private http:HttpClient
+ , private bootstrapService:BootstrapService
+ ) {console.log("AssistantService assistantURL: ", this.assistantURL); }
+
+ sendMessage(message: string): Observable {
+ const payload = { question: message };
+ return this.http.post('/ask', payload);
+ }
+}
diff --git a/src/app/shared.module.ts b/src/app/shared.module.ts
index 471910c456e807f45ea83a88327642689cedba0a..75e8f5386812d6156ecebe79913adc91bf279b16 100644
--- a/src/app/shared.module.ts
+++ b/src/app/shared.module.ts
@@ -1,4 +1,4 @@
-import { NgModule } from '@angular/core';
+import { NgModule, SecurityContext } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgProgressModule } from 'ngx-progressbar';
@@ -14,7 +14,7 @@ import { OAuthModule } from 'angular-oauth2-oidc';
import { FileUploadModule } from '@iplab/ngx-file-upload'
-import { MarkdownModule } from 'ngx-markdown'
+import { MarkdownModule, MarkedOptions } from 'ngx-markdown'
import { BootstrapComponent } from './bootstrap/bootstrap.component';
@@ -38,7 +38,7 @@ import { MatNativeDateModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
-import { MatIconModule } from '@angular/material/icon';
+import { MatIcon, MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatPaginatorModule } from '@angular/material/paginator';
@@ -52,13 +52,19 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
+import { MatCardModule } from '@angular/material/card';
import {DragDropModule} from '@angular/cdk/drag-drop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
+import { RouterModule, Routes } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgJsonEditorModule } from 'ang-jsoneditor';
+import { AssistantComponent } from './p_chat/assistant/assistant.component';
+import { AuthGuardService } from './shared/services/auth-guard.service';
+const routes: Routes = [
+ { path: '', component: AssistantComponent, canActivate: [AuthGuardService] }
+];
@NgModule({
declarations: [
@@ -71,10 +77,11 @@ import { NgJsonEditorModule } from 'ang-jsoneditor';
DeleteOrganizationComponent,
ListIndividualsComponent,
EditIndividualsComponent,
- DeleteIndividualComponent
+ DeleteIndividualComponent,
+ AssistantComponent
],
imports: [
- RouterModule,
+ RouterModule.forChild(routes),
FormsModule,
ReactiveFormsModule,
CommonModule,
@@ -99,6 +106,7 @@ import { NgJsonEditorModule } from 'ang-jsoneditor';
MatRadioModule,
MatProgressSpinnerModule,
MatListModule,
+ MatCardModule,
DragDropModule,
MatNativeDateModule,
MatDatepickerModule,
@@ -109,7 +117,15 @@ import { NgJsonEditorModule } from 'ang-jsoneditor';
// ToastrModule.forRoot({progressBar: true, preventDuplicates: true}),
// OAuthModule.forRoot(),
FileUploadModule,
- MarkdownModule.forRoot(),
+ MarkdownModule.forRoot({
+ markedOptions: {
+ provide: MarkedOptions,
+ useValue: {
+ gfm: true,
+ breaks: true, // This converts single \n into
+ },
+ },
+ }),
NgJsonEditorModule
],
exports: [
diff --git a/src/app/shared/components/redirect/redirect.component.ts b/src/app/shared/components/redirect/redirect.component.ts
index 4354f2126a6ba403a914fe4c63e5c49d90f218f5..d50453cc61cc257b3d5f9702f00acbff325f2348 100644
--- a/src/app/shared/components/redirect/redirect.component.ts
+++ b/src/app/shared/components/redirect/redirect.component.ts
@@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AppService } from '../../services/app.service';
+import { el } from '@fullcalendar/core/internal-common';
@Component({
selector: 'app-redirect',
@@ -31,6 +32,10 @@ export class RedirectComponent implements OnInit {
this.router.navigate(['products', 'marketplace'])
}
+ else if (activePortal === 'assistant') {
+ this.router.navigate(['assistant'])
+ }
+
else {
this.router.navigate(['/'])
}
diff --git a/src/app/shared/models/app-config.model.ts b/src/app/shared/models/app-config.model.ts
index 7d639d6cbe5a1a2c0cf824796f8d0725ac6b85e6..8ee673c940364b2c8e7e32fefa966928615633de 100644
--- a/src/app/shared/models/app-config.model.ts
+++ b/src/app/shared/models/app-config.model.ts
@@ -7,6 +7,7 @@ export interface IAppConfig {
"BUGZILLA": string,
"STATUS": string,
"WEBURL": string,
+ "ASSISTANT_URL": string,
"PORTAL_REPO_APIURL": string,
"ASSURANCE_SERVICE_MGMT_APIURL": string,
"APITMFURL": string,
diff --git a/src/app/shared/navbar/navbar.component.html b/src/app/shared/navbar/navbar.component.html
index 61d2099e9c28400d4d20a0fece63654667e240ef..9706b08dab308ae13cdd9d4d931f42619398d50d 100644
--- a/src/app/shared/navbar/navbar.component.html
+++ b/src/app/shared/navbar/navbar.component.html
@@ -42,6 +42,22 @@
Products
+
+
+ -
+
+
+
@@ -257,6 +273,12 @@
+
+
+
+
+
@@ -268,6 +290,20 @@
Order List
+
+
+
+