Skip to content

Commit

Permalink
[sc-4887] upload questions answers (#817)
Browse files Browse the repository at this point in the history
* Replace our custom CSV parser by PapaParse

  - Add unit tests for our CSV parser
  - rename utils to csv-parser

* Upload Q&A flow

* Fix conversation style + support <br>
  • Loading branch information
mpellerin42 authored Jun 8, 2023
1 parent 3eb493c commit accb778
Show file tree
Hide file tree
Showing 27 changed files with 382 additions and 85 deletions.
80 changes: 80 additions & 0 deletions app.babel
Original file line number Diff line number Diff line change
Expand Up @@ -17979,6 +17979,86 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>upload.q_and_a</name>
<description/>
<comment/>
<default_text/>
<translations>
<translation>
<language>ca-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>upload.qna_csv_description</name>
<description/>
<comment/>
<default_text/>
<translations>
<translation>
<language>ca-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>upload.qna_csv_preview.answer</name>
<description/>
<comment/>
<default_text/>
<translations>
<translation>
<language>ca-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>upload.qna_csv_preview.question</name>
<description/>
<comment/>
<default_text/>
<translations>
<translation>
<language>ca-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>upload.select_csv</name>
<description/>
Expand Down
3 changes: 2 additions & 1 deletion apps/search-widget-demo/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
// let kb = '5c2bc432-a579-48cd-b408-4271e5e7a43c'; // medias
// let kb = 'f5d0ec7f-9ac3-46a3-b284-a38d5333d9e6'; // le petit prince
// let kb = '89ffdada-58ee-4199-8303-ad1450de1cbe'; // multiple types
let kb = '1154f6a1-af3c-4a19-9039-35466f024448'; // Knowledge graph (daria wiki + an article)
// let kb = '1154f6a1-af3c-4a19-9039-35466f024448'; // Knowledge graph (daria wiki + an article)
// let kb = '096d9070-f7be-40c8-a24c-19c89072e3ff'; // e2e permanent
// let kb = 'dba8dfe3-cbde-4aa6-9b90-57fdd2503672'; // conversations
let kb = '02701e7d-2e67-4671-9b63-6634328ba0d6'; // Q&A Nuclia
const backend = 'https://stashify.cloud/api';
// const backend = 'https://nuclia.cloud/api';
Expand Down
4 changes: 4 additions & 0 deletions libs/common/src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,10 @@
"upload.link": "Afegeix enllaços",
"upload.once_uploaded": "Un cop pujats, des de la pàgina \"Llistat de recursos\", podràs veure'n l'estat. Quan el semàfor del recurs sigui verd el podràs cercar i visualitzar.",
"upload.progress-dialog-title": "Cua de pujades",
"upload.q_and_a": "Carregueu preguntes i respostes",
"upload.qna_csv_description": "Creeu un recurs de preguntes i respostes seleccionant un fitxer CSV amb 2 columnes: <code>Pregunta, resposta</code>",
"upload.qna_csv_preview.answer": "Resposta",
"upload.qna_csv_preview.question": "Pregunta",
"upload.select_csv": "Seleccionar CSV",
"upload.select_file": "Arrossegueu els fitxers o feu clic aquí",
"upload.size_warning_1": "Els fitxers multimèdia no poden superar {{ maxMedia }} i els fitxers no multimèdia {{ max }}.",
Expand Down
4 changes: 4 additions & 0 deletions libs/common/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,10 @@
"upload.progress-dialog-title": "Upload queue",
"upload.progress.conflicts": "Conflicts",
"upload.progress.failed": "Failed",
"upload.q_and_a": "Upload Q&A",
"upload.qna_csv_description": "Create a Q&A resource by selecting a CSV file with 2 columns: <code>Question, Answer</code>",
"upload.qna_csv_preview.answer": "Answer",
"upload.qna_csv_preview.question": "Question",
"upload.select_csv": "Select CSV",
"upload.select_file": "Drop your file(s) or click here",
"upload.size_warning_1": "Media files can not exceed {{ maxMedia }} and non-media files {{ max }}.",
Expand Down
4 changes: 4 additions & 0 deletions libs/common/src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,10 @@
"upload.link": "Agregar enlaces",
"upload.once_uploaded": "Una vez subidos, desde la página \"Listado de recursos\", podrás ver su estado. Cuando el semáforo del recurso sea verde lo podrás buscar y visualizar.",
"upload.progress-dialog-title": "Cola de subidas",
"upload.q_and_a": "Cargar preguntas y respuestas",
"upload.qna_csv_description": "Cree un recurso de preguntas y respuestas seleccionando un archivo CSV con 2 columnas: <code>Pregunta, Respuesta</code>",
"upload.qna_csv_preview.answer": "Responder",
"upload.qna_csv_preview.question": "Pregunta",
"upload.select_csv": "Seleccionar CSV",
"upload.select_file": "Arrastre sus archivos o haga clic aquí",
"upload.size_warning_1": "Los archivos multimedia no pueden exceder {{ maxMedia }} y los archivos no multimedia {{ max }}.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@
(selectOption)="upload('csv')">
{{ 'upload.upload_from_csv' | translate }}
</pa-option>
<pa-option
*ngIf="isQnAEnabled | async"
icon="chat"
(selectOption)="upload('qna')">
{{ 'upload.q_and_a' | translate }}
</pa-option>
</pa-dropdown>
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UploadDialogService } from './upload-dialog.service';
import { UploadDialogService, UploadType } from './upload-dialog.service';
import { FeatureFlagService } from '@flaps/core';
import { shareReplay } from 'rxjs/operators';

@Component({
selector: 'stf-upload-button',
templateUrl: './upload-button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UploadButtonComponent implements OnInit {
constructor(private uploadService: UploadDialogService) {}
isQnAEnabled = this.feature.isFeatureEnabled('upload-q-and-a').pipe(shareReplay(1));

constructor(private uploadService: UploadDialogService, private feature: FeatureFlagService) {}

ngOnInit(): void {}

upload(type: 'files' | 'folder' | 'link' | 'csv') {
upload(type: UploadType) {
this.uploadService.upload(type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Injectable } from '@angular/core';
import { CreateLinkComponent, UploadFilesDialogComponent, UploadTextComponent } from '@flaps/common';
import { SisModalService } from '@nuclia/sistema';
import { ModalRef } from '@guillotinaweb/pastanaga-angular';
import { UploadQnaComponent } from '../../upload/upload-qna/upload-qna.component';

export type UploadType = 'files' | 'folder' | 'link' | 'csv';
export type UploadType = 'files' | 'folder' | 'link' | 'csv' | 'qna';

@Injectable({
providedIn: 'root',
Expand All @@ -20,6 +21,8 @@ export class UploadDialogService {
return this.uploadLink();
case 'csv':
return this.uploadCsv();
case 'qna':
return this.uploadQnA();
}
}

Expand All @@ -37,4 +40,8 @@ export class UploadDialogService {
private uploadCsv(): ModalRef {
return this.modal.openModal(UploadTextComponent);
}

private uploadQnA(): ModalRef {
return this.modal.openModal(UploadQnaComponent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Observable, of, switchMap } from 'rxjs';
import { SisToastService } from '@nuclia/sistema';
import { IErrorMessages, ModalRef } from '@guillotinaweb/pastanaga-angular';
import { UploadService } from '../upload.service';
import { parseCsvLabels } from '../utils';
import { parseCsvLabels } from '../csv-parser';
import { StandaloneService } from '../../services';

interface Row {
Expand Down
41 changes: 41 additions & 0 deletions libs/common/src/lib/upload/csv-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { parseCsv } from '@flaps/common';

describe('csv-parser', () => {
describe('parseCsv', () => {
it('should parse simple csv string into a string[][]', () => {
const simpleCsv = `row1 column1,row1 column2\nrow2 column1,row2 column2`;
const simpleCsvParsed = [
['row1 column1', 'row1 column2'],
['row2 column1', 'row2 column2'],
];
expect(parseCsv(simpleCsv)).toEqual(simpleCsvParsed);
});

it('should parse csv with quotes (quotes are doubled: "")', () => {
const csvWithQuotes = `"row1 ""column1""",row1 column2\n"row2 ""column1""",row2 column2`;
const csvWithQuotesParsed = [
['row1 "column1"', 'row1 column2'],
['row2 "column1"', 'row2 column2'],
];
expect(parseCsv(csvWithQuotes)).toEqual(csvWithQuotesParsed);
});

it('should parse csv with special characters (cell containing comma or line breaks are wrapped into double quotes)', () => {
const csvWithSpecialChars = `"row1, column1","row1\ncolumn2"\n"row2, column1","row2\ncolumn2"`;
const csvWithSpecialCharsParsed = [
['row1, column1', 'row1\ncolumn2'],
['row2, column1', 'row2\ncolumn2'],
];
expect(parseCsv(csvWithSpecialChars)).toEqual(csvWithSpecialCharsParsed);
});

it('should parse csv where a cell is entirely wrapped into quotes (in that case double quotes are tripled)', () => {
const csvWrappedInQuotes = `row1 column1,"""row1 column2"""\nrow2 column1,row2 column2`;
const csvWrappedInQuotesParsed = [
['row1 column1', '"row1 column2"'],
['row2 column1', 'row2 column2'],
];
expect(parseCsv(csvWrappedInQuotes)).toEqual(csvWrappedInQuotesParsed);
});
});
});
22 changes: 22 additions & 0 deletions libs/common/src/lib/upload/csv-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Classification } from '@nuclia/core';
import { parse } from 'papaparse';

const SLUG_REGEX = /^[a-zA-Z0-9-_]+$/;

// CSV parser following RFC 4180: https://github.com/mholt/PapaParse
export function parseCsv(content: string): string[][] {
const parseResult = parse<string[]>(content, {});
return parseResult.data;
}

// Parse labels like: 'labelset1/label1|labelset2/label2'
export function parseCsvLabels(labels: string): Classification[] | null {
if (labels.length === 0) return [];
let isValid = true;
const parsedLabels = labels.split('|').map((label) => {
const items = label.split('/');
isValid &&= items.length === 2 && SLUG_REGEX.test(items[0].trim()) && items[1].trim().length > 0;
return { labelset: items[0]?.trim(), label: items[1]?.trim() };
});
return isValid ? parsedLabels : null;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { coerceNumberProperty } from '@angular/cdk/coercion';
import { parseCsv } from '../utils';
import { parseCsv } from '../csv-parser';
import { SisModalService, SisToastService } from '@nuclia/sistema';
import { ConfirmationData, Kind } from '@guillotinaweb/pastanaga-angular';
import { filter } from 'rxjs/operators';
Expand Down
2 changes: 1 addition & 1 deletion libs/common/src/lib/upload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export * from './upload-progress/upload-progress-dialog.component';
export * from './upload-text/upload-text.component';
export * from './upload.module';
export * from './upload.service';
export * from './utils';
export * from './csv-parser';
54 changes: 54 additions & 0 deletions libs/common/src/lib/upload/upload-qna/upload-qna.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<pa-modal-advanced fitContent>
<pa-modal-title>
{{ 'upload.q_and_a' | translate }}
</pa-modal-title>
<pa-modal-content>
<div class="upload-qna-content">
<ng-container *ngIf="standalone">
<div
*ngIf="(hasValidKey | async) === false"
class="warning body-s"
[innerHTML]="'standalone.upload-no-nua-key' | translate"></div>
</ng-container>

<p
class="body-m"
[innerHTML]="'upload.qna_csv_description' | translate"></p>

<pa-input [formControl]="resourceTitle">Resource title</pa-input>

<app-csv-select
[buttonKind]="qna.length === 0 ? 'primary' : 'secondary'"
[fields]="2"
(select)="displayCsv($event)"></app-csv-select>

<pa-table
*ngIf="qna.length > 0"
class="csv-preview"
columns="repeat(2,1fr)">
<pa-table-header>
<pa-table-cell header>{{ 'upload.qna_csv_preview.question' | translate }}</pa-table-cell>
<pa-table-cell header>{{ 'upload.qna_csv_preview.answer' | translate }}</pa-table-cell>
</pa-table-header>
<pa-table-row *ngFor="let row of qna">
<pa-table-cell>{{ row[0] }}</pa-table-cell>
<pa-table-cell>{{ row[1] }}</pa-table-cell>
</pa-table-row>
</pa-table>
</div>
</pa-modal-content>
<pa-modal-footer>
<pa-button
aspect="basic"
(click)="close()">
{{ 'generic.cancel' | translate }}
</pa-button>
<pa-button
type="submit"
kind="primary"
(click)="upload()"
[disabled]="resourceTitle.invalid || qna.length === 0 || isUploading">
{{ 'generic.add' | translate }}
</pa-button>
</pa-modal-footer>
</pa-modal-advanced>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import 'apps/dashboard/src/variables';

.upload-qna-content {
display: flex;
flex-direction: column;
gap: rhythm(3);
}
Loading

0 comments on commit accb778

Please sign in to comment.