change header columns elements in airwatch
This commit is contained in:
parent
219fa8c4f9
commit
fa8a7ca996
215 changed files with 26292 additions and 45 deletions
16
old-sae-airwatch/src/app/api-service.spec.ts
Normal file
16
old-sae-airwatch/src/app/api-service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ApiService } from './api-service';
|
||||
|
||||
describe('ApiService', () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ApiService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
484
old-sae-airwatch/src/app/api-service.ts
Normal file
484
old-sae-airwatch/src/app/api-service.ts
Normal file
|
@ -0,0 +1,484 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
|
||||
import {from, Observable, Subject, throwError} from 'rxjs';
|
||||
import {catchError, finalize, takeUntil, tap} from 'rxjs/operators';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ActionTypes, StateInterface} from './reducers';
|
||||
import {ChatbotMessage} from './services/chatbot.message.type';
|
||||
|
||||
/**
|
||||
* Interface for Open-Meteo API response
|
||||
*/
|
||||
export interface OpenMeteoResponse {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
generationtime_ms: number;
|
||||
utc_offset_seconds: number;
|
||||
timezone: string;
|
||||
timezone_abbreviation: string;
|
||||
elevation: number;
|
||||
hourly_units: {
|
||||
time: string;
|
||||
temperature_2m: string;
|
||||
};
|
||||
hourly: {
|
||||
time: string[];
|
||||
temperature_2m: number[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* gérer la connexion avec le backend
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
/**
|
||||
* /api/v1
|
||||
* /messages
|
||||
* /{userId}/conversations
|
||||
* /{conversationId}/feedback
|
||||
* /conversations/{conversationId}
|
||||
* /conversations/{conversationId}/last-answer
|
||||
* /send-documents
|
||||
* /file // lien pour récupérer un doc via s3
|
||||
*
|
||||
*/
|
||||
|
||||
private BASE_URL = "";
|
||||
// Ollama API URL - default to localhost:11434
|
||||
private OLLAMA_API_URL = "http://localhost:11434";
|
||||
// Default Ollama model to use
|
||||
// private OLLAMA_MODEL = "llava";
|
||||
// private OLLAMA_MODEL = "mistral:7b";
|
||||
private OLLAMA_MODEL = "tinyllama:latest";
|
||||
|
||||
private ENDPOINTS: any = {
|
||||
"main": {
|
||||
"v1": {
|
||||
"messages": "messages",
|
||||
"user": {
|
||||
"conversations": "/{userId}/conversations",
|
||||
"feedback": "/user-feedback"
|
||||
},
|
||||
// /{userId}/conversations
|
||||
// /{conversationId}/feedback
|
||||
// /conversations/{conversationId}
|
||||
// /conversations/{conversationId}/last-answer
|
||||
// /send-documents
|
||||
// /file
|
||||
"conversations": {
|
||||
"id": "/{conversationId}",
|
||||
"feedback": "/{conversationId}/feedback",
|
||||
"last-anwser": "/{conversationId}/last-answer",
|
||||
},
|
||||
"send-documents": "send-documents",
|
||||
"file": "file",
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
private CUSTOM_HEADERS: any = {
|
||||
"method.response.header.Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
|
||||
"method.response.header.Access-Control-Allow-Methods": "OPTIONS,POST",
|
||||
"method.response.header.Access-Control-Allow-Origin": "'*'",
|
||||
}
|
||||
|
||||
constructor(private http: HttpClient, private store: Store<StateInterface>) {
|
||||
}
|
||||
|
||||
async fetchEndPoint(kind: string, subKind: string): Promise<Observable<Object>> {
|
||||
// Record the start time for measuring response time
|
||||
const startTime = Date.now();
|
||||
|
||||
let fetchUrl: string = `${this.BASE_URL}/api/v1`
|
||||
if (undefined !== this.ENDPOINTS.main.v1[kind]) {
|
||||
fetchUrl += this.ENDPOINTS.main.v1[kind]
|
||||
if (undefined !== this.ENDPOINTS.main.v1[kind][subKind]) {
|
||||
fetchUrl += '/' + this.ENDPOINTS.main.v1[kind][subKind]
|
||||
}
|
||||
}
|
||||
|
||||
// Get the response and pipe it to measure response time
|
||||
const resp: Observable<Object> = await this.http.get(fetchUrl,
|
||||
{headers: this.CUSTOM_HEADERS}
|
||||
).pipe(
|
||||
tap(() => {
|
||||
// Measure and update the response time
|
||||
this.measureResponseTime(startTime);
|
||||
})
|
||||
);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches weather forecast data from the Open-Meteo API
|
||||
*
|
||||
* @returns Observable with the typed JSON response from Open-Meteo API
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* this.apiService.fetchDemoResponse().subscribe(
|
||||
* (data) => {
|
||||
* console.log('Weather data:', data);
|
||||
* console.log('Temperature at noon:', data.hourly.temperature_2m[12]);
|
||||
* },
|
||||
* (error) => console.error('Error fetching weather data:', error)
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
fetchDemoResponse(): Observable<OpenMeteoResponse> {
|
||||
// Coordinates for Corbeil-Essonne
|
||||
const latitude = "48.57727";
|
||||
const longitude = "4.42634";
|
||||
|
||||
const url = 'https://api.open-meteo.com/v1/forecast';
|
||||
|
||||
// Record the start time for measuring response time
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get the current average response time for the delay
|
||||
return new Observable<OpenMeteoResponse>(observer => {
|
||||
this.store.select(state => state.app.averageResponseTime).pipe(
|
||||
tap(averageResponseTime => {
|
||||
// Use the average response time for the delay
|
||||
setTimeout(() => {
|
||||
this.http.get<OpenMeteoResponse>(url, {
|
||||
params: {
|
||||
latitude,
|
||||
longitude,
|
||||
hourly: 'temperature_2m',
|
||||
forecast_days: '1'
|
||||
}
|
||||
}).pipe(
|
||||
tap(response => {
|
||||
console.log('Open-Meteo API Response:', response);
|
||||
// Measure and update the response time
|
||||
this.measureResponseTime(startTime);
|
||||
}),
|
||||
catchError(this.handleError)
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
observer.next(response);
|
||||
observer.complete();
|
||||
},
|
||||
error: (error) => {
|
||||
observer.error(error);
|
||||
}
|
||||
});
|
||||
}, averageResponseTime); // Use the average response time for the delay
|
||||
})
|
||||
).subscribe().unsubscribe(); // Unsubscribe after getting the value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send user feedback to the API
|
||||
* @param feedback The feedback text from the user
|
||||
* @returns Observable with the API response
|
||||
*/
|
||||
async sendUserFeedback(feedback: string): Promise<Observable<Object>> {
|
||||
// Record the start time for measuring response time
|
||||
const startTime = Date.now();
|
||||
|
||||
const fetchUrl: string = `${this.BASE_URL}/api/v1/user-feedback`;
|
||||
|
||||
// Get the response and pipe it to measure response time
|
||||
const resp: Observable<Object> = await this.http.post(
|
||||
fetchUrl,
|
||||
{feedback},
|
||||
{headers: this.CUSTOM_HEADERS}
|
||||
).pipe(
|
||||
tap(() => {
|
||||
// Measure and update the response time
|
||||
this.measureResponseTime(startTime);
|
||||
})
|
||||
);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Ollama backend is available
|
||||
* @returns Observable that emits true if Ollama is available, false otherwise
|
||||
*/
|
||||
checkOllamaAvailability(): Observable<boolean> {
|
||||
return new Observable<boolean>(observer => {
|
||||
// Make a simple request to the Ollama API to check if it's available
|
||||
this.http.get(`${this.OLLAMA_API_URL}/api/tags`, {
|
||||
headers: new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
observe: 'response'
|
||||
}).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error checking Ollama availability:', error);
|
||||
// Update the Redux store to indicate Ollama is not available
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
ollamaAvailable: false
|
||||
}
|
||||
});
|
||||
// Return false to indicate Ollama is not available
|
||||
return from([false]);
|
||||
})
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
// If we get a response, Ollama is available
|
||||
// Check if response is an HttpResponse (has status property)
|
||||
const isAvailable = typeof response === 'object' && response !== null && 'status' in response && response.status === 200;
|
||||
|
||||
// Update the Redux store with the result
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
ollamaAvailable: isAvailable
|
||||
}
|
||||
});
|
||||
|
||||
// Emit the result and complete the observable
|
||||
observer.next(isAvailable);
|
||||
observer.complete();
|
||||
},
|
||||
error: () => {
|
||||
// If there's an error, Ollama is not available
|
||||
observer.next(false);
|
||||
observer.complete();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the Ollama backend with a prompt and handle streaming responses
|
||||
* @param prompt The text prompt to send to Ollama
|
||||
* @returns Observable that completes when the full response is received
|
||||
*/
|
||||
queryOllama(prompt: string): Observable<void> {
|
||||
// Create a subject to notify when to complete the stream
|
||||
const complete$ = new Subject<void>();
|
||||
|
||||
// Record the start time for measuring response time
|
||||
const startTime = Date.now();
|
||||
|
||||
// Set loading state to true
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
loading: true
|
||||
}
|
||||
});
|
||||
|
||||
// Create an empty response to accumulate the streamed content
|
||||
let accumulatedResponse = '';
|
||||
|
||||
// Create the request payload
|
||||
const payload = {
|
||||
model: this.OLLAMA_MODEL,
|
||||
prompt: 'réponds en Français en étant un assistant amical et respectueux. Ta réponse doit être consiste steuplai. Et fais des retours à la ligne dans tes réponses. Voici ma question : ' + prompt,
|
||||
stream: false,
|
||||
// raw: true,
|
||||
};
|
||||
|
||||
// Set up headers for the request
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
// Create a subject that will emit the final response
|
||||
const result = new Subject<void>();
|
||||
|
||||
// Make the request to Ollama API
|
||||
this.http.post(`${this.OLLAMA_API_URL}/api/generate`, payload, {
|
||||
headers,
|
||||
responseType: 'json',
|
||||
observe: 'response'
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(complete$),
|
||||
finalize(() => {
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
loading: false
|
||||
}
|
||||
});
|
||||
|
||||
this.measureResponseTime(startTime);
|
||||
|
||||
// If we have accumulated a response, create a message and add it to the conversation
|
||||
if (accumulatedResponse) {
|
||||
|
||||
accumulatedResponse = accumulatedResponse.replace(/\. /g, ". <br/><br/>");
|
||||
const responseMessage = new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: accumulatedResponse,
|
||||
name: 'Ollama'
|
||||
});
|
||||
|
||||
// Dispatch the message to the active conversation
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: {
|
||||
message: responseMessage
|
||||
}
|
||||
});
|
||||
|
||||
// Get the current active conversation from the store
|
||||
this.store.select(state => state.activeConversation).subscribe(activeConv => {
|
||||
if (activeConv && Object.keys(activeConv).length > 0) {
|
||||
// Update the conversation in the conversations list
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_SINGLE_CONVERSATION,
|
||||
payload: {
|
||||
conversation: activeConv
|
||||
}
|
||||
});
|
||||
}
|
||||
}).unsubscribe(); // Unsubscribe immediately after getting the value
|
||||
}
|
||||
|
||||
// Complete the result subject
|
||||
result.complete();
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
console.log('Ollama response:', response);
|
||||
if (response.body) {
|
||||
// Process the non-streaming response
|
||||
// Ollama returns a single JSON object with a 'response' field
|
||||
try {
|
||||
const responseData = response.body as any;
|
||||
if (responseData.response) {
|
||||
// Set the accumulated response text
|
||||
accumulatedResponse = responseData.response;
|
||||
console.log('Ollama response:', accumulatedResponse);
|
||||
|
||||
// Complete the stream
|
||||
complete$.next();
|
||||
complete$.complete();
|
||||
} else {
|
||||
console.error('No response field found in Ollama response:', responseData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error processing Ollama response:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error querying Ollama:', error);
|
||||
|
||||
// Set loading state to false
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
loading: false,
|
||||
displayErrorResponse: true
|
||||
}
|
||||
});
|
||||
|
||||
// Send error message to chat
|
||||
const errorMessage = new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: "Error connecting to Ollama backend. Please check if the service is running.",
|
||||
name: 'System'
|
||||
});
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: {
|
||||
message: errorMessage
|
||||
}
|
||||
});
|
||||
|
||||
// Get the current active conversation from the store
|
||||
this.store.select(state => state.activeConversation).subscribe(activeConv => {
|
||||
if (activeConv && Object.keys(activeConv).length > 0) {
|
||||
// Update the conversation in the conversations list
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_SINGLE_CONVERSATION,
|
||||
payload: {
|
||||
conversation: activeConv
|
||||
}
|
||||
});
|
||||
}
|
||||
}).unsubscribe(); // Unsubscribe immediately after getting the value
|
||||
|
||||
// Reset error state after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displayErrorResponse: false
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
// Complete the subjects
|
||||
complete$.next();
|
||||
complete$.complete();
|
||||
result.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
return result.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the average response time in the store
|
||||
* @param responseTime The actual response time in milliseconds
|
||||
*/
|
||||
private updateAverageResponseTime(responseTime: number): void {
|
||||
// Get the current average response time from the store
|
||||
this.store.select(state => state.app.averageResponseTime).subscribe(currentAverage => {
|
||||
// Calculate the new average (average of current average and new response time)
|
||||
const newAverage = Math.round((currentAverage + responseTime) / 2);
|
||||
|
||||
// Update the store with the new average
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
averageResponseTime: newAverage
|
||||
}
|
||||
});
|
||||
}).unsubscribe(); // Unsubscribe immediately after getting the value
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the response time of an API call and updates the average
|
||||
* @param startTime The timestamp when the API call started
|
||||
*/
|
||||
private measureResponseTime(startTime: number): void {
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
this.updateAverageResponseTime(responseTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler for HTTP requests
|
||||
* @param error The HTTP error response
|
||||
* @returns An observable that errors with a user-friendly message
|
||||
*/
|
||||
private handleError(error: HttpErrorResponse): Observable<never> {
|
||||
let errorMessage = 'An unknown error occurred';
|
||||
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// Client-side error
|
||||
errorMessage = `Error: ${error.error.message}`;
|
||||
} else {
|
||||
// Server-side error
|
||||
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
|
||||
}
|
||||
|
||||
console.error(errorMessage);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
}
|
12
old-sae-airwatch/src/app/app.config.server.ts
Normal file
12
old-sae-airwatch/src/app/app.config.server.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
||||
import { provideServerRendering, withRoutes } from '@angular/ssr';
|
||||
import { appConfig } from './app.config';
|
||||
import { serverRoutes } from './app.routes.server';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(withRoutes(serverRoutes))
|
||||
]
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
20
old-sae-airwatch/src/app/app.config.ts
Normal file
20
old-sae-airwatch/src/app/app.config.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
import {provideHttpClient, withFetch} from '@angular/common/http';
|
||||
import { provideStore } from '@ngrx/store';
|
||||
import { reducers, metaReducers } from './reducers';
|
||||
import { provideStoreDevtools } from '@ngrx/store-devtools';
|
||||
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideHttpClient(withFetch()),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideStore(reducers, { metaReducers }),
|
||||
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() })
|
||||
]
|
||||
};
|
32
old-sae-airwatch/src/app/app.html
Normal file
32
old-sae-airwatch/src/app/app.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
|
||||
<h1>Hello, {{ title() }}</h1>
|
||||
<p>Congratulations! Your app is running. 🎉</p>
|
||||
</div>
|
||||
<div aria-label="Divider" class="divider" role="separator"></div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
|
||||
<router-outlet/>
|
8
old-sae-airwatch/src/app/app.routes.server.ts
Normal file
8
old-sae-airwatch/src/app/app.routes.server.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Prerender
|
||||
}
|
||||
];
|
41
old-sae-airwatch/src/app/app.routes.ts
Normal file
41
old-sae-airwatch/src/app/app.routes.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {Routes} from '@angular/router';
|
||||
import {ColorsPage} from './pages/colors-page/colors-page';
|
||||
import {AirwatchDemo} from './pages/airwatch-demo/airwatch-demo';
|
||||
import {Csc} from './pages/csc/csc';
|
||||
import {LayoutDemo} from './pages/layout-demo/layout-demo';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutDemo,
|
||||
title: 'Démo Layout'
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
component: LayoutDemo,
|
||||
title: 'Démo Layout'
|
||||
},
|
||||
{
|
||||
path: 'grid',
|
||||
loadComponent: () => import('./pages/grid-demo/grid-demo').then(m => m.GridDemo),
|
||||
title: 'Démo AG Grid'
|
||||
},
|
||||
|
||||
{
|
||||
path: 'colors',
|
||||
component: ColorsPage,
|
||||
},
|
||||
{
|
||||
path: 'airwatch',
|
||||
component: AirwatchDemo,
|
||||
},
|
||||
{
|
||||
path: 'csc',
|
||||
component: Csc,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
redirectTo: 'home',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
0
old-sae-airwatch/src/app/app.scss
Normal file
0
old-sae-airwatch/src/app/app.scss
Normal file
23
old-sae-airwatch/src/app/app.spec.ts
Normal file
23
old-sae-airwatch/src/app/app.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, my-app');
|
||||
});
|
||||
});
|
22
old-sae-airwatch/src/app/app.ts
Normal file
22
old-sae-airwatch/src/app/app.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {Component, signal} from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
template: `
|
||||
<div id="app_main_page">
|
||||
<main>
|
||||
<router-outlet/>
|
||||
</main>
|
||||
<footer class="main-footer">
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
`,
|
||||
styles: [`
|
||||
`],
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('Exemple de Boutons');
|
||||
}
|
16
old-sae-airwatch/src/app/auth-service.spec.ts
Normal file
16
old-sae-airwatch/src/app/auth-service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth-service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
35
old-sae-airwatch/src/app/auth-service.ts
Normal file
35
old-sae-airwatch/src/app/auth-service.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {Inject} from '@angular/core';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
// check if we have auth at load, or redirect to the given app url
|
||||
app_check_url: string = "https://example.com/check";
|
||||
app_auth_url: string = "https://example.com/auth";
|
||||
auth_data: any = null;
|
||||
private window: WindowProxy & typeof globalThis | null;
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
||||
this.window = this.document.defaultView;
|
||||
this.checkAuthData()
|
||||
}
|
||||
|
||||
checkAuthData() {
|
||||
if (this.auth_data) {
|
||||
|
||||
fetch(this.app_auth_url).then(resp => {
|
||||
if (resp.status !== 200) {
|
||||
|
||||
this.redirectToAuth()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
redirectToAuth() {
|
||||
this.window?.open(this.app_auth_url)
|
||||
}
|
||||
}
|
297
old-sae-airwatch/src/app/chatbot/chatbot.html
Normal file
297
old-sae-airwatch/src/app/chatbot/chatbot.html
Normal file
|
@ -0,0 +1,297 @@
|
|||
<div [ngClass]="{
|
||||
'is-expanded-left': appState.displayConversationListPanelLarge,
|
||||
'is-small-left': ! appState.displayConversationListPanelLarge,
|
||||
'is-expanded-right': appState.displaySourcesPanelLarge,
|
||||
'is-small-right': ! appState.displaySourcesPanelLarge,
|
||||
}" class="visible-imbrication chatbot-land"
|
||||
id="layout_demo"
|
||||
>
|
||||
|
||||
<div class="demo-airwatch">
|
||||
<div class="layout-split">
|
||||
<nav aria-label="main navigation" class="navbar" role="navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/#">
|
||||
<!-- <i class="ri-robot-2-fill"></i>-->
|
||||
<img alt="safran logo" class="logo" src="/safran_logo_large.svg">
|
||||
</a>
|
||||
|
||||
<a aria-expanded="false" aria-label="menu" class="navbar-burger" data-target="navbarBasicExample"
|
||||
role="button">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu" id="airwatchNavigation">
|
||||
<div class="navbar-start">
|
||||
<a (click)="toggleSidePanelConversationsList()" class="navbar-item aside-toggle-button">
|
||||
<span class="label">
|
||||
airwatch
|
||||
</span>
|
||||
@if (appState.displayConversationListPanelLarge) {
|
||||
<i class="ri-sidebar-unfold-line"></i>
|
||||
|
||||
} @else {
|
||||
|
||||
<i class="ri-sidebar-fold-line"></i>
|
||||
}
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<a (click)="toggleSourcesPanel()" class="navbar-item aside-toggle-button">
|
||||
@if (appState.displaySourcesPanelLarge) {
|
||||
<i class="ri-layout-right-2-line"></i>
|
||||
} @else {
|
||||
<i class="ri-layout-right-line"></i>
|
||||
}
|
||||
</a>
|
||||
|
||||
<a class="navbar-item">
|
||||
Documentation
|
||||
</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
<i class="ri-notification-2-line"></i>
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
About
|
||||
</a>
|
||||
<a class="navbar-item is-selected">
|
||||
Jobs
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Contact
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="chatbot-container-box">
|
||||
<main class="columns ">
|
||||
<div
|
||||
[ngClass]="{'is-expanded': appState.displayConversationListPanelLarge, 'is-small': ! appState.displayConversationListPanelLarge}"
|
||||
class="aside column-conversation ">
|
||||
|
||||
<div (click)="newChat()" class="new-button">
|
||||
<i class="ri-chat-ai-line"></i>
|
||||
<span class="label">
|
||||
New chat
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@if (appState.displayConversationListPanelLarge) {
|
||||
|
||||
<div class="search-button is-expanded is-clickable">
|
||||
<i class="ri-search-line"></i>
|
||||
|
||||
<input type="text" class="no-borders" placeholder="Search a chat" value="blah">
|
||||
<i class="funnel ri-filter-3-line"></i>
|
||||
|
||||
</div>
|
||||
} @else {
|
||||
|
||||
<div class="search-button is-clickable">
|
||||
<i class="ri-search-line"></i>
|
||||
</div>
|
||||
|
||||
}
|
||||
@if (appState.displayConversationListPanelLarge) {
|
||||
<div class="conversation-container">
|
||||
@for (conversation of conversationsList; track conversation) {
|
||||
<div class="conversation-item"
|
||||
[ngClass]="{'is-active' : conversation.name == activeConversation?.name }"
|
||||
(click)="updateActiveConversation(conversation)">
|
||||
|
||||
<!-- @if (conversation.pinned) {-->
|
||||
|
||||
<!-- }-->
|
||||
<!-- <i class="ri-arrow-right-s-line"></i>-->
|
||||
<div class="container-text">
|
||||
<span class="pinned-icon">
|
||||
<i class="ri-pushpin-line"></i>
|
||||
<!-- pinned-->
|
||||
</span>
|
||||
<span class="name">
|
||||
{{ conversation.name || 'New Conversation' }}
|
||||
</span>
|
||||
|
||||
<span class="date">
|
||||
{{ conversation.lastMessageDate | date: 'dd/MM/yyyy' }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{{ conversation.description || 'No description' }}
|
||||
</span>
|
||||
<input type="text" [(ngModel)]="conversation.name"
|
||||
[ngClass]="{'is-expanded': ! conversation.visibleInput}">
|
||||
<!-- <i class="ri-pencil-line" (click)="toggleVisibleEditInput(conversation)"></i>-->
|
||||
</div>
|
||||
<div class="active-peak">
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 7L0 13.9282V0.0717969L12 7Z" fill="#3B87CC" fill-opacity="0.1"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<app-version-infos [versionInfo]="appVersion"></app-version-infos>
|
||||
</div>
|
||||
<!-- <div class="admin-actions debug">-->
|
||||
<!-- <app-theme-selector></app-theme-selector>-->
|
||||
<!-- <app-language-selector></app-language-selector>-->
|
||||
<!-- <br>-->
|
||||
<!-- <app-main-button (click)="changeTheme()" icon="chat-new-line" label="thème"></app-main-button>-->
|
||||
<!-- <app-main-button (click)="newChat()" icon="chat-new-line" label="nouveau"></app-main-button>-->
|
||||
<!-- <app-main-button (click)="addMockMessage('user')" icon="user-line"-->
|
||||
<!-- label="ajout message utilisateur"></app-main-button>-->
|
||||
<!-- <app-main-button (click)="addMockMessage('llm')" icon="robot-2-line"-->
|
||||
<!-- label="ajout message llm"></app-main-button>-->
|
||||
|
||||
<!-- <app-tuto-tour></app-tuto-tour>-->
|
||||
<!-- </div>-->
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="chat chat-column ">
|
||||
<div id="newChatBar">
|
||||
<div class="new-chat-title">
|
||||
|
||||
<div class="title">
|
||||
|
||||
{{ translate('newChat') }}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="chips">
|
||||
@if (displayChipsTitleConversations) {
|
||||
<div class="chips-container is-grey">
|
||||
<i class="ri-pushpin-line"></i>
|
||||
<span class="label">
|
||||
Chips land
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="action">
|
||||
@if (displayExportConversation) {
|
||||
<div class="export-chat-button is-clickable">
|
||||
<i class="ri-download-2-line"></i>
|
||||
<span class="label">
|
||||
Export chat
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- tchatt-->
|
||||
@if (!activeConversation?.messages?.length) {
|
||||
<app-new-input></app-new-input>
|
||||
} @else {
|
||||
<div #conversationContainer class="main-conversation-container">
|
||||
<!-- main-conversation-container-->
|
||||
<!-- <div class="top-bar">-->
|
||||
<!-- top bar-->
|
||||
<!-- <div class="conversation-title">-->
|
||||
<!-- {{ activeConversation?.name || 'New Conversation' }}-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="conversation-typing">-->
|
||||
<!-- typing-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="conversation-messages-container">
|
||||
|
||||
@if (activeConversation && activeConversation.messages) {
|
||||
@for (message of activeConversation.messages; track message) {
|
||||
<app-message-box [message]="message" [kind]="message.kind"
|
||||
[content]="message.content"></app-message-box>
|
||||
}
|
||||
}
|
||||
|
||||
@if (appState.loading) {
|
||||
<app-loading-notification></app-loading-notification>
|
||||
}
|
||||
<!-- <app-time-separator></app-time-separator>-->
|
||||
|
||||
|
||||
<!-- <sae-alert-box _alertKind="error" message="">System error - I couldn’t process your request.-->
|
||||
<!-- Please-->
|
||||
<!-- try-->
|
||||
<!-- again in-->
|
||||
<!-- few moments-->
|
||||
<!-- </sae-alert-box>-->
|
||||
</div>
|
||||
<div class="conversation-bottom">
|
||||
<app-prompt-input></app-prompt-input>
|
||||
<app-tools-options [alignLeft]="true" [hideDisabledButtons]="true"></app-tools-options>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
<div class="bottom-warning-container">
|
||||
<app-warning-bugs></app-warning-bugs>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div [ngClass]="{'expanded': appState.displaySourcesPanelLarge}" class="column panel-more">
|
||||
<div class="has-text-right">
|
||||
|
||||
<div class="export-chat-button is-clickable">
|
||||
<i class="ri-download-2-line"></i>
|
||||
<span class="label">
|
||||
Export all sources
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-more-inside">
|
||||
<div class="main-title">Knowledge Base documents :</div>
|
||||
<div class="sources-list">
|
||||
@if (activeConversation && activeConversation.sources && activeConversation.sources.length > 0) {
|
||||
@for (source of activeConversation.sources; track source) {
|
||||
<app-source-block [source]="source"></app-source-block>
|
||||
}
|
||||
} @else {
|
||||
<div class="no-sources">
|
||||
Aucune source disponible
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-feedback-button></app-feedback-button>
|
||||
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
93
old-sae-airwatch/src/app/chatbot/chatbot.scss
Normal file
93
old-sae-airwatch/src/app/chatbot/chatbot.scss
Normal file
|
@ -0,0 +1,93 @@
|
|||
.panel-more-inside {
|
||||
border-radius: 10px;
|
||||
background: #F5F5F5;
|
||||
padding: 20px 16px;
|
||||
min-height: 100vh;
|
||||
margin-top: 14px;
|
||||
|
||||
.main-title {
|
||||
color: #1E1F22;
|
||||
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.sources-list {
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
.source {
|
||||
border-radius: 4px;
|
||||
background: #ECF3FA;
|
||||
}
|
||||
}
|
||||
|
||||
.chips-container {
|
||||
display: inline-flex;
|
||||
padding: 4px 10px 6px 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 6px;
|
||||
background: #979797;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
|
||||
i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
font-family: Barlow;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 8px; /* 66.667% */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.export-chat-button {
|
||||
position: relative;
|
||||
right: 0;
|
||||
|
||||
display: inline-flex;
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
|
||||
border-radius: 8px;
|
||||
border: 1px solid #005AA2;
|
||||
color: #005AA2;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 100px; /* 500% */
|
||||
|
||||
}
|
||||
|
||||
.aside-toggle-button {
|
||||
color: white;
|
||||
color: #FFF;
|
||||
font-family: Barlow;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
text-transform: uppercase;
|
||||
|
||||
.label {
|
||||
margin-right: 22px;
|
||||
}
|
||||
}
|
23
old-sae-airwatch/src/app/chatbot/chatbot.spec.ts
Normal file
23
old-sae-airwatch/src/app/chatbot/chatbot.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Chatbot } from './chatbot';
|
||||
|
||||
describe('Chatbot', () => {
|
||||
let component: Chatbot;
|
||||
let fixture: ComponentFixture<Chatbot>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Chatbot]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Chatbot);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
323
old-sae-airwatch/src/app/chatbot/chatbot.ts
Normal file
323
old-sae-airwatch/src/app/chatbot/chatbot.ts
Normal file
|
@ -0,0 +1,323 @@
|
|||
import {AfterViewChecked, Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {PromptInput} from './prompt-input/prompt-input';
|
||||
import {MessageBox} from './message-box/message-box';
|
||||
import {NewInput} from './new-input/new-input';
|
||||
import {FeedbackButton} from './feedback-button/feedback-button';
|
||||
import {WarningBugs} from './warning-bugs/warning-bugs';
|
||||
import {VersionInfos} from './version-infos/version-infos';
|
||||
import {LoadingNotification} from './loading-notification/loading-notification';
|
||||
|
||||
import {SourceBlock} from './source-block/source-block';
|
||||
import {ChatbotConversation, ChatbotSource, ConversationsService} from '../services/conversations.service';
|
||||
|
||||
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ActionTypes, StateInterface} from '../reducers';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {ApiService} from '../api-service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ChatbotMessage, ChatbotMessageKind} from '../services/chatbot.message.type';
|
||||
import {ToolsOptions} from './tools-options/tools-options';
|
||||
import {TranslationService} from '../services/translation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chatbot',
|
||||
imports: [
|
||||
PromptInput,
|
||||
MessageBox,
|
||||
NewInput,
|
||||
FeedbackButton,
|
||||
WarningBugs,
|
||||
VersionInfos,
|
||||
LoadingNotification,
|
||||
SourceBlock,
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
ToolsOptions,
|
||||
],
|
||||
templateUrl: './chatbot.html',
|
||||
styleUrl: './chatbot.scss'
|
||||
})
|
||||
export class Chatbot implements AfterViewChecked {
|
||||
public conversationsList: Array<ChatbotConversation> = [];
|
||||
public activeConversation: ChatbotConversation | null = null;
|
||||
public appVersion: string = '';
|
||||
public appState: any = '';
|
||||
conversationName: any;
|
||||
// fonctions d'exports
|
||||
visibleInput: boolean = false;
|
||||
// demo tour lors du premier chargement
|
||||
displayChipsTitleConversations = true;
|
||||
displayExportConversation: boolean = true;
|
||||
@ViewChild('conversationContainer') private conversationContainer: ElementRef | null = null;
|
||||
private shouldScrollToBottom: boolean = false;
|
||||
|
||||
constructor(
|
||||
private conversations: ConversationsService,
|
||||
private apiService: ApiService,
|
||||
private store: Store<StateInterface>,
|
||||
private translationService: TranslationService
|
||||
) {
|
||||
// Initialize conversations in the store
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_CONVERSATIONS_LIST,
|
||||
payload: conversations.chatbotConversations
|
||||
});
|
||||
|
||||
// Subscribe to conversations list from store
|
||||
this.store.select(state => state.conversationsList).subscribe(convList => {
|
||||
this.conversationsList = convList;
|
||||
if (!this.conversationsList.length) {
|
||||
this.newChat();
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to active conversation from store
|
||||
this.store.select(state => state.activeConversation).subscribe(activeConv => {
|
||||
if (activeConv && Object.keys(activeConv).length > 0) {
|
||||
const prevMessagesLength = this.activeConversation?.messages?.length || 0;
|
||||
this.activeConversation = activeConv as ChatbotConversation;
|
||||
const newMessagesLength = this.activeConversation?.messages?.length || 0;
|
||||
|
||||
console.log('prevMessagesLength', prevMessagesLength, newMessagesLength);
|
||||
// Set flag to scroll to bottom when active conversation changes or when messages are added
|
||||
if (prevMessagesLength !== newMessagesLength) {
|
||||
this.shouldScrollToBottom = true;
|
||||
}
|
||||
} else if (this.conversationsList.length > 0) {
|
||||
this.updateActiveConversation(this.conversationsList[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Get app version from store
|
||||
this.store.select(state => state.app.version).subscribe(version => {
|
||||
this.appVersion = version;
|
||||
});
|
||||
|
||||
// Get app state from store
|
||||
this.store.select(state => state.app).subscribe(app => {
|
||||
this.appState = app;
|
||||
});
|
||||
|
||||
// à désactiver en mode production
|
||||
this.demoConversation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called after the view has been checked
|
||||
* Used to scroll to the bottom of the conversation container when needed
|
||||
*/
|
||||
ngAfterViewChecked(): void {
|
||||
if (this.shouldScrollToBottom && this.conversationContainer) {
|
||||
this.scrollToBottom();
|
||||
this.shouldScrollToBottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a key using the translation service
|
||||
* @param key The key to translate
|
||||
* @param defaultValue Optional default value if translation is not found
|
||||
* @returns The translated string
|
||||
*/
|
||||
translate(key: string, defaultValue?: string): string {
|
||||
return this.translationService.translate(key, defaultValue);
|
||||
}
|
||||
|
||||
demoConversation() {
|
||||
|
||||
|
||||
// Add example sources if in demo mode
|
||||
if (this.appState.demoMode && this.activeConversation) {
|
||||
console.log('mode démo: on ajoute des demoConversation');
|
||||
// Create a proper instance of ChatbotConversation from the active conversation
|
||||
const updatedConversation: ChatbotConversation = ChatbotConversation.fromObject(this.activeConversation);
|
||||
|
||||
this.addMockMessage("user");
|
||||
// Add three example sources
|
||||
|
||||
|
||||
let source1 = new ChatbotSource();
|
||||
source1.title = "abc-1258.pdf";
|
||||
source1.url = "https://example.com/source1";
|
||||
source1.description = "Admittedly Air Traffic Control in smaller airports are being effected by ...";
|
||||
|
||||
let source2 = new ChatbotSource();
|
||||
source2.title = "abc-45689.pdf";
|
||||
source2.url = "https://example.com/source1";
|
||||
source2.description = "DE GAGNE DATE: JANUARY 18, 2008 VEN #: V6-CAW-M2700-10 APPROVED BY: MARTIN SWAN DATE: FEBRUARY 5, 2008 -TITLE- DHC-6 ELEVATOR CONTROL CABLE WEAR SURVEY RESULTS ISSUE: 2 PAGE 2 respondents, representing 16 aircraft, operate outside the tropics and have a cycle/hour ratio of 1.6 to 2.8. Most respondents have reported that carbon steel elevator control cables are more wear resistant than stainless steel cables. Two operators in the tropics, representing 39 aircraft, use.
\n" +
|
||||
"I remember having reconunended while dealing with the Guwahati crash in regard to a Vayudoot aircraft that the National Airports Authority should ensure that only trained and";
|
||||
|
||||
updatedConversation.sources = [source1, source2];
|
||||
|
||||
const conversation2 = new ChatbotConversation();
|
||||
conversation2.name = "Liste de courses";
|
||||
conversation2.addMessage(new ChatbotMessage({
|
||||
kind: 'user',
|
||||
user: {},
|
||||
content: "Comment puis-je utiliser cette application?",
|
||||
name: "User"
|
||||
}));
|
||||
conversation2.addMessage(new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: "Cette application vous permet de discuter avec un assistant IA. Vous pouvez poser des questions et obtenir des réponses.",
|
||||
name: "Assistant"
|
||||
}));
|
||||
|
||||
|
||||
// Update the active conversation in the store
|
||||
this.updateActiveConversation(updatedConversation);
|
||||
|
||||
// Update the conversations list in the store
|
||||
const updatedList = this.conversationsList.map(conv =>
|
||||
conv === this.activeConversation ? updatedConversation : conv
|
||||
);
|
||||
|
||||
updatedList.push(conversation2)
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_CONVERSATIONS_LIST,
|
||||
payload: updatedList
|
||||
});
|
||||
|
||||
// Set the first conversation as active
|
||||
this.updateActiveConversation(updatedConversation);
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveConversation(conversation: ChatbotConversation) {
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_ACTIVE_CONVERSATION,
|
||||
payload: conversation
|
||||
});
|
||||
}
|
||||
|
||||
addMockMessage(kind: ChatbotMessageKind, content?: string) {
|
||||
if (this.activeConversation) {
|
||||
// Create a proper instance of ChatbotConversation from the active conversation
|
||||
const updatedConversation = ChatbotConversation.fromObject(this.activeConversation);
|
||||
|
||||
// Add the message to the copy
|
||||
updatedConversation.addMessage(new ChatbotMessage({
|
||||
kind: kind,
|
||||
user: {},
|
||||
content: content ? content : "blah",
|
||||
name: "Mock Message"
|
||||
}));
|
||||
|
||||
// Update the active conversation in the store
|
||||
this.updateActiveConversation(updatedConversation);
|
||||
|
||||
// Update the conversations list in the store
|
||||
const updatedList = this.conversationsList.map(conv =>
|
||||
conv === this.activeConversation ? updatedConversation : conv
|
||||
);
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_CONVERSATIONS_LIST,
|
||||
payload: updatedList
|
||||
});
|
||||
|
||||
// Set flag to scroll to bottom after adding a message
|
||||
this.shouldScrollToBottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
newChat() {
|
||||
const newConv = new ChatbotConversation();
|
||||
newConv.name = "Other conversation " + (this.conversationsList.length + 1);
|
||||
|
||||
// Update the conversations list in the store by adding the new conversation to the existing list
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_CONVERSATIONS_LIST,
|
||||
payload: [...this.conversationsList, newConv]
|
||||
});
|
||||
|
||||
// Set the new conversation as active
|
||||
this.updateActiveConversation(newConv);
|
||||
}
|
||||
|
||||
toggleSidePanelConversationsList() {
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displayConversationListPanelLarge: !this.appState.displayConversationListPanelLarge
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSourcesPanel() {
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displaySourcesPanelLarge: !this.appState.displaySourcesPanelLarge
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
launchDemoTour() {
|
||||
|
||||
}
|
||||
|
||||
endDemoTour() {
|
||||
|
||||
}
|
||||
|
||||
exportMessage() {
|
||||
|
||||
}
|
||||
|
||||
exportConversation() {
|
||||
|
||||
}
|
||||
|
||||
copyToClipboard(text: string) {
|
||||
|
||||
}
|
||||
|
||||
toggleVisibleEditInput(conversation: ChatbotConversation) {
|
||||
// Create a copy of the conversation
|
||||
const updatedConversation = ChatbotConversation.fromObject(conversation);
|
||||
|
||||
// Toggle the visibleInput property
|
||||
updatedConversation.visibleInput = !conversation.visibleInput;
|
||||
|
||||
// Update the active conversation if this is the active one
|
||||
if (this.activeConversation === conversation) {
|
||||
this.updateActiveConversation(updatedConversation);
|
||||
}
|
||||
|
||||
// Update the single conversation in the store without replacing the entire list
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_SINGLE_CONVERSATION,
|
||||
payload: {
|
||||
conversation: updatedConversation
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changeTheme() {
|
||||
// Dispatch the action to switch to the next theme
|
||||
this.store.dispatch({type: ActionTypes.SWITCH_TO_NEXT_THEME});
|
||||
}
|
||||
|
||||
changeLang() {
|
||||
// Dispatch the action to switch to the next language
|
||||
this.store.dispatch({type: ActionTypes.SWITCH_TO_NEXT_LANGUAGE});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the bottom of the conversation container
|
||||
*/
|
||||
private scrollToBottom(): void {
|
||||
try {
|
||||
const element = this.conversationContainer?.nativeElement;
|
||||
if (element) {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error scrolling to bottom:', err);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<div class="feedback-button" (click)="toggleModal()">
|
||||
<span class="text">
|
||||
Feedback
|
||||
</span>
|
||||
<i class="ri-message-2-line"></i>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Modal -->
|
||||
@if (isModalOpen) {
|
||||
<div class="feedback-modal-overlay">
|
||||
<div class="feedback-modal">
|
||||
<div class="modal-header">
|
||||
<h3>Send Feedback</h3>
|
||||
<button class="close-button" (click)="toggleModal()">
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p class="modal-description">
|
||||
Help us improve by sharing your feedback, suggestions, or reporting issues.
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
[(ngModel)]="feedbackText"
|
||||
placeholder="Enter your feedback here..."
|
||||
[disabled]="isSubmitting"
|
||||
rows="5"
|
||||
></textarea>
|
||||
|
||||
@if (submitSuccess) {
|
||||
<div class="success-message">
|
||||
<i class="ri-check-line"></i> Thank you for your feedback!
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (submitError) {
|
||||
<div class="error-message">
|
||||
<i class="ri-error-warning-line"></i> Failed to submit feedback. Please try again.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="cancel-button"
|
||||
(click)="toggleModal()"
|
||||
[disabled]="isSubmitting"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="submit-button"
|
||||
(click)="submitFeedback()"
|
||||
[disabled]="isSubmitting || !feedbackText.trim()"
|
||||
>
|
||||
@if (isSubmitting) {
|
||||
<i class="ri-loader-4-line spinning"></i> Sending...
|
||||
} @else {
|
||||
Send Feedback
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
.feedback-button {
|
||||
background: #ecf3fa;
|
||||
color: #083b7d;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
transform: rotate(270deg);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
margin-right: -35px;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 240px;
|
||||
z-index: 100;
|
||||
|
||||
&:hover {
|
||||
background: #d9e8f6;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal styles
|
||||
.feedback-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.feedback-modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #083b7d;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.modal-description {
|
||||
margin-bottom: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #083b7d;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.success-message, .error-message {
|
||||
margin-top: 16px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: #e6f7e6;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fdecea;
|
||||
color: #d32f2f;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background: none;
|
||||
border: 1px solid #ddd;
|
||||
color: #555;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #083b7d;
|
||||
color: white;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #062c5e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spinner animation
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FeedbackButton } from './feedback-button';
|
||||
|
||||
describe('FeedbackButton', () => {
|
||||
let component: FeedbackButton;
|
||||
let fixture: ComponentFixture<FeedbackButton>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FeedbackButton]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FeedbackButton);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ApiService } from '../../api-service';
|
||||
import { ActionTypes, StateInterface } from '../../reducers';
|
||||
|
||||
@Component({
|
||||
selector: 'app-feedback-button',
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './feedback-button.html',
|
||||
styleUrl: './feedback-button.scss'
|
||||
})
|
||||
export class FeedbackButton {
|
||||
isModalOpen: boolean = false;
|
||||
feedbackText: string = '';
|
||||
isSubmitting: boolean = false;
|
||||
submitSuccess: boolean = false;
|
||||
submitError: boolean = false;
|
||||
|
||||
constructor(
|
||||
private store: Store<StateInterface>,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
toggleModal() {
|
||||
this.isModalOpen = !this.isModalOpen;
|
||||
|
||||
// Reset state when opening modal
|
||||
if (this.isModalOpen) {
|
||||
this.feedbackText = '';
|
||||
this.submitSuccess = false;
|
||||
this.submitError = false;
|
||||
}
|
||||
|
||||
// Update app state to show/hide feedback panel
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displayFeedBackPanel: this.isModalOpen,
|
||||
feedBackInput: this.feedbackText
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submitFeedback() {
|
||||
if (!this.feedbackText.trim()) {
|
||||
return; // Don't submit empty feedback
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
this.submitSuccess = false;
|
||||
this.submitError = false;
|
||||
|
||||
try {
|
||||
// Update Redux state with feedback text
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
feedBackInput: this.feedbackText
|
||||
}
|
||||
});
|
||||
|
||||
// Dispatch action to send feedback
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.SEND_USER_FEEDBACK,
|
||||
payload: {
|
||||
feedback: this.feedbackText
|
||||
}
|
||||
});
|
||||
|
||||
// Call API service directly
|
||||
await this.apiService.sendUserFeedback(this.feedbackText);
|
||||
|
||||
this.submitSuccess = true;
|
||||
|
||||
// Close modal after a short delay
|
||||
setTimeout(() => {
|
||||
this.toggleModal();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
this.submitError = true;
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<p>feedback-message works!</p>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FeedbackMessage } from './feedback-message';
|
||||
|
||||
describe('FeedbackMessage', () => {
|
||||
let component: FeedbackMessage;
|
||||
let fixture: ComponentFixture<FeedbackMessage>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FeedbackMessage]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FeedbackMessage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-feedback-message',
|
||||
imports: [],
|
||||
templateUrl: './feedback-message.html',
|
||||
styleUrl: './feedback-message.scss'
|
||||
})
|
||||
export class FeedbackMessage {
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<div class="language-selector">
|
||||
<button
|
||||
(click)="switchToNextLanguage()"
|
||||
class="language-selector__button"
|
||||
title="Switch to next language">
|
||||
<span class="language-selector__current-lang">{{ getLanguageDisplayName(currentLang) }}</span>
|
||||
<span class="language-selector__icon">🌐</span>
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,56 @@
|
|||
.language-selector {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
&__current-lang {
|
||||
margin-right: 8px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
// Language-specific styles that will be applied when the body has the corresponding class
|
||||
:host-context(body.app-theme-light) {
|
||||
.language-selector__button {
|
||||
background-color: #f8f8f8;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.app-theme-dark) {
|
||||
.language-selector__button {
|
||||
background-color: #333;
|
||||
color: #f8f8f8;
|
||||
border-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.app-theme-funky) {
|
||||
.language-selector__button {
|
||||
background-color: #ff00ff;
|
||||
color: #00ffff;
|
||||
border-color: #ffff00;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ActionTypes, StateInterface } from '../../reducers';
|
||||
import { TranslationService } from '../../services/translation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-language-selector',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './language-selector.html',
|
||||
styleUrl: './language-selector.scss'
|
||||
})
|
||||
export class LanguageSelector implements OnInit, OnDestroy {
|
||||
currentLang: string = '';
|
||||
langsList: string[] = [];
|
||||
private storeSubscription: Subscription | null = null;
|
||||
|
||||
constructor(
|
||||
private store: Store<StateInterface>,
|
||||
private translationService: TranslationService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to the store to get the current language and languages list
|
||||
this.storeSubscription = this.store.select(state => state.app)
|
||||
.subscribe(app => {
|
||||
this.currentLang = app.lang;
|
||||
this.langsList = app.langsList;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe to prevent memory leaks
|
||||
if (this.storeSubscription) {
|
||||
this.storeSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to switch to the next language
|
||||
switchToNextLanguage(): void {
|
||||
this.store.dispatch({ type: ActionTypes.SWITCH_TO_NEXT_LANGUAGE });
|
||||
}
|
||||
|
||||
// Helper method to display a friendly language name
|
||||
getLanguageDisplayName(langCode: string): string {
|
||||
switch (langCode) {
|
||||
case 'fr_FR':
|
||||
return 'Français';
|
||||
case 'en_US':
|
||||
return 'English';
|
||||
default:
|
||||
return langCode;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<div class="loading-notification">
|
||||
|
||||
<div class="progress-container">
|
||||
<div [style.animation-duration.ms]="averageResponseTime" class="progress-bar"></div>
|
||||
</div>
|
||||
<!-- <span class="text">-->
|
||||
<!-- ça charge-->
|
||||
<!-- </span>-->
|
||||
|
||||
</div>
|
|
@ -0,0 +1,40 @@
|
|||
.loading-notification {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
|
||||
.text {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #083b7d; // Same blue color as in the feedback button
|
||||
transform: translateX(-100%);
|
||||
animation-name: progress;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoadingNotification } from './loading-notification';
|
||||
|
||||
describe('LoadingNotification', () => {
|
||||
let component: LoadingNotification;
|
||||
let fixture: ComponentFixture<LoadingNotification>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoadingNotification]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoadingNotification);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {StateInterface} from '../../reducers';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading-notification',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './loading-notification.html',
|
||||
styleUrl: './loading-notification.scss'
|
||||
})
|
||||
export class LoadingNotification {
|
||||
protected loading: boolean = false;
|
||||
protected averageResponseTime: number = 500; // Default value
|
||||
|
||||
constructor(private store: Store<StateInterface>) {
|
||||
// Subscribe to the app state to get the loading state
|
||||
this.store.select(state => state.app.loading).subscribe(loading => {
|
||||
this.loading = loading;
|
||||
});
|
||||
|
||||
// Subscribe to the app state to get the average response time
|
||||
this.store.select(state => state.app.averageResponseTime).subscribe(time => {
|
||||
this.averageResponseTime = time;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<div class="message {{kind}}" id="message_{{id}}">
|
||||
<div class="actions ">
|
||||
@if (kind === 'user') {
|
||||
<button (click)="editMessage()" class="button edit">
|
||||
<i class="ri-edit-box-line"></i>
|
||||
</button>
|
||||
} @else {
|
||||
|
||||
|
||||
<div class="">
|
||||
<div class="action-feedback">
|
||||
<app-feedback-button></app-feedback-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" has-text-right">
|
||||
<button (click)="generateResponse()" class="button generate-response">
|
||||
<i class="ri-refresh-line"></i>
|
||||
<span class="label">
|
||||
Generate Response
|
||||
</span>
|
||||
</button>
|
||||
<app-copy [textToCopy]="content"></app-copy>
|
||||
<button (click)="bookmark()" class="button bookmark">
|
||||
<i class="ri-bookmark-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="user-infos ">
|
||||
<div class="avatar">
|
||||
<!-- avatar-->
|
||||
|
||||
</div>
|
||||
<div class="user-more-infos">
|
||||
<span class="user-name ">
|
||||
<!-- user name-->
|
||||
@if (kind === 'user') {
|
||||
You
|
||||
} @else {
|
||||
Response
|
||||
}
|
||||
|
||||
</span>
|
||||
<span class="time-ago">
|
||||
<!-- time ago-->
|
||||
Il y a 5 min
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-content is-{{kind}}">
|
||||
@if (content) {
|
||||
<div [innerHTML]="sanitizedContent"></div>
|
||||
} @else {
|
||||
@if (kind === 'llm') {
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur, consectetur cum eaque iure optio recusandae vel.
|
||||
} @else {
|
||||
Delectus, est, molestiae! Asperiores at consequatur cupiditate dicta iure neque pariatur perspiciatis, quia ut?
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
.avatar {
|
||||
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
background-size: contain !important;
|
||||
|
||||
.user & {
|
||||
background-size: contain;
|
||||
background: yellow url('../../../../public/user.png');
|
||||
}
|
||||
|
||||
|
||||
.llm & {
|
||||
background: yellow url('../../../../public/chatbot.png');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user-more-infos {
|
||||
margin-top: -35px;
|
||||
margin-left: 39px;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MessageBox } from './message-box';
|
||||
|
||||
describe('MessageBox', () => {
|
||||
let component: MessageBox;
|
||||
let fixture: ComponentFixture<MessageBox>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MessageBox]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MessageBox);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
53
old-sae-airwatch/src/app/chatbot/message-box/message-box.ts
Normal file
53
old-sae-airwatch/src/app/chatbot/message-box/message-box.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
|
||||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
|
||||
import {Copy} from 'sae-lib/buttons/copy/copy';
|
||||
import {FeedbackButton} from '../feedback-button/feedback-button';
|
||||
import {ChatbotMessage} from '../../services/chatbot.message.type';
|
||||
|
||||
|
||||
type MessageKind = "user" | "llm";
|
||||
|
||||
@Component({
|
||||
selector: 'app-message-box',
|
||||
imports: [
|
||||
Copy,
|
||||
FeedbackButton
|
||||
],
|
||||
templateUrl: './message-box.html',
|
||||
styleUrl: './message-box.scss'
|
||||
})
|
||||
export class MessageBox implements OnChanges {
|
||||
|
||||
@Input() kind: MessageKind = <"user" | "llm">""
|
||||
@Input() conf: any = {}
|
||||
@Input() content: any = ""
|
||||
@Input() message: ChatbotMessage = {} as ChatbotMessage;
|
||||
id: string = "00122121221312";
|
||||
sanitizedContent: SafeHtml = "";
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['content']) {
|
||||
this.sanitizeContent();
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeContent(): void {
|
||||
this.sanitizedContent = this.sanitizer.bypassSecurityTrustHtml(this.content);
|
||||
}
|
||||
|
||||
bookmark() {
|
||||
console.log("TODO bookmark")
|
||||
}
|
||||
|
||||
|
||||
generateResponse() {
|
||||
console.log("TODO generateResponse")
|
||||
}
|
||||
|
||||
editMessage() {
|
||||
console.log("TODO editMessage")
|
||||
}
|
||||
}
|
18
old-sae-airwatch/src/app/chatbot/new-input/new-input.html
Normal file
18
old-sae-airwatch/src/app/chatbot/new-input/new-input.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="new-input">
|
||||
<!-- new input-->
|
||||
|
||||
<div class="welcome-text">
|
||||
<div class="welcome-icon">
|
||||
<!-- <i class="ri-robot-2-line"></i>-->
|
||||
<img alt="chatbot image" src="/chatbot.png">
|
||||
</div>
|
||||
How can we
|
||||
<span class="emphasis">
|
||||
assist
|
||||
</span>
|
||||
you today?
|
||||
</div>
|
||||
|
||||
<app-prompt-input></app-prompt-input>
|
||||
<app-tools-options></app-tools-options>
|
||||
</div>
|
23
old-sae-airwatch/src/app/chatbot/new-input/new-input.scss
Normal file
23
old-sae-airwatch/src/app/chatbot/new-input/new-input.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
.new-input {
|
||||
min-height: 80vh;
|
||||
padding: 0 200px;
|
||||
|
||||
.welcome-text {
|
||||
margin: 100px;
|
||||
font-size: 38px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -7%;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
i {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.emphasis {
|
||||
color: #083b7d;
|
||||
}
|
||||
}
|
23
old-sae-airwatch/src/app/chatbot/new-input/new-input.spec.ts
Normal file
23
old-sae-airwatch/src/app/chatbot/new-input/new-input.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewInput } from './new-input';
|
||||
|
||||
describe('NewInput', () => {
|
||||
let component: NewInput;
|
||||
let fixture: ComponentFixture<NewInput>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NewInput]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewInput);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
16
old-sae-airwatch/src/app/chatbot/new-input/new-input.ts
Normal file
16
old-sae-airwatch/src/app/chatbot/new-input/new-input.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Component } from '@angular/core';
|
||||
import {PromptInput} from "../prompt-input/prompt-input";
|
||||
import {ToolsOptions} from "../tools-options/tools-options";
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-input',
|
||||
imports: [
|
||||
PromptInput,
|
||||
ToolsOptions
|
||||
],
|
||||
templateUrl: './new-input.html',
|
||||
styleUrl: './new-input.scss'
|
||||
})
|
||||
export class NewInput {
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<div class="selected-mode-{{inputMode}}">
|
||||
<span class="indicator"></span>
|
||||
|
||||
<div class="dropdown" [ngClass]="{'is-active': isDropdownActive }">
|
||||
<div class="dropdown-trigger">
|
||||
<button (click)="toggleDropdown()" class="button">
|
||||
<span>{{inputMode}}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="ri-arrow-down-s-line" [ngClass]="{'ri-arrow-up-s-line': isDropdownActive}"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a (click)="changeInputMode('auto')" [ngClass]="{'is-active': inputMode === 'auto'}" class="dropdown-item">
|
||||
Auto
|
||||
</a>
|
||||
<a (click)="changeInputMode('structured')" [ngClass]="{'is-active': inputMode === 'structured'}"
|
||||
class="dropdown-item">
|
||||
Structured
|
||||
</a>
|
||||
<a (click)="changeInputMode('specific')" [ngClass]="{'is-active': inputMode === 'specific'}"
|
||||
class="dropdown-item">
|
||||
Specific
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,99 @@
|
|||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0px 0 1px rgba(10, 10, 10, 0.02);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: #4a4a4a;
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.375rem 1rem;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #363636;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #3273dc;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.selected-mode-auto .indicator {
|
||||
background-color: #48c774;
|
||||
}
|
||||
|
||||
.selected-mode-structured .indicator {
|
||||
background-color: #ffdd57;
|
||||
}
|
||||
|
||||
.selected-mode-specific .indicator {
|
||||
background-color: #f14668;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: white;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 4px;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
justify-content: center;
|
||||
padding: 0.5em 0.75em;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #b5b5b5;
|
||||
color: #363636;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure dropdown is displayed when active
|
||||
.dropdown.is-active .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown:not(.is-active) .dropdown-menu {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PromptIndicator } from './prompt-indicator';
|
||||
|
||||
describe('PromptIndicator', () => {
|
||||
let component: PromptIndicator;
|
||||
let fixture: ComponentFixture<PromptIndicator>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PromptIndicator]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PromptIndicator);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
import {Component} from '@angular/core';
|
||||
|
||||
import {inputModeChoicesType} from '../../services/AirWatchState';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ActionTypes, StateInterface} from '../../reducers';
|
||||
import {ApiService} from '../../api-service';
|
||||
import {NgClass} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-prompt-indicator',
|
||||
imports: [NgClass],
|
||||
templateUrl: './prompt-indicator.html',
|
||||
styleUrl: './prompt-indicator.scss'
|
||||
})
|
||||
export class PromptIndicator {
|
||||
|
||||
inputMode: inputModeChoicesType = 'auto';
|
||||
isDropdownActive: boolean = false;
|
||||
|
||||
constructor(private store: Store<StateInterface>, private apiService: ApiService) {
|
||||
// Subscribe to the app state to get the current input mode
|
||||
this.store.select(state => state.app.inputMode).subscribe(mode => {
|
||||
if (mode) {
|
||||
this.inputMode = mode;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Toggle dropdown menu
|
||||
toggleDropdown() {
|
||||
this.isDropdownActive = !this.isDropdownActive;
|
||||
}
|
||||
|
||||
// Change input mode
|
||||
changeInputMode(mode
|
||||
:
|
||||
inputModeChoicesType
|
||||
) {
|
||||
this.inputMode = mode;
|
||||
this.isDropdownActive = false; // Close dropdown after selection
|
||||
|
||||
// Update the app state with the new input mode
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
inputMode: mode
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<div class="prompt-input">
|
||||
<!-- bottom input-->
|
||||
|
||||
<div class="columns">
|
||||
|
||||
|
||||
<div class="column is-four-fifths">
|
||||
<div class="magic">
|
||||
<i class="ri-sparkling-2-line"></i>
|
||||
</div>
|
||||
<input (keydown)="handleKeyEvent($event)" [(ngModel)]="userInput"
|
||||
placeholder="Ask questions, request or a web search" type="text">
|
||||
</div>
|
||||
<div class="column is-one-fifth">
|
||||
<app-prompt-indicator></app-prompt-indicator>
|
||||
|
||||
<button (click)="submitMessage()" class="submit">
|
||||
<i class="ri-send-plane-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
121
old-sae-airwatch/src/app/chatbot/prompt-input/prompt-input.scss
Normal file
121
old-sae-airwatch/src/app/chatbot/prompt-input/prompt-input.scss
Normal file
|
@ -0,0 +1,121 @@
|
|||
@use 'sae-lib/src/styles/variables.scss';
|
||||
|
||||
.prompt-input {
|
||||
margin: 20px auto;
|
||||
max-width: 914px;
|
||||
background: variables.$neutral-white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 10px 10px;
|
||||
box-shadow: 0 5px 10px #ededed;
|
||||
|
||||
|
||||
.column {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
@extend .is-shadowed !optional;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
margin-top: -40px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover {
|
||||
background: grey;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.magic {
|
||||
padding: 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: #f5f5f5;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
// Dropdown styles
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&.is-active {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 12rem;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.02);
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: #4a4a4a;
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.375rem 1rem;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #3273dc;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-mode-specific,
|
||||
.selected-mode-structured {
|
||||
.indicator {
|
||||
background: red;
|
||||
border-radius: 100%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: block;
|
||||
right: 0;
|
||||
top: 0.3rem;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PromptInput } from './prompt-input';
|
||||
|
||||
describe('PromptInput', () => {
|
||||
let component: PromptInput;
|
||||
let fixture: ComponentFixture<PromptInput>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PromptInput]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PromptInput);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
313
old-sae-airwatch/src/app/chatbot/prompt-input/prompt-input.ts
Normal file
313
old-sae-airwatch/src/app/chatbot/prompt-input/prompt-input.ts
Normal file
|
@ -0,0 +1,313 @@
|
|||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ActionTypes, StateInterface} from '../../reducers';
|
||||
import {ChatbotMessage} from '../../services/chatbot.message.type';
|
||||
import {ChatbotConversation} from '../../services/conversations.service';
|
||||
import {ApiService, OpenMeteoResponse} from '../../api-service';
|
||||
import {interval, Subscription} from 'rxjs';
|
||||
import {PromptIndicator} from '../prompt-indicator/prompt-indicator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-prompt-input',
|
||||
imports: [CommonModule, FormsModule, PromptIndicator],
|
||||
templateUrl: './prompt-input.html',
|
||||
styleUrl: './prompt-input.scss'
|
||||
})
|
||||
export class PromptInput implements OnInit, OnDestroy {
|
||||
// Static counter to track API calls
|
||||
static apiCallCounter: number = 0;
|
||||
|
||||
userInput: string = '';
|
||||
activeConversation: ChatbotConversation | null = null;
|
||||
private appState: any = '';
|
||||
private ollamaCheckSubscription: Subscription | null = null;
|
||||
|
||||
// Check interval in milliseconds (30 seconds)
|
||||
private readonly CHECK_INTERVAL = 30000;
|
||||
|
||||
constructor(private store: Store<StateInterface>, private apiService: ApiService) {
|
||||
|
||||
|
||||
// Get the active conversation from the store
|
||||
this.store.select(state => state.activeConversation).subscribe(activeConv => {
|
||||
if (activeConv && Object.keys(activeConv).length > 0) {
|
||||
this.activeConversation = activeConv as ChatbotConversation;
|
||||
}
|
||||
})
|
||||
this.store.select(state => state.app).subscribe(app => {
|
||||
this.appState = app;
|
||||
});
|
||||
|
||||
// Check if Ollama is available when the component is initialized
|
||||
this.checkOllamaAvailability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Ollama backend is available
|
||||
* This will update the Redux store with the result
|
||||
*/
|
||||
checkOllamaAvailability(): void {
|
||||
this.apiService.checkOllamaAvailability().subscribe({
|
||||
next: (isAvailable) => {
|
||||
console.log('Ollama availability:', isAvailable);
|
||||
// The store is already updated in the ApiService
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error checking Ollama availability:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular lifecycle hook that is called after the component is initialized
|
||||
* Sets up periodic checking for Ollama availability
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Set up periodic checking for Ollama availability
|
||||
this.ollamaCheckSubscription = interval(this.CHECK_INTERVAL).subscribe(() => {
|
||||
console.log('Performing periodic Ollama availability check');
|
||||
this.checkOllamaAvailability();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular lifecycle hook that is called when the component is destroyed
|
||||
* Cleans up subscriptions to prevent memory leaks
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Clean up the subscription when the component is destroyed
|
||||
if (this.ollamaCheckSubscription) {
|
||||
this.ollamaCheckSubscription.unsubscribe();
|
||||
this.ollamaCheckSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
submitMessage() {
|
||||
console.log(this.userInput);
|
||||
|
||||
if (!this.userInput.trim() || !this.activeConversation) {
|
||||
console.info('no input to send')
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the user input in a local variable before clearing it
|
||||
const userInputText = this.userInput.trim();
|
||||
console.log('userInputText', userInputText)
|
||||
// Create a new message
|
||||
const newMessage = new ChatbotMessage({
|
||||
kind: 'user',
|
||||
user: {},
|
||||
content: userInputText,
|
||||
name: 'User Message'
|
||||
});
|
||||
// Update the active conversation in the store
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: newMessage
|
||||
});
|
||||
// Clear the input
|
||||
this.userInput = '';
|
||||
const elemFound: HTMLInputElement | null = document.querySelector('.prompt-input input') as HTMLInputElement
|
||||
if (elemFound) {
|
||||
elemFound.focus();
|
||||
}
|
||||
// Check if Ollama is available
|
||||
if (this.appState.ollamaAvailable) {
|
||||
// If Ollama is available, use it regardless of demo mode
|
||||
console.log('Using Ollama backend for response');
|
||||
|
||||
// Query Ollama with the stored user input
|
||||
this.apiService.queryOllama(userInputText).subscribe({
|
||||
error: (error) => {
|
||||
console.error('Error querying Ollama:', error);
|
||||
}
|
||||
});
|
||||
} else if (this.appState.demoMode) {
|
||||
// If Ollama is not available and we're in demo mode, use the demo response
|
||||
console.log('Using demo response (Ollama not available)');
|
||||
|
||||
// Static counter to track API calls
|
||||
if (!PromptInput.apiCallCounter) {
|
||||
PromptInput.apiCallCounter = 0;
|
||||
}
|
||||
PromptInput.apiCallCounter++;
|
||||
|
||||
// Set loading state to true before API call
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
loading: true
|
||||
}
|
||||
});
|
||||
|
||||
this.apiService.fetchDemoResponse().subscribe({
|
||||
next: (resp: OpenMeteoResponse) => {
|
||||
console.log('fetchDemoResponse', resp);
|
||||
|
||||
// Set loading state to false after API call
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
loading: false
|
||||
}
|
||||
});
|
||||
|
||||
// Every third call, show an error instead of the actual response
|
||||
if (PromptInput.apiCallCounter % 3 === 0) {
|
||||
// Set error state to true
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displayErrorResponse: true
|
||||
}
|
||||
});
|
||||
|
||||
// Send error message to chat
|
||||
const errorMessage = new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: "System error - I couldn't process your request. Please try again in few moments",
|
||||
name: 'Assistant'
|
||||
});
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: errorMessage
|
||||
});
|
||||
|
||||
// Reset error state after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displayErrorResponse: false
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the weather data in a readable way
|
||||
const currentTemp = resp.hourly.temperature_2m[new Date().getHours()];
|
||||
const minTemp = Math.min(...resp.hourly.temperature_2m);
|
||||
const maxTemp = Math.max(...resp.hourly.temperature_2m);
|
||||
|
||||
const weatherApiDemoResult = `
|
||||
<h2>
|
||||
|
||||
Météo pour aujourd'hui à Corbeil-Essonne
|
||||
</h2>
|
||||
<br>
|
||||
- <strong>Température actuelle:</strong> ${currentTemp}${resp.hourly_units.temperature_2m}
|
||||
<br>
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
Élévation: ${resp.elevation}m<br>
|
||||
</li>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
<table class="table is-striped">
|
||||
<thead>
|
||||
<th>label</th>
|
||||
<th>valeur</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Température min
|
||||
</td>
|
||||
<td>
|
||||
${minTemp}${resp.hourly_units.temperature_2m}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Température max
|
||||
</td>
|
||||
<td>
|
||||
${maxTemp}${resp.hourly_units.temperature_2m}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="https://www.open-meteo.com">
|
||||
*Données fournies par Open-Meteo API*
|
||||
</a>
|
||||
`;
|
||||
|
||||
const newMessage = new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: `Voici les données météo que vous avez demandées:\n${weatherApiDemoResult}`,
|
||||
name: 'Assistant'
|
||||
});
|
||||
|
||||
// Update the active conversation in the store
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: newMessage
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error fetching weather data:', error);
|
||||
|
||||
// Set loading state to false after API call
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
loading: false
|
||||
}
|
||||
});
|
||||
|
||||
// Send error message to chat
|
||||
const errorMessage = new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: "Désolé, je n'ai pas pu récupérer les données météo. Veuillez réessayer plus tard.",
|
||||
name: 'Assistant'
|
||||
});
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: errorMessage
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Not in demo mode and Ollama is not available
|
||||
console.error('Error: Ollama is not available and not in demo mode');
|
||||
|
||||
// Send error message to chat
|
||||
const errorMessage = new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: "Error: The AI backend is not available. Please check your configuration or try again later.",
|
||||
name: 'System'
|
||||
});
|
||||
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
|
||||
payload: errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Handle key events for Ctrl+Enter
|
||||
handleKeyEvent(event: KeyboardEvent) {
|
||||
// Check if Ctrl key is pressed and Enter key is pressed
|
||||
if (event.ctrlKey && event.key === 'Enter') {
|
||||
this.submitMessage();
|
||||
event.preventDefault(); // Prevent default behavior
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<div class="source">
|
||||
<i class="ri-file-3-line"></i>
|
||||
<span class="title">{{ source.title || 'Source sans nom' }}</span>
|
||||
|
||||
<div class="actions has-text-right">
|
||||
|
||||
<i class="ri-file-add-line"></i>
|
||||
<i class="ri-arrow-down-s-line"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="description">
|
||||
|
||||
{{ source.description || description }}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
.source {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: white;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
.title {
|
||||
color: #1E1F22;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
color: #005AA2;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #ECF3FA;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SourceBlock } from './source-block';
|
||||
|
||||
describe('SourceBlock', () => {
|
||||
let component: SourceBlock;
|
||||
let fixture: ComponentFixture<SourceBlock>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SourceBlock]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SourceBlock);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {ChatbotSource} from '../../services/conversations.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-source-block',
|
||||
imports: [],
|
||||
templateUrl: './source-block.html',
|
||||
styleUrl: './source-block.scss'
|
||||
})
|
||||
export class SourceBlock {
|
||||
@Input() source: ChatbotSource = {} as ChatbotSource;
|
||||
description: string = "lorem blah blah";
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<div class="theme-selector">
|
||||
<button
|
||||
class="theme-selector__button"
|
||||
(click)="switchToNextTheme()"
|
||||
title="Switch to next theme">
|
||||
<span class="theme-selector__current-theme">{{ activeTheme }}</span>
|
||||
<span class="theme-selector__icon">🎨</span>
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,56 @@
|
|||
.theme-selector {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
&__current-theme {
|
||||
margin-right: 8px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme-specific styles that will be applied when the body has the corresponding class
|
||||
:host-context(body.app-theme-light) {
|
||||
.theme-selector__button {
|
||||
background-color: #f8f8f8;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.app-theme-dark) {
|
||||
.theme-selector__button {
|
||||
background-color: #333;
|
||||
color: #f8f8f8;
|
||||
border-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.app-theme-funky) {
|
||||
.theme-selector__button {
|
||||
background-color: #ff00ff;
|
||||
color: #00ffff;
|
||||
border-color: #ffff00;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ThemeSelector } from './theme-selector';
|
||||
|
||||
describe('ThemeSelector', () => {
|
||||
let component: ThemeSelector;
|
||||
let fixture: ComponentFixture<ThemeSelector>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ThemeSelector]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ThemeSelector);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ActionTypes, StateInterface} from '../../reducers';
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-selector',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './theme-selector.html',
|
||||
styleUrl: './theme-selector.scss'
|
||||
})
|
||||
export class ThemeSelector implements OnInit, OnDestroy {
|
||||
activeTheme: string = '';
|
||||
themesList: string[] = [];
|
||||
private storeSubscription: Subscription | null = null;
|
||||
|
||||
constructor(private store: Store<StateInterface>) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to the store to get the active theme and themes list
|
||||
this.storeSubscription = this.store.select(state => state.app)
|
||||
.subscribe(app => {
|
||||
// If the active theme has changed, update the body class
|
||||
if (this.activeTheme !== app.activeTheme) {
|
||||
// Remove the previous theme class if it exists
|
||||
if (this.activeTheme) {
|
||||
document.body.classList.remove(`app-theme-${this.activeTheme}`);
|
||||
}
|
||||
document.body.classList.add(`app-theme-${app.activeTheme}`);
|
||||
this.activeTheme = app.activeTheme;
|
||||
}
|
||||
this.themesList = app.themesList;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe to prevent memory leaks
|
||||
if (this.storeSubscription) {
|
||||
this.storeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Remove the theme class from the body when the component is destroyed
|
||||
if (this.activeTheme) {
|
||||
document.body.classList.remove(this.activeTheme);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to switch to the next theme
|
||||
switchToNextTheme(): void {
|
||||
this.store.dispatch({type: ActionTypes.SWITCH_TO_NEXT_THEME});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<div class="time-separator">
|
||||
|
||||
<div class="columns">
|
||||
<div class="column ">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<div class="column time-ago">
|
||||
<!-- some time ago-->
|
||||
|
||||
{{timeAgo}}
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
.time-separator {
|
||||
padding: 0 16px;
|
||||
font-size: 0.8rem;
|
||||
color: grey;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.column {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
margin-top: -1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid grey;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TimeSeparator } from './time-separator';
|
||||
|
||||
describe('TimeSeparator', () => {
|
||||
let component: TimeSeparator;
|
||||
let fixture: ComponentFixture<TimeSeparator>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TimeSeparator]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TimeSeparator);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-time-separator',
|
||||
imports: [],
|
||||
templateUrl: './time-separator.html',
|
||||
styleUrl: './time-separator.scss'
|
||||
})
|
||||
export class TimeSeparator {
|
||||
timeAgo: string = 'today';
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<button (click)="toggleState()"
|
||||
class="togglable-button {{kind}} {{active ? 'is-active' : ''}}">
|
||||
@if (icon) {
|
||||
<span class="icon ri-{{icon}}"></span>
|
||||
}
|
||||
{{ label }}
|
||||
</button>
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
.togglable-button {
|
||||
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 8px;
|
||||
font-weight: 600;
|
||||
|
||||
border-radius: 20px;
|
||||
padding: 10px;
|
||||
border-width: 2px;
|
||||
background: transparent;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&.local {
|
||||
border: solid 1px #005aa2;
|
||||
color: #005aa2;
|
||||
|
||||
&.is-active {
|
||||
background: #083b7d;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.outside {
|
||||
border: solid 1px #50d494;
|
||||
color: #50d494;
|
||||
|
||||
&.is-active {
|
||||
background: #159256;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: solid 1px black;
|
||||
color: black;
|
||||
|
||||
&.is-active {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
type ToggleButtonKind = "local" | "outside";
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle-button',
|
||||
imports: [],
|
||||
templateUrl: './toggle-button.html',
|
||||
styleUrl: './toggle-button.scss'
|
||||
})
|
||||
export class ToggleButton {
|
||||
@Input() label: string = "(sans label)";
|
||||
@Input() kind: ToggleButtonKind = "local";
|
||||
@Input() active: boolean = false;
|
||||
@Input() icon: string = "";
|
||||
@Output() stateChanged = new EventEmitter<boolean>();
|
||||
|
||||
toggleState() {
|
||||
this.active = !this.active;
|
||||
this.stateChanged.emit(this.active);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<div class="tools-options {{alignLeft ? 'has-text-left' : ''}}">
|
||||
@for (filter of filters; track filter.name) {
|
||||
@if (filter.enabled && hideDisabledButtons) {
|
||||
|
||||
<app-toggle-button
|
||||
[active]="filter.enabled"
|
||||
[icon]="filter.kind === 'local' ? 'database-2-line' : 'google-line'"
|
||||
[kind]="filter.kind"
|
||||
[label]="filter.name"
|
||||
(stateChanged)="toggleFilter(filter, $event)">
|
||||
</app-toggle-button>
|
||||
}
|
||||
}
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
.tools-options {
|
||||
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: solid 0.5px #b7b7b7;
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToolsOptions } from './tools-options';
|
||||
|
||||
describe('ToolsOptions', () => {
|
||||
let component: ToolsOptions;
|
||||
let fixture: ComponentFixture<ToolsOptions>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ToolsOptions]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ToolsOptions);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {ToggleButton} from '../toggle-button/toggle-button';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ActionTypes, StateInterface} from '../../reducers';
|
||||
import {filterType} from '../../services/AirWatchState';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tools-options',
|
||||
imports: [
|
||||
ToggleButton
|
||||
],
|
||||
templateUrl: './tools-options.html',
|
||||
styleUrl: './tools-options.scss'
|
||||
})
|
||||
export class ToolsOptions {
|
||||
filters: filterType[] = [];
|
||||
@Input() public hideDisabledButtons: boolean = false;
|
||||
@Input() public alignLeft: boolean = false;
|
||||
|
||||
constructor(private store: Store<StateInterface>) {
|
||||
this.store.select(state => state.app.filters).subscribe(filters => {
|
||||
this.filters = filters;
|
||||
});
|
||||
}
|
||||
|
||||
toggleFilter(filter: filterType, newState: boolean) {
|
||||
// Find the index of the filter in the array
|
||||
const filterIndex = this.filters.findIndex(f => f.name === filter.name);
|
||||
|
||||
if (filterIndex !== -1) {
|
||||
// Create a new array with the updated filter
|
||||
const updatedFilters = [...this.filters];
|
||||
updatedFilters[filterIndex] = {
|
||||
...filter,
|
||||
enabled: newState
|
||||
};
|
||||
|
||||
// Dispatch the action to update the store
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
filters: updatedFilters
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<p class="version">version {{ versionInfo }}</p>
|
|
@ -0,0 +1,8 @@
|
|||
.version {
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
margin: 10px 0;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VersionInfos } from './version-infos';
|
||||
|
||||
describe('VersionInfos', () => {
|
||||
let component: VersionInfos;
|
||||
let fixture: ComponentFixture<VersionInfos>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VersionInfos]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VersionInfos);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-version-infos',
|
||||
imports: [],
|
||||
templateUrl: './version-infos.html',
|
||||
styleUrl: './version-infos.scss'
|
||||
})
|
||||
export class VersionInfos {
|
||||
@Input() versionInfo: string="version 1.0.0";
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@if (displayError) {
|
||||
<sae-alert-box _alertKind="error"
|
||||
message="System error - I couldn't process your request. Please try again in few moments"></sae-alert-box>
|
||||
} @else {
|
||||
<sae-alert-box _alertKind="warning"
|
||||
message="AI can make mistakes - consider verifying important information."></sae-alert-box>
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WarningBugs } from './warning-bugs';
|
||||
|
||||
describe('WarningBugs', () => {
|
||||
let component: WarningBugs;
|
||||
let fixture: ComponentFixture<WarningBugs>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WarningBugs]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WarningBugs);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {StateInterface} from '../../reducers';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
import {AlertBox} from 'sae-lib/alert-box/alert-box';
|
||||
|
||||
@Component({
|
||||
selector: 'app-warning-bugs',
|
||||
imports: [
|
||||
CommonModule,
|
||||
AlertBox
|
||||
],
|
||||
templateUrl: './warning-bugs.html',
|
||||
styleUrl: './warning-bugs.scss'
|
||||
})
|
||||
export class WarningBugs {
|
||||
protected displayError: boolean = false;
|
||||
|
||||
constructor(private store: Store<StateInterface>) {
|
||||
// Subscribe to the app state to get the error state
|
||||
this.store.select(state => state.app.displayErrorResponse).subscribe(displayError => {
|
||||
this.displayError = displayError;
|
||||
});
|
||||
}
|
||||
}
|
15
old-sae-airwatch/src/app/color-display/color-display.html
Normal file
15
old-sae-airwatch/src/app/color-display/color-display.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<div class="color-box"
|
||||
[ngStyle]="{ background: backgroundColor, 'border-color': backgroundColor }"
|
||||
>
|
||||
<div class="top" >
|
||||
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="name">
|
||||
{{name}}
|
||||
</div>
|
||||
<div class="hexacode">
|
||||
{{hexaCode}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
31
old-sae-airwatch/src/app/color-display/color-display.scss
Normal file
31
old-sae-airwatch/src/app/color-display/color-display.scss
Normal file
|
@ -0,0 +1,31 @@
|
|||
:host {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 11.4rem;
|
||||
border-radius: 0.5rem;
|
||||
border: solid 2px transparent;
|
||||
box-shadow: 0 5px 10px #eee;
|
||||
color: #aaa;
|
||||
|
||||
.top {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
border: solid 1px #aaa;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.name{
|
||||
color: #333;
|
||||
}
|
||||
.name, .hexacode {
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
}
|
||||
.hexacode{
|
||||
padding-top:0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
23
old-sae-airwatch/src/app/color-display/color-display.spec.ts
Normal file
23
old-sae-airwatch/src/app/color-display/color-display.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ColorDisplay } from './color-display';
|
||||
|
||||
describe('ColorDisplay', () => {
|
||||
let component: ColorDisplay;
|
||||
let fixture: ComponentFixture<ColorDisplay>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ColorDisplay]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ColorDisplay);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
22
old-sae-airwatch/src/app/color-display/color-display.ts
Normal file
22
old-sae-airwatch/src/app/color-display/color-display.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {NgStyle} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-color-display',
|
||||
imports: [
|
||||
NgStyle
|
||||
],
|
||||
templateUrl: './color-display.html',
|
||||
styleUrl: './color-display.scss'
|
||||
})
|
||||
export class ColorDisplay implements OnInit {
|
||||
@Input() public hexaCode: string = '#cc0000';
|
||||
@Input() public name: string = 'color name';
|
||||
@Input() public backgroundColor: string = '';
|
||||
|
||||
ngOnInit() {
|
||||
if (this.hexaCode) {
|
||||
this.backgroundColor = this.hexaCode
|
||||
}
|
||||
}
|
||||
}
|
74
old-sae-airwatch/src/app/ds-navbar/ds-navbar.html
Normal file
74
old-sae-airwatch/src/app/ds-navbar/ds-navbar.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
<header class="container">
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="https://cipherbliss.com">
|
||||
<app-logo></app-logo>
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false"
|
||||
data-target="navbarBasicExample">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarBasicExample" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item">
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<button class="navbar-item" routerLink="home" routerLinkActive="active-link">
|
||||
accueil
|
||||
</button>
|
||||
<span class="navbar-item" routerLink="colors" routerLinkActive="active-link">
|
||||
colors
|
||||
</span>
|
||||
<a class="navbar-item" routerLink="grid" routerLinkActive="active-link">
|
||||
tableau
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="csc" routerLinkActive="active-link">
|
||||
CSC démo
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="layout" routerLinkActive="active-link">
|
||||
layout
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Ask question
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Knowledge base
|
||||
</a>
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
<i class="ri-notification-2-line"></i>
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
About
|
||||
</a>
|
||||
<a class="navbar-item is-selected">
|
||||
Jobs
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Contact
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
0
old-sae-airwatch/src/app/ds-navbar/ds-navbar.scss
Normal file
0
old-sae-airwatch/src/app/ds-navbar/ds-navbar.scss
Normal file
23
old-sae-airwatch/src/app/ds-navbar/ds-navbar.spec.ts
Normal file
23
old-sae-airwatch/src/app/ds-navbar/ds-navbar.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DsNavbar } from './ds-navbar';
|
||||
|
||||
describe('DsNavbar', () => {
|
||||
let component: DsNavbar;
|
||||
let fixture: ComponentFixture<DsNavbar>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DsNavbar]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DsNavbar);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
17
old-sae-airwatch/src/app/ds-navbar/ds-navbar.ts
Normal file
17
old-sae-airwatch/src/app/ds-navbar/ds-navbar.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Component } from '@angular/core';
|
||||
import {Logo} from "../logo/logo";
|
||||
import {RouterLink, RouterLinkActive} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-ds-navbar',
|
||||
imports: [
|
||||
Logo,
|
||||
RouterLink,
|
||||
RouterLinkActive
|
||||
],
|
||||
templateUrl: './ds-navbar.html',
|
||||
styleUrl: './ds-navbar.scss'
|
||||
})
|
||||
export class DsNavbar {
|
||||
|
||||
}
|
10
old-sae-airwatch/src/app/grid-demo/grid-demo.html
Normal file
10
old-sae-airwatch/src/app/grid-demo/grid-demo.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div>
|
||||
<h1>Démo de données tabulaires</h1>
|
||||
<!-- <div id="demo_table"></div>-->
|
||||
<ag-grid-angular
|
||||
style="width: 100%; height: 550px;"
|
||||
[rowData]="rowData"
|
||||
[columnDefs]="colDefs"
|
||||
(gridReady)="onGridReady($event)"
|
||||
/>
|
||||
</div>
|
0
old-sae-airwatch/src/app/grid-demo/grid-demo.scss
Normal file
0
old-sae-airwatch/src/app/grid-demo/grid-demo.scss
Normal file
23
old-sae-airwatch/src/app/grid-demo/grid-demo.spec.ts
Normal file
23
old-sae-airwatch/src/app/grid-demo/grid-demo.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GridDemo } from './grid-demo';
|
||||
|
||||
describe('GridDemo', () => {
|
||||
let component: GridDemo;
|
||||
let fixture: ComponentFixture<GridDemo>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GridDemo]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GridDemo);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
49
old-sae-airwatch/src/app/grid-demo/grid-demo.ts
Normal file
49
old-sae-airwatch/src/app/grid-demo/grid-demo.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {AgGridAngular} from "ag-grid-angular";
|
||||
import type {ColDef, GridReadyEvent} from "ag-grid-community";
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
|
||||
interface IRow {
|
||||
mission: string;
|
||||
company: string;
|
||||
location: string;
|
||||
date: string;
|
||||
time: string;
|
||||
rocket: string;
|
||||
price: number;
|
||||
successful: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-demo',
|
||||
imports: [
|
||||
AgGridAngular
|
||||
],
|
||||
templateUrl: './grid-demo.html',
|
||||
styleUrl: './grid-demo.scss'
|
||||
})
|
||||
export class GridDemo {
|
||||
|
||||
rowData: IRow[] = [];
|
||||
colDefs: ColDef[] = [
|
||||
{field: "mission"},
|
||||
{field: "company"},
|
||||
{field: "location"},
|
||||
{field: "date"},
|
||||
{field: "price"},
|
||||
{field: "successful"},
|
||||
{field: "rocket"}
|
||||
];
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
onGridReady(params: GridReadyEvent) {
|
||||
let self = this;
|
||||
this.http
|
||||
.get<any[]>('https://www.ag-grid.com/example-assets/space-mission-data.json')
|
||||
.subscribe((data): any => self.rowData = data);
|
||||
|
||||
}
|
||||
|
||||
}
|
17
old-sae-airwatch/src/app/logo/logo.stories.ts
Normal file
17
old-sae-airwatch/src/app/logo/logo.stories.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { Logo } from './logo';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||
const meta: Meta<Logo> = {
|
||||
title: 'Components/Logo',
|
||||
component: Logo,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<Logo>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
15
old-sae-airwatch/src/app/logo/logo.ts
Normal file
15
old-sae-airwatch/src/app/logo/logo.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logo',
|
||||
imports: [],
|
||||
template: `
|
||||
<span class="logo">
|
||||
😎 logo
|
||||
</span>
|
||||
`,
|
||||
styles: ``
|
||||
})
|
||||
export class Logo {
|
||||
|
||||
}
|
38
old-sae-airwatch/src/app/main-button/main-button.stories.ts
Normal file
38
old-sae-airwatch/src/app/main-button/main-button.stories.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { MainButton } from './main-button';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||
const meta: Meta<MainButton> = {
|
||||
title: 'Components/MainButton',
|
||||
component: MainButton,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: { control: 'text' },
|
||||
icon: { control: 'text' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<MainButton>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
label: 'Button',
|
||||
icon: 'ri-home-line',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutIcon: Story = {
|
||||
args: {
|
||||
label: 'Button without icon',
|
||||
icon: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
label: 'Button with icon',
|
||||
icon: 'ri-user-line',
|
||||
},
|
||||
};
|
29
old-sae-airwatch/src/app/main-button/main-button.ts
Normal file
29
old-sae-airwatch/src/app/main-button/main-button.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
|
||||
export type ButtonKind = '' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'danger';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-button',
|
||||
imports: [],
|
||||
template: `
|
||||
<button class="is-{{kind}}">
|
||||
|
||||
@if (icon) {
|
||||
<i class="ri ri-{{icon}}"></i>
|
||||
}
|
||||
<span class="label">
|
||||
|
||||
{{ label }}
|
||||
</span>
|
||||
</button>
|
||||
`,
|
||||
styles: `
|
||||
|
||||
`
|
||||
})
|
||||
export class MainButton {
|
||||
|
||||
@Input() label: string = '';
|
||||
@Input() icon: string = '';
|
||||
@Input() kind: ButtonKind = '';
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<div class="welcome-input">
|
||||
<h2>Bienvenue </h2>
|
||||
<input type="text">
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WelcomeInput } from './welcome-input';
|
||||
|
||||
describe('WelcomeInput', () => {
|
||||
let component: WelcomeInput;
|
||||
let fixture: ComponentFixture<WelcomeInput>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WelcomeInput]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WelcomeInput);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-welcome-input',
|
||||
imports: [],
|
||||
templateUrl: './welcome-input.html',
|
||||
styleUrl: './welcome-input.scss'
|
||||
})
|
||||
export class WelcomeInput {
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<div>
|
||||
|
||||
<h1 class="title is-1">
|
||||
AirWatch démo
|
||||
</h1>
|
||||
<app-welcome-input></app-welcome-input>
|
||||
|
||||
<app-layout-demo>
|
||||
salut la démo
|
||||
</app-layout-demo>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AirwatchDemo } from './airwatch-demo';
|
||||
|
||||
describe('AirwatchDemo', () => {
|
||||
let component: AirwatchDemo;
|
||||
let fixture: ComponentFixture<AirwatchDemo>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AirwatchDemo]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AirwatchDemo);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {LayoutDemo} from '../layout-demo/layout-demo';
|
||||
import {WelcomeInput} from '../../molecules/welcome-input/welcome-input';
|
||||
|
||||
@Component({
|
||||
selector: 'app-airwatch-demo',
|
||||
imports: [
|
||||
LayoutDemo,
|
||||
WelcomeInput
|
||||
],
|
||||
templateUrl: './airwatch-demo.html',
|
||||
styleUrl: './airwatch-demo.scss'
|
||||
})
|
||||
export class AirwatchDemo {
|
||||
|
||||
}
|
1
old-sae-airwatch/src/app/pages/airwatch/airwatch.html
Normal file
1
old-sae-airwatch/src/app/pages/airwatch/airwatch.html
Normal file
|
@ -0,0 +1 @@
|
|||
<p>airwatch works!</p>
|
0
old-sae-airwatch/src/app/pages/airwatch/airwatch.scss
Normal file
0
old-sae-airwatch/src/app/pages/airwatch/airwatch.scss
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue