diff --git a/docs/README.md b/docs/README.md index f2b95bd079..4f678c8a8e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -368,6 +368,7 @@ for more information about installing and using the source code. | Name | Description | Source link | | ---- | ----------- | ----------- | +| [Agent Service](content-services/services/agent.service.md) | Manages agents in Content Services. | [Source](../lib/content-services/src/lib/agent/services/agent.service.ts) | | [Audit Service](content-services/services/audit.service.md) | Manages Audit apps and entries. | [Source](../lib/content-services/src/lib/audit/audit.service.ts) | | [Card View Content Update Service](content-services/services/card-view-content-update.service.md) | Manages Card View properties in the content services environment. Implements BaseCardViewContentUpdate. | [Source](../lib/content-services/src/lib/common/services/card-view-content-update.service.ts) | | | [Category tree datasource service](content-services/services/category-tree-datasource.service.md) | Datasource service for category tree. | [Source](../lib/content-services/src/lib/category/services/category-tree-datasource.service.ts) | @@ -383,6 +384,7 @@ for more information about installing and using the source code. | [Node Comments Service](content-services/services/node-comments.service.md) | Adds and retrieves comments for nodes in Content Services. | [Source](../lib/content-services/src/lib/node-comments/services/node-comments.service.ts) | | [Node permission dialog service](content-services/services/node-permission-dialog.service.md) | Displays dialogs to let the user set node permissions. | [Source](../lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts) | | [Node Permission service](content-services/services/node-permission.service.md) | Manages role permissions for content nodes. | [Source](../lib/content-services/src/lib/permission-manager/services/node-permission.service.ts) | +| [Search Ai Service](content-services/services/search-ai.service.md) | Manages search AI in Content Services. | [Source](../lib/content-services/src/lib/search-ai/services/search-ai.service.ts) | | [Search filter service](content-services/services/search-filter.service.md) | Registers widgets for use with the Search Filter component. | [Source](../lib/content-services/src/lib/search/services/search-filter.service.ts) | | [Search Query Builder service](content-services/services/search-query-builder.service.md) | Stores information from all the custom search and faceted search widgets, compiles and runs the final search query. | [Source](../lib/content-services/src/lib/search/services/search-query-builder.service.ts) | | [Security Controls service](content-services/services/security-controls.service.md) | Manages security groups & marks in Content Services. | [Source](../lib/content-services/src/lib/security/services/security-controls-groups-marks-security.service.ts) | diff --git a/docs/content-services/services/agent.service.md b/docs/content-services/services/agent.service.md new file mode 100644 index 0000000000..67d9a7b54b --- /dev/null +++ b/docs/content-services/services/agent.service.md @@ -0,0 +1,27 @@ +--- +Title: Agent service +Added: v7.0.0-alpha.3 +Status: Active +Last reviewed: 2024-07-12 +--- + +# [Agent service](../../../lib/content-services/src/lib/agent/services/agent.service.ts "Defined in agent.service.ts") + +Manages agents in Content Services.
+ +In order to use this service, you need to have the HX Insights Connector (additional ACS module) installed. + +## Class members + +### Methods + +- **getAgents**(): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AgentPaging`](../../../lib/js-api/src/api/content-rest-api/docs/AgentsApi.md#agentpaging)`>`
+ Gets all agents. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AgentPaging`](../../../lib/js-api/src/api/content-rest-api/docs/AgentsApi.md#agentpaging)`>` - AgentPaging object containing the agents. + +## Details + +See the +[Agents API](../../../lib/js-api/src/api/content-rest-api/docs/AgentsApi.md) for more information about the types returned by [Agent +service](agent.service.md) methods and for the implementation of the REST API the service is +based on. diff --git a/docs/content-services/services/search-ai.service.md b/docs/content-services/services/search-ai.service.md new file mode 100644 index 0000000000..2ad3ede85e --- /dev/null +++ b/docs/content-services/services/search-ai.service.md @@ -0,0 +1,43 @@ +--- +Title: Search Ai service +Added: v7.0.0-alpha.3 +Status: Active +Last reviewed: 2024-07-12 +--- + +# [Search Ai service](../../../lib/content-services/src/lib/search-ai/services/search-ai.service.ts "Defined in search-ai.service.ts") + +Manages search AI in Content Services. + +In order to use this service, you need to have the HX Insights Connector (additional ACS module) installed. + +## Class members + +### Methods + +- **updateSearchAiInputState**(state: `SearchAiInputState`): `void`
+ Update the state of the search AI input. + - _state:_ `SearchAiInputState` - The new state of the search AI input. +- **ask**(question: [`QuestionRequest`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionrequest)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`QuestionModel`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionmodel)`>`
+ Ask a question to the AI. + - _question:_ [`QuestionRequest`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionrequest) - The question to ask. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`QuestionModel`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionmodel)`>` - QuestionModel object containing information about questions. +- **getAnswer**(questionId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AiAnswerEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#aianswerentry)`>`
+ Get an answer to specific question. + - _questionId:_ `string` - The ID of the question to get an answer for. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AiAnswerEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#aianswerentry)`>` - AiAnswerEntry object containing the answer. +- **getConfig**(): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`KnowledgeRetrievalConfigEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#knowledgeretrievalconfigentry)`>`
+ Get the knowledge retrieval configuration. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`KnowledgeRetrievalConfigEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#knowledgeretrievalconfigentry)`>` - KnowledgeRetrievalConfigEntry object containing the configuration. +- **checkSearchAvailability**(selectedNodesState: `SelectionState`, maxSelectedNodes: `number`): `string`
+ Check if using of search is possible (if all conditions are met). + - _selectedNodesState:_ `SelectionState` - information about selected nodes. + - _maxSelectedNodes:_ `number` - max number of selected nodes. Default 100. + - **Returns** `string` - string with error if any condition is not met, empty string otherwise. + +## Details + +See the +[Search Ai API](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md) for more information about the types returned by [Search Ai +service](search-ai.service.md) methods and for the implementation of the REST API the service is +based on. diff --git a/docs/versionIndex.md b/docs/versionIndex.md index 12a3bf2340..0dc74bfd3f 100644 --- a/docs/versionIndex.md +++ b/docs/versionIndex.md @@ -12,6 +12,7 @@ backend services have been tested with each released version of ADF. ## Versions +- [v7.0.0-alpha.3](#v700-alpha3) - [v6.8.0](#v680) - [v6.7.0](#v670) - [v6.4.0](#v640) @@ -46,6 +47,16 @@ backend services have been tested with each released version of ADF. - [v2.1.0](#v210) - [v2.0.0](#v200) +## v7.0.0-alpha.3 + + + +- [AgentService](content-services/services/agent.service.md) +- [SearchAiService](content-services/services/search-ai.service.md) + + + + ## v6.8.0 diff --git a/lib/content-services/src/lib/agent/index.ts b/lib/content-services/src/lib/agent/index.ts new file mode 100644 index 0000000000..54beb1a252 --- /dev/null +++ b/lib/content-services/src/lib/agent/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './public-api'; diff --git a/lib/content-services/src/lib/agent/public-api.ts b/lib/content-services/src/lib/agent/public-api.ts new file mode 100644 index 0000000000..24441c57dd --- /dev/null +++ b/lib/content-services/src/lib/agent/public-api.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './services/agent.service'; diff --git a/lib/content-services/src/lib/agent/services/agent.service.spec.ts b/lib/content-services/src/lib/agent/services/agent.service.spec.ts new file mode 100644 index 0000000000..08693bc0c7 --- /dev/null +++ b/lib/content-services/src/lib/agent/services/agent.service.spec.ts @@ -0,0 +1,72 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { CoreTestingModule } from '@alfresco/adf-core'; +import { AgentService } from './agent.service'; +import { Agent, AgentPaging } from '@alfresco/js-api'; + +const agent1: Agent = { + id: '1', + name: 'HR Agent', + description: 'Your Claims Doc Agent streamlines the extraction, analysis, and management of data from insurance claims documents.', + avatarUrl: '' +}; + +const agent2: Agent = { + id: '2', + name: 'Policy Agent', + description: 'Your Claims Doc Agent streamlines the extraction, analysis, and management of data from insurance claims documents.', + avatarUrl: '' +}; + +const agentPagingObjectMock: AgentPaging = { + list: { + entries: [ + { + entry: agent1 + }, + { + entry: agent2 + } + ] + } +}; + +const agentListMock: Agent[] = [agent1, agent2]; + +describe('AgentService', () => { + let agentService: AgentService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CoreTestingModule] + }); + + agentService = TestBed.inject(AgentService); + }); + + it('should load agents', (done) => { + spyOn(agentService.agentsApi, 'getAgents').and.returnValue(Promise.resolve(agentPagingObjectMock)); + + agentService.getAgents().subscribe((pagingResponse) => { + expect(pagingResponse).toEqual(agentListMock); + expect(agentService.agentsApi.getAgents).toHaveBeenCalled(); + done(); + }); + }); +}); diff --git a/lib/content-services/src/lib/agent/services/agent.service.ts b/lib/content-services/src/lib/agent/services/agent.service.ts new file mode 100644 index 0000000000..739997ee93 --- /dev/null +++ b/lib/content-services/src/lib/agent/services/agent.service.ts @@ -0,0 +1,61 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { Agent, AgentsApi } from '@alfresco/js-api'; +import { BehaviorSubject, from, Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { AlfrescoApiService } from '../../services'; + +@Injectable({ + providedIn: 'root' +}) +export class AgentService { + private _agentsApi: AgentsApi; + private agents = new BehaviorSubject([]); + + get agentsApi(): AgentsApi { + this._agentsApi = this._agentsApi ?? new AgentsApi(this.apiService.getInstance()); + return this._agentsApi; + } + + agents$ = this.agents.asObservable(); + + constructor(private apiService: AlfrescoApiService) {} + + /** + * Gets all agents from cache. If cache is empty, fetches agents from backend. + * + * @returns Agent[] list containing agents. + */ + getAgents(): Observable { + return this.agents$.pipe( + switchMap((agentsList) => { + if (agentsList.length) { + return of(agentsList); + } + return from(this.agentsApi.getAgents()).pipe( + map((paging) => { + const agentEntries = paging.list.entries.map((agentEntry) => agentEntry.entry); + this.agents.next(agentEntries); + return agentEntries; + }) + ); + }) + ); + } +} diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index c27ac04e91..aee5071e6d 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -701,5 +701,13 @@ "JOIN_CANCELED": "Canceled the request to join the library", "JOIN_REQUESTED": "Request sent to join this library" } + }, + "KNOWLEDGE_RETRIEVAL": { + "SEARCH": { + "WARNINGS": { + "TOO_MANY_FILES_SELECTED": "Please select no more than {{ maxFiles }} files.", + "FOLDER_SELECTED": "Folders are not compatible with AI Agents." + } + } } } diff --git a/lib/content-services/src/lib/search-ai/index.ts b/lib/content-services/src/lib/search-ai/index.ts new file mode 100644 index 0000000000..54beb1a252 --- /dev/null +++ b/lib/content-services/src/lib/search-ai/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './public-api'; diff --git a/lib/content-services/src/lib/search-ai/models/search-ai-input-state.ts b/lib/content-services/src/lib/search-ai/models/search-ai-input-state.ts new file mode 100644 index 0000000000..fcb7eece87 --- /dev/null +++ b/lib/content-services/src/lib/search-ai/models/search-ai-input-state.ts @@ -0,0 +1,21 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface SearchAiInputState { + active: boolean; + selectedAgentId?: string; +} diff --git a/lib/content-services/src/lib/search-ai/public-api.ts b/lib/content-services/src/lib/search-ai/public-api.ts new file mode 100644 index 0000000000..e9fb74a3bc --- /dev/null +++ b/lib/content-services/src/lib/search-ai/public-api.ts @@ -0,0 +1,19 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './services/search-ai.service'; +export * from './models/search-ai-input-state'; diff --git a/lib/content-services/src/lib/search-ai/services/search-ai.service.spec.ts b/lib/content-services/src/lib/search-ai/services/search-ai.service.spec.ts new file mode 100644 index 0000000000..5e67fbbf15 --- /dev/null +++ b/lib/content-services/src/lib/search-ai/services/search-ai.service.spec.ts @@ -0,0 +1,220 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { AiAnswerEntry, KnowledgeRetrievalConfigEntry, Node, QuestionModel, QuestionRequest } from '@alfresco/js-api'; +import { ContentTestingModule } from '../../testing/content.testing.module'; +import { SearchAiService } from './search-ai.service'; +import { SearchAiInputState } from '../models/search-ai-input-state'; +import { TranslateService } from '@ngx-translate/core'; + +describe('SearchAiService', () => { + let service: SearchAiService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ContentTestingModule] + }); + service = TestBed.inject(SearchAiService); + }); + + describe('ask', () => { + it('should load information about question', (done) => { + const question: QuestionModel = { + question: 'some question', + questionId: 'some id', + restrictionQuery: { nodesIds: ['nodeId1', 'nodeId2'] } + }; + spyOn(service.searchAiApi, 'ask').and.returnValue(Promise.resolve(question)); + const questionRequest: QuestionRequest = { + question: 'some question', + nodeIds: ['nodeId1', 'nodeId2'], + agentId: 'some id' + }; + + service.ask(questionRequest).subscribe((questionResponse) => { + expect(questionResponse).toBe(question); + expect(service.searchAiApi.ask).toHaveBeenCalledWith([questionRequest]); + done(); + }); + }); + }); + + describe('getAnswer', () => { + it('should load information about question', (done) => { + const questionId = 'some id'; + const answer: AiAnswerEntry = { + entry: { + answer: 'Some answer 1', + questionId, + references: [ + { + referenceId: 'some reference id 1', + referenceText: 'some reference text 1' + } + ] + } + }; + spyOn(service.searchAiApi, 'getAnswer').and.returnValue(Promise.resolve(answer)); + + service.getAnswer(questionId).subscribe((answerResponse) => { + expect(answerResponse).toBe(answer); + expect(service.searchAiApi.getAnswer).toHaveBeenCalledWith(questionId); + done(); + }); + }); + }); + + describe('getConfig', () => { + it('should load knowledge retrieval configuration', (done) => { + const config: KnowledgeRetrievalConfigEntry = { + entry: { + knowledgeRetrievalUrl: 'https://some-url' + } + }; + spyOn(service.searchAiApi, 'getConfig').and.returnValue(Promise.resolve(config)); + + service.getConfig().subscribe((configResponse) => { + expect(configResponse).toBe(config); + expect(service.searchAiApi.getConfig).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('updateSearchAiInputState', () => { + it('should trigger toggleSearchAiInput$', () => { + const state: SearchAiInputState = { + active: true, + selectedAgentId: 'some id' + }; + service.updateSearchAiInputState(state); + + service.toggleSearchAiInput$.subscribe((receivedState) => { + expect(receivedState).toBe(state); + }); + }); + }); + + describe('checkSearchAvailability', () => { + let translateService: TranslateService; + + const tooManyFilesSelectedError = 'Please select no more than 100 files.'; + const folderSelectedError = 'Folders are not compatible with AI Agents.'; + + beforeEach(() => { + translateService = TestBed.inject(TranslateService); + spyOn(translateService, 'instant').and.callFake((key) => { + switch (key) { + case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED': + return tooManyFilesSelectedError; + case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.FOLDER_SELECTED': + return folderSelectedError; + default: + return ''; + } + }); + }); + + it('should not return error if user did not select any files', () => { + expect( + service.checkSearchAvailability({ + count: 0, + nodes: [], + libraries: [], + isEmpty: true + }) + ).toEqual(''); + }); + + it('should return error for too many files selected', () => { + expect( + service.checkSearchAvailability({ + count: 101, + nodes: [], + libraries: [], + isEmpty: false + }) + ).toBe(tooManyFilesSelectedError); + expect(translateService.instant).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED', { + maxFiles: 100, + key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED' + }); + }); + + it('should return error for folder selected', () => { + expect( + service.checkSearchAvailability({ + count: 1, + nodes: [ + { + entry: { + isFolder: true + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).toBe(folderSelectedError); + }); + + it('should return error for folder and if non text mime type node is selected', () => { + expect( + service.checkSearchAvailability({ + count: 1, + nodes: [ + { + entry: { + isFolder: true, + content: { + mimeType: 'some mime type', + mimeTypeName: 'some mime type', + sizeInBytes: 100 + } + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).toBe(folderSelectedError); + }); + + it('should return more than one error if more validators detected issues', () => { + expect( + service.checkSearchAvailability({ + count: 101, + nodes: [ + { + entry: { + isFolder: true, + content: { + mimeType: 'image/jpeg', + mimeTypeName: 'image/jpeg', + sizeInBytes: 100 + } + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).toBe(`${tooManyFilesSelectedError} ${folderSelectedError}`); + }); + }); +}); diff --git a/lib/content-services/src/lib/search-ai/services/search-ai.service.ts b/lib/content-services/src/lib/search-ai/services/search-ai.service.ts new file mode 100644 index 0000000000..d301253ad3 --- /dev/null +++ b/lib/content-services/src/lib/search-ai/services/search-ai.service.ts @@ -0,0 +1,110 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { AiAnswerEntry, KnowledgeRetrievalConfigEntry, QuestionModel, QuestionRequest, SearchAiApi } from '@alfresco/js-api'; +import { BehaviorSubject, from, Observable } from 'rxjs'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { TranslateService } from '@ngx-translate/core'; +import { SearchAiInputState } from '../models/search-ai-input-state'; +import { AlfrescoApiService } from '../../services'; + +@Injectable({ + providedIn: 'root' +}) +export class SearchAiService { + private toggleSearchAiInput = new BehaviorSubject({ + active: false + }); + private _searchAiApi: SearchAiApi; + + get searchAiApi(): SearchAiApi { + this._searchAiApi = this._searchAiApi ?? new SearchAiApi(this.apiService.getInstance()); + return this._searchAiApi; + } + + toggleSearchAiInput$ = this.toggleSearchAiInput.asObservable(); + + constructor( + private apiService: AlfrescoApiService, + private translateService: TranslateService + ) {} + + /** + * Update the state of the search AI input. + * + * @param state The new state of the search AI input. + */ + updateSearchAiInputState(state: SearchAiInputState): void { + this.toggleSearchAiInput.next(state); + } + + /** + * Ask a question to the AI. + * + * @param question The question to ask. + * @returns QuestionModel object containing information about questions. + */ + ask(question: QuestionRequest): Observable { + return from(this.searchAiApi.ask([question])); + } + + /** + * Get an answer to specific question. + * + * @param questionId The ID of the question to get an answer for. + * @returns AiAnswerEntry object containing the answer. + */ + getAnswer(questionId: string): Observable { + return from(this.searchAiApi.getAnswer(questionId)); + } + + /** + * Get the knowledge retrieval configuration. + * + * @returns KnowledgeRetrievalConfigEntry object containing the configuration. + */ + getConfig(): Observable { + return from(this.searchAiApi.getConfig()); + } + + /** + * Check if using of search is possible (if all conditions are met). + * + * @param selectedNodesState information about selected nodes. + * @param maxSelectedNodes max number of selected nodes. Default 100. + * @returns string with error if any condition is not met, empty string otherwise. + */ + checkSearchAvailability(selectedNodesState: SelectionState, maxSelectedNodes = 100): string { + const messages: { + key: string; + [parameter: string]: number | string; + }[] = []; + if (selectedNodesState.count > maxSelectedNodes) { + messages.push({ + key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED', + maxFiles: maxSelectedNodes + }); + } + if (selectedNodesState.nodes.some((node) => node.entry.isFolder)) { + messages.push({ + key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.FOLDER_SELECTED' + }); + } + return messages.map((message) => this.translateService.instant(message.key, message)).join(' '); + } +} diff --git a/lib/content-services/src/public-api.ts b/lib/content-services/src/public-api.ts index 79e0afd2d3..d169b6d036 100644 --- a/lib/content-services/src/public-api.ts +++ b/lib/content-services/src/public-api.ts @@ -49,6 +49,8 @@ export * from './lib/prediction/index'; export * from './lib/legal-hold/index'; export * from './lib/api-factories'; export * from './lib/mock/alfresco-api.service.mock'; +export * from './lib/agent/index'; +export * from './lib/search-ai/index'; export * from './lib/content.module'; export * from './lib/material.module'; diff --git a/lib/core/src/lib/app-config/app-config.service.ts b/lib/core/src/lib/app-config/app-config.service.ts index a2343d9b5c..25e507a25e 100644 --- a/lib/core/src/lib/app-config/app-config.service.ts +++ b/lib/core/src/lib/app-config/app-config.service.ts @@ -47,7 +47,8 @@ export enum AppConfigValues { STORAGE_PREFIX = 'application.storagePrefix', NOTIFY_DURATION = 'notificationDefaultDuration', CONTENT_TICKET_STORAGE_LABEL = 'ticket-ECM', - PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM' + PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM', + UNSAVED_CHANGES_MODAL_HIDDEN = 'unsaved_changes__modal_hidden' } // eslint-disable-next-line no-shadow diff --git a/lib/core/src/lib/avatar/avatar.component.html b/lib/core/src/lib/avatar/avatar.component.html index 88e7930230..b3aff1d883 100644 --- a/lib/core/src/lib/avatar/avatar.component.html +++ b/lib/core/src/lib/avatar/avatar.component.html @@ -1,5 +1,5 @@
- +
diff --git a/lib/core/src/lib/avatar/avatar.component.spec.ts b/lib/core/src/lib/avatar/avatar.component.spec.ts index 07bd1623f5..7fa3b41e87 100644 --- a/lib/core/src/lib/avatar/avatar.component.spec.ts +++ b/lib/core/src/lib/avatar/avatar.component.spec.ts @@ -32,6 +32,8 @@ describe('AvatarComponent', () => { fixture.detectChanges(); }); + const getAvatarImageElement = (): HTMLImageElement => fixture.nativeElement.querySelector('.adf-avatar__image'); + it('should create', () => { expect(component).toBeTruthy(); }); @@ -46,8 +48,7 @@ describe('AvatarComponent', () => { it('should display image when src is provided', () => { component.src = 'path/to/image.jpg'; fixture.detectChanges(); - const imgElement: HTMLImageElement = fixture.nativeElement.querySelector('.adf-avatar__image'); - expect(imgElement.src).toContain(component.src); + expect(getAvatarImageElement().src).toContain(component.src); }); it('should use default initials when not provided', () => { @@ -67,7 +68,7 @@ describe('AvatarComponent', () => { component.size = '48px'; fixture.detectChanges(); - const style = getComputedStyle(fixture.nativeElement.querySelector('.adf-avatar__image')); + const style = getComputedStyle(getAvatarImageElement()); expect(style.width).toBe('48px'); expect(style.height).toBe('48px'); }); @@ -76,14 +77,22 @@ describe('AvatarComponent', () => { component.cursor = 'pointer'; fixture.detectChanges(); - const style = getComputedStyle(fixture.nativeElement.querySelector('.adf-avatar__image')); + const style = getComputedStyle(getAvatarImageElement()); expect(style.cursor).toBe('pointer'); }); it('should display tooltip when provided', () => { component.tooltip = 'User Tooltip'; fixture.detectChanges(); - const avatarElement: HTMLElement = fixture.nativeElement.querySelector('.adf-avatar__image'); - expect(avatarElement.getAttribute('title')).toBe('User Tooltip'); + expect(getAvatarImageElement().getAttribute('title')).toBe('User Tooltip'); + }); + + it('should call onImageError when the image fails to load', () => { + component.src = 'path/to/image.jpg'; + fixture.detectChanges(); + + getAvatarImageElement().dispatchEvent(new Event('error')); + + expect(component.src).toEqual(''); }); }); diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html index 3de4be2a69..3d4a7d0363 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html @@ -1,29 +1,35 @@ -

- {{ 'CORE.DIALOG.UNSAVED_CHANGES.TITLE' | translate }} +

+ {{ dialogData.headerText | translate }}

- - {{ 'CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION' | translate }} + + {{ dialogData.descriptionText | translate }} +
+ {{ dialogData.checkboxText | translate }} +
- + diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss index 4ebe13cbc4..44ce960f43 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss @@ -1,32 +1,61 @@ -adf-unsaved-changes-dialog { - margin-top: -4px; - display: block; - +.adf-unsaved-changes-dialog { .adf-unsaved-changes-dialog { - &-title { + &-header { display: flex; align-items: center; justify-content: space-between; - font-size: 16px; - font-weight: bold; + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + height: 24px; + + &-close-button { + margin-right: -16px; + margin-bottom: 2px; + } + + &::before { + display: none; + } } - &-cancel-button { - background-color: var(--adf-secondary-button-background); - margin-right: 4px; + &-content { + padding: 0 8px 0 0; + overflow: unset; + color: var(--adf-secondary-modal-text-color); + + &-checkbox { + margin-top: 20px; + + label { + color: var(--adf-secondary-modal-text-color); + } + } } - &-discard-changes-button { - color: var(--theme-warn-color-default-contrast); - background-color: var(--adf-danger-button-background); - min-width: 143px; - } + &-actions { + margin-top: 11px; + margin-bottom: 1px; + padding: 0; + align-items: flex-end; - &-cancel-button, &-discard-changes-button { - padding: 4px 14px; - height: 32px; - display: flex; - align-items: center; + &-cancel-button { + background-color: var(--adf-secondary-button-background); + margin-right: 4px; + } + + &-discard-changes-button { + color: white; + background-color: var(--adf-error-color); + } + + &-cancel-button, + &-discard-changes-button { + padding: 4px 12px; + height: 32px; + display: flex; + align-items: center; + } } } } diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts index 459c9a2bac..51e7fbfcab 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts @@ -16,39 +16,61 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CoreTestingModule, UnsavedChangesDialogComponent } from '@alfresco/adf-core'; +import { AppConfigValues, CoreTestingModule, UnsavedChangesDialogComponent, UserPreferencesService } from '@alfresco/adf-core'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -import { MatDialogClose } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogClose } from '@angular/material/dialog'; +import { UnsavedChangesDialogData } from './unsaved-changes-dialog.model'; describe('UnsavedChangesDialog', () => { let fixture: ComponentFixture; + let userPreferencesService: UserPreferencesService; + let savePreferenceCheckbox: DebugElement; - beforeEach(() => { + const setupBeforeEach = (unsavedChangesDialogData?: UnsavedChangesDialogData) => { TestBed.configureTestingModule({ - imports: [CoreTestingModule] + imports: [CoreTestingModule], + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: unsavedChangesDialogData ?? {} + } + ] }); + + userPreferencesService = TestBed.inject(UserPreferencesService); fixture = TestBed.createComponent(UnsavedChangesDialogComponent); fixture.detectChanges(); - }); + savePreferenceCheckbox = fixture.debugElement.query(By.css('[data-automation-id="adf-unsaved-changes-dialog-content-checkbox"]')); + }; - describe('Close icon button', () => { - let closeIconButton: DebugElement; + const getElements = (): { header: HTMLElement; content: HTMLElement; discardChangesButton: HTMLElement } => { + const header = fixture.nativeElement.querySelector('.adf-unsaved-changes-dialog-header'); + const content = fixture.nativeElement.querySelector('.adf-unsaved-changes-dialog-content'); + const discardChangesButton = fixture.nativeElement.querySelector('.adf-unsaved-changes-dialog-actions-discard-changes-button'); + return { header, content, discardChangesButton }; + }; + describe('when data is not present in dialog', () => { beforeEach(() => { - closeIconButton = fixture.debugElement.query(By.css('[data-automation-id="adf-unsaved-changes-dialog-close-button"]')); + setupBeforeEach(); }); - it('should have assigned dialog close button with false as result', () => { - expect(closeIconButton.injector.get(MatDialogClose).dialogResult).toBeFalse(); + it('should display correct text if there is no data object', () => { + const { header, content, discardChangesButton } = getElements(); + expect(header.textContent).toContain('CORE.DIALOG.UNSAVED_CHANGES.TITLE'); + expect(content.textContent).toContain('CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION'); + expect(discardChangesButton.textContent).toContain('CORE.DIALOG.UNSAVED_CHANGES.DISCARD_CHANGES_BUTTON'); }); - it('should have displayed correct icon', () => { - expect(closeIconButton.nativeElement.textContent).toBe('close'); + it('should have assigned dialog close button with true as result', () => { + expect( + fixture.debugElement + .query(By.css('[data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"]')) + .injector.get(MatDialogClose).dialogResult + ).toBeTrue(); }); - }); - describe('Cancel button', () => { it('should have assigned dialog close button with false as result', () => { expect( fixture.debugElement.query(By.css('[data-automation-id="adf-unsaved-changes-dialog-cancel-button"]')).injector.get(MatDialogClose) @@ -57,13 +79,38 @@ describe('UnsavedChangesDialog', () => { }); }); - describe('Discard changes button', () => { - it('should have assigned dialog close button with true as result', () => { - expect( - fixture.debugElement - .query(By.css('[data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"]')) - .injector.get(MatDialogClose).dialogResult - ).toBeTrue(); + describe('when data is present in dialog', () => { + let userPreferencesServiceSetSpy: jasmine.Spy<(property: string, value: any) => void>; + + beforeEach(() => { + setupBeforeEach({ + headerText: 'headerText', + descriptionText: 'descriptionText', + confirmButtonText: 'confirmButtonText', + checkboxText: 'checkboxText' + }); + userPreferencesServiceSetSpy = spyOn(userPreferencesService, 'set'); + fixture.detectChanges(); + }); + + it('should display correct text if there is data object', () => { + const { header, content, discardChangesButton } = getElements(); + + expect(header.textContent).toContain('headerText'); + expect(content.textContent).toContain('descriptionText checkboxText'); + expect(discardChangesButton.textContent).toContain('confirmButtonText'); + }); + + it('should call UserPreferences Service and update it to true when checkbox is checked', () => { + const event = { checked: true }; + savePreferenceCheckbox.triggerEventHandler('change', event); + expect(userPreferencesServiceSetSpy).toHaveBeenCalledWith(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, 'true'); + }); + + it('should call UserPreferences Service and update it to false when checkbox is unchecked', () => { + const event = { checked: false }; + savePreferenceCheckbox.triggerEventHandler('change', event); + expect(userPreferencesServiceSetSpy).toHaveBeenCalledWith(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, 'false'); }); }); }); diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts index e178f06827..7630867756 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts @@ -15,22 +15,55 @@ * limitations under the License. */ -import { Component, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatDialogModule } from '@angular/material/dialog'; +import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { UnsavedChangesDialogData } from './unsaved-changes-dialog.model'; +import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox'; +import { ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; +import { CommonModule } from '@angular/common'; +import { UserPreferencesService } from '../../common'; +import { AppConfigValues } from '../../app-config'; /** * Dialog which informs about unsaved changes. Allows discard them and proceed or close dialog and stop proceeding. + * Can be customized with data object - UnsavedChangesDialogData. + * If data.checkboxText is provided, checkbox will be displayed with the checkbox description. + * If data.confirmButtonText is provided, it will be displayed on the confirm button. + * If data.headerText is provided, it will be displayed as the header. + * If data.descriptionText is provided, it will be displayed as dialog content. */ @Component({ - selector: 'adf-unsaved-changes-dialog', standalone: true, - imports: [CommonModule, MatDialogModule, TranslateModule, MatButtonModule, MatIconModule], + selector: 'adf-unsaved-changes-dialog', encapsulation: ViewEncapsulation.None, templateUrl: './unsaved-changes-dialog.component.html', - styleUrls: ['./unsaved-changes-dialog.component.scss'] + styleUrls: ['./unsaved-changes-dialog.component.scss'], + host: { class: 'adf-unsaved-changes-dialog' }, + imports: [MatDialogModule, TranslateModule, MatButtonModule, MatIconModule, CommonModule, MatCheckboxModule, ReactiveFormsModule] }) -export class UnsavedChangesDialogComponent {} +export class UnsavedChangesDialogComponent implements OnInit { + dialogData: UnsavedChangesDialogData; + + constructor(@Inject(MAT_DIALOG_DATA) public data: UnsavedChangesDialogData, private userPreferencesService: UserPreferencesService) {} + + ngOnInit() { + this.dialogData = { + headerText: this.data?.headerText ?? 'CORE.DIALOG.UNSAVED_CHANGES.TITLE', + descriptionText: this.data?.descriptionText ?? 'CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION', + confirmButtonText: this.data?.confirmButtonText ?? 'CORE.DIALOG.UNSAVED_CHANGES.DISCARD_CHANGES_BUTTON', + checkboxText: this.data?.checkboxText ?? '' + }; + } + + /** + * Sets 'unsaved_changes__modal_visible' checked state (true or false string) as new item in local storage. + * + * @param savePreferences - MatCheckboxChange object with information about checkbox state. + */ + onToggleCheckboxPreferences(savePreferences: MatCheckboxChange) { + this.userPreferencesService.set(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, savePreferences.checked.toString()); + } +} diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.model.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.model.ts new file mode 100644 index 0000000000..fb753c3229 --- /dev/null +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.model.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface UnsavedChangesDialogData { + checkboxText?: string; + confirmButtonText?: string; + descriptionText?: string; + headerText?: string; +} diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.spec.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.spec.ts index d72d763f8e..7da1eaf899 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.spec.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.spec.ts @@ -91,5 +91,15 @@ describe('UnsavedChangesGuard', () => { }); afterClosed$.next(true); }); + + it('should return false if afterClosed subject value was undefined', (done) => { + guard.unsaved = true; + + (guard.canDeactivate() as Observable).subscribe((result) => { + expect(result).toBeFalse(); + done(); + }); + afterClosed$.next(undefined); + }); }); }); diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts index d18353b3c2..606d72899b 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts @@ -20,7 +20,8 @@ import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { UnsavedChangesDialogComponent } from './unsaved-changes-dialog.component'; -import { tap } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; +import { UnsavedChangesDialogData } from './unsaved-changes-dialog.model'; /** * Guard responsible for protecting leaving page with unsaved changes. @@ -30,6 +31,7 @@ import { tap } from 'rxjs/operators'; }) export class UnsavedChangesGuard implements CanDeactivate { unsaved = false; + data: UnsavedChangesDialogData; constructor(private dialog: MatDialog) {} @@ -39,9 +41,17 @@ export class UnsavedChangesGuard implements CanDeactivate { * @returns boolean | Observable true when there is no unsaved changes or changes can be discarded, false otherwise. */ canDeactivate(): boolean | Observable { - return this.unsaved ? - this.dialog.open(UnsavedChangesDialogComponent, { - maxWidth: 346 - }).afterClosed().pipe(tap((confirmed) => this.unsaved = !confirmed)) : true; + return this.unsaved + ? this.dialog + .open(UnsavedChangesDialogComponent, { + maxWidth: 346, + data: this.data + }) + .afterClosed() + .pipe( + tap((confirmed) => (this.unsaved = !confirmed)), + map((confirmed) => !!confirmed) + ) + : true; } } diff --git a/lib/core/src/lib/styles/_components-variables.scss b/lib/core/src/lib/styles/_components-variables.scss index 06e7cc9ec5..4df3beb1ea 100644 --- a/lib/core/src/lib/styles/_components-variables.scss +++ b/lib/core/src/lib/styles/_components-variables.scss @@ -86,8 +86,9 @@ --adf-header-icon-button-hover-color: $adf-ref-header-icon-color, --adf-header-icon-button-pressed-color: $adf-ref-header-icon-color, --adf-header-icon-button-disabled-color: $adf-ref-header-icon-color, - --adf-danger-button-background: $adf-danger-button-background, + --adf-error-color: $adf-error-color, --adf-secondary-button-background: $adf-secondary-button-background, + --adf-secondary-modal-text-color: $adf-secondary-modal-text-color, --adf-display-external-property-widget-preview-selection-color: mat.get-color-from-palette($foreground, secondary-text) ); diff --git a/lib/core/src/lib/styles/_mat-selectors.scss b/lib/core/src/lib/styles/_mat-selectors.scss index ab97021fcc..e88401e7ec 100644 --- a/lib/core/src/lib/styles/_mat-selectors.scss +++ b/lib/core/src/lib/styles/_mat-selectors.scss @@ -121,6 +121,7 @@ $mat-notched-outline-trailing: '.mdc-notched-outline__trailing'; $mat-notched-outline-notch: '.mdc-notched-outline__notch'; $mat-evolution-chip-set: '.mdc-evolution-chip-set'; $mat-button-base: '.mat-mdc-button-base'; +$mat-button-touch-target: '.mat-mdc-button-touch-target'; $mat-evolution-chip-text-label: '.mdc-evolution-chip__text-label'; $cdk-overlay-pane: '.cdk-overlay-pane'; $cdk-drag-preview: '.cdk-drag-preview'; diff --git a/lib/core/src/lib/styles/_reference-variables.scss b/lib/core/src/lib/styles/_reference-variables.scss index 6f72688cbe..dc662e7780 100644 --- a/lib/core/src/lib/styles/_reference-variables.scss +++ b/lib/core/src/lib/styles/_reference-variables.scss @@ -25,5 +25,6 @@ $adf-ref-metadata-property-panel-label-color: rgba(33, 33, 33, 0.24); $adf-ref-metadata-property-panel-title-color: rgb(33, 33, 33); $adf-ref-header-icon-color: inherit; $adf-ref-header-icon-border-radius: 50%; -$adf-danger-button-background: #ba1b1b; +$adf-error-color: #ba1b1b; $adf-secondary-button-background: #2121210d; +$adf-secondary-modal-text-color: #212121; diff --git a/lib/js-api/src/api/content-rest-api/api/agents.api.ts b/lib/js-api/src/api/content-rest-api/api/agents.api.ts new file mode 100644 index 0000000000..b00ef0a558 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/api/agents.api.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AgentPaging } from '../model/agentPaging'; +import { BaseApi } from '../../hxi-connector-api/api/base.api'; + +/** + * Agents Api. + * In order to use this api, you need to have the HX Insights Connector (additional ACS module) installed. + */ +export class AgentsApi extends BaseApi { + /** + * Gets all agents. + * + * @returns AgentPaging object containing the agents. + */ + getAgents(): Promise { + return this.get({ + path: '/agents' + }); + } +} diff --git a/lib/js-api/src/api/content-rest-api/api/index.ts b/lib/js-api/src/api/content-rest-api/api/index.ts index 8f7c57a64c..a3f3f822f6 100644 --- a/lib/js-api/src/api/content-rest-api/api/index.ts +++ b/lib/js-api/src/api/content-rest-api/api/index.ts @@ -18,6 +18,7 @@ export * from './types'; export * from './actions.api'; export * from './activities.api'; +export * from './agents.api'; export * from './audit.api'; export * from './categories.api'; export * from './comments.api'; @@ -32,6 +33,7 @@ export * from './probes.api'; export * from './queries.api'; export * from './ratings.api'; export * from './renditions.api'; +export * from './search-ai.api'; export * from './sharedlinks.api'; export * from './sites.api'; export * from './tags.api'; diff --git a/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts b/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts new file mode 100644 index 0000000000..1ee3c6c389 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts @@ -0,0 +1,64 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { QuestionModel, QuestionRequest, AiAnswerEntry, KnowledgeRetrievalConfigEntry } from '../model'; +import { BaseApi } from '../../hxi-connector-api/api/base.api'; + +/** + * Search AI API. + */ +export class SearchAiApi extends BaseApi { + /** + * Ask a question to the AI. + * + * @param questions QuestionRequest array containing questions to ask. + * @returns QuestionModel object containing information about questions. + */ + ask(questions: QuestionRequest[]): Promise { + const agentId = questions[0].agentId; + return this.post({ + path: `agents/${agentId}/questions`, + bodyParam: questions.map((questionRequest) => ({ + question: questionRequest.question, + restrictionQuery: { nodesIds: questionRequest.nodeIds } + })) + }).then((response) => response.entry); + } + + /** + * Get an answer to specific question. + * + * @param questionId The ID of the question to get an answer for. + * @returns AiAnswerEntry object containing the answer. + */ + getAnswer(questionId: string): Promise { + return this.get({ + path: `questions/${questionId}/answers/-default-` + }); + } + + /** + * Get the knowledge retrieval configuration. + * + * @returns KnowledgeRetrievalConfigEntry object containing the configuration. + */ + getConfig(): Promise { + return this.get({ + path: '/config/-default-' + }); + } +} diff --git a/lib/js-api/src/api/content-rest-api/docs/AgentsApi.md b/lib/js-api/src/api/content-rest-api/docs/AgentsApi.md new file mode 100644 index 0000000000..03ac08d0a1 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/docs/AgentsApi.md @@ -0,0 +1,87 @@ +# AgentsApi + +| Method | HTTP request | Description | +|-----------------------------------|----------------------------------------------|--------------------------| +| [getAgents](#getAgents) | **GET** /agents | Gets all agents. | + +## getAgents + +Gets all agents. +A paginated list is returned in the response body. For example: + +```json +{ + "list": { + "pagination": { + "count": 2, + "hasMoreItems": false, + "totalItems": 2, + "skipCount": 0, + "maxItems": 100 + }, + "entries": [ + { + "entry": { + "id": "Some id", + "name": "Some name", + "description": "Some description", + "avatarUrl": "Some avatar url" + } + } + ] + } +} +``` + +**Example** + +```javascript +import { AlfrescoApi, AgentsApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const agentsApi = new AgentsApi(alfrescoApi); + +agentsApi.getAgents().then((agents) => { + console.log('API called successfully. Returned data: ' + agents); +}); +``` + +**Return type**: [AgentPaging](#AgentPaging) + +# Models + +## AgentPaging + +**Properties** + +| Name | Type | +|------|-------------------------------------| +| list | [AgentPagingList](#AgentPagingList) | + +## AgentPagingList + +**Properties** + +| Name | Type | +|----------------|-----------------------------| +| **pagination** | [Pagination](Pagination.md) | +| **entries** | [AgentEntry[]](#AgentEntry) | + +## AgentEntry + +**Properties** + +| Name | Type | +|-----------|-----------------| +| **entry** | [Agent](#Agent) | + +## Agent + +**Properties** + +| Name | Type | Description | +|-----------------|-----------|-------------------------------| +| **id** | string | Agent id | +| **name** | string | Agent name | +| **description** | string | Agent description | +| **avatarUrl** | string | (optional) Agent avatar image | diff --git a/lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md b/lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md new file mode 100644 index 0000000000..74e2597a80 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md @@ -0,0 +1,199 @@ +# SearchAiApi + +| Method | HTTP request | Description | +|-------------------------|----------------------------|--------------------------------------------| +| [ask](#ask) | **GET** /questions | Ask a question to the AI. | +| [getAnswer](#getAnswer) | **GET** /answers/-default- | Get an answer to specific question. | +| [getConfig](#getConfig) | **GET** /config/-default- | Get the knowledge retrieval configuration. | + +## ask + +Ask a question to the AI. +A list is returned in the response body. For example: + +```json +[ + { + "question": "Some question", + "questionId": "Some question id", + "restrictionQuery": "Some restriction query" + } +] +``` + +**Example** + +```javascript +import { AlfrescoApi, AgentsApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const searchAiApi = new SearchAiApi(alfrescoApi); + +searchAiApi.ask([{ + question: 'Some question', + restrictionQuery: 'Some restriction query', + agentId: 'Some agent id' +}]).then((questionInformation) => { + console.log('API called successfully. Returned data: ' + questionInformation); +}); +``` +**Parameters** + +| Name | Type | Description | +|---------------|---------------------------------------|-----------------------| +| **questions** | [QuestionRequest](#QuestionRequest)[] | The questions to ask. | + +**Return type**: [QuestionModel](#QuestionModel)[] + +## getAnswer + +Get an answer to specific question. +A paginated list is returned in the response body. For example: + +```json +{ + "list": { + "pagination": { + "count": 2, + "hasMoreItems": false, + "totalItems": 2, + "skipCount": 0, + "maxItems": 100 + }, + "entries": [ + { + "entry": { + "answer": "Some answer", + "questionId": "Some question id", + "references": [ + { + "referenceId": "Some reference id", + "referenceText": "Some reference text" + } + ] + } + } + ] + } +} +``` + +**Example** + +```javascript +import { AlfrescoApi, AgentsApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const searchAiApi = new SearchAiApi(alfrescoApi); + +searchAiApi.getAnswer('some question id').then((answer) => { + console.log('API called successfully. Returned data: ' + answer); +}); +``` +**Parameters** + +| Name | Type | Description | +|----------------|--------|----------------------------------------------| +| **questionId** | string | The ID of the question to get an answer for. | + +**Return type**: [AiAnswerEntry](#AiAnswerEntry) + +## getConfig + +Get the knowledge retrieval configuration. For example: + +```json +{ + "entry": { + "knowledgeRetrievalUrl": "https://some-url" + } +} +``` + +**Example** + +```javascript +import { AlfrescoApi, AgentsApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const searchAiApi = new SearchAiApi(alfrescoApi); + +searchAiApi.getConfig().then((answer) => { + console.log('API called successfully. Returned data: ', answer.entry.knowledgeRetrievalUrl); +}); +``` + +**Return type**: [KnowledgeRetrievalConfigEntry](#KnowledgeRetrievalConfigEntry) + +# Models + +## AiAnswerEntry + +**Properties** + +| Name | Type | +|-----------|-----------------------| +| **entry** | [AiAnswer](#AiAnswer) | + +## AiAnswer + +**Properties** + +| Name | Type | +|----------------|-------------------------------------------| +| **answer** | string | +| **questionId** | string | +| **references** | [AiAnswerReference](#AiAnswerReference)[] | + +## AiAnswerReference + +**Properties** + +| Name | Type | +|-------------------|--------| +| **referenceId** | string | +| **referenceText** | string | + +## QuestionModel + +**Properties** + +| Name | Type | +|----------------------|------------------| +| **question** | string | +| **questionId** | string | +| **restrictionQuery** | RestrictionQuery | + +## RestrictionQuery + +**Properties** + +| Name | Type | +|--------------|----------| +| **nodesIds** | string[] | + +## QuestionRequest + +**Properties** + +| Name | Type | +|--------------|----------| +| **question** | string | +| **nodeIds** | string[] | +| **agentId** | string | + +## KnowledgeRetrievalConfigEntry + +**Properties** + +| Name | Type | +|-------|-------------------------------------------------------| +| entry | [KnowledgeRetrievalConfig](#KnowledgeRetrievalConfig) | + +## KnowledgeRetrievalConfig + +**Properties** + +| Name | Type | +|-----------------------|--------| +| knowledgeRetrievalUrl | string | diff --git a/lib/js-api/src/api/content-rest-api/model/agent.ts b/lib/js-api/src/api/content-rest-api/model/agent.ts new file mode 100644 index 0000000000..3c19941cce --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/agent.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Agent { + id: string; + name: string; + description: string; + avatarUrl?: string; +} diff --git a/lib/js-api/src/api/content-rest-api/model/agentEntry.ts b/lib/js-api/src/api/content-rest-api/model/agentEntry.ts new file mode 100644 index 0000000000..8e2d948c59 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/agentEntry.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Agent } from './agent'; + +export interface AgentEntry { + entry: Agent; +} diff --git a/lib/js-api/src/api/content-rest-api/model/agentPaging.ts b/lib/js-api/src/api/content-rest-api/model/agentPaging.ts new file mode 100644 index 0000000000..0f6219a0cd --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/agentPaging.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AgentPagingList } from './agentPagingList'; + +export interface AgentPaging { + list?: AgentPagingList; +} diff --git a/lib/js-api/src/api/content-rest-api/model/agentPagingList.ts b/lib/js-api/src/api/content-rest-api/model/agentPagingList.ts new file mode 100644 index 0000000000..00b4baf6a3 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/agentPagingList.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pagination } from './pagination'; +import { AgentEntry } from './agentEntry'; + +export interface AgentPagingList { + entries?: AgentEntry[]; + pagination?: Pagination; +} diff --git a/lib/js-api/src/api/content-rest-api/model/aiAnswer.ts b/lib/js-api/src/api/content-rest-api/model/aiAnswer.ts new file mode 100644 index 0000000000..432d9baa24 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/aiAnswer.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AiAnswerReference } from './aiAnswerReference'; + +export interface AiAnswer { + answer: string; + questionId: string; + references: AiAnswerReference[]; +} diff --git a/lib/js-api/src/api/content-rest-api/model/aiAnswerEntry.ts b/lib/js-api/src/api/content-rest-api/model/aiAnswerEntry.ts new file mode 100644 index 0000000000..17e77b817a --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/aiAnswerEntry.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AiAnswer } from './aiAnswer'; + +export interface AiAnswerEntry { + entry: AiAnswer; +} diff --git a/lib/js-api/src/api/content-rest-api/model/aiAnswerReference.ts b/lib/js-api/src/api/content-rest-api/model/aiAnswerReference.ts new file mode 100644 index 0000000000..f6a8369d07 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/aiAnswerReference.ts @@ -0,0 +1,21 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface AiAnswerReference { + referenceId: string; + referenceText: string; +} diff --git a/lib/js-api/src/api/content-rest-api/model/index.ts b/lib/js-api/src/api/content-rest-api/model/index.ts index 4d421b0043..b375236317 100644 --- a/lib/js-api/src/api/content-rest-api/model/index.ts +++ b/lib/js-api/src/api/content-rest-api/model/index.ts @@ -27,6 +27,13 @@ export * from './activity'; export * from './activityEntry'; export * from './activityPaging'; export * from './activityPagingList'; +export * from './agent'; +export * from './agentEntry'; +export * from './agentPaging'; +export * from './agentPagingList'; +export * from './aiAnswer'; +export * from './aiAnswerEntry'; +export * from './aiAnswerReference'; export * from './association'; export * from './associationBody'; export * from './associationEntry'; @@ -91,6 +98,8 @@ export * from './groupMemberPagingList'; export * from './groupMembershipBodyCreate'; export * from './groupPaging'; export * from './groupPagingList'; +export * from './knowledgeRetrievalConfig'; +export * from './knowledgeRetrievalConfigEntry'; export * from './modelError'; export * from './networkQuota'; export * from './node'; @@ -133,6 +142,8 @@ export * from './preferencePagingList'; export * from './probeEntry'; export * from './probeEntryEntry'; export * from './property'; +export * from './questionModel'; +export * from './questionRequest'; export * from './rating'; export * from './ratingAggregate'; export * from './ratingBody'; diff --git a/lib/js-api/src/api/content-rest-api/model/knowledgeRetrievalConfig.ts b/lib/js-api/src/api/content-rest-api/model/knowledgeRetrievalConfig.ts new file mode 100644 index 0000000000..b92011f2e2 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/knowledgeRetrievalConfig.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface KnowledgeRetrievalConfig { + knowledgeRetrievalUrl: string; +} diff --git a/lib/js-api/src/api/content-rest-api/model/knowledgeRetrievalConfigEntry.ts b/lib/js-api/src/api/content-rest-api/model/knowledgeRetrievalConfigEntry.ts new file mode 100644 index 0000000000..c01b52600b --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/knowledgeRetrievalConfigEntry.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KnowledgeRetrievalConfig } from './knowledgeRetrievalConfig'; + +export interface KnowledgeRetrievalConfigEntry { + entry: KnowledgeRetrievalConfig; +} diff --git a/lib/js-api/src/api/content-rest-api/model/questionModel.ts b/lib/js-api/src/api/content-rest-api/model/questionModel.ts new file mode 100644 index 0000000000..17b994ef44 --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/questionModel.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RestrictionQuery } from './restrictionQuery'; + +export interface QuestionModel { + questionId: string; + question: string; + restrictionQuery: RestrictionQuery; +} diff --git a/lib/js-api/src/api/content-rest-api/model/questionRequest.ts b/lib/js-api/src/api/content-rest-api/model/questionRequest.ts new file mode 100644 index 0000000000..2eaabfac7a --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/questionRequest.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface QuestionRequest { + question: string; + nodeIds: string[]; + agentId: string; +} diff --git a/lib/js-api/src/api/content-rest-api/model/restrictionQuery.ts b/lib/js-api/src/api/content-rest-api/model/restrictionQuery.ts new file mode 100644 index 0000000000..05d5186bcc --- /dev/null +++ b/lib/js-api/src/api/content-rest-api/model/restrictionQuery.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface RestrictionQuery { + nodesIds: string[]; +} diff --git a/lib/js-api/test/content-services/agentsApi.spec.ts b/lib/js-api/test/content-services/agentsApi.spec.ts new file mode 100644 index 0000000000..8b81fffd16 --- /dev/null +++ b/lib/js-api/test/content-services/agentsApi.spec.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AgentMock, EcmAuthMock } from '../mockObjects'; +import { AgentsApi, AlfrescoApi } from '../../src'; +import assert from 'assert'; + +describe('AgentsApi', () => { + let agentMock: AgentMock; + let agentsApi: AgentsApi; + + beforeEach((done) => { + const hostEcm = 'https://127.0.0.1:8080'; + const authResponseMock = new EcmAuthMock(hostEcm); + agentMock = new AgentMock(hostEcm); + authResponseMock.get201Response(); + const alfrescoJsApi = new AlfrescoApi({ + hostEcm + }); + alfrescoJsApi.login('admin', 'admin').then(() => done()); + agentsApi = new AgentsApi(alfrescoJsApi); + }); + + describe('getAgents', () => { + it('should load list of agents', (done) => { + agentMock.mockGetAgents200Response(); + + agentsApi.getAgents().then((paging) => { + assert.deepStrictEqual(paging, { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + id: 'some id 1', + name: 'some name 1' + } + }, + { + entry: { + id: 'some id 2', + name: 'some name 2' + } + } + ] + } + }); + done(); + }); + }); + }); +}); diff --git a/lib/js-api/test/content-services/searchAiApi.spec.ts b/lib/js-api/test/content-services/searchAiApi.spec.ts new file mode 100644 index 0000000000..40c57c87a6 --- /dev/null +++ b/lib/js-api/test/content-services/searchAiApi.spec.ts @@ -0,0 +1,99 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AlfrescoApi, SearchAiApi } from '../../src'; +import { EcmAuthMock, SearchAiMock } from '../mockObjects'; +import assert from 'assert'; + +describe('SearchAiApi', () => { + let searchAiApi: SearchAiApi; + let searchAiMock: SearchAiMock; + + beforeEach((done) => { + const hostEcm = 'https://127.0.0.1:8080'; + const authResponseMock = new EcmAuthMock(hostEcm); + searchAiMock = new SearchAiMock(hostEcm); + authResponseMock.get201Response(); + const alfrescoJsApi = new AlfrescoApi({ + hostEcm + }); + alfrescoJsApi.login('admin', 'admin').then(() => done()); + searchAiApi = new SearchAiApi(alfrescoJsApi); + }); + + describe('ask', () => { + it('should load question information', (done) => { + searchAiMock.mockGetAsk200Response(); + + searchAiApi + .ask([ + { + question: 'some question 1', + nodeIds: ['some node id 1'], + agentId: 'id1' + } + ]) + .then((questions) => { + assert.deepStrictEqual(questions, { + questionId: 'some id 1', + question: 'some question 1', + restrictionQuery: { + nodesIds: ['some node id 1'] + } + }); + done(); + }); + }); + }); + + describe('getAnswer', () => { + it('should load question answer', (done) => { + searchAiMock.mockGetAnswer200Response(); + + searchAiApi.getAnswer('id1').then((answer) => { + assert.deepStrictEqual(answer, { + entry: { + answer: 'Some answer 1', + questionId: 'some id 1', + references: [ + { + referenceId: 'some reference id 1', + referenceText: 'some reference text 1' + } + ] + } + }); + done(); + }); + }); + }); + + describe('getConfig', () => { + it('should load knowledge retrieval configuration', (done) => { + searchAiMock.mockGetConfig200Response(); + + searchAiApi.getConfig().then((config) => { + assert.deepStrictEqual(config, { + entry: { + knowledgeRetrievalUrl: 'https://some-url' + } + }); + done(); + }); + }); + }); +}); diff --git a/lib/js-api/test/mockObjects/content-services/agent.mock.ts b/lib/js-api/test/mockObjects/content-services/agent.mock.ts new file mode 100644 index 0000000000..fc657e2193 --- /dev/null +++ b/lib/js-api/test/mockObjects/content-services/agent.mock.ts @@ -0,0 +1,50 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseMock } from '../base.mock'; +import nock from 'nock'; + +export class AgentMock extends BaseMock { + mockGetAgents200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .get('/alfresco/api/-default-/private/hxi/versions/1/agents') + .reply(200, { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + id: 'some id 1', + name: 'some name 1' + } + }, + { + entry: { + id: 'some id 2', + name: 'some name 2' + } + } + ] + } + }); + } +} diff --git a/lib/js-api/test/mockObjects/content-services/search-ai.mock.ts b/lib/js-api/test/mockObjects/content-services/search-ai.mock.ts new file mode 100644 index 0000000000..f0dcbce15a --- /dev/null +++ b/lib/js-api/test/mockObjects/content-services/search-ai.mock.ts @@ -0,0 +1,69 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseMock } from '../base.mock'; +import nock from 'nock'; + +export class SearchAiMock extends BaseMock { + mockGetAsk200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .post('/alfresco/api/-default-/private/hxi/versions/1/agents/id1/questions', [ + { + question: 'some question 1', + restrictionQuery: { + nodesIds: ['some node id 1'] + } + } + ]) + .reply(200, { + entry: { + question: 'some question 1', + questionId: 'some id 1', + restrictionQuery: { + nodesIds: ['some node id 1'] + } + } + }); + } + + mockGetAnswer200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .get('/alfresco/api/-default-/private/hxi/versions/1/questions/id1/answers/-default-') + .reply(200, { + entry: { + answer: 'Some answer 1', + questionId: 'some id 1', + references: [ + { + referenceId: 'some reference id 1', + referenceText: 'some reference text 1' + } + ] + } + }); + } + + mockGetConfig200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .get('/alfresco/api/-default-/private/hxi/versions/1/config/-default-') + .reply(200, { + entry: { + knowledgeRetrievalUrl: 'https://some-url' + } + }); + } +} diff --git a/lib/js-api/test/mockObjects/index.ts b/lib/js-api/test/mockObjects/index.ts index c3df053f61..717eb97009 100644 --- a/lib/js-api/test/mockObjects/index.ts +++ b/lib/js-api/test/mockObjects/index.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +export * from './content-services/agent.mock'; export * from './content-services/categories.mock'; export * from './content-services/comment.mock'; export * from './content-services/ecm-auth.mock'; @@ -26,6 +27,7 @@ export * from './content-services/groups.mock'; export * from './content-services/find-nodes.mock'; export * from './content-services/rendition.mock'; export * from './content-services/search.mock'; +export * from './content-services/search-ai.mock'; export * from './content-services/tag.mock'; export * from './content-services/upload.mock'; export * from './content-services/version.mock';