Commit 14a817b1 authored by Kostis Trantzas's avatar Kostis Trantzas
Browse files

Merge branch 'Branch_chat-ui' into 'develop'

Adding an AI Assistant UI (fix for #43)

See merge request !37
parents 11294fab 620a4e03
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -54,3 +54,4 @@ src/assets/config/config.theming.json
/src/assets/config/theming.scss
/.angular
/.tmp/
/proxy.conf.json
+3 −0
Original line number Diff line number Diff line
@@ -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}

+28 −26
Original line number Diff line number Diff line
@@ -130,6 +130,8 @@ export function initializeAppTheme(bootstrap: BootstrapService) {
        MatInputModule,
        MatDialogModule,
        MatCheckboxModule,
        MatIconModule,
        MatTooltipModule,
        OAuthModule.forRoot(),
        ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }),
        NgChartsModule,
+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
+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