dropdowns added to airwatch

This commit is contained in:
Tykayn 2025-08-20 12:47:37 +02:00 committed by tykayn
parent fff843e959
commit 316ebc0acc
15 changed files with 519 additions and 192 deletions

View file

@ -158,8 +158,8 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
"license": "MIT",
"peer": true
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
@ -175,8 +175,8 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.6.0.tgz",
"integrity": "sha512-bKM5odjqE1yzVxEZGJE7F79WHhNrJFIKHXR+GG+P1IWXn8AnJZhl8SbIRDJsNAvIqx4VPkNwjuHfc42tutMDpQ==",
"license": "Apache-2.0",
"peer": true
"dev": true,
"license": "Apache-2.0"
},
"node_modules/rxjs": {
"version": "7.8.2",

View file

@ -10,6 +10,10 @@
"dependencies": {
"tslib": "^2.3.0"
},
"devDependencies": {
"bulma": "^1.0.4",
"remixicon": "^4.6.0"
},
"peerDependencies": {
"@angular/common": "^20.1.0",
"@angular/core": "^20.1.0",
@ -175,8 +179,8 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
"license": "MIT",
"peer": true
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
@ -192,8 +196,8 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.6.0.tgz",
"integrity": "sha512-bKM5odjqE1yzVxEZGJE7F79WHhNrJFIKHXR+GG+P1IWXn8AnJZhl8SbIRDJsNAvIqx4VPkNwjuHfc42tutMDpQ==",
"license": "Apache-2.0",
"peer": true
"dev": true,
"license": "Apache-2.0"
},
"node_modules/rxjs": {
"version": "7.8.2",

View file

@ -5,12 +5,11 @@
"@angular/common": "^20.1.0",
"@angular/core": "^20.1.0",
"@angular/forms": "^20.1.0",
"@ngrx/store": "^20.0.0",
"@ngrx/store-devtools": "^20.0.0",
"bulma": "^1.0.4",
"remixicon": "^4.6.0",
"shepherd.js": "^14.5.1",
"@ngrx/store": "^20.0.0",
"@ngrx/store-devtools": "^20.0.0"
"shepherd.js": "^14.5.1"
},
"dependencies": {
"tslib": "^2.3.0"
@ -20,5 +19,9 @@
".": {
"sass": "./src/styles/index.scss"
}
},
"devDependencies": {
"bulma": "^1.0.4",
"remixicon": "^4.6.0"
}
}

View file

@ -172,7 +172,7 @@
align-items: center;
flex-shrink: 0;
&:hover{
&:hover {
background: blue;
color: white;
}
@ -234,9 +234,44 @@
top: 0;
right: 0;
background: white;
width: 30%;
width: 24px;
height: 24px;
border-radius: 8px;
float: right;
.trash {
color: red;
fill: red;
}
.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;
}
}
}
button {
background: transparent;
border: none;
@ -251,7 +286,6 @@
&:hover {
button {
display: block;
}
}
@ -275,6 +309,13 @@
border-radius: 8px;
color: #6D717C;
.actions {
width: 28px;
height: 28px;
border-radius: 6px;
background: #B9D6ED;
}
&:hover {
background: #3B87CC1A;
}
@ -295,8 +336,7 @@
.pinned-icon {
position: relative;
left: -1.5em;
left: -1.7em;
i {
color: #FEC553;
@ -312,9 +352,10 @@
color: #1E1F22;
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
display: block;
margin-bottom: 4px;
margin-top: -1.5em;
margin-left: 0;
}
.description {
@ -411,6 +452,9 @@
padding-right: 60px;
overflow: auto;
min-width: 915px;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.main-conversation-container {

View file

@ -22,7 +22,6 @@
"@sjmc11/tourguidejs": "^0.0.27",
"angular-shepherd": "^20.0.0",
"express": "^5.1.0",
"remixicon": "^4.6.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@ -50,6 +49,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"remixicon": "^4.6.0",
"storybook": "^9.0.17",
"typescript": "~5.8.2"
}
@ -14959,6 +14959,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.6.0.tgz",
"integrity": "sha512-bKM5odjqE1yzVxEZGJE7F79WHhNrJFIKHXR+GG+P1IWXn8AnJZhl8SbIRDJsNAvIqx4VPkNwjuHfc42tutMDpQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/renderkid": {

View file

@ -39,7 +39,6 @@
"@sjmc11/tourguidejs": "^0.0.27",
"angular-shepherd": "^20.0.0",
"express": "^5.1.0",
"remixicon": "^4.6.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@ -67,6 +66,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"remixicon": "^4.6.0",
"storybook": "^9.0.17",
"typescript": "~5.8.2"
}

View file

@ -119,13 +119,13 @@
@if (appState.displayConversationListPanelLarge) {
<!-- <div class="search-button is-expanded is-clickable">-->
<!-- <i class="ri-search-line"></i>-->
<!-- <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>-->
<!-- <input type="text" class="no-borders" placeholder="Search a chat" value="blah">-->
<!-- <i class="funnel ri-filter-3-line"></i>-->
<!-- </div>-->
<!-- </div>-->
} @else {
<div class="search-button is-clickable">
@ -139,16 +139,45 @@
<div class="conversation-item"
[ngClass]="{'is-active' : conversation.name == activeConversation?.name }"
(click)="updateActiveConversation(conversation)">
<!-- @if (conversation.pinned) {-->
<!-- }-->
<!-- <i class="ri-arrow-right-s-line"></i>-->
<div class="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)="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>
<!-- pinned-->
</span>
} @else {
<div class="pinned-icon llm-avatar">
</div>
}
<span class="name">
{{ conversation.name || 'New Conversation' }}
</span>
@ -163,28 +192,7 @@
[ngClass]="{'is-expanded': ! conversation.visibleInput}">
<!-- <i class="ri-pencil-line" (click)="toggleVisibleEditInput(conversation)"></i>-->
</div>
<div class="actions">
<div class="menu">
<!-- menu-->
<i class="ri-more-2-line"></i>
</div>
<div class="dropdown">
<button (click)="pinConversation(conversation)">
<i class="ri-pushpin-fill"></i>
<span class="label">
pin
</span>
</button>
<button (click)="copyConversationLink(conversation)">
<i class="ri-link-m"></i>
<span class="label">
copy
</span>
</button>
</div>
</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"/>
@ -216,7 +224,6 @@
<div class="chat chat-column ">
<div id="newChatBar">
<div class="new-chat-title">
<div class="title">
{{ translate('newChat') }}
@ -237,12 +244,29 @@
</div>
<div class="action">
@if (displayExportConversation) {
<div class="export-chat-button is-clickable">
<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>
@ -299,16 +323,38 @@
<div [ngClass]="{'expanded': appState.displaySourcesPanelLarge}" class="column panel-more">
<div class="has-text-right">
<div class="export-chat-button is-clickable">
<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>
<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) {
@ -320,6 +366,9 @@
</div>
}
</div>
<div class="bottom-gradient">
</div>
</div>
</div>

View file

@ -1,12 +1,13 @@
@use "sae-lib/src/styles/variables";
@use "sae-lib/src/styles/variables.scss" as variables;
.panel-more-inside {
border-radius: 10px;
background: #F5F5F5;
padding: 20px 16px;
min-height: 100vh;
height: 100vh;
margin-top: 14px;
.main-title {
color: #1E1F22;
@ -18,12 +19,34 @@
.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 {
@ -82,6 +105,62 @@
}
}
// 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: 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;
}
}
}
.aside-toggle-button {
color: white;
color: #FFF;
@ -96,3 +175,12 @@
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;
}

View file

@ -51,6 +51,8 @@ export class Chatbot implements AfterViewChecked {
// 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;
@ -139,19 +141,6 @@ export class Chatbot implements AfterViewChecked {
// Add three example sources
let source1 = new ChatbotSource();
source1.title = "abc-1258.pdf";
source1.url = "https://example.com/source1";
source1.description = "Admittedly Air Traffic Control in smaller airports are being effected by ...";
let source2 = new ChatbotSource();
source2.title = "abc-45689.pdf";
source2.url = "https://example.com/source1";
source2.description = "DE GAGNE DATE: JANUARY 18, 2008 VEN #: V6-CAW-M2700-10 APPROVED BY: MARTIN SWAN DATE: FEBRUARY 5, 2008 -TITLE- DHC-6 ELEVATOR CONTROL CABLE WEAR SURVEY RESULTS ISSUE: 2 PAGE 2 respondents, representing 16 aircraft, operate outside the tropics and have a cycle/hour ratio of 1.6 to 2.8. Most respondents have reported that carbon steel elevator control cables are more wear resistant than stainless steel cables. Two operators in the tropics, representing 39 aircraft, use.\n" +
"I remember having reconunended while dealing with the Guwahati crash in regard to a Vayudoot aircraft that the National Airports Authority should ensure that only trained and";
updatedConversation.sources = [source1, source2];
const conversation2 = new ChatbotConversation();
conversation2.name = "Liste de courses";
conversation2.addMessage(new ChatbotMessage({
@ -174,6 +163,20 @@ export class Chatbot implements AfterViewChecked {
}));
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);
@ -272,16 +275,85 @@ export class Chatbot implements AfterViewChecked {
}
exportMessage() {
toggleExportDropdown() {
this.isExportDropdownActive = !this.isExportDropdownActive;
// Close the other dropdown if it's open
if (this.isExportDropdownActive) {
this.isExportSourcesDropdownActive = false;
}
}
exportConversation() {
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) {
@ -324,6 +396,11 @@ export class Chatbot implements AfterViewChecked {
}
deleteConversation(conversation: ChatbotConversation) {
console.log('todo delete conversation', conversation);
}
/**
* Scrolls to the bottom of the conversation container
*/

View file

@ -63,7 +63,14 @@
</button>
<button (click)="toggleSources()" class="button sources">
<i class="ri-book-2-fill"></i>
@if (displaySourcesPanelLarge) {
hide sources
} @else {
see sources
}
</button>
</div>
}

View file

@ -26,9 +26,7 @@
}
.message {
.message {
.user-more-infos {
margin-top: -35px;
@ -141,10 +139,10 @@
}
}
}
}
.expanded-message-fullscreen {
.expanded-message-fullscreen {
display: none;
width: 50%;
@ -153,5 +151,10 @@
padding: 20px;
background: #ccc;
border-radius: 3px;
position: relative;
top: 0;
left: 0;
z-index: 100;
}
}
}

View file

@ -6,7 +6,7 @@ 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 {StateInterface} from '../../reducers';
import {ActionTypes, StateInterface} from '../../reducers';
type MessageKind = "user" | "llm";
@ -30,8 +30,14 @@ export class MessageBox implements OnChanges {
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;
});
constructor(private sanitizer: DomSanitizer, private store: Store<StateInterface>) {
}
ngOnChanges(changes: SimpleChanges): void {
@ -60,9 +66,9 @@ export class MessageBox implements OnChanges {
toggleSources() {
console.log("TODO toggle sources")
this.store.dispatch({
type: 'UPDATE_APP',
type: ActionTypes.UPDATE_APP,
payload: {
displaySourcesPanelLarge: !this.store.select(state => state.app.displaySourcesPanelLarge)
displaySourcesPanelLarge: !this.displaySourcesPanelLarge
}
})
}

View file

@ -89,11 +89,12 @@
}
}
// Ensure dropdown is displayed when active
.dropdown.is-active .dropdown-menu {
// 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) .dropdown-menu {
.dropdown:not(.is-active):not(:hover) .dropdown-menu {
display: none;
}

View file

@ -68,7 +68,8 @@
position: relative;
display: inline-block;
&.is-active {
&.is-active,
&.is-hoverable:hover {
.dropdown-menu {
display: block;
}

View file

@ -0,0 +1,43 @@
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { ActionTypes, StateInterface } from './reducers';
/**
* This is a test script to verify that the toggleSources function correctly updates the Redux store.
*
* To use this script:
* 1. Import it in a component where you want to test
* 2. Call the testToggleSourcesAction() function
* 3. Check the console logs to verify the state changes
*/
export class ToggleSourcesTest {
constructor(private store: Store<StateInterface>) {}
testToggleSourcesAction() {
console.log('Testing toggleSources action...');
// Get the current state of displaySourcesPanelLarge
let currentState = false;
this.store.select(state => state.app.displaySourcesPanelLarge).subscribe(value => {
currentState = value;
console.log('Current displaySourcesPanelLarge state:', currentState);
});
// Dispatch the action to toggle the state
console.log('Dispatching UPDATE_APP action to toggle displaySourcesPanelLarge...');
this.store.dispatch({
type: ActionTypes.UPDATE_APP,
payload: {
displaySourcesPanelLarge: !currentState
}
});
// Verify the state has changed
setTimeout(() => {
this.store.select(state => state.app.displaySourcesPanelLarge).subscribe(value => {
console.log('New displaySourcesPanelLarge state:', value);
console.log('State changed correctly:', value !== currentState);
});
}, 100);
}
}