mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[ACS-9427] support md format of response for knowledge retrieval (#4502)
* [ACS-9427] Added library for markdown format * [ACS-9427] Display tooltip with mermaid source code and preprocess markdown format * [ACS-9427] Rendering mathematical formulas * [ACS-9427] Styling tables * [ACS-9427] Added support for programming code language in MD * [ACS-9427] Fixed existing unit tests for search ai results component * [ACS-9427] Unit tests for markdown data and rendering of markdown * [ACS-9427] Unit tests for markdown containing mermaid and latex blocks * [ACS-9427] Reverted unwanted change * [ACS-9427] Fixed Sonar issues for regex * [ACS-9427] Fixed Sonar issues for regex * [ACS-9427] Fixed Sonar issues for regex * [ACS-9427] Fixed Sonar issues for regex * [ACS-9427] Fixed unit test * [ACS-9427] Fixed unit test * [ACS-9427] Fixed unit test
This commit is contained in:
parent
46a7113e32
commit
6a3c888f18
@ -89,10 +89,21 @@
|
|||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/cropperjs/dist/cropper.min.css",
|
"node_modules/cropperjs/dist/cropper.min.css",
|
||||||
"node_modules/pdfjs-dist/web/pdf_viewer.css",
|
"node_modules/pdfjs-dist/web/pdf_viewer.css",
|
||||||
|
"node_modules/katex/dist/katex.min.css",
|
||||||
|
"node_modules/prismjs/themes/prism-okaidia.css",
|
||||||
"projects/aca-content/src/lib/ui/application.scss",
|
"projects/aca-content/src/lib/ui/application.scss",
|
||||||
"app/src/styles.scss"
|
"app/src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": ["node_modules/pdfjs-dist/build/pdf.js", "node_modules/pdfjs-dist/web/pdf_viewer.js"],
|
"scripts": [
|
||||||
|
"node_modules/pdfjs-dist/build/pdf.js",
|
||||||
|
"node_modules/pdfjs-dist/web/pdf_viewer.js",
|
||||||
|
"node_modules/mermaid/dist/mermaid.min.js",
|
||||||
|
"node_modules/katex/dist/katex.min.js",
|
||||||
|
"node_modules/katex/dist/contrib/auto-render.min.js",
|
||||||
|
"node_modules/prismjs/prism.js",
|
||||||
|
"node_modules/prismjs/components/prism-csharp.min.js",
|
||||||
|
"node_modules/prismjs/components/prism-css.min.js"
|
||||||
|
],
|
||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"buildOptimizer": false,
|
"buildOptimizer": false,
|
||||||
|
2122
package-lock.json
generated
2122
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -54,9 +54,13 @@
|
|||||||
"@ngrx/store-devtools": "17.0.1",
|
"@ngrx/store-devtools": "17.0.1",
|
||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"katex": "^0.16.21",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
|
"mermaid": "^11.5.0",
|
||||||
"minimatch-browser": "^1.0.0",
|
"minimatch-browser": "^1.0.0",
|
||||||
|
"ngx-markdown": "^17.2.1",
|
||||||
"pdfjs-dist": "3.3.122",
|
"pdfjs-dist": "3.3.122",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"zone.js": "0.14.8"
|
"zone.js": "0.14.8"
|
||||||
|
@ -11,5 +11,11 @@ module.exports = function (config) {
|
|||||||
...baseConfig.coverageReporter,
|
...baseConfig.coverageReporter,
|
||||||
dir: join(__dirname, '../../coverage/aca-content'),
|
dir: join(__dirname, '../../coverage/aca-content'),
|
||||||
},
|
},
|
||||||
|
files: [
|
||||||
|
'../../node_modules/katex/dist/katex.min.js',
|
||||||
|
'../../node_modules/katex/dist/contrib/auto-render.min.js',
|
||||||
|
'../../node_modules/katex/dist/katex.min.css',
|
||||||
|
'../../node_modules/mermaid/dist/mermaid.min.js'
|
||||||
|
]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -79,6 +79,7 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro
|
|||||||
import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component';
|
import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component';
|
||||||
import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component';
|
import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component';
|
||||||
import { SaveSearchSidenavComponent } from './components/search/search-save/sidenav/save-search-sidenav.component';
|
import { SaveSearchSidenavComponent } from './components/search/search-save/sidenav/save-search-sidenav.component';
|
||||||
|
import { MarkdownModule } from 'ngx-markdown';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -100,7 +101,8 @@ import { SaveSearchSidenavComponent } from './components/search/search-save/side
|
|||||||
AcaFolderRulesModule,
|
AcaFolderRulesModule,
|
||||||
CreateFromTemplateDialogComponent,
|
CreateFromTemplateDialogComponent,
|
||||||
OpenInAppComponent,
|
OpenInAppComponent,
|
||||||
UploadFilesDialogComponent
|
UploadFilesDialogComponent,
|
||||||
|
MarkdownModule.forRoot()
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ContentVersionService, useClass: ContentUrlService },
|
{ provide: ContentVersionService, useClass: ContentUrlService },
|
||||||
|
@ -21,11 +21,13 @@
|
|||||||
<div
|
<div
|
||||||
class="aca-search-ai-response-container-body"
|
class="aca-search-ai-response-container-body"
|
||||||
*ngIf="!hasAnsweringError">
|
*ngIf="!hasAnsweringError">
|
||||||
<div
|
<markdown
|
||||||
class="aca-search-ai-response-container-body-response"
|
class="aca-search-ai-response-container-body-response"
|
||||||
data-automation-id="aca-search-ai-results-response">
|
data-automation-id="aca-search-ai-results-response"
|
||||||
{{ queryAnswer?.answer }}
|
[data]="displayedAnswer"
|
||||||
</div>
|
(ready)="addSourceCodeTooltips()"
|
||||||
|
mermaid
|
||||||
|
katex />
|
||||||
<button
|
<button
|
||||||
class="aca-search-ai-response-container-body-response-action aca-search-ai-response-container-body-response-action-regeneration"
|
class="aca-search-ai-response-container-body-response-action aca-search-ai-response-container-body-response-action-regeneration"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
|
@ -122,6 +122,35 @@
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
box-shadow: 0 2px 4px var(--theme-grey-divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& th {
|
||||||
|
background-color: var(--adf-theme-mat-grey-color-a200);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
& th,
|
||||||
|
& td {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--adf-theme-foreground-divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
& tr {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--adf-theme-background-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: var(--theme-card-background-grey-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-divider {
|
&-divider {
|
||||||
|
@ -25,9 +25,9 @@
|
|||||||
import { TestBed, ComponentFixture, tick, fakeAsync } from '@angular/core/testing';
|
import { TestBed, ComponentFixture, tick, fakeAsync } from '@angular/core/testing';
|
||||||
import { SearchAiResultsComponent } from './search-ai-results.component';
|
import { SearchAiResultsComponent } from './search-ai-results.component';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
import { of, Subject, throwError } from 'rxjs';
|
import { Observable, of, Subject, throwError } from 'rxjs';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { EmptyContentComponent, UnsavedChangesGuard, UserPreferencesService } from '@alfresco/adf-core';
|
import { EmptyContentComponent, UnitTestingUtils, UnsavedChangesGuard, UserPreferencesService } from '@alfresco/adf-core';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { AppTestingModule } from '../../../../testing/app-testing.module';
|
import { AppTestingModule } from '../../../../testing/app-testing.module';
|
||||||
import { MatIconTestingModule } from '@angular/material/icon/testing';
|
import { MatIconTestingModule } from '@angular/material/icon/testing';
|
||||||
@ -35,15 +35,15 @@ import { AgentService, NodesApiService, SearchAiService } from '@alfresco/adf-co
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ModalAiService } from '../../../../services/modal-ai.service';
|
import { ModalAiService } from '../../../../services/modal-ai.service';
|
||||||
import { delay } from 'rxjs/operators';
|
import { delay } from 'rxjs/operators';
|
||||||
import { AiAnswer, AiAnswerEntry, QuestionModel } from '@alfresco/js-api/typings';
|
import { AiAnswerEntry, QuestionModel } from '@alfresco/js-api/typings';
|
||||||
import { SearchAiInputComponent } from '../search-ai-input/search-ai-input.component';
|
import { SearchAiInputComponent } from '../search-ai-input/search-ai-input.component';
|
||||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||||
import { getAppSelection, getCurrentFolder, ViewNodeAction } from '@alfresco/aca-shared/store';
|
import { getAppSelection, getCurrentFolder, ViewNodeAction } from '@alfresco/aca-shared/store';
|
||||||
import { ViewerService } from '@alfresco/aca-content/viewer';
|
import { ViewerService } from '@alfresco/aca-content/viewer';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { MarkdownComponent, MarkdownModule } from 'ngx-markdown';
|
||||||
|
|
||||||
const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: { nodesIds: [] } };
|
const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: { nodesIds: [] } };
|
||||||
const aiAnswerMock: AiAnswer = { answer: 'Some answer', questionId: 'some id', references: [] };
|
|
||||||
const getAiAnswerEntry = (noAnswer?: boolean): AiAnswerEntry => {
|
const getAiAnswerEntry = (noAnswer?: boolean): AiAnswerEntry => {
|
||||||
return { entry: { answer: noAnswer ? '' : 'Some answer', questionId: 'some id', references: [] } };
|
return { entry: { answer: noAnswer ? '' : 'Some answer', questionId: 'some id', references: [] } };
|
||||||
};
|
};
|
||||||
@ -59,6 +59,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
let store: MockStore;
|
let store: MockStore;
|
||||||
let viewerService: ViewerService;
|
let viewerService: ViewerService;
|
||||||
let unsavedChangesGuard: UnsavedChangesGuard;
|
let unsavedChangesGuard: UnsavedChangesGuard;
|
||||||
|
let unitTestingUtils: UnitTestingUtils;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
store.resetSelectors();
|
store.resetSelectors();
|
||||||
@ -68,7 +69,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [AppTestingModule, SearchAiResultsComponent, MatSnackBarModule, MatDialogModule, MatIconTestingModule],
|
imports: [AppTestingModule, SearchAiResultsComponent, MatSnackBarModule, MatDialogModule, MatIconTestingModule, MarkdownModule.forRoot()],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: NodesApiService,
|
provide: NodesApiService,
|
||||||
@ -112,6 +113,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
spyOn(searchAiService, 'ask').and.returnValue(of(questionMock));
|
spyOn(searchAiService, 'ask').and.returnValue(of(questionMock));
|
||||||
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(of([]));
|
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(of([]));
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -161,7 +163,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
tick(30000);
|
tick(30000);
|
||||||
|
|
||||||
expect(component.queryAnswer).toBeUndefined();
|
expect(component.displayedAnswer).toBeUndefined();
|
||||||
expect(component.hasAnsweringError).toBeTrue();
|
expect(component.hasAnsweringError).toBeTrue();
|
||||||
expect(component.loading).toBeFalse();
|
expect(component.loading).toBeFalse();
|
||||||
}));
|
}));
|
||||||
@ -173,7 +175,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
tick(3000);
|
tick(3000);
|
||||||
|
|
||||||
expect(component.queryAnswer).toEqual({ answer: 'Some answer', questionId: 'some id', references: [] });
|
expect(component.displayedAnswer).toEqual('Some answer');
|
||||||
expect(component.hasAnsweringError).toBeFalse();
|
expect(component.hasAnsweringError).toBeFalse();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -184,7 +186,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
tick(50000);
|
tick(50000);
|
||||||
|
|
||||||
expect(component.queryAnswer).toEqual({ answer: 'Some answer', questionId: 'some id', references: [] });
|
expect(component.displayedAnswer).toEqual('Some answer');
|
||||||
expect(component.hasAnsweringError).toBeFalse();
|
expect(component.hasAnsweringError).toBeFalse();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -195,7 +197,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
tick(30000);
|
tick(30000);
|
||||||
|
|
||||||
expect(component.queryAnswer).toBeUndefined();
|
expect(component.displayedAnswer).toBeUndefined();
|
||||||
expect(component.hasAnsweringError).toBeTrue();
|
expect(component.hasAnsweringError).toBeTrue();
|
||||||
expect(component.loading).toBeFalse();
|
expect(component.loading).toBeFalse();
|
||||||
}));
|
}));
|
||||||
@ -207,7 +209,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
tick(30000);
|
tick(30000);
|
||||||
|
|
||||||
expect(component.queryAnswer).toBeUndefined();
|
expect(component.displayedAnswer).toBeUndefined();
|
||||||
expect(component.hasAnsweringError).toBeTrue();
|
expect(component.hasAnsweringError).toBeTrue();
|
||||||
expect(component.loading).toBeFalse();
|
expect(component.loading).toBeFalse();
|
||||||
}));
|
}));
|
||||||
@ -219,7 +221,7 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
tick(30000);
|
tick(30000);
|
||||||
|
|
||||||
expect(component.queryAnswer).toEqual({ answer: 'Some answer', questionId: 'some id', references: [] });
|
expect(component.displayedAnswer).toEqual('Some answer');
|
||||||
expect(component.hasAnsweringError).toBeFalse();
|
expect(component.hasAnsweringError).toBeFalse();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -332,10 +334,167 @@ describe('SearchAiResultsComponent', () => {
|
|||||||
|
|
||||||
fixture.debugElement.query(By.css(`[data-automation-id="aca-search-ai-results-regeneration-button"]`)).nativeElement.click();
|
fixture.debugElement.query(By.css(`[data-automation-id="aca-search-ai-results-regeneration-button"]`)).nativeElement.click();
|
||||||
expect(modalAiSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
expect(modalAiSpy).toHaveBeenCalledWith(jasmine.any(Function));
|
||||||
expect(component.queryAnswer).toEqual(aiAnswerMock);
|
expect(component.displayedAnswer).toEqual('Some answer');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Markdown', () => {
|
||||||
|
const getMarkdown = (): MarkdownComponent => unitTestingUtils.getByDirective(MarkdownComponent)?.componentInstance;
|
||||||
|
|
||||||
|
const removeTabs = (answer: string): string =>
|
||||||
|
answer
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
let queryParams: Params;
|
||||||
|
let getAnswerSpyAnd: jasmine.SpyAnd<(questionId: string) => Observable<AiAnswerEntry>>;
|
||||||
|
let answerEntry: AiAnswerEntry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
|
||||||
|
queryParams = {
|
||||||
|
query: 'test',
|
||||||
|
agentId: 'agentId1'
|
||||||
|
};
|
||||||
|
getAnswerSpyAnd = spyOn(searchAiService, 'getAnswer').and;
|
||||||
|
answerEntry = getAiAnswerEntry();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be rendered when answer is loaded successfully', fakeAsync(() => {
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getMarkdown()).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not be rendered when answer loading is failed', fakeAsync(() => {
|
||||||
|
getAnswerSpyAnd.returnValue(throwError(() => 'error').pipe(delay(100)));
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(30000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getMarkdown()).toBeUndefined();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should have assigned mermaid to true', fakeAsync(() => {
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getMarkdown().mermaid).toBeTrue();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should have assigned katex to true', fakeAsync(() => {
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getMarkdown().katex).toBeTrue();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should have assigned correct data', fakeAsync(() => {
|
||||||
|
const answer = '#### Some title\n\nSome description';
|
||||||
|
answerEntry.entry.answer = answer;
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getMarkdown().data).toEqual(answer);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should have assigned correct data when answer contains mermaids', fakeAsync(() => {
|
||||||
|
answerEntry.entry.answer =
|
||||||
|
'First example:\\n\\n```mermaid\\ngraph LR\\n animal --> dog\\n animal --> cat\\n```\\n\\n' +
|
||||||
|
'Second example:\\n\\n```mermaid\\ngraph LR\\n animal[label="Animal"] --> dog[label="Dog"]\\n animal[label="Animal"] --> cat[label="Cat"]\\n```\\n\\n';
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(removeTabs(getMarkdown().data)).toEqual(
|
||||||
|
removeTabs(`First example:\\n\\n\`\`\`mermaid
|
||||||
|
\\ngraph LR\\n animal --> dog\\n animal --> cat\\n
|
||||||
|
\`\`\`\\n\\nSecond example:\\n\\n\`\`\`mermaid
|
||||||
|
\\ngraph LR\\n animal[Animal] --> dog[Dog]\\n animal[Animal] --> cat[Cat]\\n
|
||||||
|
\`\`\`\\n\\n`)
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should have assigned correct data when answer contains latex', fakeAsync(() => {
|
||||||
|
answerEntry.entry.answer = '\n\n### Mathematical Formula\n\n```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx\n```';
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getMarkdown().data).toEqual('\n\n### Mathematical Formula\n\n$$f(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx$$');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should set source code tooltip for mermaid when answer contains mermaid', fakeAsync(() => {
|
||||||
|
answerEntry.entry.answer =
|
||||||
|
'First example:\\n\\n```mermaid\\ngraph LR\\n animal --> dog\\n animal --> cat\\n```\\n\\n' +
|
||||||
|
'Second example:\\n\\n```mermaid\\ngraph LR\\n animal[label="Animal"] --> dog[label="Dog"]\\n animal[label="Animal"] --> cat[label="Cat"]\\n```\\n\\n';
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const elements = [document.createElement('div'), document.createElement('div')];
|
||||||
|
spyOn(fixture.nativeElement, 'querySelectorAll').withArgs('.mermaid').and.returnValue(elements).and.returnValue([]);
|
||||||
|
|
||||||
|
getMarkdown().ready.emit();
|
||||||
|
expect(elements[0].title).toBe('```mermaid\\ngraph LR\\n animal --> dog\\n animal --> cat\\n```');
|
||||||
|
expect(elements[1].title).toBe(
|
||||||
|
'```mermaid\\ngraph LR\\n animal[label="Animal"] --> dog[label="Dog"]\\n animal[label="Animal"] --> cat[label="Cat"]\\n```'
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should set source code tooltip for mermaid when answer contains latex', fakeAsync(() => {
|
||||||
|
answerEntry.entry.answer =
|
||||||
|
'\n\n### Mathematical Formula 1\n\n```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx\n```' +
|
||||||
|
'\n\n### Mathematical Formula 2\n\n```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{5 (pi i Ixi x} dx\n```';
|
||||||
|
getAnswerSpyAnd.returnValues(
|
||||||
|
throwError(() => 'error'),
|
||||||
|
of(answerEntry)
|
||||||
|
);
|
||||||
|
mockQueryParams.next(queryParams);
|
||||||
|
tick(3000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const elements = [document.createElement('div'), document.createElement('div')];
|
||||||
|
spyOn(fixture.nativeElement, 'querySelectorAll').withArgs('.katex').and.returnValue(elements).and.returnValue([]);
|
||||||
|
|
||||||
|
getMarkdown().ready.emit();
|
||||||
|
expect(elements[0].title).toBe('```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx\n```');
|
||||||
|
expect(elements[1].title).toBe('```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{5 (pi i Ixi x} dx\n```');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('References', () => {
|
describe('References', () => {
|
||||||
let documentElement: HTMLDivElement;
|
let documentElement: HTMLDivElement;
|
||||||
let nodesOrder: string[];
|
let nodesOrder: string[];
|
||||||
|
@ -22,11 +22,11 @@
|
|||||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { PageComponent, PageLayoutComponent, ToolbarActionComponent, ToolbarComponent } from '@alfresco/aca-shared';
|
import { PageComponent, PageLayoutComponent } from '@alfresco/aca-shared';
|
||||||
import { concatMap, delay, filter, finalize, retryWhen, skipWhile, switchMap } from 'rxjs/operators';
|
import { concatMap, delay, filter, finalize, retryWhen, skipWhile, switchMap } from 'rxjs/operators';
|
||||||
import { AvatarComponent, ClipboardService, EmptyContentComponent, ThumbnailService, ToolbarModule, UnsavedChangesGuard } from '@alfresco/adf-core';
|
import { ClipboardService, EmptyContentComponent, ThumbnailService, UnsavedChangesGuard } from '@alfresco/adf-core';
|
||||||
import { AiAnswer, Node } from '@alfresco/js-api';
|
import { AiAnswer, Node } from '@alfresco/js-api';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component';
|
import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component';
|
||||||
@ -43,15 +43,13 @@ import { ModalAiService } from '../../../../services/modal-ai.service';
|
|||||||
import { ViewNodeAction } from '@alfresco/aca-shared/store';
|
import { ViewNodeAction } from '@alfresco/aca-shared/store';
|
||||||
import { ViewerService } from '@alfresco/aca-content/viewer';
|
import { ViewerService } from '@alfresco/aca-content/viewer';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { MarkdownComponent } from 'ngx-markdown';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
PageLayoutComponent,
|
PageLayoutComponent,
|
||||||
ToolbarActionComponent,
|
|
||||||
ToolbarModule,
|
|
||||||
ToolbarComponent,
|
|
||||||
SearchAiInputContainerComponent,
|
SearchAiInputContainerComponent,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
@ -59,8 +57,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|||||||
MatListModule,
|
MatListModule,
|
||||||
EmptyContentComponent,
|
EmptyContentComponent,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
AvatarComponent,
|
MatTooltipModule,
|
||||||
MatTooltipModule
|
MarkdownComponent
|
||||||
],
|
],
|
||||||
selector: 'aca-search-ai-results',
|
selector: 'aca-search-ai-results',
|
||||||
templateUrl: './search-ai-results.component.html',
|
templateUrl: './search-ai-results.component.html',
|
||||||
@ -69,6 +67,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|||||||
host: { class: 'aca-search-ai-results' }
|
host: { class: 'aca-search-ai-results' }
|
||||||
})
|
})
|
||||||
export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
||||||
|
private static readonly MERMAID_BLOCK_REGEX = /```mermaid([\s\S]*?)```/g;
|
||||||
|
private static readonly LATEX_BLOCK_REGEX = /```latex([\s\S]*?)```/g;
|
||||||
|
|
||||||
private _agentId: string;
|
private _agentId: string;
|
||||||
private _hasAnsweringError = false;
|
private _hasAnsweringError = false;
|
||||||
private _hasError = false;
|
private _hasError = false;
|
||||||
@ -78,7 +79,8 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
|||||||
private openedViewer = false;
|
private openedViewer = false;
|
||||||
private _selectedNodesState: SelectionState;
|
private _selectedNodesState: SelectionState;
|
||||||
private _searchQuery = '';
|
private _searchQuery = '';
|
||||||
private _queryAnswer: AiAnswer;
|
private queryAnswer: AiAnswer;
|
||||||
|
private _displayedAnswer: string;
|
||||||
|
|
||||||
get agentId(): string {
|
get agentId(): string {
|
||||||
return this._agentId;
|
return this._agentId;
|
||||||
@ -104,23 +106,24 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
|||||||
return this._nodes;
|
return this._nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
get queryAnswer(): AiAnswer {
|
|
||||||
return this._queryAnswer;
|
|
||||||
}
|
|
||||||
|
|
||||||
get searchQuery(): string {
|
get searchQuery(): string {
|
||||||
return this._searchQuery;
|
return this._searchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayedAnswer(): string {
|
||||||
|
return this._displayedAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private clipboardService: ClipboardService,
|
private readonly clipboardService: ClipboardService,
|
||||||
private thumbnailService: ThumbnailService,
|
private readonly thumbnailService: ThumbnailService,
|
||||||
private nodesApiService: NodesApiService,
|
private readonly nodesApiService: NodesApiService,
|
||||||
private translateService: TranslateService,
|
private readonly translateService: TranslateService,
|
||||||
private unsavedChangesGuard: UnsavedChangesGuard,
|
private readonly unsavedChangesGuard: UnsavedChangesGuard,
|
||||||
private modalAiService: ModalAiService,
|
private readonly modalAiService: ModalAiService,
|
||||||
private viewerService: ViewerService
|
private readonly viewerService: ViewerService,
|
||||||
|
private readonly elementRef: ElementRef
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -184,7 +187,8 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
|||||||
if (!response.entry?.answer) {
|
if (!response.entry?.answer) {
|
||||||
return throwError((e) => e);
|
return throwError((e) => e);
|
||||||
}
|
}
|
||||||
this._queryAnswer = response.entry;
|
this.queryAnswer = response.entry;
|
||||||
|
this._displayedAnswer = this.preprocessMarkdownFormat(response.entry.answer);
|
||||||
return forkJoin(this.queryAnswer.references.map((reference) => this.nodesApiService.getNode(reference.referenceId)));
|
return forkJoin(this.queryAnswer.references.map((reference) => this.nodesApiService.getNode(reference.referenceId)));
|
||||||
}),
|
}),
|
||||||
retryWhen((errors: Observable<Error>) => this.aiSearchRetryWhen(errors)),
|
retryWhen((errors: Observable<Error>) => this.aiSearchRetryWhen(errors)),
|
||||||
@ -213,6 +217,19 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSourceCodeTooltips(): void {
|
||||||
|
this.setTooltip(SearchAiResultsComponent.MERMAID_BLOCK_REGEX, '.mermaid');
|
||||||
|
this.setTooltip(SearchAiResultsComponent.LATEX_BLOCK_REGEX, '.katex');
|
||||||
|
}
|
||||||
|
|
||||||
|
private setTooltip(codeBlockRegexp: RegExp, targetElementsSelector: string): void {
|
||||||
|
const codeBlocks = [...this.queryAnswer.answer.matchAll(codeBlockRegexp)].map((match) => match[0].trim());
|
||||||
|
const elements: HTMLElement[] = this.elementRef.nativeElement.querySelectorAll(targetElementsSelector);
|
||||||
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
elements[i].title = codeBlocks[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private aiSearchRetryWhen(errors: Observable<Error>): Observable<Error> {
|
private aiSearchRetryWhen(errors: Observable<Error>): Observable<Error> {
|
||||||
this._hasAnsweringError = false;
|
this._hasAnsweringError = false;
|
||||||
const delayBetweenRetries = 3000;
|
const delayBetweenRetries = 3000;
|
||||||
@ -231,4 +248,29 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private preprocessMarkdownFormat(answer: string): string {
|
||||||
|
return this.transformLatex(this.transformMermaid(answer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformMermaid(answer: string): string {
|
||||||
|
return answer.replace(SearchAiResultsComponent.MERMAID_BLOCK_REGEX, (_mermaidBlockRegex, blockContent: string) => {
|
||||||
|
const transformedLines = blockContent.split('\n').map((line) => {
|
||||||
|
const label = 'label="';
|
||||||
|
while (line.includes(label)) {
|
||||||
|
const labelIndex = line.indexOf(label);
|
||||||
|
const start = labelIndex + label.length;
|
||||||
|
const end = line.indexOf('"', start);
|
||||||
|
line = line.slice(0, labelIndex) + line.slice(start, end) + line.slice(end + 1);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `\`\`\`mermaid\n${transformedLines.join('\n')}\n\`\`\``;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformLatex(answer: string): string {
|
||||||
|
return answer.replace(SearchAiResultsComponent.LATEX_BLOCK_REGEX, (_, latexContent: string) => `$$${latexContent.trim()}$$`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"typeRoots": ["node_modules/@types"],
|
"typeRoots": ["node_modules/@types"],
|
||||||
"lib": ["es2019", "dom"],
|
"lib": ["es2020", "dom"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@alfresco/aca-content": ["projects/aca-content/src/public-api.ts"],
|
"@alfresco/aca-content": ["projects/aca-content/src/public-api.ts"],
|
||||||
"@alfresco/aca-content/about": ["projects/aca-content/about/src/public-api.ts"],
|
"@alfresco/aca-content/about": ["projects/aca-content/about/src/public-api.ts"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user