rename airwatch folder
This commit is contained in:
parent
a05388fcbc
commit
949641d881
262 changed files with 21196 additions and 245 deletions
12
airwatch/src/app/app.config.server.ts
Normal file
12
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
airwatch/src/app/app.config.ts
Normal file
20
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
airwatch/src/app/app.html
Normal file
32
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
airwatch/src/app/app.routes.server.ts
Normal file
8
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
|
||||
}
|
||||
];
|
46
airwatch/src/app/app.routes.ts
Normal file
46
airwatch/src/app/app.routes.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
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';
|
||||
import {TestingApi} from './pages/testing-api/testing-api';
|
||||
|
||||
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: 'api-testing',
|
||||
component: TestingApi,
|
||||
},
|
||||
{
|
||||
path: 'csc',
|
||||
component: Csc,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
redirectTo: 'home',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
0
airwatch/src/app/app.scss
Normal file
0
airwatch/src/app/app.scss
Normal file
23
airwatch/src/app/app.spec.ts
Normal file
23
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
airwatch/src/app/app.ts
Normal file
22
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
airwatch/src/app/auth-service.spec.ts
Normal file
16
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
airwatch/src/app/auth-service.ts
Normal file
35
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)
|
||||
}
|
||||
}
|
70
airwatch/src/app/buttons/main-button/main-button.scss
Normal file
70
airwatch/src/app/buttons/main-button/main-button.scss
Normal file
|
@ -0,0 +1,70 @@
|
|||
@use "sae-lib/src/styles/shadows.scss";
|
||||
@use "sae-lib/src/styles/states.scss";
|
||||
@use "sae-lib/src/styles/variables.scss";
|
||||
@use "sass:color";
|
||||
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
||||
button {
|
||||
background: shadows.$primary-color;
|
||||
color: shadows.$neutral-white;
|
||||
border-radius: shadows.$radius-main;
|
||||
padding: 1rem 2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
|
||||
i {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
background: shadows.$main-bg-color-active;
|
||||
color: shadows.$main-color-active;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
|
||||
// state colors
|
||||
&.is-primary {
|
||||
background-color: variables.$primary-color;
|
||||
color: variables.$neutral-white;
|
||||
border-color: color.adjust(variables.$primary-color, $lightness: - 10%);
|
||||
}
|
||||
|
||||
&.is-secondary {
|
||||
background-color: variables.$secondary-color;
|
||||
color: variables.$neutral-white;
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background: variables.$neutral-white;
|
||||
color: variables.$warning-color-text;
|
||||
border: 0;
|
||||
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background: variables.$info-color;
|
||||
color: variables.$neutral-white;
|
||||
border-color: color.adjust(variables.$info-color, $lightness: -50%);
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background: variables.$success-color;
|
||||
color: variables.$neutral-white;
|
||||
border-color: color.adjust(variables.$success-color, $lightness: -50%);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
background: rgba(variables.$danger-color, 10%);
|
||||
color: variables.$danger-color;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
94
airwatch/src/app/buttons/main-button/main-button.stories.ts
Normal file
94
airwatch/src/app/buttons/main-button/main-button.stories.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import type {Meta, StoryObj} from '@storybook/angular';
|
||||
import {MainButton} from './main-button';
|
||||
import {moduleMetadata} from '@storybook/angular';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||
const meta: Meta<MainButton> = {
|
||||
title: 'UI/Buttons/MainButton',
|
||||
component: MainButton,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [CommonModule],
|
||||
providers: []
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
label: {control: 'text'},
|
||||
icon: {control: 'text'},
|
||||
kind: {
|
||||
control: 'select',
|
||||
options: ['', 'primary', 'secondary', 'info', 'success', 'warning', 'danger'],
|
||||
description: 'Style du bouton'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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: 'home-line-2',
|
||||
kind: 'primary'
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
label: 'Secondary Button',
|
||||
icon: 'settings-line',
|
||||
kind: 'secondary'
|
||||
},
|
||||
};
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
label: 'Info Button',
|
||||
icon: 'information-line',
|
||||
kind: 'info'
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
label: 'Success Button',
|
||||
icon: 'check-line',
|
||||
kind: 'success'
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
label: 'Warning Button',
|
||||
icon: 'alert-line',
|
||||
kind: 'warning'
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
label: 'Danger Button',
|
||||
icon: 'close-circle-line',
|
||||
kind: 'danger'
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutIcon: Story = {
|
||||
args: {
|
||||
label: 'Button without icon',
|
||||
icon: '',
|
||||
kind: 'primary'
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
label: 'Button with icon',
|
||||
icon: 'user-line',
|
||||
kind: ''
|
||||
},
|
||||
};
|
28
airwatch/src/app/buttons/main-button/main-button.ts
Normal file
28
airwatch/src/app/buttons/main-button/main-button.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
export type ButtonKind = '' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'danger';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-button',
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<button class="is-{{kind}}">
|
||||
|
||||
@if (icon) {
|
||||
<i class="ri ri-{{icon}}"></i>
|
||||
}
|
||||
<span class="label">
|
||||
|
||||
{{ label }}
|
||||
</span>
|
||||
</button>
|
||||
`,
|
||||
styleUrl: './main-button.scss'
|
||||
})
|
||||
export class MainButton {
|
||||
|
||||
@Input() label: string = '';
|
||||
@Input() icon: string = '';
|
||||
@Input() kind: ButtonKind = '';
|
||||
}
|
384
airwatch/src/app/chatbot/chatbot.html
Normal file
384
airwatch/src/app/chatbot/chatbot.html
Normal file
|
@ -0,0 +1,384 @@
|
|||
<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-start">
|
||||
<a (click)="toggleSidePanelConversationsList()" class="navbar-item aside-toggle-button">
|
||||
|
||||
|
||||
@if (appState.displayConversationListPanelLarge) {
|
||||
<i class="ri-sidebar-fold-line"></i>
|
||||
} @else {
|
||||
<i class="ri-sidebar-unfold-line"></i>
|
||||
}
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/#">
|
||||
<!-- <i class="ri-robot-2-fill"></i>-->
|
||||
|
||||
@if (appState.displayConversationListPanelLarge) {
|
||||
<img alt="safran logo" class="logo" src="/safran_logo_large.svg">
|
||||
<span class="label">
|
||||
airwatch
|
||||
</span>
|
||||
|
||||
} @else {
|
||||
<img src="safran_logo_small.svg" alt="logo">
|
||||
}
|
||||
|
||||
</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-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 is-active">
|
||||
<i class="ri-chat-4-line"></i>
|
||||
Ask question
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
<i class="ri-database-2-line"></i>
|
||||
Knowledge base
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
<i class="ri-compass-3-line"></i>
|
||||
Quick start
|
||||
</a>
|
||||
<div class="navbar-item ">
|
||||
<!-- <div class="navbar-item has-dropdown is-hoverable">-->
|
||||
<!-- <a aria-expanded="false" aria-haspopup="true" class="navbar-linking">-->
|
||||
<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>
|
||||
<a class="navbar-item user-account-item">
|
||||
<i class="ri-layout-right-line"></i>
|
||||
Borhène
|
||||
</a>
|
||||
</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)">
|
||||
<div class="actions">
|
||||
<div class="dropdown is-hoverable">
|
||||
<div class="dropdown-trigger">
|
||||
<div class="menu">
|
||||
<i class="ri-more-line"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item" (click)="copyConversationLink(conversation)">
|
||||
<i class="ri-link-m"></i>
|
||||
<span>Share</span>
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="toggleVisibleEditInput(conversation)">
|
||||
<i class="ri-edit-line"></i>
|
||||
<span>Rename</span>
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="pinConversation(conversation)">
|
||||
<i class="ri-pushpin-fill"></i>
|
||||
<span>Pin</span>
|
||||
</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a class="dropdown-item trash" (click)="deleteConversation(conversation)">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
<span>Delete</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-text">
|
||||
@if (conversation.pinned) {
|
||||
<span class="pinned-icon">
|
||||
<i class="ri-pushpin-line"></i>
|
||||
</span>
|
||||
} @else {
|
||||
<div class="pinned-icon llm-avatar">
|
||||
</div>
|
||||
}
|
||||
<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>
|
||||
}
|
||||
|
||||
</div>
|
||||
<app-version-infos [versionInfo]="appVersion"></app-version-infos>
|
||||
<!-- <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">
|
||||
Auto mode
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="action">
|
||||
@if (displayExportConversation) {
|
||||
<div class="dropdown" [ngClass]="{'is-active': isExportDropdownActive}">
|
||||
<div class="dropdown-trigger">
|
||||
<div class="export-chat-button is-clickable" (click)="toggleExportDropdown()">
|
||||
<i class="ri-download-2-line"></i>
|
||||
<span class="label">
|
||||
Export chat
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item" (click)="exportConversation('csv')">
|
||||
CSV
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="exportConversation('excel')">
|
||||
Excel
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="exportConversation('txt')">
|
||||
TXT
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<app-time-separator></app-time-separator>
|
||||
@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>
|
||||
}
|
||||
|
||||
|
||||
</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 [ngClass]="{'is-active': isExportSourcesDropdownActive}" class="dropdown">
|
||||
<div class="dropdown-trigger">
|
||||
<div (click)="toggleExportSourcesDropdown()" class="export-chat-button is-clickable">
|
||||
<i class="ri-download-2-line"></i>
|
||||
<span class="label">
|
||||
Export all sources
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a (click)="exportSources('csv')" class="dropdown-item">
|
||||
CSV
|
||||
</a>
|
||||
<a (click)="exportSources('excel')" class="dropdown-item">
|
||||
Excel
|
||||
</a>
|
||||
<a (click)="exportSources('txt')" class="dropdown-item">
|
||||
TXT
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-more-inside">
|
||||
<div class="main-title">Knowledge Base documents :
|
||||
<div class="filter">
|
||||
<i class="ri-download-2-line"></i>
|
||||
|
||||
</div>
|
||||
</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 class="bottom-gradient">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-feedback-button></app-feedback-button>
|
||||
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
204
airwatch/src/app/chatbot/chatbot.scss
Normal file
204
airwatch/src/app/chatbot/chatbot.scss
Normal file
|
@ -0,0 +1,204 @@
|
|||
@use "sae-lib/src/styles/variables.scss" as variables;
|
||||
|
||||
.panel-more-inside {
|
||||
border-radius: 10px;
|
||||
background: #F5F5F5;
|
||||
padding: 20px 16px;
|
||||
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;
|
||||
height: 90vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.source {
|
||||
border-radius: 4px;
|
||||
background: #ECF3FA;
|
||||
}
|
||||
|
||||
.bottom-gradient {
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: linear-gradient(354deg, #F5F5F5 27.6%, rgba(255, 255, 255, 0.72) 47.82%, rgba(245, 245, 245, 0.00) 72.79%);
|
||||
}
|
||||
|
||||
.filter {
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
background: rgba(59, 135, 204, 0.5);
|
||||
display: flex;
|
||||
width: 34px;
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 100px;
|
||||
top: 170px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.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-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% */
|
||||
|
||||
&:hover {
|
||||
background: #005AA2;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown styles for export buttons
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&.is-active,
|
||||
&.is-hoverable:hover {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 175px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
margin-top: 2px;
|
||||
margin-right: 0;
|
||||
|
||||
box-shadow: 0 4px 13px 0 rgba(37, 91, 142, 0.10);
|
||||
|
||||
}
|
||||
|
||||
.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;
|
||||
padding-bottom: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #3273dc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 16px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
hr {
|
||||
stroke-width: 0.5px;
|
||||
stroke: #ABABAB;
|
||||
color: #ABABAB;
|
||||
border-color: #ABABAB;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.llm-avatar {
|
||||
background: white url("./../../../public/chatbot.png") no-repeat center center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
background-size: contain;
|
||||
margin-right: 10px;
|
||||
}
|
23
airwatch/src/app/chatbot/chatbot.spec.ts
Normal file
23
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();
|
||||
});
|
||||
});
|
105
airwatch/src/app/chatbot/chatbot.stories.ts
Normal file
105
airwatch/src/app/chatbot/chatbot.stories.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import type {Meta, StoryObj} from '@storybook/angular';
|
||||
import {moduleMetadata} from '@storybook/angular';
|
||||
import {Chatbot} from './chatbot';
|
||||
import {Store, StoreModule} from '@ngrx/store';
|
||||
import {reducers} from '../reducers';
|
||||
import {ConversationsService} from '../services/conversations.service';
|
||||
import {ApiService} from '../services/api-service';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {TranslationService} from '../services/translation.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
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 {ToolsOptions} from './tools-options/tools-options';
|
||||
|
||||
const appReducer = reducers.app;
|
||||
|
||||
const meta: Meta<Chatbot> = {
|
||||
title: 'App/Features/Chatbot',
|
||||
component: Chatbot,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
StoreModule.forRoot({app: appReducer as any}),
|
||||
PromptInput,
|
||||
MessageBox,
|
||||
NewInput,
|
||||
FeedbackButton,
|
||||
WarningBugs,
|
||||
VersionInfos,
|
||||
LoadingNotification,
|
||||
SourceBlock,
|
||||
ToolsOptions,
|
||||
],
|
||||
providers: [
|
||||
ConversationsService,
|
||||
ApiService,
|
||||
TranslationService,
|
||||
Store
|
||||
]
|
||||
})
|
||||
],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<Chatbot>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Les propriétés sont injectées par les services
|
||||
}
|
||||
};
|
||||
|
||||
export const DemoMode: Story = {
|
||||
args: {
|
||||
// Les propriétés sont injectées par les services
|
||||
},
|
||||
parameters: {
|
||||
store: {
|
||||
init: (store: Store) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_APP',
|
||||
payload: {
|
||||
demoMode: true,
|
||||
displayConversationListPanelLarge: true,
|
||||
displaySourcesPanelLarge: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const CompactView: Story = {
|
||||
args: {
|
||||
// Les propriétés sont injectées par les services
|
||||
},
|
||||
parameters: {
|
||||
store: {
|
||||
init: (store: Store) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_APP',
|
||||
payload: {
|
||||
demoMode: true,
|
||||
displayConversationListPanelLarge: false,
|
||||
displaySourcesPanelLarge: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
417
airwatch/src/app/chatbot/chatbot.ts
Normal file
417
airwatch/src/app/chatbot/chatbot.ts
Normal file
|
@ -0,0 +1,417 @@
|
|||
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 '../services/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';
|
||||
import {TimeSeparator} from './time-separator/time-separator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chatbot',
|
||||
imports: [
|
||||
PromptInput,
|
||||
MessageBox,
|
||||
NewInput,
|
||||
FeedbackButton,
|
||||
WarningBugs,
|
||||
VersionInfos,
|
||||
LoadingNotification,
|
||||
SourceBlock,
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
ToolsOptions,
|
||||
TimeSeparator,
|
||||
],
|
||||
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;
|
||||
isExportDropdownActive: boolean = false;
|
||||
isExportSourcesDropdownActive: boolean = false;
|
||||
@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
|
||||
|
||||
|
||||
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: "The documents also highlight various post- incident safety measures implemented, including improved training procedures, enhanced maintenance protocols, and aircraft design modifications. Regulatory agencies such as the NTSB, BEA, AAIB, and others were involved in investigating these incidents and making safety recommendations.
\n" +
|
||||
"This summary provides an overview of significant aviation incidents, their causes, and outcomes, which may be useful for understanding patterns in aircraft accidents and areas for improving aviation safety.\n" +
|
||||
"<br/>" +
|
||||
"Internet Search Not Activated.<br/>" +
|
||||
"No internet search was activated for this query<br/>" +
|
||||
"<br/>" +
|
||||
"Search results.",
|
||||
name: "Assistant"
|
||||
}));
|
||||
|
||||
|
||||
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";
|
||||
|
||||
conversation2.sources = [source1, source2, source2, source2, source2, source2, source2, source2, source2, source2, source2, source2, source2];
|
||||
|
||||
|
||||
// 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() {
|
||||
|
||||
}
|
||||
|
||||
toggleExportDropdown() {
|
||||
this.isExportDropdownActive = !this.isExportDropdownActive;
|
||||
// Close the other dropdown if it's open
|
||||
if (this.isExportDropdownActive) {
|
||||
this.isExportSourcesDropdownActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
toggleExportSourcesDropdown() {
|
||||
this.isExportSourcesDropdownActive = !this.isExportSourcesDropdownActive;
|
||||
// Close the other dropdown if it's open
|
||||
if (this.isExportSourcesDropdownActive) {
|
||||
this.isExportDropdownActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
exportMessage() {
|
||||
// This method is kept for backward compatibility
|
||||
this.exportConversation('txt');
|
||||
}
|
||||
|
||||
exportConversation(format: 'csv' | 'excel' | 'txt') {
|
||||
console.log(`Exporting conversation in ${format} format`);
|
||||
// Close the dropdown after selection
|
||||
this.isExportDropdownActive = false;
|
||||
|
||||
if (!this.activeConversation) {
|
||||
console.error('No active conversation to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementation will depend on the specific requirements
|
||||
// This is a placeholder for the actual export functionality
|
||||
switch (format) {
|
||||
case 'csv':
|
||||
// Export as CSV
|
||||
console.log('TODO Exporting as CSV');
|
||||
break;
|
||||
case 'excel':
|
||||
// Export as Excel
|
||||
console.log('TODO Exporting as Excel');
|
||||
break;
|
||||
case 'txt':
|
||||
// Export as TXT
|
||||
console.log('TODO Exporting as TXT');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
exportSources(format: 'csv' | 'excel' | 'txt') {
|
||||
console.log(`Exporting sources in ${format} format`);
|
||||
// Close the dropdown after selection
|
||||
this.isExportSourcesDropdownActive = false;
|
||||
|
||||
if (!this.activeConversation || !this.activeConversation.sources || this.activeConversation.sources.length === 0) {
|
||||
console.error('No sources to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementation will depend on the specific requirements
|
||||
// This is a placeholder for the actual export functionality
|
||||
switch (format) {
|
||||
case 'csv':
|
||||
// Export as CSV
|
||||
console.log('Exporting sources as CSV');
|
||||
break;
|
||||
case 'excel':
|
||||
// Export as Excel
|
||||
console.log('Exporting sources as Excel');
|
||||
break;
|
||||
case 'txt':
|
||||
// Export as TXT
|
||||
console.log('Exporting sources as TXT');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
copyToClipboard(text: string) {
|
||||
// Implementation for copying to clipboard
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
pinConversation(conversation: ChatbotConversation) {
|
||||
conversation.pinned = !conversation.pinned;
|
||||
}
|
||||
|
||||
copyConversationLink(conversation: ChatbotConversation) {
|
||||
console.log('todo copy conversation link', conversation);
|
||||
|
||||
}
|
||||
|
||||
deleteConversation(conversation: ChatbotConversation) {
|
||||
console.log('todo delete conversation', conversation);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,65 @@
|
|||
<div class="conversation-item"
|
||||
[ngClass]="{'is-active': isActive}"
|
||||
(click)="selectConversation()">
|
||||
<div class="actions">
|
||||
<div class="dropdown is-hoverable">
|
||||
<div class="dropdown-trigger">
|
||||
<div class="menu">
|
||||
<i class="ri-more-2-line"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item" (click)="shareConversation($event)">
|
||||
<i class="ri-link-m"></i>
|
||||
<span>Share</span>
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="renameConversation($event)">
|
||||
<i class="ri-edit-line"></i>
|
||||
<span>Rename</span>
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="pinConversation($event)">
|
||||
<i class="ri-pushpin-fill"></i>
|
||||
<span>Pin</span>
|
||||
</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a class="dropdown-item trash" (click)="deleteConversation($event)">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
<span>Delete</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-text">
|
||||
<ng-container *ngIf="conversation.pinned; else avatarTemplate">
|
||||
<span class="pinned-icon">
|
||||
<i class="ri-pushpin-line"></i>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #avatarTemplate>
|
||||
<div class="pinned-icon llm-avatar"></div>
|
||||
</ng-template>
|
||||
|
||||
<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-visible': conversation.visibleInput}">
|
||||
</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>
|
|
@ -0,0 +1,186 @@
|
|||
.conversation-item {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
padding: 16px 16px 18px 32px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
color: #6D717C;
|
||||
|
||||
.actions {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: #B9D6ED;
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
float: right;
|
||||
|
||||
.menu {
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
|
||||
i {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.dropdown-menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-hoverable:hover {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trash {
|
||||
color: red;
|
||||
fill: red;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #3B87CC1A;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(#3B87CC1A, 10%);
|
||||
font-weight: 600;
|
||||
|
||||
.active-peak {
|
||||
position: relative;
|
||||
right: calc(-100% - 16px);
|
||||
top: -35px;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.pinned-icon {
|
||||
position: relative;
|
||||
left: -1.7em;
|
||||
|
||||
i {
|
||||
color: #FEC553;
|
||||
fill: #FEC553;
|
||||
}
|
||||
}
|
||||
|
||||
.active-peak {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #1E1F22;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
margin-top: -1.5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #6D717C;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: rgba(#5F5F5F99, 60%);
|
||||
float: right;
|
||||
text-align: right;
|
||||
font-family: Barlow;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 8px;
|
||||
margin-top: -1.5em;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
|
||||
&.is-visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown styles
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&.is-active,
|
||||
&.is-hoverable:hover {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 100%;
|
||||
right: 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;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LLM avatar
|
||||
.llm-avatar {
|
||||
background: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
background-size: contain;
|
||||
margin-right: 10px;
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { ConversationItem } from './conversation-item';
|
||||
import { moduleMetadata } from '@storybook/angular';
|
||||
import { NgClass, NgIf, DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ChatbotConversation } from '../../services/conversations.service';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||
const meta: Meta<ConversationItem> = {
|
||||
title: 'Chatbot/Conversation/ConversationItem',
|
||||
component: ConversationItem,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [NgClass, NgIf, DatePipe, FormsModule],
|
||||
providers: []
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
isActive: { control: 'boolean' },
|
||||
onSelect: { action: 'selected' },
|
||||
onShare: { action: 'shared' },
|
||||
onRename: { action: 'renamed' },
|
||||
onPin: { action: 'pinned' },
|
||||
onDelete: { action: 'deleted' }
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<ConversationItem>;
|
||||
|
||||
// Create a mock conversation
|
||||
const createMockConversation = (name: string, description: string, pinned: boolean = false): ChatbotConversation => {
|
||||
const conversation = new ChatbotConversation();
|
||||
conversation.name = name;
|
||||
conversation.description = description;
|
||||
conversation.pinned = pinned;
|
||||
conversation.lastMessageDate = new Date();
|
||||
return conversation;
|
||||
};
|
||||
|
||||
// Default story
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
conversation: createMockConversation('Project Discussion', 'Latest updates on the project timeline'),
|
||||
isActive: false
|
||||
},
|
||||
};
|
||||
|
||||
// Active conversation
|
||||
export const Active: Story = {
|
||||
args: {
|
||||
conversation: createMockConversation('Project Discussion', 'Latest updates on the project timeline'),
|
||||
isActive: true
|
||||
},
|
||||
};
|
||||
|
||||
// Pinned conversation
|
||||
export const Pinned: Story = {
|
||||
args: {
|
||||
conversation: createMockConversation('Important Meeting', 'CEO quarterly update', true),
|
||||
isActive: false
|
||||
},
|
||||
};
|
||||
|
||||
// Long name and description
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
conversation: createMockConversation(
|
||||
'Very Long Conversation Name That Should Truncate',
|
||||
'This is a very long description that should demonstrate how the component handles overflow text in the description area of the conversation item component',
|
||||
),
|
||||
isActive: false
|
||||
},
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { NgClass, NgIf, DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ChatbotConversation } from '../../services/conversations.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-conversation-item',
|
||||
standalone: true,
|
||||
imports: [NgClass, NgIf, DatePipe, FormsModule],
|
||||
templateUrl: './conversation-item.html',
|
||||
styleUrl: './conversation-item.scss'
|
||||
})
|
||||
export class ConversationItem {
|
||||
@Input() conversation: ChatbotConversation = new ChatbotConversation();
|
||||
@Input() isActive: boolean = false;
|
||||
|
||||
@Output() onSelect = new EventEmitter<ChatbotConversation>();
|
||||
@Output() onShare = new EventEmitter<ChatbotConversation>();
|
||||
@Output() onRename = new EventEmitter<ChatbotConversation>();
|
||||
@Output() onPin = new EventEmitter<ChatbotConversation>();
|
||||
@Output() onDelete = new EventEmitter<ChatbotConversation>();
|
||||
|
||||
isDropdownActive: boolean = false;
|
||||
|
||||
toggleDropdown() {
|
||||
this.isDropdownActive = !this.isDropdownActive;
|
||||
}
|
||||
|
||||
selectConversation() {
|
||||
this.onSelect.emit(this.conversation);
|
||||
}
|
||||
|
||||
shareConversation(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.onShare.emit(this.conversation);
|
||||
}
|
||||
|
||||
renameConversation(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.onRename.emit(this.conversation);
|
||||
}
|
||||
|
||||
pinConversation(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.onPin.emit(this.conversation);
|
||||
}
|
||||
|
||||
deleteConversation(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.onDelete.emit(this.conversation);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<div class="dropdown" [ngClass]="{'is-active': isDropdownActive}">
|
||||
<div class="dropdown-trigger">
|
||||
<div class="export-chat-button is-clickable" (click)="toggleDropdown()">
|
||||
<i class="ri-download-2-line"></i>
|
||||
<span class="label">
|
||||
Export chat
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item" (click)="exportAs('csv', $event)">
|
||||
CSV
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="exportAs('excel', $event)">
|
||||
Excel
|
||||
</a>
|
||||
<a class="dropdown-item" (click)="exportAs('txt', $event)">
|
||||
TXT
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,85 @@
|
|||
.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% */
|
||||
|
||||
&:hover {
|
||||
background: #005AA2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown styles
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&.is-active,
|
||||
&.is-hoverable:hover {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 100%;
|
||||
right: 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;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { ExportChatButton } from './export-chat-button';
|
||||
import { moduleMetadata } from '@storybook/angular';
|
||||
import { NgClass } from '@angular/common';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||
const meta: Meta<ExportChatButton> = {
|
||||
title: 'Chatbot/Actions/ExportChatButton',
|
||||
component: ExportChatButton,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [NgClass],
|
||||
providers: []
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
onExport: { action: 'exported' }
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<ExportChatButton>;
|
||||
|
||||
// Default state
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
// Active dropdown
|
||||
export const ActiveDropdown: Story = {
|
||||
args: {},
|
||||
play: async ({ canvasElement, component }) => {
|
||||
// Simulate clicking the button to open the dropdown
|
||||
component.isDropdownActive = true;
|
||||
},
|
||||
};
|
||||
|
||||
// Custom story to demonstrate hover state
|
||||
export const HoverState: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Hover over the button to see the hover state. The button background changes to blue and text becomes white.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Custom story to demonstrate the export functionality
|
||||
export const ExportFunctionality: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Click on the button to open the dropdown, then select a format to export. The onExport event will be emitted with the selected format.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { NgClass } from '@angular/common';
|
||||
|
||||
export type ExportFormat = 'csv' | 'excel' | 'txt';
|
||||
|
||||
@Component({
|
||||
selector: 'app-export-chat-button',
|
||||
standalone: true,
|
||||
imports: [NgClass],
|
||||
templateUrl: './export-chat-button.html',
|
||||
styleUrl: './export-chat-button.scss'
|
||||
})
|
||||
export class ExportChatButton {
|
||||
@Output() onExport = new EventEmitter<ExportFormat>();
|
||||
|
||||
isDropdownActive: boolean = false;
|
||||
|
||||
toggleDropdown() {
|
||||
this.isDropdownActive = !this.isDropdownActive;
|
||||
}
|
||||
|
||||
exportAs(format: ExportFormat, event: Event) {
|
||||
event.stopPropagation();
|
||||
this.onExport.emit(format);
|
||||
this.isDropdownActive = false;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
191
airwatch/src/app/chatbot/feedback-button/feedback-button.scss
Normal file
191
airwatch/src/app/chatbot/feedback-button/feedback-button.scss
Normal file
|
@ -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: 197px;
|
||||
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,131 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { FeedbackButton } from './feedback-button';
|
||||
import { moduleMetadata } from '@storybook/angular';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { ApiService } from '../../services/api-service';
|
||||
import { reducers } from '../../reducers';
|
||||
|
||||
// Mock ApiService
|
||||
class MockApiService {
|
||||
async sendUserFeedback(feedback: string): Promise<any> {
|
||||
// Simulate API call
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ success: true });
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const meta: Meta<FeedbackButton> = {
|
||||
title: 'UI/Feedback/FeedbackButton',
|
||||
component: FeedbackButton,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
StoreModule.forRoot(reducers)
|
||||
],
|
||||
providers: [
|
||||
Store,
|
||||
{ provide: ApiService, useClass: MockApiService }
|
||||
]
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
isModalOpen: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the feedback modal is open'
|
||||
},
|
||||
feedbackText: {
|
||||
control: 'text',
|
||||
description: 'The feedback text entered by the user'
|
||||
},
|
||||
isSubmitting: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the feedback is being submitted'
|
||||
},
|
||||
submitSuccess: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the feedback was submitted successfully'
|
||||
},
|
||||
submitError: {
|
||||
control: 'boolean',
|
||||
description: 'Whether there was an error submitting the feedback'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<FeedbackButton>;
|
||||
|
||||
// Default state - just the button
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isModalOpen: false,
|
||||
feedbackText: '',
|
||||
isSubmitting: false,
|
||||
submitSuccess: false,
|
||||
submitError: false
|
||||
}
|
||||
};
|
||||
|
||||
// Modal open state
|
||||
export const ModalOpen: Story = {
|
||||
args: {
|
||||
isModalOpen: true,
|
||||
feedbackText: '',
|
||||
isSubmitting: false,
|
||||
submitSuccess: false,
|
||||
submitError: false
|
||||
}
|
||||
};
|
||||
|
||||
// Modal with text entered
|
||||
export const WithFeedbackText: Story = {
|
||||
args: {
|
||||
isModalOpen: true,
|
||||
feedbackText: 'This is some feedback text that the user has entered.',
|
||||
isSubmitting: false,
|
||||
submitSuccess: false,
|
||||
submitError: false
|
||||
}
|
||||
};
|
||||
|
||||
// Submitting state
|
||||
export const Submitting: Story = {
|
||||
args: {
|
||||
isModalOpen: true,
|
||||
feedbackText: 'This is some feedback text that the user has entered.',
|
||||
isSubmitting: true,
|
||||
submitSuccess: false,
|
||||
submitError: false
|
||||
}
|
||||
};
|
||||
|
||||
// Success state
|
||||
export const SubmitSuccess: Story = {
|
||||
args: {
|
||||
isModalOpen: true,
|
||||
feedbackText: 'This is some feedback text that the user has entered.',
|
||||
isSubmitting: false,
|
||||
submitSuccess: true,
|
||||
submitError: false
|
||||
}
|
||||
};
|
||||
|
||||
// Error state
|
||||
export const SubmitError: Story = {
|
||||
args: {
|
||||
isModalOpen: true,
|
||||
feedbackText: 'This is some feedback text that the user has entered.',
|
||||
isSubmitting: false,
|
||||
submitSuccess: false,
|
||||
submitError: true
|
||||
}
|
||||
};
|
||||
|
89
airwatch/src/app/chatbot/feedback-button/feedback-button.ts
Normal file
89
airwatch/src/app/chatbot/feedback-button/feedback-button.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ApiService} from '../../services/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;
|
||||
});
|
||||
}
|
||||
}
|
84
airwatch/src/app/chatbot/message-box/message-box.html
Normal file
84
airwatch/src/app/chatbot/message-box/message-box.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
<div class="message message-{{kind}} {{kind}}" id="message_{{id}}">
|
||||
<div class="actions top-actions">
|
||||
@if (kind === 'user') {
|
||||
<button (click)="editMessage()" class="button edit">
|
||||
<i class="ri-edit-box-line"></i>
|
||||
</button>
|
||||
}
|
||||
<!-- @else {-->
|
||||
<!-- <button (click)="toggleFullScreen()" class="button fullscreen">-->
|
||||
<!-- <i class="ri-fullscreen-line"></i>-->
|
||||
<!-- </button>-->
|
||||
<!-- }-->
|
||||
</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 class="actions bottom-actions is-{{kind}}">
|
||||
@if (kind === 'llm') {
|
||||
|
||||
<div class="action-feedback">
|
||||
<app-feedback-button></app-feedback-button>
|
||||
</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>
|
||||
<button (click)="toggleSources()" class="button sources">
|
||||
<i class="ri-book-2-fill"></i>
|
||||
|
||||
@if (displaySourcesPanelLarge) {
|
||||
|
||||
hide sources
|
||||
} @else {
|
||||
|
||||
see sources
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- <div [ngClass]="{'is-expanded': expanded}" class="expanded-message-fullscreen">-->
|
||||
<!-- @if (content) {-->
|
||||
<!-- <div [innerHTML]="sanitizedContent"></div>-->
|
||||
<!-- }-->
|
||||
<!-- </div>-->
|
||||
</div>
|
160
airwatch/src/app/chatbot/message-box/message-box.scss
Normal file
160
airwatch/src/app/chatbot/message-box/message-box.scss
Normal file
|
@ -0,0 +1,160 @@
|
|||
@use 'sae-lib/src/styles/variables' as variables;
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.message {
|
||||
|
||||
.user-more-infos {
|
||||
margin-top: -35px;
|
||||
margin-left: 39px;
|
||||
}
|
||||
|
||||
&.user {
|
||||
background: white;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&.llm {
|
||||
.message-content {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.button {
|
||||
background: rgba(59, 135, 204, 0.7);
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
.fullscreen {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.app-theme-light & {
|
||||
|
||||
background: #F5F5F5;
|
||||
|
||||
}
|
||||
|
||||
.app-theme-dark & {
|
||||
|
||||
background: #2c2c2c;
|
||||
color: #d5d5d5;
|
||||
|
||||
.message {
|
||||
|
||||
&.user {
|
||||
background: #232432;
|
||||
color: #8b8ecf;
|
||||
}
|
||||
|
||||
&.llm {
|
||||
background: #232432;
|
||||
|
||||
.message-content {
|
||||
color: #8b8ecf;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
.button {
|
||||
background: #0d0e15;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.app-theme-funky & {
|
||||
color: #1B1D27;
|
||||
background: #ffe8e8;
|
||||
|
||||
.message {
|
||||
.message-content {
|
||||
color: #fff3f3;
|
||||
}
|
||||
|
||||
&.user {
|
||||
background: #d6a3a3;
|
||||
color: #1B1D27;
|
||||
}
|
||||
|
||||
&.llm {
|
||||
|
||||
background: #9f36bc;
|
||||
color: #bba7d6;
|
||||
|
||||
.actions {
|
||||
|
||||
.button {
|
||||
background: #b08eba;
|
||||
color: #d4b4ff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.message {
|
||||
|
||||
&.user {
|
||||
background: variables.$neutral-white;
|
||||
}
|
||||
|
||||
&.llm {
|
||||
background: rgba(#3B87CC1A, 10%);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.expanded-message-fullscreen {
|
||||
display: none;
|
||||
width: 50%;
|
||||
|
||||
&.is-visible {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
|
||||
}
|
||||
}
|
23
airwatch/src/app/chatbot/message-box/message-box.spec.ts
Normal file
23
airwatch/src/app/chatbot/message-box/message-box.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
91
airwatch/src/app/chatbot/message-box/message-box.stories.ts
Normal file
91
airwatch/src/app/chatbot/message-box/message-box.stories.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { MessageBox } from './message-box';
|
||||
import { moduleMetadata } from '@storybook/angular';
|
||||
import { DomSanitizer, BrowserModule } 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';
|
||||
|
||||
const meta: Meta<MessageBox> = {
|
||||
title: 'Chatbot/Messages/MessageBox',
|
||||
component: MessageBox,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [Copy, FeedbackButton, BrowserModule],
|
||||
providers: []
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
kind: {
|
||||
control: 'select',
|
||||
options: ['user', 'llm'],
|
||||
description: "Type de message (utilisateur ou IA)"
|
||||
},
|
||||
content: {
|
||||
control: 'text',
|
||||
description: "Contenu du message"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<MessageBox>;
|
||||
|
||||
export const UserMessage: Story = {
|
||||
args: {
|
||||
kind: 'user',
|
||||
content: 'Voici un message de l\'utilisateur pour tester l\'affichage.',
|
||||
message: new ChatbotMessage({
|
||||
kind: 'user',
|
||||
user: {},
|
||||
content: 'Voici un message de l\'utilisateur pour tester l\'affichage.',
|
||||
name: 'User'
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export const AIMessage: Story = {
|
||||
args: {
|
||||
kind: 'llm',
|
||||
content: 'Je suis un assistant IA qui répond à vos questions. Voici une réponse détaillée qui peut contenir du <strong>HTML</strong> et des listes:<ul><li>Point 1</li><li>Point 2</li></ul>',
|
||||
message: new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: 'Je suis un assistant IA qui répond à vos questions. Voici une réponse détaillée qui peut contenir du <strong>HTML</strong> et des listes:<ul><li>Point 1</li><li>Point 2</li></ul>',
|
||||
name: 'Assistant'
|
||||
})
|
||||
}
|
||||
};
|
||||
export const AIMessageExpanded: Story = {
|
||||
args: {
|
||||
kind: 'llm',
|
||||
content: 'Je suis un assistant IA qui répond à vos questions. Voici une réponse détaillée qui peut contenir du <strong>HTML</strong> et des listes:<ul><li>Point 1</li><li>Point 2</li></ul>',
|
||||
message: new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: 'Je suis un assistant IA qui répond à vos questions. Voici une réponse détaillée qui peut contenir du <strong>HTML</strong> et des listes:<ul><li>Point 1</li><li>Point 2</li></ul>',
|
||||
name: 'Assistant'
|
||||
}),
|
||||
expanded: true,
|
||||
}
|
||||
};
|
||||
export const LongAIMessage: Story = {
|
||||
args: {
|
||||
kind: 'llm',
|
||||
content: `<p>Voici une réponse longue avec beaucoup de contenu pour tester comment le composant gère les grands blocs de texte.</p>
|
||||
<p>Les documents mettent également en évidence diverses mesures de sécurité post-incident mises en œuvre, notamment des procédures de formation améliorées, des protocoles de maintenance renforcés et des modifications de la conception des aéronefs. Des organismes de réglementation tels que le NTSB, le BEA, l'AAIB et d'autres ont participé à l'enquête sur ces incidents et ont formulé des recommandations de sécurité.</p>
|
||||
<p>Ce résumé donne un aperçu des incidents aéronautiques importants, de leurs causes et de leurs conséquences, ce qui peut être utile pour comprendre les schémas d'accidents d'aéronefs et les domaines à améliorer en matière de sécurité aérienne.</p>
|
||||
<br/>
|
||||
<p>Pas de recherche Internet activée.<br/>
|
||||
Aucune recherche Internet n'a été activée pour cette requête</p>
|
||||
<br/>
|
||||
<p>Résultats de recherche.</p>`,
|
||||
message: new ChatbotMessage({
|
||||
kind: 'llm',
|
||||
user: {},
|
||||
content: 'Contenu long...',
|
||||
name: 'Assistant'
|
||||
})
|
||||
}
|
||||
};
|
80
airwatch/src/app/chatbot/message-box/message-box.ts
Normal file
80
airwatch/src/app/chatbot/message-box/message-box.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
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';
|
||||
import {NgClass} from '@angular/common';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {ActionTypes, StateInterface} from '../../reducers';
|
||||
|
||||
|
||||
type MessageKind = "user" | "llm";
|
||||
|
||||
@Component({
|
||||
selector: 'app-message-box',
|
||||
imports: [
|
||||
Copy,
|
||||
FeedbackButton,
|
||||
NgClass
|
||||
],
|
||||
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 = "";
|
||||
expanded: boolean = true;
|
||||
displaySourcesPanelLarge: boolean = false;
|
||||
|
||||
constructor(private sanitizer: DomSanitizer,
|
||||
public store: Store<StateInterface>) {
|
||||
this.store.select(state => state.app.displaySourcesPanelLarge).subscribe(value => {
|
||||
this.displaySourcesPanelLarge = value;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
toggleSources() {
|
||||
console.log("TODO toggle sources")
|
||||
this.store.dispatch({
|
||||
type: ActionTypes.UPDATE_APP,
|
||||
payload: {
|
||||
displaySourcesPanelLarge: !this.displaySourcesPanelLarge
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleFullScreen() {
|
||||
console.log("TODO toggle fullscreen")
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
}
|
16
airwatch/src/app/chatbot/new-input/new-input.html
Normal file
16
airwatch/src/app/chatbot/new-input/new-input.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="new-input">
|
||||
<div class="main-conversation-container">
|
||||
<div class="welcome-text">
|
||||
<div class="welcome-icon">
|
||||
<img alt="chatbot image" src="/chatbot.png">
|
||||
</div>
|
||||
How can we
|
||||
<span class="emphasis">
|
||||
assist
|
||||
</span>
|
||||
you today?
|
||||
</div>
|
||||
<app-tools-options [hideDisabledButtons]="false"></app-tools-options>
|
||||
<app-prompt-input></app-prompt-input>
|
||||
</div>
|
||||
</div>
|
25
airwatch/src/app/chatbot/new-input/new-input.scss
Normal file
25
airwatch/src/app/chatbot/new-input/new-input.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
.new-input {
|
||||
padding: 0 200px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
min-height: 100vh;
|
||||
|
||||
.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
airwatch/src/app/chatbot/new-input/new-input.spec.ts
Normal file
23
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();
|
||||
});
|
||||
});
|
72
airwatch/src/app/chatbot/new-input/new-input.stories.ts
Normal file
72
airwatch/src/app/chatbot/new-input/new-input.stories.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import type {Meta, StoryObj} from '@storybook/angular';
|
||||
import {moduleMetadata} from '@storybook/angular';
|
||||
import {NewInput} from './new-input';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {PromptInput} from '../prompt-input/prompt-input';
|
||||
import {ToolsOptions} from '../tools-options/tools-options';
|
||||
import {Store, StoreModule} from '@ngrx/store';
|
||||
import {reducers} from '../../reducers';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {ApiService} from '../../services/api-service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
const appReducer = reducers.app;
|
||||
|
||||
const meta: Meta<NewInput> = {
|
||||
title: 'Chatbot/Input/NewInput',
|
||||
component: NewInput,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
PromptInput,
|
||||
ToolsOptions,
|
||||
HttpClientModule,
|
||||
StoreModule.forRoot({app: appReducer as any})
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
Store
|
||||
]
|
||||
})
|
||||
],
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{name: 'light', value: '#f5f5f5'},
|
||||
{name: 'dark', value: '#333'},
|
||||
],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<NewInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Propriétés par défaut
|
||||
}
|
||||
};
|
||||
|
||||
export const DarkTheme: Story = {
|
||||
args: {
|
||||
// Propriétés par défaut
|
||||
},
|
||||
parameters: {
|
||||
backgrounds: {default: 'dark'},
|
||||
store: {
|
||||
init: (store: Store) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_APP',
|
||||
payload: {
|
||||
theme: 'dark'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
16
airwatch/src/app/chatbot/new-input/new-input.ts
Normal file
16
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,33 @@
|
|||
<div class="options-dropdown is-mode-{{mode}}">
|
||||
<div class="button">
|
||||
<i class="ri-settings-2-line "></i>
|
||||
<div class="indicator"></div>
|
||||
</div>
|
||||
|
||||
<div class="dropwdown">
|
||||
<div class="dropdown-item" (click)="changeModeOption('auto')" [ngClass]="{
|
||||
'is-active' : 'auto' == mode
|
||||
}">
|
||||
<span class="radio"></span>
|
||||
<div class="label">
|
||||
Auto mode
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-item" (click)="changeModeOption('structured')" [ngClass]="{
|
||||
'is-active' : 'structured' == mode
|
||||
}">
|
||||
<span class="radio"></span>
|
||||
<div class="label">
|
||||
Structured list
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-item" (click)="changeModeOption('specific')" [ngClass]="{
|
||||
'is-active' : 'specific' == mode
|
||||
}">
|
||||
<span class="radio"></span>
|
||||
<div class="label">
|
||||
Specific question
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,59 @@
|
|||
.options-dropdown {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
top: 0.15em;
|
||||
right: 0;
|
||||
float: right;
|
||||
margin-right: -6em;
|
||||
|
||||
|
||||
|
||||
.dropwdown {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.button {
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.dropwdown {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 6px;
|
||||
width: 2em;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.is-mode-structured,
|
||||
.is-mode-other,
|
||||
{
|
||||
background: red;
|
||||
border-radius: 100px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
background: white;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
padding-left: 12px;
|
||||
|
||||
&.is-active {
|
||||
color: cornflowerblue;
|
||||
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: cornflowerblue;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OptionsDropdown } from './options-dropdown';
|
||||
|
||||
describe('OptionsDropdown', () => {
|
||||
let component: OptionsDropdown;
|
||||
let fixture: ComponentFixture<OptionsDropdown>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [OptionsDropdown]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OptionsDropdown);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {NgClass} from '@angular/common';
|
||||
|
||||
|
||||
export type optionsDropdownOptions = 'auto' | 'structured' | 'specific';
|
||||
|
||||
@Component({
|
||||
selector: 'app-options-dropdown',
|
||||
|
||||
templateUrl: './options-dropdown.html',
|
||||
imports: [
|
||||
NgClass
|
||||
],
|
||||
styleUrl: './options-dropdown.scss'
|
||||
})
|
||||
export class OptionsDropdown {
|
||||
|
||||
@Input() public mode: optionsDropdownOptions = 'auto'
|
||||
|
||||
changeModeOption(mode: optionsDropdownOptions) {
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
|
@ -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>
|
100
airwatch/src/app/chatbot/prompt-indicator/prompt-indicator.scss
Normal file
100
airwatch/src/app/chatbot/prompt-indicator/prompt-indicator.scss
Normal file
|
@ -0,0 +1,100 @@
|
|||
: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 or on hover
|
||||
.dropdown.is-active .dropdown-menu,
|
||||
.dropdown.is-hoverable:hover .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown:not(.is-active):not(:hover) .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 '../../services/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
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
16
airwatch/src/app/chatbot/prompt-input/prompt-input.html
Normal file
16
airwatch/src/app/chatbot/prompt-input/prompt-input.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="prompt-input">
|
||||
<!-- bottom input-->
|
||||
|
||||
|
||||
<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">
|
||||
|
||||
<button (click)="submitMessage()" class="submit">
|
||||
<i class="ri-send-plane-2-line"></i>
|
||||
</button>
|
||||
<app-options-dropdown></app-options-dropdown>
|
||||
|
||||
</div>
|
136
airwatch/src/app/chatbot/prompt-input/prompt-input.scss
Normal file
136
airwatch/src/app/chatbot/prompt-input/prompt-input.scss
Normal file
|
@ -0,0 +1,136 @@
|
|||
@use 'sae-lib/src/styles/_variables.scss' as variables;
|
||||
|
||||
.prompt-input {
|
||||
margin: 20px auto;
|
||||
background: variables.$neutral-white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 10px 10px;
|
||||
box-shadow: 0 5px 10px variables.$shadow-color;
|
||||
height: 3.8em;
|
||||
|
||||
|
||||
.column {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 326px;
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
@extend .is-shadowed !optional;
|
||||
float: left;
|
||||
|
||||
.is-expanded-right & {
|
||||
width: 968px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: variables.$neutral-white;
|
||||
padding: 10px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover {
|
||||
color: variables.$main-color-active;
|
||||
fill: variables.$main-color-active;
|
||||
background: 1px solid variables.$bg-color-button;
|
||||
}
|
||||
|
||||
&.submit {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
float: right;
|
||||
display: inline-block;
|
||||
z-index: 10;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.magic {
|
||||
padding: 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: variables.$bg-color-chat;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
// Dropdown styles
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&.is-active,
|
||||
&.is-hoverable:hover {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
23
airwatch/src/app/chatbot/prompt-input/prompt-input.spec.ts
Normal file
23
airwatch/src/app/chatbot/prompt-input/prompt-input.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
import type {Meta, StoryObj} from '@storybook/angular';
|
||||
import {moduleMetadata} from '@storybook/angular';
|
||||
import {PromptInput} from './prompt-input';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Store, StoreModule} from '@ngrx/store';
|
||||
import {reducers} from '../../reducers';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {ApiService} from '../../services/api-service';
|
||||
|
||||
const appReducer = reducers.app;
|
||||
|
||||
const meta: Meta<PromptInput> = {
|
||||
title: 'Chatbot/Input/PromptInput',
|
||||
component: PromptInput,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
StoreModule.forRoot({app: appReducer as any})
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
Store
|
||||
]
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
// Vous pouvez définir des contrôles pour les propriétés ici
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<PromptInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Les propriétés par défaut
|
||||
}
|
||||
};
|
||||
|
||||
export const WithPlaceholder: Story = {
|
||||
args: {
|
||||
// Les propriétés avec un placeholder personnalisé
|
||||
},
|
||||
parameters: {
|
||||
// Vous pouvez surcharger certains paramètres ici
|
||||
}
|
||||
};
|
||||
|
||||
export const LoadingState: Story = {
|
||||
args: {
|
||||
// Les propriétés de base
|
||||
},
|
||||
parameters: {
|
||||
store: {
|
||||
init: (store: Store) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_APP',
|
||||
payload: {
|
||||
loading: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
313
airwatch/src/app/chatbot/prompt-input/prompt-input.ts
Normal file
313
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 '../../services/api-service';
|
||||
import {interval, Subscription} from 'rxjs';
|
||||
import {OptionsDropdown} from '../options-dropdown/options-dropdown';
|
||||
|
||||
@Component({
|
||||
selector: 'app-prompt-input',
|
||||
imports: [CommonModule, FormsModule, OptionsDropdown],
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
16
airwatch/src/app/chatbot/source-block/source-block.html
Normal file
16
airwatch/src/app/chatbot/source-block/source-block.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div [ngClass]="{'expanded': expanded}" class="source">
|
||||
<i class="ri-file-3-line"></i>
|
||||
<a class="title">{{ source.title || 'Source sans nom' }}</a>
|
||||
|
||||
<div class="actions has-text-right">
|
||||
|
||||
<i class="ri-file-add-line"></i>
|
||||
<i (click)="toggleExpanded()" class="ri-arrow-down-s-line"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<div (click)="toggleExpanded()" class="description">
|
||||
|
||||
{{ source.description || description }}
|
||||
</div>
|
||||
</div>
|
53
airwatch/src/app/chatbot/source-block/source-block.scss
Normal file
53
airwatch/src/app/chatbot/source-block/source-block.scss
Normal file
|
@ -0,0 +1,53 @@
|
|||
//@use "remixicon/fonts/remixicon.scss";
|
||||
//@use "sae-lib/src/styles/variables";
|
||||
|
||||
|
||||
.source {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: white;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.expanded {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #1E1F22;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-left: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #005AA2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
color: #005AA2;
|
||||
float: right;
|
||||
|
||||
i {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #ECF3FA;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
23
airwatch/src/app/chatbot/source-block/source-block.spec.ts
Normal file
23
airwatch/src/app/chatbot/source-block/source-block.spec.ts
Normal file
|
@ -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,65 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { SourceBlock } from './source-block';
|
||||
import { ChatbotSource } from '../../services/conversations.service';
|
||||
import { moduleMetadata } from '@storybook/angular';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
const meta: Meta<SourceBlock> = {
|
||||
title: 'Chatbot/Sources/SourceBlock',
|
||||
component: SourceBlock,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [CommonModule],
|
||||
providers: []
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
source: {
|
||||
control: 'object',
|
||||
description: 'Objet contenant les informations de la source'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<SourceBlock>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
source: new ChatbotSource({
|
||||
title: 'Document technique.pdf',
|
||||
url: 'https://example.com/document-technique.pdf',
|
||||
description: 'Ce document contient des informations techniques sur les avions de ligne modernes et leur maintenance.'
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
export const ShortDescription: Story = {
|
||||
args: {
|
||||
source: new ChatbotSource({
|
||||
title: 'Rapport d\'accident.pdf',
|
||||
url: 'https://example.com/rapport-accident.pdf',
|
||||
description: 'Rapport concernant l\'incident de vol 123.'
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
source: new ChatbotSource({
|
||||
title: 'Documentation complète.pdf',
|
||||
url: 'https://example.com/documentation-complete.pdf',
|
||||
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.'
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutUrl: Story = {
|
||||
args: {
|
||||
source: new ChatbotSource({
|
||||
title: 'Document interne.pdf',
|
||||
description: 'Document à usage interne uniquement, sans lien externe.'
|
||||
})
|
||||
},
|
||||
};
|
20
airwatch/src/app/chatbot/source-block/source-block.ts
Normal file
20
airwatch/src/app/chatbot/source-block/source-block.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {ChatbotSource} from '../../services/conversations.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-source-block',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './source-block.html',
|
||||
styleUrl: './source-block.scss'
|
||||
})
|
||||
export class SourceBlock {
|
||||
|
||||
public expanded: boolean = false;
|
||||
@Input() source: ChatbotSource = {} as ChatbotSource;
|
||||
description: string = "lorem blah blah";
|
||||
|
||||
toggleExpanded() {
|
||||
this.expanded = !this.expanded
|
||||
}
|
||||
}
|
|
@ -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>
|
56
airwatch/src/app/chatbot/theme-selector/theme-selector.scss
Normal file
56
airwatch/src/app/chatbot/theme-selector/theme-selector.scss
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
55
airwatch/src/app/chatbot/theme-selector/theme-selector.ts
Normal file
55
airwatch/src/app/chatbot/theme-selector/theme-selector.ts
Normal file
|
@ -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});
|
||||
}
|
||||
}
|
16
airwatch/src/app/chatbot/time-separator/time-separator.html
Normal file
16
airwatch/src/app/chatbot/time-separator/time-separator.html
Normal file
|
@ -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>
|
35
airwatch/src/app/chatbot/time-separator/time-separator.scss
Normal file
35
airwatch/src/app/chatbot/time-separator/time-separator.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
.time-separator {
|
||||
padding: 0 16px;
|
||||
font-size: 0.8rem;
|
||||
color: grey;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
display: block;
|
||||
|
||||
.column {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
|
||||
|
||||
&.time-ago {
|
||||
color: #1E1F22;
|
||||
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 8px;
|
||||
|
||||
margin-top: -32px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
width: 100px !important;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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();
|
||||
});
|
||||
});
|
12
airwatch/src/app/chatbot/time-separator/time-separator.ts
Normal file
12
airwatch/src/app/chatbot/time-separator/time-separator.ts
Normal file
|
@ -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';
|
||||
|
||||
}
|
11
airwatch/src/app/chatbot/toggle-button/toggle-button.html
Normal file
11
airwatch/src/app/chatbot/toggle-button/toggle-button.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<button (click)="toggleState()"
|
||||
[ngClass]="{
|
||||
'is-hoverable' : tooltip?.length
|
||||
}"
|
||||
[title]="tooltip"
|
||||
class="togglable-button {{kind}} {{active ? 'is-active' : ''}}">
|
||||
@if (icon) {
|
||||
<span class="icon ri-{{icon}}"></span>
|
||||
}
|
||||
{{ label }}
|
||||
</button>
|
87
airwatch/src/app/chatbot/toggle-button/toggle-button.scss
Normal file
87
airwatch/src/app/chatbot/toggle-button/toggle-button.scss
Normal file
|
@ -0,0 +1,87 @@
|
|||
|
||||
.togglable-button {
|
||||
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 8px;
|
||||
font-weight: 600;
|
||||
|
||||
color: #005AA2;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
|
||||
display: inline-flex;
|
||||
padding: 10px 10px 12px 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 20px;
|
||||
background: transparent;
|
||||
|
||||
// Custom tooltip styling
|
||||
&[title] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.is-hoverable {
|
||||
&:hover::after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 100;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&.local {
|
||||
border: solid 1px #005aa2;
|
||||
color: #005aa2;
|
||||
|
||||
|
||||
&.is-active {
|
||||
background: #083b7d;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
color: #7FB8E7;
|
||||
border: solid 1px #7FB8E7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.outside {
|
||||
border: solid 1px #50d494;
|
||||
color: #50d494;
|
||||
|
||||
&:hover {
|
||||
color: #159256;
|
||||
border: solid 1px #159256;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #159256;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: solid 1px black;
|
||||
color: black;
|
||||
|
||||
&.is-active {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import type {Meta, StoryObj} from '@storybook/angular';
|
||||
import {ToggleButton} from './toggle-button';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||
const meta: Meta<ToggleButton> = {
|
||||
title: 'UI/Buttons/ToggleButton',
|
||||
component: ToggleButton,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: {control: 'text'},
|
||||
icon: {control: 'text'},
|
||||
tooltip: {control: 'text'},
|
||||
kind: {control: 'text'},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<ToggleButton>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
export const LocalButton: Story = {
|
||||
args: {
|
||||
label: 'Button local',
|
||||
kind: 'local',
|
||||
icon: 'arrow-right-circle-fill',
|
||||
tooltip: 'un tooltip détaillant l\'action ',
|
||||
},
|
||||
};
|
||||
export const LocalButtonActive: Story = {
|
||||
args: {
|
||||
label: 'Button local',
|
||||
kind: 'local',
|
||||
icon: 'award-line',
|
||||
active: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const OutsideButton: Story = {
|
||||
args: {
|
||||
label: 'Button outside',
|
||||
kind: 'outside',
|
||||
active: false,
|
||||
},
|
||||
};
|
||||
export const OutsideButtonActive: Story = {
|
||||
args: {
|
||||
label: 'Button outside',
|
||||
kind: 'outside',
|
||||
active: true,
|
||||
},
|
||||
};
|
26
airwatch/src/app/chatbot/toggle-button/toggle-button.ts
Normal file
26
airwatch/src/app/chatbot/toggle-button/toggle-button.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {NgClass} from '@angular/common';
|
||||
|
||||
type ToggleButtonKind = "local" | "outside";
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle-button',
|
||||
imports: [
|
||||
NgClass
|
||||
],
|
||||
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 = "";
|
||||
@Input() tooltip: string = "";
|
||||
@Output() stateChanged = new EventEmitter<boolean>();
|
||||
|
||||
toggleState() {
|
||||
this.active = !this.active;
|
||||
this.stateChanged.emit(this.active);
|
||||
}
|
||||
}
|
26
airwatch/src/app/chatbot/tools-options/tools-options.html
Normal file
26
airwatch/src/app/chatbot/tools-options/tools-options.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<div class="tools-options {{alignLeft ? 'has-text-left' : ''}}">
|
||||
@for (filter of filters; track filter.name) {
|
||||
@if (hideDisabledButtons) {
|
||||
@if (filter.enabled) {
|
||||
|
||||
<app-toggle-button
|
||||
[active]="filter.enabled"
|
||||
[icon]="filter.kind === 'local' ? 'database-2-line' : 'google-line'"
|
||||
[kind]="filter.kind"
|
||||
[label]="filter.name"
|
||||
[tooltip]="getTooltipForFilter(filter)"
|
||||
(stateChanged)="toggleFilter(filter, $event)">
|
||||
</app-toggle-button>
|
||||
}
|
||||
} @else {
|
||||
<app-toggle-button
|
||||
[active]="filter.enabled"
|
||||
[icon]="filter.kind === 'local' ? 'database-2-line' : 'google-line'"
|
||||
[kind]="filter.kind"
|
||||
[label]="filter.name"
|
||||
[tooltip]="getTooltipForFilter(filter)"
|
||||
(stateChanged)="toggleFilter(filter, $event)">
|
||||
</app-toggle-button>
|
||||
}
|
||||
}
|
||||
</div>
|
16
airwatch/src/app/chatbot/tools-options/tools-options.scss
Normal file
16
airwatch/src/app/chatbot/tools-options/tools-options.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
.tools-options {
|
||||
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: solid 0.5px #b7b7b7;
|
||||
|
||||
|
||||
|
||||
app-toggle-button {
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
+ app-toggle-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
23
airwatch/src/app/chatbot/tools-options/tools-options.spec.ts
Normal file
23
airwatch/src/app/chatbot/tools-options/tools-options.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
55
airwatch/src/app/chatbot/tools-options/tools-options.ts
Normal file
55
airwatch/src/app/chatbot/tools-options/tools-options.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
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 => {
|
||||
|
||||
console.log("filters", 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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getTooltipForFilter(filter: filterType): string {
|
||||
// Generate descriptive tooltips based on filter name and kind
|
||||
const kindDescription = filter.kind === 'local' ? 'Données internes' : 'Données externes';
|
||||
return `${filter.name} - ${kindDescription}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<p class="version">Version as of {{ versionInfo }}</p>
|
|
@ -0,0 +1,8 @@
|
|||
.version {
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
margin: 10px 0;
|
||||
}
|
23
airwatch/src/app/chatbot/version-infos/version-infos.spec.ts
Normal file
23
airwatch/src/app/chatbot/version-infos/version-infos.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
12
airwatch/src/app/chatbot/version-infos/version-infos.ts
Normal file
12
airwatch/src/app/chatbot/version-infos/version-infos.ts
Normal file
|
@ -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";
|
||||
|
||||
}
|
7
airwatch/src/app/chatbot/warning-bugs/warning-bugs.html
Normal file
7
airwatch/src/app/chatbot/warning-bugs/warning-bugs.html
Normal file
|
@ -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="<strong>AI can make mistakes</strong> - consider verifying important information."></sae-alert-box>
|
||||
}
|
0
airwatch/src/app/chatbot/warning-bugs/warning-bugs.scss
Normal file
0
airwatch/src/app/chatbot/warning-bugs/warning-bugs.scss
Normal file
23
airwatch/src/app/chatbot/warning-bugs/warning-bugs.spec.ts
Normal file
23
airwatch/src/app/chatbot/warning-bugs/warning-bugs.spec.ts
Normal file
|
@ -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,83 @@
|
|||
import type {Meta, StoryObj} from '@storybook/angular';
|
||||
import {moduleMetadata} from '@storybook/angular';
|
||||
import {WarningBugs} from './warning-bugs';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AlertBox} from 'sae-lib/alert-box/alert-box';
|
||||
import {Store} from '@ngrx/store';
|
||||
// import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
// Mock Store
|
||||
const storeMock = {};
|
||||
|
||||
const meta: Meta<WarningBugs> = {
|
||||
title: 'Chatbot/Feedback/WarningBugs',
|
||||
component: WarningBugs,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
CommonModule,
|
||||
AlertBox,
|
||||
// BrowserAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: Store,
|
||||
useValue: storeMock
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'The WarningBugs component displays either a warning or an error message based on the application state.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<WarningBugs>;
|
||||
|
||||
// Default state: Warning about AI making mistakes
|
||||
export const Warning: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Default state showing a warning about AI potentially making mistakes.'
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: Store,
|
||||
useValue: false
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
// Error state: System error message
|
||||
export const ErrorState: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Error state showing a system error message when the request cannot be processed.'
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: Store,
|
||||
useValue: true
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
};
|
27
airwatch/src/app/chatbot/warning-bugs/warning-bugs.ts
Normal file
27
airwatch/src/app/chatbot/warning-bugs/warning-bugs.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
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',
|
||||
standalone: true,
|
||||
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
airwatch/src/app/color-display/color-display.html
Normal file
15
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
airwatch/src/app/color-display/color-display.scss
Normal file
31
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
airwatch/src/app/color-display/color-display.spec.ts
Normal file
23
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();
|
||||
});
|
||||
});
|
97
airwatch/src/app/color-display/color-display.stories.ts
Normal file
97
airwatch/src/app/color-display/color-display.stories.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { ColorDisplay } from './color-display';
|
||||
import { moduleMetadata } from '@storybook/angular';
|
||||
import { NgStyle } from '@angular/common';
|
||||
|
||||
const meta: Meta<ColorDisplay> = {
|
||||
title: 'Design/Colors/ColorDisplay',
|
||||
component: ColorDisplay,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [NgStyle],
|
||||
providers: []
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
hexaCode: {
|
||||
control: 'text',
|
||||
description: "Code hexadécimal de la couleur"
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
description: "Nom de la couleur"
|
||||
},
|
||||
backgroundColor: {
|
||||
control: 'text',
|
||||
description: "Couleur de fond (si différente du code hexa)"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<ColorDisplay>;
|
||||
|
||||
export const Red: Story = {
|
||||
args: {
|
||||
hexaCode: '#cc0000',
|
||||
name: 'Rouge',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const Blue: Story = {
|
||||
args: {
|
||||
hexaCode: '#0000cc',
|
||||
name: 'Bleu',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const Green: Story = {
|
||||
args: {
|
||||
hexaCode: '#00cc00',
|
||||
name: 'Vert',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const Yellow: Story = {
|
||||
args: {
|
||||
hexaCode: '#ffcc00',
|
||||
name: 'Jaune',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const Purple: Story = {
|
||||
args: {
|
||||
hexaCode: '#9900cc',
|
||||
name: 'Violet',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const Orange: Story = {
|
||||
args: {
|
||||
hexaCode: '#ff6600',
|
||||
name: 'Orange',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const Black: Story = {
|
||||
args: {
|
||||
hexaCode: '#000000',
|
||||
name: 'Noir',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const White: Story = {
|
||||
args: {
|
||||
hexaCode: '#ffffff',
|
||||
name: 'Blanc',
|
||||
backgroundColor: ''
|
||||
}
|
||||
};
|
22
airwatch/src/app/color-display/color-display.ts
Normal file
22
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
|
||||
}
|
||||
}
|
||||
}
|
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