Loading .gitignore +1 −0 Original line number Diff line number Diff line Loading @@ -54,3 +54,4 @@ src/assets/config/config.theming.json /src/assets/config/theming.scss /.angular /.tmp/ /proxy.conf.json src/app/app-routing.module.ts +3 −0 Original line number Diff line number Diff line Loading @@ -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 = [ Loading @@ -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} Loading src/app/app.module.ts +28 −26 Original line number Diff line number Diff line Loading @@ -130,6 +130,8 @@ export function initializeAppTheme(bootstrap: BootstrapService) { MatInputModule, MatDialogModule, MatCheckboxModule, MatIconModule, MatTooltipModule, OAuthModule.forRoot(), ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }), NgChartsModule, Loading src/app/p_chat/assistant/assistant.component.html 0 → 100644 +58 −0 Original line number Diff line number Diff line <div class="container py-3"> <div class="row"> <div class="col-12"> <mat-card class="chat-container mat-elevation-z8"> <mat-card-header class="chat-header large-header"> <div class="header-wrapper"> <div class="brand-logo-container"> <img src="assets/images/osl_ai_assistant.png" alt="OpenSlice Logo" class="brand-logo"> </div> <div class="title-group"> <mat-card-title class="gradient-title">OpenSlice AI Assistant</mat-card-title> <mat-card-subtitle class="status-subtitle"> <span class="status-dot"></span> Powered by MCP Backend & LLM </mat-card-subtitle> </div> </div> </mat-card-header> <mat-card-content #chatHistory class="chat-history"> <div *ngFor="let message of messages" [ngClass]="{'user-row': message.sender === 'user', 'ai-row': message.sender === 'ai'}"> <div class="message-bubble"> <markdown [data]="message.text"></markdown> <span class="timestamp">{{ message.timestamp | date:'shortTime' }}</span> </div> </div> <div *ngIf="isLoading" class="ai-row"> <div class="message-bubble ai-loading-bubble"> <div class="typing-indicator"> <span></span> <span></span> <span></span> </div> <span class="loading-text">OpenSlice AI is thinking...</span> </div> </div> </mat-card-content> <mat-card-actions class="chat-input-area"> <mat-form-field appearance="outline" class="full-width"> <mat-label>Type your message</mat-label> <textarea matInput cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="1" cdkAutosizeMaxRows="5" [(ngModel)]="userInput" (keydown.enter)="handleEnter($event)" [disabled]="isLoading" placeholder="Ask me anything..."> </textarea> <button mat-icon-button matSuffix color="primary" (click)="sendMessage()" [disabled]="!userInput.trim() || isLoading"> <mat-icon>send</mat-icon> </button> </mat-form-field> </mat-card-actions> </mat-card> </div> </div> </div> No newline at end of file src/app/p_chat/assistant/assistant.component.scss 0 → 100644 +293 −0 Original line number Diff line number Diff line .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 Loading
.gitignore +1 −0 Original line number Diff line number Diff line Loading @@ -54,3 +54,4 @@ src/assets/config/config.theming.json /src/assets/config/theming.scss /.angular /.tmp/ /proxy.conf.json
src/app/app-routing.module.ts +3 −0 Original line number Diff line number Diff line Loading @@ -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 = [ Loading @@ -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} Loading
src/app/app.module.ts +28 −26 Original line number Diff line number Diff line Loading @@ -130,6 +130,8 @@ export function initializeAppTheme(bootstrap: BootstrapService) { MatInputModule, MatDialogModule, MatCheckboxModule, MatIconModule, MatTooltipModule, OAuthModule.forRoot(), ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }), NgChartsModule, Loading
src/app/p_chat/assistant/assistant.component.html 0 → 100644 +58 −0 Original line number Diff line number Diff line <div class="container py-3"> <div class="row"> <div class="col-12"> <mat-card class="chat-container mat-elevation-z8"> <mat-card-header class="chat-header large-header"> <div class="header-wrapper"> <div class="brand-logo-container"> <img src="assets/images/osl_ai_assistant.png" alt="OpenSlice Logo" class="brand-logo"> </div> <div class="title-group"> <mat-card-title class="gradient-title">OpenSlice AI Assistant</mat-card-title> <mat-card-subtitle class="status-subtitle"> <span class="status-dot"></span> Powered by MCP Backend & LLM </mat-card-subtitle> </div> </div> </mat-card-header> <mat-card-content #chatHistory class="chat-history"> <div *ngFor="let message of messages" [ngClass]="{'user-row': message.sender === 'user', 'ai-row': message.sender === 'ai'}"> <div class="message-bubble"> <markdown [data]="message.text"></markdown> <span class="timestamp">{{ message.timestamp | date:'shortTime' }}</span> </div> </div> <div *ngIf="isLoading" class="ai-row"> <div class="message-bubble ai-loading-bubble"> <div class="typing-indicator"> <span></span> <span></span> <span></span> </div> <span class="loading-text">OpenSlice AI is thinking...</span> </div> </div> </mat-card-content> <mat-card-actions class="chat-input-area"> <mat-form-field appearance="outline" class="full-width"> <mat-label>Type your message</mat-label> <textarea matInput cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="1" cdkAutosizeMaxRows="5" [(ngModel)]="userInput" (keydown.enter)="handleEnter($event)" [disabled]="isLoading" placeholder="Ask me anything..."> </textarea> <button mat-icon-button matSuffix color="primary" (click)="sendMessage()" [disabled]="!userInput.trim() || isLoading"> <mat-icon>send</mat-icon> </button> </mat-form-field> </mat-card-actions> </mat-card> </div> </div> </div> No newline at end of file
src/app/p_chat/assistant/assistant.component.scss 0 → 100644 +293 −0 Original line number Diff line number Diff line .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