Skip to content

Commit

Permalink
Merge pull request #7 from ls1intum/integrate-new-server
Browse files Browse the repository at this point in the history
Integrate new server
  • Loading branch information
ninori9 authored Jan 5, 2025
2 parents 46289b3 + 3c48bc8 commit 7ce6ac5
Show file tree
Hide file tree
Showing 22 changed files with 284 additions and 131 deletions.
14 changes: 8 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy Angelos-UI
name: Deploy Chatbot

on:
push:
Expand All @@ -13,13 +13,15 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

# Create the updated environment.prod.ts file
- name: Create Environment File
run: |
echo "export const environment = {
production: true,
angelosUrl: '/api/v1/question/chat',
angelosToken: '/api/token',
angelosAppApiKey: '${{secrets.ANGELOS_APP_API_KEY}}'
angelosUrl: '/api/chat',
angelosAppApiKey: '${{secrets.ANGELOS_APP_API_KEY}}',
organisation: 2,
filterByOrg: false
};" > src/environments/environment.prod.ts
- name: Verify Environment File Creation
Expand Down Expand Up @@ -58,7 +60,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: checkout
- name: Checkout code
uses: actions/checkout@v4

- name: Copy Docker Compose File From Repo to VM Host
Expand Down Expand Up @@ -90,4 +92,4 @@ jobs:
docker network create angelos-network
fi
docker network ls
docker compose -f /home/${{ vars.VM_USERNAME }}/${{ github.repository }}/docker-compose.yml up --pull=always -d --force-recreate --remove-orphans
docker compose -f /home/${{ vars.VM_USERNAME }}/${{ github.repository }}/docker-compose.yml up --pull=always -d --force-recreate --remove-orphans
12 changes: 8 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# Stage 1: Build the Angular app
FROM node:20-alpine AS build
WORKDIR /app

# Copy package files and install dependencies
COPY package.json package-lock.json ./
RUN npm install

# Copy the application code and build the app
COPY . ./
RUN npm install
RUN npm run build -- --configuration=production
RUN npm run build -- --configuration=production --base-href=/chatbot/

# Stage 2: Serve the app with NGINX
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/angelos-ui/browser /usr/share/nginx/html
COPY --from=build /app/dist/chatbot/browser /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
# Start NGINX
CMD ["nginx", "-g", "daemon off;"]
3 changes: 2 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
"outputHashing": "all",
"baseHref": "/chat/"
},
"development": {
"optimization": false,
Expand Down
16 changes: 5 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
services:
angelos-ui:
chatbot:
image: "ghcr.io/ls1intum/angelos-ui:latest"
container_name: angelos-ui
ports:
- "443:443"
container_name: chatbot
expose:
- "443"
volumes:
- type: bind
source: /var/lib/rbg-cert/2024-10-31T11:10:43+01:00
target: /etc/ssl/certs
- "80"
networks:
- angelos-network
- angelos-network

networks:
angelos-network:
external: true
external: true
68 changes: 20 additions & 48 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -1,51 +1,23 @@
server {
listen 80;
listen [::]:80;
server_name chatbot.ase.cit.tum.de www.chatbot.ase.cit.tum.de;

return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name chatbot.ase.cit.tum.de www.chatbot.ase.cit.tum.de;

# SSL Certificate files
ssl_certificate /etc/ssl/certs/host:f:asevm83.cit.tum.de.cert.pem;
ssl_certificate_key /etc/ssl/certs/host:f:asevm83.cit.tum.de.privkey.pem;

# SSL Settings (recommended for security)
# ssl_dhparam /etc/nginx/dhparam.pem;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# ssl_early_data on;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://angelos-app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Internal-Access "true";
}

error_page 500 502 503 504 /50x.html;

location = /50x.html {
root /usr/share/nginx/html;
}

server_name localhost;

# Serve Angular app from /usr/share/nginx/html
root /usr/share/nginx/html;
index index.csr.html;

# Handle Angular routing
location /chatbot/ {
try_files $uri /index.csr.html;
}

# Redirect 404 errors to Angular's index.csr.html
error_page 404 /index.csr.html;

# Optional: Cache static files
location ~* \.(?:ico|css|js|woff2?|eot|ttf|otf|svg|png|jpg|jpeg|gif|webp|avif|mp4|webm|ogg|mp3|wav|flac|aac)$ {
expires 6M;
access_log off;
add_header Cache-Control "public";
}
}
4 changes: 2 additions & 2 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { AuthGuard } from './utils/auth.guard';

export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'chat/en', component: ChatComponent, canActivate: [AuthGuard], data: { language: 'en' } },
{ path: 'chat/de', component: ChatComponent, canActivate: [AuthGuard], data: { language: 'de' } },
{ path: 'en', component: ChatComponent, canActivate: [AuthGuard], data: { language: 'en' } },
{ path: 'de', component: ChatComponent, canActivate: [AuthGuard], data: { language: 'de' } },
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: '**', redirectTo: 'login', pathMatch: 'full' }
];
5 changes: 3 additions & 2 deletions src/app/chat/chat.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<div class="chat-container">
<app-error-snackbar #errorSnackbar></app-error-snackbar>

<div class="chat-header">
<ng-select class="study-program-dropdown" [items]="studyPrograms" bindLabel="label" bindValue="value"
[formControl]="studyProgramControl" [placeholder]="dropdownLabel">
<ng-select class="study-program-dropdown" [items]="studyPrograms" bindLabel="name" bindValue="id"
(change)="onProgramChange($event)" [placeholder]="dropdownLabel">
</ng-select>
<img src="assets/logo.png" alt="Logo" class="chat-logo" />
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/app/chat/chat.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,5 @@ $logo-size: 40px;
transform: translateY(-2px);
opacity: 0.7;
}
}
}

52 changes: 40 additions & 12 deletions src/app/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Component, ElementRef, ViewChild, OnInit, ViewChildren, QueryList, Afte
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ChatbotService } from '../services/chatbot.service';
import { NgSelectModule } from '@ng-select/ng-select';
import { studyPrograms } from '../utils/study_programs';
import { ActivatedRoute } from '@angular/router';
import { ErrorSnackbarComponent } from '../utils/error-snackbar/error-snackbar.component';
import { StudyProgramService } from '../services/study-program.service';
import { StudyProgram } from '../data/study-program';

export interface ChatMessage {
message: string;
Expand Down Expand Up @@ -41,7 +43,13 @@ export const MESSAGES = {
@Component({
selector: 'app-chat',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, ReactiveFormsModule],
imports: [
CommonModule,
FormsModule,
NgSelectModule,
ReactiveFormsModule,
ErrorSnackbarComponent
],
providers: [ChatbotService],
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.scss']
Expand All @@ -50,6 +58,7 @@ export class ChatComponent implements OnInit, AfterViewChecked {
@ViewChild('chatBody', { static: false }) chatBody: ElementRef | undefined;
@ViewChild('messageInput') messageInput!: ElementRef;
@ViewChildren('messageElements') messageElements!: QueryList<ElementRef>;
@ViewChild('errorSnackbar') errorSnackbar!: ErrorSnackbarComponent;

messages: ChatMessage[] = [];
userMessage: string = '';
Expand All @@ -58,18 +67,29 @@ export class ChatComponent implements OnInit, AfterViewChecked {
errorMessage: string = '';
dropdownLabel: string = '';

// FormControl for the study program dropdown
studyProgramControl = new FormControl(null);
studyPrograms = studyPrograms;
studyPrograms: StudyProgram[] = [];
selectedStudyProgram: StudyProgram | null = null;

language: 'en' | 'de' = 'en'; // Default language is English
private needScrollToBottom: boolean = false;
disableSending: boolean = false;

constructor(private chatbotService: ChatbotService, private route: ActivatedRoute) { }
constructor(private chatbotService: ChatbotService, private studyProgramService: StudyProgramService, private route: ActivatedRoute) { }

ngOnInit() {
// Get language from route data (or query params if applicable)
this.studyProgramService.getStudyPrograms().subscribe({
next: (studyPrograms) => {
this.studyPrograms = studyPrograms.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
},
error: (error) => {
this.errorSnackbar.showError('Studiengänge konnten nicht geladen werden. Bitte versuchen Sie es zu einem späteren Zeitpunkt erneut.', 5000);
}
});
this.route.data.subscribe(data => {
this.language = data['language'] || 'en';
this.setLanguageContent();
Expand Down Expand Up @@ -113,8 +133,11 @@ export class ChatComponent implements OnInit, AfterViewChecked {
this.userMessage = '';
this.resetTextAreaHeight();
this.needScrollToBottom = true;

const selectedProgram = this.studyProgramControl.value? this.studyProgramControl.value as string : '';

// Study program name in request format
const selectedProgramName = this.selectedStudyProgram?.name
.toLowerCase()
.replace(/\s+/g, '-') || '';

// Add a loading message to indicate the bot is typing
const loadingMessage: ChatMessage = { message: '', type: 'loading' };
Expand All @@ -126,7 +149,7 @@ export class ChatComponent implements OnInit, AfterViewChecked {
const messagesToSend = nonLoadingMessages.slice(-5);

// Call the bot service with the filtered messages
this.chatbotService.getBotResponse(messagesToSend, selectedProgram).subscribe({
this.chatbotService.getBotResponse(messagesToSend, selectedProgramName).subscribe({
next: (response: any) => {
// Remove the loading message
this.messages.pop();
Expand All @@ -136,8 +159,7 @@ export class ChatComponent implements OnInit, AfterViewChecked {
this.needScrollToBottom = true;
this.disableSending = false;
},
error: (error) => {
console.error('Error fetching response:', error);
error: (error: any) => {
// Remove the loading message
this.messages.pop();
// Add an error message
Expand All @@ -147,6 +169,9 @@ export class ChatComponent implements OnInit, AfterViewChecked {
});
this.needScrollToBottom = true;
this.disableSending = false;
if (error.message && error.message === 'TokenMissing') {
this.errorSnackbar.showError('Ihre Session ist abgelaufen. Bitte melden Sie sich erneut an.', 5000);
}
}
});
}
Expand Down Expand Up @@ -211,7 +236,6 @@ export class ChatComponent implements OnInit, AfterViewChecked {
protected scrollToBottom(): void {
setTimeout(() => {
if (this.messageElements && this.chatBody && this.messageElements.length > 0) {
console.log("Scrolling down")
const lastMessageElement = this.messageElements.last;
lastMessageElement.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'end' });

Expand All @@ -221,4 +245,8 @@ export class ChatComponent implements OnInit, AfterViewChecked {
}
}, 0);
}

onProgramChange(program: StudyProgram | null): void {
this.selectedStudyProgram = program;
}
}
4 changes: 4 additions & 0 deletions src/app/data/study-program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface StudyProgram {
id: number;
name: string;
}
2 changes: 1 addition & 1 deletion src/app/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class LoginComponent {
if (this.loginForm.valid) {
const { username, password } = this.loginForm.value;
this.authService.login(username, password).subscribe({
next: () => this.router.navigate(['/chat/en']), // Redirect to chat after login
next: () => this.router.navigate(['/en']), // Redirect to chat after login
error: (err) => (this.errorMessage = 'Invalid username or password')
});
}
Expand Down
12 changes: 6 additions & 6 deletions src/app/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { environment } from '../../environments/environment';
import * as CryptoJS from 'crypto-js';

export interface AuthResponse {
access_token: string;
token_type: string;
accessToken: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private isAuthenticated = new BehaviorSubject<boolean>(this.getToken() !== null);

private url = environment.angelosUrl;

constructor(private http: HttpClient) { }

login(username: string, password: string): Observable<AuthResponse> {
const headers = new HttpHeaders().set('x-api-key', environment.angelosAppApiKey);
const body = { username: username, password: password };
return this.http.post<AuthResponse>(environment.angelosToken, body, { headers }).pipe(
const body = { email: username, password: password };
return this.http.post<AuthResponse>(this.url + "/login", body, { headers }).pipe(
tap((response: AuthResponse) => {
sessionStorage.setItem('access_token', response.access_token);
sessionStorage.setItem('access_token', response.accessToken);
this.isAuthenticated.next(true);
})
);
Expand Down
Loading

0 comments on commit 7ce6ac5

Please sign in to comment.