diff --git a/.meteor/packages b/.meteor/packages index 43602babb6b8..56838aa7d508 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -166,3 +166,8 @@ underscorestring:underscore.string yasaricli:slugify yasinuslu:blaze-meta deepwell:bootstrap-datepicker2 + +dbs:common +dbs:ai +assistify:help-request +assistify:bot diff --git a/.meteor/versions b/.meteor/versions index a412929b7690..8ea2112fb957 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -8,6 +8,9 @@ accounts-password@1.3.4 accounts-twitter@1.2.0 aldeed:simple-schema@1.5.3 allow-deny@1.0.5 +assistify@0.0.1 +assistify:bot@0.0.1 +assistify:help-request@0.0.1 autoupdate@1.2.11 babel-compiler@6.14.1 babel-runtime@1.0.1 @@ -24,6 +27,8 @@ cfs:http-methods@0.0.32 check@1.2.4 coffeescript@1.12.0_1 dandv:caret-position@2.1.1 +dbs:ai@0.0.1 +dbs:common@0.0.1 ddp@1.2.5 ddp-client@1.3.3 ddp-common@1.2.8 diff --git a/packages/assistify-bot/README.md b/packages/assistify-bot/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/assistify-bot/bot.js b/packages/assistify-bot/bot.js new file mode 100644 index 000000000000..c3de554af23a --- /dev/null +++ b/packages/assistify-bot/bot.js @@ -0,0 +1,5 @@ +// Write your package code here! + +// Variables exported by this module can be imported by other packages and +// applications. See bot-tests.js for an example of importing. +export const name = 'bot'; diff --git a/packages/assistify-bot/config.js b/packages/assistify-bot/config.js new file mode 100644 index 000000000000..285679b0bbb5 --- /dev/null +++ b/packages/assistify-bot/config.js @@ -0,0 +1,17 @@ +Meteor.startup(()=> { + RocketChat.settings.add('Assistify_Bot_Username', "", { + group: 'Assistify', + i18nLabel: 'Assistify_Bot_Username', + type: 'string', + section: 'Bot', + public: true + }); + + RocketChat.settings.add('Assistify_Bot_Automated_Response_Threshold', 50, { + group: 'Assistify', + i18nLabel: 'Assistify_Bot_Automated_Response_Threshold', + type: 'int', + section: 'Bot', + public: true + }); +}); diff --git a/packages/assistify-bot/package.js b/packages/assistify-bot/package.js new file mode 100644 index 000000000000..8706e6cc9271 --- /dev/null +++ b/packages/assistify-bot/package.js @@ -0,0 +1,26 @@ +Package.describe({ + name: 'assistify:bot', + version: '0.0.1', + // Brief, one-line summary of the package. + summary: 'Adds a bot which propagates AI-results', + // URL to the Git repository containing the source code for this package. + git: '', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.onUse(function (api) { + api.versionsFrom('1.4.2.6'); + api.use('ecmascript'); + api.use('assistify:help-request'); + api.mainModule('bot.js'); + + //Server + api.addFiles('config.js', 'server'); + + //hooks + api.addFiles('server/hooks/onKnowledgeProviderResult.js', 'server'); + + //i18n in Rocket.Chat-package (packages/rocketchat-i18n/i18n +}); diff --git a/packages/assistify-bot/server/hooks/onKnowledgeProviderResult.js b/packages/assistify-bot/server/hooks/onKnowledgeProviderResult.js new file mode 100755 index 000000000000..d5430aee32ed --- /dev/null +++ b/packages/assistify-bot/server/hooks/onKnowledgeProviderResult.js @@ -0,0 +1,67 @@ +Meteor.startup(() => { + /* + Trigger a bot to reply with the most relevant result one AI has retrieved results + Do this only once in order to avoid user-frustration + */ + RocketChat.callbacks.add('afterExternalMessage', function (externalMessage) { + + const helpRequest = RocketChat.models.HelpRequests.findOneByRoomId(externalMessage.rid); + if(!helpRequest || helpRequest.latestBotReply){ + return; + } + + let totalResults = []; + + if (externalMessage.result && externalMessage.result.queryTemplates) { + externalMessage.result.queryTemplates.forEach((queryTemplate, templateIndex) => { + queryTemplate.queries.forEach((query) => { + if (query.inlineResultSupport) { + const results = _dbs.getKnowledgeAdapter().getQueryResults(externalMessage.rid, templateIndex, query.creator); + if (results) { + totalResults = totalResults.concat( + results.map((result)=> { + return { + overallScore: query.confidence * (result.score || 1), + replySuggestion: result.replySuggestion + } + }) + ); + } + } + }); + }); + + if (totalResults.length > 0) { + // AI believes it can contribute to the conversation => create a bot-response + const mostRelevantResult = totalResults.reduce((best, current) => current.overallScore > best.overallScore ? current : best, + { + overallScore: 0, + replySuggestion: "" + }); + const scoreThreshold = RocketChat.settings.get('Assistify_Bot_Automated_Response_Threshold'); + if(mostRelevantResult.replySuggestion && (mostRelevantResult.overallScore * 1000) >= scoreThreshold ) { //multiply by 1000 to simplify configuration + const botUsername = RocketChat.settings.get('Assistify_Bot_Username'); + const botUser = RocketChat.models.Users.findOneByUsername(botUsername); + + if (!botUser) { + throw new Meteor.Error('Erroneous Bot-Configuration: Check username') + } + try { + + const botMessage = RocketChat.sendMessage({ + username: botUser.username, + _id: botUser._id + }, {msg: mostRelevantResult.replySuggestion}, {_id: externalMessage.rid}); + + helpRequest.latestBotReply = botMessage; + RocketChat.models.HelpRequests.registerBotResponse(helpRequest._id, botMessage); + + } catch (err) { + console.error('Could not add bot help message', err); + throw new Meteor.Error(err); + } + } + } + } + }, RocketChat.callbacks.priority.LOW) +}); diff --git a/packages/assistify-help-request/README.md b/packages/assistify-help-request/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/assistify-help-request/assets/stylesheets/helpRequestContext.less b/packages/assistify-help-request/assets/stylesheets/helpRequestContext.less new file mode 100755 index 000000000000..e42325c39073 --- /dev/null +++ b/packages/assistify-help-request/assets/stylesheets/helpRequestContext.less @@ -0,0 +1,40 @@ +.assistify-create-section { + margin-bottom: 30px; + + .input-submit { + margin-top: 5px; + } +} +.help-request-context { + padding: 15px; + + .title { + background-color: #ccc; + font-weight: 700; + padding: 10px; + font-size: 15px; + color: #757575 + } + + .context-parameters-wrapper { + border: solid; + border-width: 1px; + border-color: #CCCCCC; + + .context-parameter { + margin: 0; + border: 0; + border-color: #ccc; + border-top: 1px solid #ccc; + font-size: 100%; + vertical-align: baseline; + background-color: white; + padding: 10px 15px; + font-style: italic; + + .value { + font-weight: bold; + } + } + } +} diff --git a/packages/assistify-help-request/client/hooks/openAiTab.js b/packages/assistify-help-request/client/hooks/openAiTab.js new file mode 100644 index 000000000000..d9fa441f2c1f --- /dev/null +++ b/packages/assistify-help-request/client/hooks/openAiTab.js @@ -0,0 +1,10 @@ +/** + * Makes the knowledge base panel open on opening a room in which it is active + * (a request, an expertise or a livechat) + */ +RocketChat.callbacks.add('enter-room', function(subscription){ + const roomOpened = RocketChat.models.Rooms.findOne({_id: subscription.rid}); + if(roomOpened.t === 'r' || roomOpened.t === 'e' || roomOpened.t === 'l'){ + $('.flex-tab-container:not(.opened) .flex-tab-bar .hidden .icon-lightbulb').click(); //there is no ID of the tabbar's Button which we could use so far + } +}); diff --git a/packages/assistify-help-request/client/lib/collections.js b/packages/assistify-help-request/client/lib/collections.js new file mode 100644 index 000000000000..927f035d1cd6 --- /dev/null +++ b/packages/assistify-help-request/client/lib/collections.js @@ -0,0 +1 @@ +var expertiseSearchCache = new Meteor.Collection(null); diff --git a/packages/assistify-help-request/client/views/sideNav/AssistifyCreateChannel.html b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateChannel.html new file mode 100755 index 000000000000..fdd79428429d --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateChannel.html @@ -0,0 +1,8 @@ + diff --git a/packages/assistify-help-request/client/views/sideNav/AssistifyCreateExpertise.html b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateExpertise.html new file mode 100644 index 000000000000..5d90774ce15e --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateExpertise.html @@ -0,0 +1,27 @@ + diff --git a/packages/assistify-help-request/client/views/sideNav/AssistifyCreateExpertise.js b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateExpertise.js new file mode 100755 index 000000000000..6e47500ac3e9 --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateExpertise.js @@ -0,0 +1,127 @@ +import toastr from 'toastr'; + +Template.AssistifyCreateExpertise.helpers({ + selectedUsers: function () { + const instance = Template.instance(); + return instance.selectedUsers.get(); + }, + autocompleteSettings: function () { + return { + limit: 10, + // inputDelay: 300 + rules: [ + { + // @TODO maybe change this 'collection' and/or template + collection: 'UserAndRoom', + subscription: 'userAutocomplete', + field: 'username', + template: Template.userSearch, + noMatchTemplate: Template.userSearchEmpty, + matchAll: true, + filter: { + exceptions: Template.instance().selectedUsers.get() + }, + selector(match) { + return {term: match}; + }, + sort: 'username' + } + ] + }; + } +}); + +Template.AssistifyCreateExpertise.events({ + 'autocompleteselect #experts'(event, instance, doc) { + instance.selectedUsers.set(instance.selectedUsers.get().concat(doc.username)); + + instance.selectedUserNames[doc.username] = doc.name; + + event.currentTarget.value = ''; + return event.currentTarget.focus(); + }, + + 'click .remove-expert'(e, instance) { + let self = this; + + let users = instance().selectedUsers.get(); + users = _.reject(instance().selectedUsers.get(), _id => _id === self.valueOf()); + + instance().selectedUsers.set(users); + + return $('#experts').focus(); + }, + + + 'keyup #expertise': function (e, instance) { + if (e.keyCode == 13) { + instance.$('#experts').focus(); + } + }, + + 'keydown #channel-members'(e, instance) { + if (($(e.currentTarget).val() === '') && (e.keyCode === 13)) { + return instance.$('.save-channel').click(); + } + }, + + 'click .cancel-expertise': function (event, instance) { + SideNav.closeFlex(() => { + instance.clearForm() + }); + }, + + 'click .save-expertise': function (event, instance) { + event.preventDefault(); + const name = instance.find('#expertise').value.toLowerCase().trim(); + instance.expertiseRoomName.set(name); + + if (name) { + Meteor.call('createExpertise', name, instance.selectedUsers.get(), (err, result) => { + if (err) { + console.log(err); + switch (err.error) { + case 'error-invalid-name': + toastr.error(TAPi18n.__('Invalid_room_name', name)); + return; + case 'error-duplicate-channel-name': + toastr.error(TAPi18n.__('Duplicate_channel_name', name)); + return; + case 'error-archived-duplicate-name': + toastr.error(TAPi18n.__('Duplicate_archived_channel_name', name)); + return; + case 'error-no-members': + toastr.error(TAPi18n.__('Expertise_needs_experts', name)); + return; + default: + return handleError(err) + } + } + + // we're done, so close side navigation and navigate to created request-channel + SideNav.closeFlex(() => { + instance.clearForm() + }); + RocketChat.callbacks.run('aftercreateCombined', {_id: result.rid, name: name}); + FlowRouter.go('expertise', {name: name}, FlowRouter.current().queryParams); + }); + } else { + console.log(err); + toastr.error(TAPi18n.__('The_field_is_required', TAPi18n.__('expertise'))); + } + } +}); + +Template.AssistifyCreateExpertise.onCreated(function () { + const instance = this; + instance.expertiseRoomName = new ReactiveVar(''); + instance.selectedUsers = new ReactiveVar([]); + instance.selectedUserNames = {}; + + instance.clearForm = function () { + instance.expertiseRoomName.set(''); + instance.selectedUsers.set([]); + instance.find('#expertise').value = ''; + instance.find('#experts').value = ''; + }; +}); diff --git a/packages/assistify-help-request/client/views/sideNav/AssistifyCreateRequest.html b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateRequest.html new file mode 100644 index 000000000000..149943b9aae6 --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateRequest.html @@ -0,0 +1,13 @@ + diff --git a/packages/assistify-help-request/client/views/sideNav/AssistifyCreateRequest.js b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateRequest.js new file mode 100755 index 000000000000..08ad012585b3 --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/AssistifyCreateRequest.js @@ -0,0 +1,95 @@ +import toastr from 'toastr'; + +Template.AssistifyCreateRequest.helpers({ + autocompleteExpertiseSettings() { + return { + limit: 10, + // inputDelay: 300 + rules: [ + { + // @TODO maybe change this 'collection' and/or template + collection: 'expertise', + subscription: 'autocompleteExpertise', //a server side publication providing the query + field: 'name', + template: Template.roomSearch, + noMatchTemplate: Template.roomSearchEmpty, + matchAll: true, + selector(match) { + return { name: match }; + }, + sort: 'name' + } + ] + }; + } +}); + +Template.AssistifyCreateRequest.events({ + 'autocompleteselect #expertise-search'(event, instance, doc) { + instance.expertise.set(doc.name); + + return instance.find('.save-request').focus(); + }, + + 'keydown #request-name': function (e, instance) { + if ($(e.currentTarget).val().trim() != '' && e.keyCode == 13) { + instance.$('.save-request').click(); + SideNav.closeFlex(()=>{instance.clearForm()}); + } + }, + + 'click .cancel-request': function (event, instance) { + SideNav.closeFlex(()=>{instance.clearForm()}); + }, + + 'click .save-request': function (event, instance) { + event.preventDefault(); + // const name = instance.find('#request-name').value.toLowerCase().trim(); + const expertise = instance.find('#expertise-search').value.toLowerCase().trim(); + instance.requestRoomName.set(name); + + if(name || expertise){ + Meteor.call('createRequest', name, expertise, (err, result) => { + if (err) { + console.log(err); + switch (err.error) { + case 'error-invalid-name': + toastr.error(TAPi18n.__('Invalid_room_name', name)); + return; + case 'error-duplicate-channel-name': + toastr.error(TAPi18n.__('Duplicate_channel_name', name)); + return; + case 'error-archived-duplicate-name': + toastr.error(TAPi18n.__('Duplicate_archived_channel_name', name)); + return; + default: + return handleError(err) + } + } + + // we're done, so close side navigation and navigate to created request-channel + SideNav.closeFlex(()=>{instance.clearForm()}); + RocketChat.callbacks.run('aftercreateCombined', { _id: result.rid, name: name }); + const roomCreated = RocketChat.models.Rooms.findOne({_id: result.rid}); + FlowRouter.go('request', { name: roomCreated.name }, FlowRouter.current().queryParams); + }); + } else { + console.log(err); + toastr.error(TAPi18n.__('The_field_is_required', TAPi18n.__('request-name'))); + } + } +}); + +Template.AssistifyCreateRequest.onCreated(function () { + instance = this; + instance.requestRoomName = new ReactiveVar(''); + instance.expertise = new ReactiveVar(''); + + instance.clearForm = function () { + instance.requestRoomName.set(''); + instance.expertise.set(''); + instance.find('#expertise-search').value = ''; + }; + + +}); diff --git a/packages/assistify-help-request/client/views/sideNav/expertise.html b/packages/assistify-help-request/client/views/sideNav/expertise.html new file mode 100644 index 000000000000..0a99e4b6a3c7 --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/expertise.html @@ -0,0 +1,12 @@ + diff --git a/packages/assistify-help-request/client/views/sideNav/expertise.js b/packages/assistify-help-request/client/views/sideNav/expertise.js new file mode 100644 index 000000000000..3bca3e8473fc --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/expertise.js @@ -0,0 +1,39 @@ +Template.expertise.helpers({ + isActive: function () { + if (ChatSubscription.findOne({ + t: {$in: ['e']}, + f: {$ne: true}, + open: true, + rid: Session.get('openedRoom') + }, {fields: {_id: 1}})) { + return 'active'; + } + }, + + rooms: function () { + let query = { + t: {$in: ['e']}, + open: true + }; + + if (RocketChat.settings.get('Favorite_Rooms')) { + query.f = {$ne: true} + } + + const user = Meteor.user(); + if (user && user.settings && user.settings.preferences && user.settings.preferences.unreadRoomsMode) { + query.alert = { + $ne: true + }; + } + + return ChatSubscription.find(query, {sort: {'t': 1, 'name': 1}}) + } +}); + +Template.expertise.events({ + 'click .more-channels': function () { + SideNav.setFlex("listChannelsFlex"); + SideNav.openFlex(); + } +}); diff --git a/packages/assistify-help-request/client/views/sideNav/requests.html b/packages/assistify-help-request/client/views/sideNav/requests.html new file mode 100644 index 000000000000..6222222f4b46 --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/requests.html @@ -0,0 +1,12 @@ + diff --git a/packages/assistify-help-request/client/views/sideNav/requests.js b/packages/assistify-help-request/client/views/sideNav/requests.js new file mode 100644 index 000000000000..15789e789dbc --- /dev/null +++ b/packages/assistify-help-request/client/views/sideNav/requests.js @@ -0,0 +1,39 @@ +Template.requests.helpers({ + isActive: function () { + if (ChatSubscription.findOne({ + t: {$in: ['r']}, + f: {$ne: true}, + open: true, + rid: Session.get('openedRoom') + }, {fields: {_id: 1}})) { + return 'active'; + } + }, + + rooms: function () { + let query = { + t: {$in: ['r']}, + open: true + }; + + if (RocketChat.settings.get('Favorite_Rooms')) { + query.f = {$ne: true} + } + + const user = Meteor.user(); + if (user && user.settings && user.settings.preferences && user.settings.preferences.unreadRoomsMode) { + query.alert = { + $ne: true + }; + } + + return ChatSubscription.find(query, {sort: {'t': 1, 'name': 1}}) + } +}); + +Template.channels.events({ + 'click .more-channels': function () { + SideNav.setFlex("listChannelsFlex"); + SideNav.openFlex(); + } +}); diff --git a/packages/assistify-help-request/client/views/tabbar/HelpRequestActions.html b/packages/assistify-help-request/client/views/tabbar/HelpRequestActions.html new file mode 100755 index 000000000000..0d1aab850c72 --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/HelpRequestActions.html @@ -0,0 +1,11 @@ + diff --git a/packages/assistify-help-request/client/views/tabbar/HelpRequestActions.js b/packages/assistify-help-request/client/views/tabbar/HelpRequestActions.js new file mode 100755 index 000000000000..2e23a2484f82 --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/HelpRequestActions.js @@ -0,0 +1,129 @@ +Template.HelpRequestActions.helpers({ + helprequestOpen() { + const instance = Template.instance(); + return instance.data.resolutionStatus != 'resolved'; + } +}); + +Template.HelpRequestActions.dialogs = { + ClosingDialog: class { + /** + * @param room the room to get the values from + * @param properties (optional) SweetAlert options + */ + constructor(helpRequest, properties) { + this.room = ChatRoom.findOne(helpRequest.roomId); + this.properties = _.isObject(properties) ? properties : {}; + } + + /** + * @return Promise (keep in mind that native es6-promises aren't cancelable. So always provide a then & catch) + */ + display() { + var self = this; + return new Promise(function (resolve, reject) { + swal.withForm(_.extend({ + title: t('Closing_chat'), + text: '', + formFields: [{ + id: 'comment', + value: self.room.comment, + type: 'input', + label: t("comment"), + placeholder: t('Please_add_a_comment') + }, { + id: 'tags', + value: self.room.tags ? self.room.tags.join(", ") : "", + type: 'input', + placeholder: t('Please_add_a_tag') + }, { + id: 'knowledgeProviderUsage', + type: 'select', + options: [ + {value: 'Unknown', text: t("knowledge_provider_usage_unknown")}, + {value: 'Perfect', text: t("knowledge_provider_usage_perfect")}, + {value: 'Helpful', text: t("knowledge_provider_usage_helpful")}, + {value: 'NotUsed', text: t("knowledge_provider_usage_not_used")}, + {value: 'Useless', text: t("knowledge_provider_usage_useless")} + ] + }], + showCancelButton: true, + closeOnConfirm: false + }, self.properties), function (isConfirm) { + if (!isConfirm) { //on cancel + $('.swal-form').remove(); //possible bug? why I have to do this manually + reject(); + return false; + } + let form = this.swalForm; + for (let key in form) { + if (!form.hasOwnProperty(key)) { + continue; + } + } + resolve(form); + }); + }).then((r) => { + $('.sa-input-error').show(); + return r; + }).catch((reason) => { + throw reason + }); + } + } +}; + +Template.HelpRequestActions.events({ + 'click .close-helprequest': function (event, instance) { + event.preventDefault(); + + swal(_.extend({ + title: t('Closing_chat'), + type: 'input', + inputPlaceholder: t('Please_add_a_comment'), + showCancelButton: true, + closeOnConfirm: false, + roomId: instance.data.roomId + }), (inputValue) => { + if (!inputValue) { + swal.showInputError(t('Please_add_a_comment_to_close_the_room')); + return false; + } + + if (s.trim(inputValue) === '') { + swal.showInputError(t('Please_add_a_comment_to_close_the_room')); + return false; + } + + Meteor.call('assistify:closeHelpRequest', this.roomId, {comment: inputValue}, function(error) { + if (error) { + return handleError(error); + } else { + swal({ + title: t('Chat_closed'), + text: t('Chat_closed_successfully'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + + instance.helpRequest.set( + RocketChat.models.HelpRequests.findOneByRoomId(Template.currentData()) + ); + } + }); + }); + } +}); + +Template.HelpRequestActions.onCreated( function() { + this.helpRequest = new ReactiveVar({}); + this.autorun(() => { + if (Template.currentData().roomId && this.helpRequest.get()) { + this.subscribe('assistify:helpRequests', Template.currentData().roomId); + this.helpRequest.set( + RocketChat.models.HelpRequests.findOneByRoomId(Template.currentData().roomId) + ); + } + }); +}); diff --git a/packages/assistify-help-request/client/views/tabbar/HelpRequestContext.html b/packages/assistify-help-request/client/views/tabbar/HelpRequestContext.html new file mode 100755 index 000000000000..8fe18a0e3356 --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/HelpRequestContext.html @@ -0,0 +1,12 @@ + diff --git a/packages/assistify-help-request/client/views/tabbar/HelpRequestContext.js b/packages/assistify-help-request/client/views/tabbar/HelpRequestContext.js new file mode 100755 index 000000000000..3d8d38bf67a1 --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/HelpRequestContext.js @@ -0,0 +1,73 @@ +Template.HelpRequestContext.helpers({ + /** + * Create a set of name-value-pairs which are being used to visualize the context from which the question has been asked + * @returns {Array} + */ + relevantParameters(){ + const instance = Template.instance(); + const environment = instance.data.environment; + let relevantParameters = []; + + if (environment) { + let value = ''; + let name = ''; + + // transaction + title + name = ''; + value = environment.tcode || environment.program || environment.wd_application; + if (environment.title) { + value = value + ' - ' + environment.title; + } + if (environment.tcode) { + name = 'transaction'; + } else { + if (environment.program) { + name = 'program'; + } else { + if (environment.wd_application) { + name = 'application' + } + } + + } + + if (name) { + relevantParameters.push({ + name, + value + }); + } + + //system information + if (environment.system) { + let systemInfo = environment.system; + if (environment.client) { + systemInfo = systemInfo + "(" + environment.client + ")"; + } + + if (environment.release) { + systemInfo = systemInfo + ', ' + t('release') + ': ' + environment.release; + } + + relevantParameters.push({ + name: 'system', + value: systemInfo + }) + } + } + + return relevantParameters; + } +}); + +Template.HelpRequestContext.onCreated(function () { + this.helpRequest = new ReactiveVar({}); + this.autorun(() => { + if (Template.currentData().rid && this.helpRequest.get()) { + this.subscribe('assistify:helpRequests', Template.currentData().rid); + this.helpRequest.set( + RocketChat.models.HelpRequests.findOneByRoomId(Template.currentData()) + ); + } + }); +}); diff --git a/packages/assistify-help-request/client/views/tabbar/HelpRequestContextParameter.html b/packages/assistify-help-request/client/views/tabbar/HelpRequestContextParameter.html new file mode 100755 index 000000000000..7443711db12b --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/HelpRequestContextParameter.html @@ -0,0 +1,6 @@ + diff --git a/packages/assistify-help-request/client/views/tabbar/HelpRequestContextParameter.js b/packages/assistify-help-request/client/views/tabbar/HelpRequestContextParameter.js new file mode 100755 index 000000000000..86ee92991841 --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/HelpRequestContextParameter.js @@ -0,0 +1,3 @@ +Template.HelpRequestContextParameter.helpers({ + +}); diff --git a/packages/assistify-help-request/client/views/tabbar/tabbarConfig.js b/packages/assistify-help-request/client/views/tabbar/tabbarConfig.js new file mode 100644 index 000000000000..45dc33af462c --- /dev/null +++ b/packages/assistify-help-request/client/views/tabbar/tabbarConfig.js @@ -0,0 +1,21 @@ +/** + * fills the righthandside flexTab-Bar with the relevant tools + * @see packages/rocketchat-livechat/client/ui.js + * @see packages/rocketchat-lib/client/defaultTabBars.js + */ + +RocketChat.TabBar.addGroup('starred-messages', ['request', 'expertise']); +RocketChat.TabBar.addGroup('push-notifications', ['request', 'expertise']); +RocketChat.TabBar.addGroup('channel-settings', ['request', 'expertise']); +RocketChat.TabBar.addGroup('members-list', ['request', 'expertise']); +RocketChat.TabBar.addGroup('message-search',['request', 'expertise']); +RocketChat.TabBar.addGroup('uploaded-files-list',['request', 'expertise']); + +RocketChat.TabBar.addButton({ + groups: ['request', 'expertise', 'live'], + id: 'dbsai', + i18nTitle: 'Knowledge_Base', + icon: 'icon-lightbulb', + template: 'dbsAI_externalSearch', + order: 0 +}); diff --git a/packages/assistify-help-request/config.js b/packages/assistify-help-request/config.js new file mode 100644 index 000000000000..8ea96d009ad8 --- /dev/null +++ b/packages/assistify-help-request/config.js @@ -0,0 +1,38 @@ +const _createExpertsChannel = function(){ + let expertsRoomName = RocketChat.settings.get('Assistify_Expert_Channel'); + + if(expertsRoomName) { + expertsRoomName.toLowerCase(); + } + + if (!RocketChat.models.Rooms.findByTypeAndName('c', expertsRoomName).fetch()) { + RocketChat.models.Rooms.createWithIdTypeAndName(Random.id(), 'c', expertsRoomName); + } +}; + +Meteor.startup(() => { + + + + RocketChat.settings.add('Assistify_Room_Count', 1, { + group: 'Assistify', + i18nLabel: 'Assistify_room_count', + type: 'int', + public: true, + section: 'General' + }); + + RocketChat.settings.add('Assistify_Expert_Channel', TAPi18n.__('Experts'), { + group: 'Assistify', + i18nLabel: 'Experts_channel', + type: 'string', + public: true, + section: 'General' + }); + + _createExpertsChannel(); + + RocketChat.theme.addPackageAsset(() => { + return Assets.getText('assets/stylesheets/helpRequestContext.less'); + }); +}); diff --git a/packages/assistify-help-request/help-request.js b/packages/assistify-help-request/help-request.js new file mode 100644 index 000000000000..df5083724c4c --- /dev/null +++ b/packages/assistify-help-request/help-request.js @@ -0,0 +1,10 @@ +// Write your package code here! + +// Variables exported by this module can be imported by other packages and +// applications. See help-request-tests.js for an example of importing. +export const name = 'help-request'; + +/** + * Public object to attach public functions and classes to + */ +export const helpRequest = {}; diff --git a/packages/assistify-help-request/package.js b/packages/assistify-help-request/package.js new file mode 100644 index 000000000000..8780d3618fd5 --- /dev/null +++ b/packages/assistify-help-request/package.js @@ -0,0 +1,81 @@ +Package.describe({ + name: 'assistify:help-request', + version: '0.0.1', + summary: 'Adds rooms which are to be closed once the initial question has been resolved', + // URL to the Git repository containing the source code for this package. + git: '', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.onUse(function (api) { + api.versionsFrom('1.2.1'); + api.use(['ecmascript', 'underscore', 'coffeescript', 'less@2.5.1']); + api.use(['assistify']); + api.use('rocketchat:lib'); //In order to be able to attach to RocketChat-Global + api.use('rocketchat:livechat'); //Due to external messages + api.use('rocketchat:authorization'); //In order to create custom permissions + api.use(['nimble:restivus', 'rocketchat:authorization', 'rocketchat:api'], 'server'); + api.use('templating', 'client'); + + api.addFiles('help-request.js', 'server'); + api.addFiles('server/types.js', 'server'); + api.addFiles('server/api.js', 'server'); + api.addFiles('server/routes.js', 'server'); + api.addFiles('config.js', 'server'); + api.addFiles('startup/customRoomTypes.js'); + api.addFiles('startup/rolesAndPermissions.js', 'server'); + + // Models + api.addFiles('server/models/Users.js', ['server', 'client']); + api.addFiles('server/models/Rooms.js', ['server', 'client']); + api.addFiles('server/models/HelpRequests.js', ['server', 'client']); + api.addFiles('server/models/LivechatExternalMessage.js', ['server', 'client']); + + api.addFiles('server/publications/Rooms.js', 'server'); + api.addFiles('server/publications/HelpRequests.js', 'server'); + api.addFiles('server/publications/Expertise.js', 'server'); + + //Methods + api.addFiles('server/methods/helpRequestByRoomId.js', 'server'); + api.addFiles('server/methods/closeHelpRequest.js', 'server'); + api.addFiles('server/methods/createRequest.js', 'server'); + api.addFiles('server/methods/createExpertise.js', 'server'); + + // Hooks + api.addFiles('server/hooks/sendMessageToKnowledgeAdapter.js', 'server'); + + ///////// Client + + //Templates + api.addFiles('client/views/tabbar/HelpRequestContext.html', 'client'); + api.addFiles('client/views/tabbar/HelpRequestContext.js', 'client'); + api.addFiles('client/views/tabbar/HelpRequestContextParameter.html', 'client'); + api.addFiles('client/views/tabbar/HelpRequestContextParameter.js', 'client'); + api.addFiles('client/views/tabbar/HelpRequestActions.html', 'client'); + api.addFiles('client/views/tabbar/HelpRequestActions.js', 'client'); + api.addFiles('client/views/sideNav/AssistifyCreateChannel.html', 'client'); + api.addFiles('client/views/sideNav/AssistifyCreateRequest.html', 'client'); + api.addFiles('client/views/sideNav/AssistifyCreateRequest.js', 'client'); + api.addFiles('client/views/sideNav/AssistifyCreateExpertise.html', 'client'); + api.addFiles('client/views/sideNav/AssistifyCreateExpertise.js', 'client'); + api.addFiles('client/views/sideNav/requests.html', 'client'); + api.addFiles('client/views/sideNav/requests.js', 'client'); + api.addFiles('client/views/sideNav/expertise.html', 'client'); + api.addFiles('client/views/sideNav/expertise.js', 'client'); + + //Libraries + // api.addFiles('client/lib/collections.js', 'client'); + + //Hooks + api.addFiles('client/hooks/openAiTab.js', 'client'); + + //Assets + api.addAssets('assets/stylesheets/helpRequestContext.less', 'server'); //has to be done on the server, it exposes the completed css to the client + + //global UI modifications + api.addFiles('client/views/tabbar/tabbarConfig.js', 'client'); + + //i18n in Rocket.Chat-package (packages/rocketchat-i18n/i18n +}); diff --git a/packages/assistify-help-request/server/api.js b/packages/assistify-help-request/server/api.js new file mode 100755 index 000000000000..7ee737720952 --- /dev/null +++ b/packages/assistify-help-request/server/api.js @@ -0,0 +1,152 @@ +// import { HelpDiscussionCreatedResponse } from './types'; +import {helpRequest} from '../help-request'; + +class HelpRequestApi { + + /** + * + * @param bodyParams + * @throws Meteor.Error on invalid request + */ + static validateHelpDiscussionPostRequest(bodyParams) { + // transport the user's information in the header, just like it's done in the RC rest-api + + if (!bodyParams) { + throw new Meteor.Error('Post body empty'); + } + + if (!bodyParams.support_area || bodyParams.support_area.trim() === '') { + throw new Meteor.Error('No support area defined'); + } + + if (!bodyParams.seeker) { + throw new Meteor.Error('No user provided who is seeking help'); + } + + if (!bodyParams.providers || bodyParams.providers.length === 0) { + throw new Meteor.Error('At least one user who could potentially help needs to be supplied'); + } + } + + processHelpDiscussionPostRequest(bodyParams) { + let environment = bodyParams.environment || {}; + let callbackUrl = bodyParams.callbackUrl || ""; + + const creationResult = this._createHelpDiscussion(bodyParams.support_area, bodyParams.seeker, bodyParams.providers, bodyParams.message, environment, callbackUrl); + + //todo: record the helpdesk metadata + + return new helpRequest.HelpDiscussionCreatedResponse( + HelpRequestApi.getUrlForRoom(creationResult.room), + creationResult.providers + ) + } + + static getUrlForRoom(room) { + const siteUrl = RocketChat.settings.get('Site_Url'); + + return siteUrl + 'channel/' + room.name; + } + + _findUsers(userDescriptions) { + const REGEX_OBJECTID = /^[a-f\d]{24}$/i; + let potentialIds = []; + let potentialEmails = []; + + let users = []; + + userDescriptions.forEach((userDescription) => { + if (userDescription.id && userDescription.id.match(REGEX_OBJECTID)) { + potentialIds.push(userDescription.id); + } + + if (userDescription.email && userDescription.email.search('@') !== -1) { + potentialEmails.push(userDescription.email); + } + }); + + if (potentialEmails.length > 0) { + potentialEmails.forEach((emailAddress) => { + users.push(RocketChat.models.Users.findOneByEmailAddress(emailAddress)); + }); + // users = users.concat( + // RocketChat.models.Users.findByEmailAddresses(potentialEmails).fetch() + // ); + } + + if (potentialIds.length > 0) { + potentialIds.forEach((_id) => { + users.push(RocketChat.models.Users.findById(_id).fetch()); + }); + } + + return users; + + } + + static getNextAssistifyRoomCode() { + const settingsRaw = RocketChat.models.Settings.model.rawCollection(); + const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw); + + const query = { + _id: 'Assistify_Room_Count' + }; + + const update = { + $inc: { + value: 1 + } + }; + + const findAndModifyResult = findAndModify(query, null, update); + + return findAndModifyResult.value.value; + } + + /** + * Creates a new room and adds users who potential might be able to help + * @param seeker: The user looking for help. EMail-address and ID accepted + * @param providers: An array of Users who should join the conversation in order to resolve the question. EMail-addresses and IDs accepted + * @param message: The message describing the problematic situation + * @param environment: Context information about the current system-context of the seeker + * @param callback_url: An optional URL which shall be called on reply of a provider + * @private + */ + _createHelpDiscussion(support_area, seeker, providers, message, environment = {}, callback_url = "") { + const seekerUser = this._findUsers([seeker])[0]; + const providerUsers = this._findUsers(providers); + if (!seekerUser) { + throw new Meteor.Error("Invalid user " + JSON.stringify(seeker) + ' provided'); + } + + let channel = {}; + let helpRequestId = ""; + try { + Meteor.runAsUser(seekerUser._id, () => { + channel = Meteor.call('createRequest', 'Assistify_' + HelpRequestApi.getNextAssistifyRoomCode(), providerUsers.map((user) => user.username)); + try { + if (message) { + RocketChat.sendMessage({ + username: seekerUser.username, + _id: seekerUser._id + }, {msg: message}, {_id: channel.rid}); + } + + } catch (err) { + console.error('Could not add help message', err); + throw new Meteor.Error(err); + } + }); + } catch (err) { + //todo: Duplicate channel (same question): Join the seeker + throw new Meteor.Error(err); + } + + return { + room: RocketChat.models.Rooms.findOne({_id: channel.rid}), + providers + }; + } +} + +helpRequest.HelpRequestApi = HelpRequestApi; diff --git a/packages/assistify-help-request/server/hooks/sendMessageToKnowledgeAdapter.js b/packages/assistify-help-request/server/hooks/sendMessageToKnowledgeAdapter.js new file mode 100755 index 000000000000..7d205c1e4c2c --- /dev/null +++ b/packages/assistify-help-request/server/hooks/sendMessageToKnowledgeAdapter.js @@ -0,0 +1,51 @@ +/* globals SystemLogger */ +Meteor.startup( () => { + RocketChat.callbacks.add('afterSaveMessage', function (message, room) { + + let knowledgeEnabled = false; + RocketChat.settings.get('DBS_AI_Enabled', function (key, value) { //todo: Own stting + knowledgeEnabled = value; + }); + + if (!knowledgeEnabled) { + return message; + } + + //help request supplied + if (!(typeof room.t !== 'undefined' && (room.t === 'r' || room.t === 'e'))) { + return message; + } + + const knowledgeAdapter = _dbs.getKnowledgeAdapter(); + if (!knowledgeAdapter) { + return; + } + + //do not trigger a new evaluation if the message was sent from a bot (particularly by assistify itself) + //todo: Remove dependency to bot. It should actually be the other way round. + //proposal: Make bot create metadata in the help-request collection + const botUsername = RocketChat.settings.get('Assistify_Bot_Username'); + if(message.u.username === botUsername){ + return; + } + + Meteor.defer(() => { + + const helpRequest = RocketChat.models.HelpRequests.findOneById(room.helpRequestId); + let context = {}; + if(helpRequest) { //there might be rooms without help request objects if they have been created inside the chat-application + context.contextType = 'ApplicationHelp'; + context.environmentType = helpRequest.supportArea; + context.environment = helpRequest.environment; + } + try { + knowledgeAdapter.onMessage(message, context); + } + catch (e) { + SystemLogger.error('Error using knowledge provider ->', e); + } + }); + + return message; + }, RocketChat.callbacks.priority.LOW) +}); diff --git a/packages/assistify-help-request/server/methods/closeHelpRequest.js b/packages/assistify-help-request/server/methods/closeHelpRequest.js new file mode 100755 index 000000000000..69a0c1391138 --- /dev/null +++ b/packages/assistify-help-request/server/methods/closeHelpRequest.js @@ -0,0 +1,16 @@ +Meteor.methods({ + 'assistify:closeHelpRequest'(roomId, closingProps={}) { + const room = RocketChat.models.Rooms.findOneByIdOrName(roomId); + if(room.helpRequestId) { + RocketChat.models.HelpRequests.close(room.helpRequestId, closingProps); + + // delete subscriptions in order to make the room disappear from the user's clients + const nonOwners = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, room.u._id).fetch(); + nonOwners.forEach((nonOwner)=>{ + RocketChat.models.Subscriptions.removeByRoomIdAndUserId(roomId,nonOwner.u._id); + }); + + RocketChat.callbacks.run('assistify.closeRoom', room, closingProps); + } + } +}); diff --git a/packages/assistify-help-request/server/methods/createExpertise.js b/packages/assistify-help-request/server/methods/createExpertise.js new file mode 100644 index 000000000000..9e8016652534 --- /dev/null +++ b/packages/assistify-help-request/server/methods/createExpertise.js @@ -0,0 +1,20 @@ +Meteor.methods({ + createExpertise(name, members) { + check(name, String); + check(members, Array); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createExpertise' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'create-e')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createExpertise' }); + } + + if (!members || members.length == 0) { + throw new Meteor.Error('error-no-members', 'No members supplied', { method: 'createExpertise' }); + } + + return RocketChat.createRoom('e', name, Meteor.user() && Meteor.user().username, members, false, {}); + } +}); diff --git a/packages/assistify-help-request/server/methods/createRequest.js b/packages/assistify-help-request/server/methods/createRequest.js new file mode 100644 index 000000000000..532952efaa81 --- /dev/null +++ b/packages/assistify-help-request/server/methods/createRequest.js @@ -0,0 +1,62 @@ +Meteor.methods({ + createRequest(name, expertise="", members=[]) { + check(name, String); + check(expertise, String); + + const getNextId = function(expertise){ + // return HelpRequestApi.getNextAssistifyRoomCode(); + const settingsRaw = RocketChat.models.Settings.model.rawCollection(); + const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw); + + const query = { + _id: 'Assistify_Room_Count' + }; + + const update = { + $inc: { + value: 1 + } + }; + + const findAndModifyResult = findAndModify(query, null, update); + + return findAndModifyResult.value.value; + }; + + const getExperts = function(expertise){ + const expertiseRoom = RocketChat.models.Rooms.findOneByName(expertise); + if(expertiseRoom){ + return expertiseRoom.usernames; + } else { + return []; // even if there are no experts in the room, this is valid. A bot could notify lateron about this flaw + } + }; + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createRequest' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'create-r')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createRequest' }); + } + + // If an expertise has been selected, that means that a new room shall be created which addresses the + // experts of this expertise. A new room name shall be created + if(expertise && !name){ + name = expertise + '-' + getNextId(expertise); + } + + if(expertise){ + members = getExperts(expertise); + } + + const roomCreateResult = RocketChat.createRoom('r', name, Meteor.user() && Meteor.user().username, members, false, {}); + + const room = RocketChat.models.Rooms.findOneById(roomCreateResult.rid); + const helpRequestId = RocketChat.models.HelpRequests.createForSupportArea(expertise, roomCreateResult.rid); + //propagate help-id to room in order to identify it as a "helped" room + RocketChat.models.Rooms.addHelpRequestInfo(room, helpRequestId); + + return roomCreateResult; + } +}); diff --git a/packages/assistify-help-request/server/methods/helpRequestByRoomId.js b/packages/assistify-help-request/server/methods/helpRequestByRoomId.js new file mode 100755 index 000000000000..11d461c8eedb --- /dev/null +++ b/packages/assistify-help-request/server/methods/helpRequestByRoomId.js @@ -0,0 +1,24 @@ +Meteor.methods({ + 'assistify:helpRequestByRoomId'(roomId) { + if (roomId) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', {publish: 'assistify:helpRequests'}); + } + const helpRequest = RocketChat.models.HelpRequests.findOneByRoomId(roomId); + if(helpRequest) { + const room = RocketChat.models.Rooms.findOneById(helpRequest.roomId, { + fields: { + helpRequestId: 1, + usernames: 1 + } + }); + const user = RocketChat.models.Users.findOne({_id: Meteor.userId()}); + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'view-r-rooms') && !(room.usernames.indexOf(user.username) > -1)) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', {publish: 'assistify:helpRequests'}); + } + + return helpRequest; + } + } + } +}); diff --git a/packages/assistify-help-request/server/models/HelpRequests.js b/packages/assistify-help-request/server/models/HelpRequests.js new file mode 100755 index 000000000000..cf68ac52b613 --- /dev/null +++ b/packages/assistify-help-request/server/models/HelpRequests.js @@ -0,0 +1,91 @@ +class HelpRequest extends RocketChat.models._Base { + constructor() { + super('helpRequest'); + if(Meteor.isClient) { + this._initModel('helpRequest'); + } + + this.tryEnsureIndex({'roomId': 1}, {unique: 1, sparse: 1}); + this.tryEnsureIndex({'supportArea': 1}); + } + +//-------------------------- FIND ONE + findOneById(_id, options) { + const query = {_id: _id}; + return this.findOne(query, options); + } + + findOneByRoomId(roomId, options) { + const query = {roomId: roomId}; + return this.findOne(query, options); + } + + +//----------------------------- FIND + findById(_id, options) { + return this.find({_id: _id}, options) + } + + findByIds(_ids, options) { + return this.find({_id: {$in: [].concat(_ids)}}, options) + } + + findBySupportArea(supportArea, options) { + const query = {supportArea: supportArea}; + return this.find(query, options); + } + + findByRoomId(roomId, options) { + const query = {roomId: roomId}; + return this.find(query, options); + } + +//---------------------------- CREATE + createForSupportArea(supportArea, roomId, question="", environment={}) { + const helpRequest = { + createdOn: new Date(), + supportArea: supportArea, + roomId: roomId, + question: question, + environment: environment, + resolutionStatus: HelpRequest.RESOLUTION_STATUS.open, + }; + + return this.insert(helpRequest); + } + +//---------------------------- UPDATE + close(_id, closingProperties={}) { + const query = {_id: _id}; + const update = {$set: { + resolutionStatus: HelpRequest.RESOLUTION_STATUS.resolved, + closingProperties: closingProperties + }}; + + + + return this.update(query, update); + } + + registerBotResponse(_id, botMessage) { + const query = {_id: _id}; + const update = {$set: {latestBotReply: botMessage}}; + + return this.update(query, update); + } + +//----------------------------- REMOVE + removeById(_id) { + const query = {_id: _id}; + return this.remove(query); + } +} + +// -------------------------Constants +HelpRequest.RESOLUTION_STATUS = { + open: 'open', + authorAction: 'authorAction', + resolved: 'resolved' +}; + +RocketChat.models.HelpRequests = new HelpRequest(); diff --git a/packages/assistify-help-request/server/models/LivechatExternalMessage.js b/packages/assistify-help-request/server/models/LivechatExternalMessage.js new file mode 100755 index 000000000000..fa0b84aa49be --- /dev/null +++ b/packages/assistify-help-request/server/models/LivechatExternalMessage.js @@ -0,0 +1,11 @@ +/** + * Created by OliverJaegle on 01.08.2016. + */ +// var _ = Npm.require('underscore'); + +_.extend(RocketChat.models.LivechatExternalMessage, { + findOneById: function (_id, options) { + const query = { '_id': _id }; + return this.findOne(query, options) + } +}); diff --git a/packages/assistify-help-request/server/models/Rooms.js b/packages/assistify-help-request/server/models/Rooms.js new file mode 100755 index 000000000000..ae02249eb8ca --- /dev/null +++ b/packages/assistify-help-request/server/models/Rooms.js @@ -0,0 +1,19 @@ +/** + * Created by OliverJaegle on 01.08.2016. + */ +// var _ = Npm.require('underscore'); + +_.extend(RocketChat.models.Rooms, { + addHelpRequestInfo: function (room, helpRequestId) { + const query = { _id: room._id }; + + const update = { + $set: { + helpRequestId: helpRequestId, + v: room.u //Depict the owner of the room as visitor, similar like in livechat + } + }; + + return this.update(query, update); + } +}); diff --git a/packages/assistify-help-request/server/models/Users.js b/packages/assistify-help-request/server/models/Users.js new file mode 100755 index 000000000000..de74b56b4ee5 --- /dev/null +++ b/packages/assistify-help-request/server/models/Users.js @@ -0,0 +1,16 @@ +/** + * Created by OliverJaegle on 01.08.2016. + * Expose features of the users-collection which are not exposed by default + */ +// var _ = Npm.require('underscore'); + +RocketChat.models.Users.findByEmailAddresses = function (emailAddresses, options) { + const query = {'emails.address': {$in: emailAddresses.map((emailAddress) => new RegExp("^" + s.escapeRegExp(emailAddress) + "$", 'i'))}}; + + return this.find(query, options); +}; + +RocketChat.models.Users.findByIds = function (ids, options) { + const query = {'_id': {$in: ids}}; + return this.find(query, options); +}; diff --git a/packages/assistify-help-request/server/publications/Expertise.js b/packages/assistify-help-request/server/publications/Expertise.js new file mode 100644 index 000000000000..62ec7c15b0a3 --- /dev/null +++ b/packages/assistify-help-request/server/publications/Expertise.js @@ -0,0 +1,39 @@ +Meteor.publish('autocompleteExpertise', function (selector, limit = 50) { + if (typeof this.userId === 'undefined' || this.userId === null) { + return this.ready(); + } + + const publication = this; + + const user = RocketChat.models.Users.findOneById(this.userId); + + if (typeof user === 'undefined' || user === null) { + return this.ready(); + } + + const pub = this; + const options = { + fields: { + name: 1, + t: 1 + } + }; + + const cursorHandle = RocketChat.models.Rooms.findByNameContainingTypesWithUsername(selector.name, [{type: 'e'}], options).observeChanges({ + added: function (_id, record) { + return pub.added('autocompleteRecords', _id, record); + }, + changed: function (_id, record) { + return pub.changed('autocompleteRecords', _id, record); + }, + removed: function (_id, record) { + return pub.removed('autocompleteRecords', _id, record); + } + }); + + this.ready(); + + this.onStop(function () { + return cursorHandle.stop(); + }); +}); diff --git a/packages/assistify-help-request/server/publications/HelpRequests.js b/packages/assistify-help-request/server/publications/HelpRequests.js new file mode 100755 index 000000000000..dc260cd85659 --- /dev/null +++ b/packages/assistify-help-request/server/publications/HelpRequests.js @@ -0,0 +1,35 @@ +/** + * Created by OliverJaegle on 10.08.2016. + * Publish Peer-to-peer-specific enhancements to Rocket.Chat models + * + */ + + +Meteor.publish('assistify:helpRequests', function (roomId) { + if (!this.userId) { + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', {publish: 'assistify:helpRequests'})); + } + + const room = RocketChat.models.Rooms.findOneById(roomId, {fields: {helpRequestId: 1}}); + const user = RocketChat.models.Users.findOne({_id: this.userId}); + if (!RocketChat.authz.hasPermission(this.userId, 'view-r-rooms') && !(room.usernames.indexOf(user.username) > -1)) { + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', {publish: 'assistify:helpRequests'})); + } + + + if(room.helpRequestId) { + // this.ready(); + return RocketChat.models.HelpRequests.findByRoomId(room._id, { + fields: { + _id: 1, + roomId: 1, + supportArea: 1, + question: 1, + environment: 1, + resolutionStatus: 1 + } + }); + } else { + return this.ready(); + } +}); diff --git a/packages/assistify-help-request/server/publications/Rooms.js b/packages/assistify-help-request/server/publications/Rooms.js new file mode 100755 index 000000000000..d3f6f4f7a1ed --- /dev/null +++ b/packages/assistify-help-request/server/publications/Rooms.js @@ -0,0 +1,17 @@ +/** + * Created by OliverJaegle on 10.08.2016. + * Publish Peer-to-peer-specific enhancements to Rocket.Chat models + * + */ +Meteor.publish('assistify:room', function ({rid: roomId}) { + if (!this.userId) { + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized')); + } + + // todo: add permission + // if (!RocketChat.authz.hasPermission(this.userId, 'view-livechat-rooms')) { + // return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', {publish: 'livechat:visitorInfo'})); + // } + + return RocketChat.models.Rooms.findOneById(roomId, {fields: {helpRequestId: 1}}); +}); diff --git a/packages/assistify-help-request/server/routes.js b/packages/assistify-help-request/server/routes.js new file mode 100755 index 000000000000..44b60ef1d720 --- /dev/null +++ b/packages/assistify-help-request/server/routes.js @@ -0,0 +1,96 @@ +/** + * Restful API endpoints for interaction with external systems + */ + +import {helpRequest} from '../help-request'; +// import {Restivus} from 'nimble:restivus'; + +const API = new Restivus({ + apiPath: 'assistify/', + useDefaultAuth: true, + prettyJson: true +}); + + +function keysToUpperCase(obj) { + for (let prop in obj){ + if(typeof obj[prop] === "object"){ + obj[prop] = keysToUpperCase(obj[prop]); + } + if(prop != prop.toUpperCase()){ + obj[prop.toUpperCase()] = obj[prop]; + delete obj[prop]; + } + } + return obj; +} + +function keysToLowerCase(obj) { + for (let prop in obj){ + if(typeof obj[prop] === "object"){ + obj[prop] = keysToLowerCase(obj[prop]); + } + if(prop != prop.toLowerCase()){ + obj[prop.toLowerCase()] = obj[prop]; + delete obj[prop]; + } + } + return obj; +} + +function preProcessBody(body) { + body = keysToLowerCase(body); + // Extract properties from the load encapsulated in an additional REQUEST-object + if (body.request) { + for (let key in body.request){ + body[key] = body.request[key]; + } + delete body.request; + } + + //dereference references + for (let key in body){ + if(typeof body[key] === 'object'){ + for (let prop in body[key]){ + if( prop === "%ref") { + let refId = body[key][prop].replace(/\#/, ""); + body[key] = body["%heap"][refId]["%val"]; + } + } + } + } + delete body["%heap"]; +} + +API.addRoute('helpDiscussion', { + /** + * Creates a room with an initial question and adds users who could possibly help + * @see packages\rocketchat-api\server\routes.coffee + * @return {*} statusCode 40x on error or 200 and information on the created room on success + */ + post() { + + // keysToLowerCase(this.bodyParams); + preProcessBody(this.bodyParams); + + const api = new helpRequest.HelpRequestApi(); + try { + helpRequest.HelpRequestApi.validateHelpDiscussionPostRequest(this.bodyParams) + } catch (err) { + console.log('Assistify rejected malformed request:', JSON.stringify(this.request.body, " ", 2)); + throw new Meteor.Error('Malformed request:' + JSON.stringify(err, " ", 2)); + } + + // if (!this.request.headers['X-Auth-Token'])) { //todo: Check authorization - or is this done by Restivus once setting another parameter? + // return {statusCode: 401, message: "Authentication failed."}; + // } + + const creationResult = api.processHelpDiscussionPostRequest(this.bodyParams); + + return { + status_code: 200, + result: creationResult, + RESULT: keysToUpperCase(creationResult) + } + } +}); diff --git a/packages/assistify-help-request/server/types.js b/packages/assistify-help-request/server/types.js new file mode 100755 index 000000000000..abf7e8e0e656 --- /dev/null +++ b/packages/assistify-help-request/server/types.js @@ -0,0 +1,11 @@ +import {helpRequest} from '../help-request'; + +// Definition of value objects. No clue why export interface is not supported +class HelpDiscussionCreatedResponse{ + constructor(url, providersJoined){ + this.url = url; + this.providers_joined = providersJoined; + } +} + +helpRequest.HelpDiscussionCreatedResponse = HelpDiscussionCreatedResponse; diff --git a/packages/assistify-help-request/startup/customRoomTypes.js b/packages/assistify-help-request/startup/customRoomTypes.js new file mode 100644 index 000000000000..eaace6fb91c0 --- /dev/null +++ b/packages/assistify-help-request/startup/customRoomTypes.js @@ -0,0 +1,73 @@ +/** + * Help requests need two additional room types + * @see packages/rocketchat-lib/startup/defaultRoomTypes.js + */ + +/** + * A request is a channel which has a dedicated aim: resolve an issue which + * is asked by the one who started the request (the owner) + * Expertise shall join the room (automagically) and help to get it resolved + */ +RocketChat.roomTypes.add('r', 6, { //5 is livechat + template: 'requests', + icon: 'icon-attention', + route: { + name: 'request', + path: '/request/:name', + action(params) { + return openRoom('r', params.name); + } + }, + + findRoom(identifier) { + const query = { + t: 'r', + name: identifier + }; + return ChatRoom.findOne(query); + }, + + roomName(roomData) { + return roomData.name; + }, + + condition() { + return RocketChat.authz.hasAtLeastOnePermission(['view-c-room', 'view-joined-room']); //todo: own permission + }, + + showJoinLink(roomId) { + return !!ChatRoom.findOne({ _id: roomId, t: 'r' }); + } +}); + +/** + * An expert group is a private group of people who know something + * An expert group is being added to a request-channel on creation based on naming conventions + */ +RocketChat.roomTypes.add('e', 15, { //20 = private messages + template: 'expertise', + icon: 'icon-lightbulb', + route: { + name: 'expertise', + path: '/expertise/:name', + action(params) { + return openRoom('e', params.name); + } + }, + + findRoom(identifier) { + const query = { + t: 'e', + name: identifier + }; + return ChatRoom.findOne(query); + }, + + roomName(roomData) { + return roomData.name; + }, + + condition() { + return RocketChat.authz.hasAllPermission('view-p-room'); //todo: Own authorization + } +}); diff --git a/packages/assistify-help-request/startup/rolesAndPermissions.js b/packages/assistify-help-request/startup/rolesAndPermissions.js new file mode 100644 index 000000000000..c32c3608f667 --- /dev/null +++ b/packages/assistify-help-request/startup/rolesAndPermissions.js @@ -0,0 +1,46 @@ +const _createRolesAndPermissions = function(){ + const permissions = [ + {_id: 'create-r', roles: ['admin', 'user', 'bot', 'guest', 'expert']}, + {_id: 'create-e', roles: ['admin', 'expert', 'bot']}, + {_id: 'view-r-room', roles: ['admin', 'user', 'bot', 'expert']}, //guests shall not view other requests + {_id: 'view-e-room', roles: ['admin', 'user', 'bot', 'expert']}, + ]; + + for (const permission of permissions) { + if (!RocketChat.models.Permissions.findOneById(permission._id)) { + RocketChat.models.Permissions.upsert(permission._id, {$set: permission}); + } + } + + const defaultRoles = [ + { name: 'expert', scope: 'Subscription', description: 'Expert' } //scope obviously has something to do with what access is enabled with this role. No clear idea what though + ]; + + for (const role of defaultRoles) { + RocketChat.models.Roles.upsert({ _id: role.name }, { $setOnInsert: { scope: role.scope, description: role.description || '', protected: true } }); + } +}; + +const _registerExpertsChannelCallback = function () { + RocketChat.callbacks.add('afterJoinRoom', function(user, room){ + const expertRoomName = RocketChat.settings.get('Assistify_Expert_Channel').toLowerCase(); + + if(room.name === expertRoomName){ + RocketChat.authz.addUserRoles(user._id, 'expert'); + } + }); + + RocketChat.callbacks.add('afterLeaveRoom', function(user, room){ + const expertRoomName = RocketChat.settings.get('Assistify_Expert_Channel').toLowerCase(); + + if(room.name === expertRoomName){ + RocketChat.authz.removeUserFromRoles(user._id, 'expert'); + } + }); +}; + +Meteor.startup(()=> { + _createRolesAndPermissions(); + + _registerExpertsChannelCallback(); +}); diff --git a/packages/assistify/README.md b/packages/assistify/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/assistify/assistify.js b/packages/assistify/assistify.js new file mode 100644 index 000000000000..3373b7488be5 --- /dev/null +++ b/packages/assistify/assistify.js @@ -0,0 +1,5 @@ +// Write your package code here! + +// Variables exported by this module can be imported by other packages and +// applications. See assistify-tests.js for an example of importing. +export const name = 'assistify'; diff --git a/packages/assistify/config.js b/packages/assistify/config.js new file mode 100644 index 000000000000..608589af8b66 --- /dev/null +++ b/packages/assistify/config.js @@ -0,0 +1,78 @@ +Meteor.startup(()=>{ + RocketChat.settings.addGroup('Assistify'); + + RocketChat.settings.add('Assistify_Show_Standard_Features', false, { + group: 'Assistify', + i18nLabel: 'Assistify_Show_Standard_Features', + type: 'boolean', + public: true + }); + + // Copy Settings from dbs-ai in order to simplify maintenance. + // Consequently hide the original group + RocketChat.settings.removeById('dbsAI'); + + RocketChat.settings.add('DBS_AI_Enabled', false, { + type: 'boolean', + group: 'Assistify', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'Enabled' + }); + + RocketChat.settings.add('DBS_AI_Source', '', { + type: 'select', + group: 'Assistify', + section: 'Knowledge_Base', + values: [ + { key: '0', i18nLabel: 'DBS_AI_Source_APIAI'}, + { key: '1', i18nLabel: 'DBS_AI_Source_Redlink'} + ], + public: true, + i18nLabel: 'DBS_AI_Source' + }); + + RocketChat.settings.add('DBS_AI_Redlink_URL', '', { + type: 'string', + group: 'Assistify', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'DBS_AI_Redlink_URL' + }); + + /* Currently, Redlink does not offer hashed API_keys, but uses simple password-auth + * This is of course far from perfect and is hopeully going to change sometime later */ + RocketChat.settings.add('DBS_AI_Redlink_Auth_Token', '', { + type: 'string', + group: 'Assistify', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'DBS_AI_Redlink_Auth_Token' + }); + + let domain = RocketChat.settings.get('Site_Url'); + if(domain){ + domain = domain + .replace("https://", "") + .replace("http://", ""); + while(domain.charAt(domain.length - 1) === '/'){ + domain = domain.substr(0, domain.length - 1); + } + } + RocketChat.settings.add('DBS_AI_Redlink_Domain', domain, { + type: 'string', + group: 'Assistify', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'DBS_AI_Redlink_Domain' + }); + + RocketChat.settings.add('Assistify_AI_DBSearch_Suffix','', { + type: 'code', + multiline: true, + group: 'Assistify', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'Assistify_AI_DBSearch_Suffix' + }); +}); diff --git a/packages/assistify/package.js b/packages/assistify/package.js new file mode 100644 index 000000000000..c6f93f7d3469 --- /dev/null +++ b/packages/assistify/package.js @@ -0,0 +1,22 @@ +Package.describe({ + name: 'assistify', + version: '0.0.1', + summary: '', + // URL to the Git repository containing the source code for this package. + git: '', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.onUse(function (api) { + api.versionsFrom('1.4.2.6'); + api.use('ecmascript'); + api.use('dbs:ai'); + api.mainModule('assistify.js'); + + //Server + api.addFiles('config.js', 'server'); + + //i18n in Rocket.Chat-package (packages/rocketchat-i18n/i18n +}); diff --git a/packages/dbs-ai/README.md b/packages/dbs-ai/README.md new file mode 100755 index 000000000000..549cf740ca69 --- /dev/null +++ b/packages/dbs-ai/README.md @@ -0,0 +1,3 @@ +This package contains all artifacts of the redlink-integration which can be isolated. +Some parts deeply integrate with existing Rocket.Chat-components. +For the sake of reduced dependency, potential standard-components shall implement a switch an load modified parts from this package if needed. diff --git a/packages/dbs-ai/assets/icons/Hasso_MLT.png b/packages/dbs-ai/assets/icons/Hasso_MLT.png new file mode 100755 index 000000000000..5e542f3c583d Binary files /dev/null and b/packages/dbs-ai/assets/icons/Hasso_MLT.png differ diff --git a/packages/dbs-ai/assets/icons/Hasso_Search.png b/packages/dbs-ai/assets/icons/Hasso_Search.png new file mode 100755 index 000000000000..5e542f3c583d Binary files /dev/null and b/packages/dbs-ai/assets/icons/Hasso_Search.png differ diff --git a/packages/dbs-ai/assets/icons/assistify.png b/packages/dbs-ai/assets/icons/assistify.png new file mode 100644 index 000000000000..299737197459 Binary files /dev/null and b/packages/dbs-ai/assets/icons/assistify.png differ diff --git a/packages/dbs-ai/assets/icons/communication.png b/packages/dbs-ai/assets/icons/communication.png new file mode 100755 index 000000000000..5e542f3c583d Binary files /dev/null and b/packages/dbs-ai/assets/icons/communication.png differ diff --git a/packages/dbs-ai/assets/icons/dbsearch.png b/packages/dbs-ai/assets/icons/dbsearch.png new file mode 100755 index 000000000000..d461e4ac5bcb Binary files /dev/null and b/packages/dbs-ai/assets/icons/dbsearch.png differ diff --git a/packages/dbs-ai/assets/icons/deselected-circle.png b/packages/dbs-ai/assets/icons/deselected-circle.png new file mode 100644 index 000000000000..8535449286f5 Binary files /dev/null and b/packages/dbs-ai/assets/icons/deselected-circle.png differ diff --git a/packages/dbs-ai/assets/icons/sapTransaction.png b/packages/dbs-ai/assets/icons/sapTransaction.png new file mode 100755 index 000000000000..24ca93e60b1d Binary files /dev/null and b/packages/dbs-ai/assets/icons/sapTransaction.png differ diff --git a/packages/dbs-ai/assets/icons/selected-circle.png b/packages/dbs-ai/assets/icons/selected-circle.png new file mode 100644 index 000000000000..b1872d31a8e1 Binary files /dev/null and b/packages/dbs-ai/assets/icons/selected-circle.png differ diff --git a/packages/dbs-ai/assets/stylesheets/redlink.less b/packages/dbs-ai/assets/stylesheets/redlink.less new file mode 100755 index 000000000000..a3ea77cc9a92 --- /dev/null +++ b/packages/dbs-ai/assets/stylesheets/redlink.less @@ -0,0 +1,629 @@ +.suppressDatetimepicker { + .xdsoft_datetimepicker { + left: 5000px !important; + display: none !important; + position: absolute !important; + } +} +.flex-tab-container .flex-tab .content { + overflow-y: scroll; +} + +.external-search-content { + .title { + .title-icon { + position: relative; + top: 1px; + margin-right: 10px; + img { + width: 20px; + } + } + } + + .icon-spinner:before { + -webkit-animation: spin 2s 20 linear; + -o-animation: spin 2s 20 linear; + animation: spin 2s 20 linear; + } + + .external-message { + padding: 0; + position: relative; + padding-bottom: 20px; + &:after { + border: none; + } + .knowledge-title, .queries-title { + background-color: #ccc; + font-weight: 700; + padding: 10px; + font-size: 15px; + } + .queries-title { + text-align: center; + color: #666; + font-size: 14px; + } + .collapsed > div.query-preview-result-body { + display: none; + } + .query-template-wrapper { + position: relative; + margin: 0 15px 10px; + max-height: 2000px; + transition: max-height .25s ease-in-out; + &.collapsed { + .icon-up-open::before { + content: '\e855' + } + } + &.Rejected { + display: none; + } + &.Confirmed { + .query-template-tools-icon { + &.icon-ok { + color: #1b5ab8; + } + &.icon-cancel { + display: none; + } + } + } + + &.spinner { + .knowledge-queries-wrapper { + opacity: 0.5; + .queries-spinner { + display: block; + } + } + } + + &.collapsed > div.knowledge-queries-wrapper { + display: none; + } + + .query-template-tools-wrapper { + position: absolute; + right: 5px; + top: 10px; + z-index: 10; + .query-template-tools-icon { + // padding: 10px 5px; + cursor: pointer; + &.icon-up-open { + padding-right: 5px; + } + } + } + + .query-template { + background-color: #fff; + border: 1px solid #ccc; + color: #666; + margin-bottom: -1px; + + .value-line-wrapper > div.field-with-label:last-child { + float: right; + } + .value-line-wrapper { + padding: 0 10px 2px; + position: relative; + min-height: 40px; + .field-with-label { + display: inline-block; + &.editing { + position: absolute; + width: 100%; + left: 0; + top: 0; + z-index: 5; + background-color: #fff; + .knowledge-input-wrapper { + width: 78%; + padding: 0; + margin: 0; + .knowledge-base-value { + width: 80%; + background-color: #fff !important; + color: #444; + font-weight: bold; + border: 1px solid #1b5ab8; + } + } + .knowledge-base-label { + padding: 0 2%; + width: 18%; + } + .edit-icons-set { + cursor: pointer; + display: inline-block; + } + .knowledge-input-wrapper.active .knowledge-base-tooltip { + display: none; + } + } + } + } + + .edit-icons-set { + display: none; + } + + .icon-wrapper { + position: relative; + background-color: #efefef; + border-radius: 5px; + cursor: pointer; + .icon-spinner { + position: absolute; + left:6px; + background-color: #efefef; + display: none; + } + &.spinner { + cursor: default; + .icon-spinner { + display: inline-block!important; + } + } + } + + .value-seperator, .icon-wrapper { + min-width: 33px; + display: inline-block; + padding: 5px 6px; + margin: 0 3px; + } + + .knowledge-base-label { + font-weight: bold; + margin-right: 5px; + display: inline-block; + min-width: 55px; + } + .knowledge-input-wrapper { + position: relative; + display: inline-block; + .knowledge-base-tooltip { + display: none; + position: absolute; + height: auto; + width: 200px; + background-color: white; + left: -45px; + color: rgb(68, 68, 68); + margin-top: -8px; + z-index: 100; + box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.1); + &::after { + bottom: 100%; + left: 40%; + content: ""; + position: absolute; + border: solid transparent; + border-bottom-color: white; + border-width: 10px; + margin-left: -10px; + } + .knowledge-context-menu-item { + padding: 6px; + cursor: pointer; + border-bottom: 1px solid #ccc; + &:hover:not(.disabled) { + background-color: #1b5ab8; + color: #fff; + } + &.disabled { + color: rgba(68, 68, 68, 0.4); + cursor: default; + } + } + } + .knowledge-base-value { + font-weight: 400; + background-color: #1b5ab8 !important; + color: #fff; + padding: 5px; + height: 30px; + border-radius: 5px; + margin-bottom: 8px; + display: inline-block; + min-width: 100px; + width: auto; + cursor: pointer; + position: relative; + &.empty-style { + background-color: #efefef !important; + color: #666; + border: 1px dotted #444; + .delete-item { + display: none; + } + } + } + &.active { + .knowledge-base-tooltip { + display: block; + } + } + } + } + } + + .knowledge-queries-wrapper { + padding-bottom: 10px; + position: relative; + .queries-spinner { + position: absolute; + left: 40%; + font-size: 60px; + color: #000; + z-index: 5; + top: 20px; + display: none; + } + .confidence { + font-size: 0.7rem; + display: inline-block; + margin-left: 5px; + } + .query-item-wrapper { + max-height: 2000px; + transition: max-height .25s ease-in-out; + margin: 0 0 -1px; + &.collapsed { + max-height: 50px; + overflow: hidden; + .icon-up-open::before { + content: '\e855' + } + .arrow-navigation-wrapper { + visibility: hidden; + } + } + .query-item { + padding: 10px; + border: 1px solid #CCC; + position: relative; + .query-servicename { + font-size: 12px; + max-width: 225px; + word-wrap: break-word; + display: inline-block; + overflow: hidden; + white-space: nowrap; + vertical-align: middle; + text-overflow: ellipsis; + } + .icon-wrapper { + position: relative; + display: inline-block; + top: 1px; + font-size: 15px; + } + .query-item-logo { + float: left; + line-height: 0px; + margin-top: 0px; + img { + width: 20px; + margin-top: 7px; + margin-bottom: -3px; + } + .creator_label { + position: relative; + top: -2px; + color: #666; + font-weight: bold; + font-size: 15px; + } + } + .query-item-url, .query-results-toggle { + display: inline-block; + padding: 5px; + padding-right: 2px; + color: #666; + float: right; + cursor: pointer; + } + .query-item-url { + margin-right: -6px; + margin-top: 1px; + background-color: #e2e2e2; + &:hover { + color: #444; + } + } + .query-results-toggle { + .js-toggle-results-expanded { + cursor: pointer; + padding: 3px; + } + // position: absolute; + } + } + + .results-slider > div:last-child { + border-bottom: 0; + } + .results-dirty { + min-height: 40px; + padding: 40px 15px; + text-align: center; + background-color: #fff; + .icon-wrapper { + font-size: 40px; + } + } + .no-results { + background-color: white; + border: 1px solid #ccc; + border-top: none; + padding: 5px; + font-style: italic; + text-align: center; + } + .query-preview { + margin-bottom: 0px; + border: 1px solid #ccc; + border-top: none; + background-color: #fff; + position: relative; + em { + font-weight: bold; + } + .travel-bahn { + border: 1px solid #ccc; + margin: 0 8px -1px 0; + width: 274px; + border-left: none; + float: left; + &:last-child { + border-left: 1px solid #ccc; + border-right: none; + margin-right: 0px; + float: right; + } + + .from-to-wrapper { + background-color: #ccc; + padding: 10px 15px; + max-width: 100%; + min-height: 90px; + .from-wrapper { + float: left; + } + .to-wrapper { + float: right; + text-align: right; + } + .location { + font-size: 12px; + font-weight: bold; + margin-bottom: 10px; + max-width: 136px; + } + .time { + font-size: 25px; + font-weight: bold; + margin-bottom: 10px; + } + .day-offset { + font-size: 12px; + font-weight: normal; + } + } + .details-wrapper { + background-color: #fff; + padding: 10px 15px; + min-height: 100px; + .details { + font-size: 12px; + font-weight: bold; + margin-bottom: 5px; + } + } + .price-wrapper { + min-height: 60px; + padding: 10px 15px; + .price-low { + float: left; + } + .price-standard { + float: right; + text-align: right; + } + .price-label { + font-size: 12px; + font-weight: bold; + font-style: italic; + margin-bottom: 5px; + } + .price { + font-size: 22px; + font-weight: bold; + } + } + .action-wrapper { + min-height: 40px; + padding: 10px 15px; + color: #fff; + a { + color: #fff; + } + .action-icon { + display: inline-block; + margin: 0 10px 0 0; + font-size: 15px; + float: right; + } + .link-external, .link-message { + padding: 10px; + cursor: pointer; + width: 110px; + } + .link-external { + float: left; + background-color: #ff0000; + } + .link-message { + float: right; + text-align: right; + background-color: #097E28; + } + } + .not-available { + min-height: 40px; + padding: 10px 15px; + color: #fff; + .action-icon { + display: inline-block; + margin: 0 10px 0 0; + font-size: 15px; + float: right; + color: #ff0000; + } + .js-not-available { + background-color: #ccc; + padding: 10px; + cursor: not-allowed; + } + } + } + + .query-preview-navigation { + text-align: center; + min-width: 50px; + position: relative; + padding-top: 3px; + padding-bottom: 3px; + border-top: 1px solid #ccc; + margin-bottom: 5px; + .js-previous-result, .js-next-result { + cursor: pointer; + padding: 3px; + } + } + + .query-preview-headline-wrapper { + display: none; + border-bottom: 1px solid #ccc; + .query-preview-headline { + float: left; + padding: 10px 15px; + } + } + .result-item-wrapper { + max-height: 2000px; + transition: max-height .25s ease-in-out; + border-bottom: 1px solid #ccc; + overflow: hidden; + &.collapsed { + .icon-up-open::before { + content: '\e855' + } + } + .icon-mail-link { + cursor: pointer; + padding: 2px 0; + display: block; + float: right; + padding-right: 4px; + } + .icon-up-open { + display: block; + padding: 2px 0; + float: right; + cursor: pointer; + } + } + .query-preview-result-title { + padding: 10px; + font-style: italic; + .result-title-wrapper { + float: left; + width: 85%; + overflow: hidden; + padding-top: 3px; + } + .icon-wrapper { + float: right; + // width: 12%; + text-align: right; + .icon-link-ext { + font-size: 15px; + color: #444; + padding-top: 4px; + display: inline-block; + margin-right: 3px; + } + } + } + .query-preview-result-body { + padding: 0px 10px 10px 10px; + .conversationMessages { + overflow: hidden; + width: auto; + height: auto; + .conversationMessage { + overflow: hidden; + width: auto; + height: auto; + padding-left: 30px; + background: url(/packages/dbs_ai/assets/icons/deselected-circle.png) no-repeat 3px center; + background-size: 20px; + cursor: pointer; + &.selected { + background: url(/packages/dbs_ai/assets/icons/selected-circle.png) no-repeat 3px center; + background-size: 20px; + } + } + .messagePart { + clear: both; + float: left; + padding: 6px 10px 7px; + border-radius: 10px 10px 10px 0; + background: #dfdee5; + margin: 6px 0; + font-size: 12px; + line-height: 1.4; + position: relative; + text-shadow: 0 1px 1px rgba(0,0,0,0.2); + width: 280px; + } + .messagePart::before { + content: ''; + position: absolute; + bottom: -6px; + border-top: 6px solid #DFDEE5; + left: 0; + border-right: 7px solid transparent; + } + .messagePart { + &.provider { + float: right; + color: #fff; + background-color: #389FF9; + border-radius: 10px 10px 0 10px; + } + } + .messagePart { + &.provider::before { + left: auto; + right: 0; + border-right: none; + border-left: 5px solid transparent; + border-top: 4px solid #389FF9; + bottom: -4px; + } + } + } + } + } + } + } + } +} diff --git a/packages/dbs-ai/client/lib/ClientResultProvider.js b/packages/dbs-ai/client/lib/ClientResultProvider.js new file mode 100644 index 000000000000..673f10eda2ca --- /dev/null +++ b/packages/dbs-ai/client/lib/ClientResultProvider.js @@ -0,0 +1,921 @@ +export class ClientResultFactory { + getInstance(creator, endpointUrl) { + switch (creator) { + case 'dbsearch': + return new SolrProvider(creator, endpointUrl); + } + } +} + +class SolrProvider { + constructor(creator, endpointUrl) { + this.creator = creator; + this.endpointUrl = endpointUrl; + } + + /** + * Executes an asynchronous data retrieval and hands it over to the callback as single parameter + * @return Promise + */ + executeSearch(queryParameters) { + let customSuffix = RocketChat.settings.get('Assistify_AI_DBSearch_Suffix') || ""; + customSuffix = customSuffix.replace(/\r\n|\r|\n/g, ''); + console.log("executeSearch " + this.endpointUrl + customSuffix); + return new Promise(function (resolve, reject) { + if(mock) { + resolve(SolrProvider.transformResponse(mockData)); + } else { + $.ajax({ + url: this.endpointUrl + customSuffix, + dataType: "jsonp", + jsonp: 'json.wrf', + success: function (data) { + resolve(SolrProvider.transformResponse(data)); + }, + error: function(error){ + reject(new Error('no-dbsearch-result')); + } + }); + } + }) + } + + static transformResponse(data) { + for (var j = 0; j < data.response.docs.length; j++) { + var doc = data.response.docs[j]; + var hl = data.highlighting[doc.id]; + var results = new Array(); + var body; + if (hl && hl['dbsearch_highlight_t_de'] && hl['dbsearch_highlight_t_de'].length > 0) { + body = hl['dbsearch_highlight_t_de']; + } else { + body = [ doc.dbsearch_excerpt_s ]; + } + for (var i = 0; i < body.length; i++) { + var message = { + content: body[i], + user: { + displayName: 'provider' + } + }; + results.push(message); + } + doc['body'] = results; + } + console.log(data); + return data; + } +} + +let mock = false; + +//temporär wenn nicht im Intranet: JSON statisch erzeugen +var mockData = { + "responseHeader": { + "status": 0, + "QTime": 1431, + "params": { + "facet.field": ["{!ex=selSource}dbsearch_source_path", "{!ex=selAuthor}dbsearch_author_ss", "{!ex=selKeywords}dbsearch_keywords_ss", "{!ex=selType}dbsearch_content_type_aggregated_s"], + "f.dbsearch_source_path.facet.limit": "-1", + "AuthenticatedUserDomain": "BKU", + "AuthenticatedUserName": "ruedigerkurz|Ruediger.Kurz@deutschebahn.com", + "json.nl": "map", + "f.dbsearch_author_ss.facet.limit": "20", + "boostlang": "de", + "dbsearch_prefilter": "true", + "rows": "8", + "facet.query": ["{!key=all ex=dateFacet}id:*", "{!key=last_day ex=dateFacet}dbsearch_pub_date_tdt:[NOW-1DAY TO *]", "{!key=last_week ex=dateFacet}dbsearch_pub_date_tdt:[NOW-7DAYS TO *]", "{!key=last_month ex=dateFacet}dbsearch_pub_date_tdt:[NOW-1MONTH TO *]", "{!key=last_year ex=dateFacet}dbsearch_pub_date_tdt:[NOW-1YEAR TO *]", "{!key=older ex=dateFacet}dbsearch_pub_date_tdt:[* TO NOW-1YEAR]", "{!key=missing ex=dateFacet}-dbsearch_pub_date_tdt:[* TO *]"], + "mcf": "true", + "f.dbsearch_keywords_ss.facet.limit": "20", + "q": "test", + "json.wrf": "jQuery221010457950976332464_1489574282541", + "f.dbsearch_author_ss.facet.missing": "true", + "dbsearch_context": "dbsearch", + "facet": "true", + "wt": "json" + } + }, + "securityStatus": "ok", + "response": { + "numFound": 139378, + "start": 0, + "docs": [{ + "dbsearch_mod_date_tdt": "2017-03-01T10:44:11Z", + "dbsearch_source_name_s": "Wiki-Pool DB Systel", + "dbsearch_space_id_s": "eq2016", + "dbsearch_source_type_s": "confluence", + "id": "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=57855715", + "dbsearch_pub_date_tdt": "2017-03-01T09:58:18Z", + "dbsearch_space_name_t": "Einstiegsqualifizierung DB Systel", + "dbsearch_content_type_s": "text/html", + "dbsearch_content_type_aggregated_s": "html", + "dbsearch_keywords_txt": ["test"], + "dbsearch_link_s": "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=57855715", + "dbsearch_doctype_s": "Document", + "dbsearch_author_t": "Mohammad-Sharif Moradi", + "dbsearch_content_size_l": 554, + "dbsearch_title_s": "EQ-Praktikanten bei der DB Systel", + "dbsearch_excerpt_s": "† Einstiegsqualifizierung (EQ): Berichte Was bedeutet EQ Praktikum†Programm? Einstiegsqualifizierung Programm ist eine Mˆglichkeit, Chance und Zugang †f¸r junge†Menschen mit Migrationshintergrund. Es", + "dbsearch_language_s": "de", + "[elevated]": false + }, { + "dbsearch_source_type_s": "sharepoint", + "dbsearch_source_name_s": "TIKA", + "dbsearch_pub_date_tdt": "2017-03-10T15:04:30Z", + "dbsearch_content_type_s": "application/pdf", + "dbsearch_content_type_aggregated_s": "pdf", + "dbsearch_space_id_s": "Muster WA / WA Example", + "id": "https://m00014.sharepoint.noncd.rz.db.de/workingareas/musterwawaexample/Documents/Projekt%202/Test%20Anna%2023.pdf", + "dbsearch_space_name_t": "Muster WA / WA Example", + "dbsearch_content_size_l": 85588, + "dbsearch_mod_date_tdt": "2017-03-10T15:04:30Z", + "dbsearch_link_s": "https://tika.sharepoint.noncd.rz.db.de/workingareas/musterwawaexample/Documents/Projekt%202/Test%20Anna%2023.pdf", + "dbsearch_title_s": "Test Anna 23", + "dbsearch_classifier_ss": ["gassc"], + "dbsearch_extracted_pub_date_tdt": "2017-03-10T15:04:01Z", + "dbsearch_extracted_mod_date_tdt": "2017-03-10T15:04:01Z", + "dbsearch_excerpt_s": "Test Anna 23 Anna 01.03.2017 Mit Kommentaren am 10.03.2017 Und noch mehr Kommentare am 10.03.2017", + "dbsearch_language_s": "de", + "dbsearch_doctype_s": "Document", + "[elevated]": false + }, { + "dbsearch_source_type_s": "sharepoint", + "dbsearch_source_name_s": "TIKA", + "dbsearch_pub_date_tdt": "2017-03-10T15:03:05Z", + "dbsearch_content_type_s": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "dbsearch_content_type_aggregated_s": "msword", + "dbsearch_space_id_s": "Muster WA / WA Example", + "id": "https://m00014.sharepoint.noncd.rz.db.de/workingareas/musterwawaexample/Documents/Projekt%202/Test%20Anna%2023.docx", + "dbsearch_space_name_t": "Muster WA / WA Example", + "dbsearch_content_size_l": 21276, + "dbsearch_mod_date_tdt": "2017-03-10T15:03:05Z", + "dbsearch_link_s": "https://tika.sharepoint.noncd.rz.db.de/workingareas/musterwawaexample/Documents/Projekt%202/Test%20Anna%2023.docx", + "dbsearch_title_s": "Test Anna 23", + "dbsearch_classifier_ss": ["gassc"], + "dbsearch_extracted_pub_date_tdt": "2015-12-04T11:25:00Z", + "dbsearch_extracted_mod_date_tdt": "2017-03-10T15:02:47Z", + "dbsearch_excerpt_s": "Test Anna 23 Anna 01.03.2017 Mit Kommentaren am 10.03.2017 Und noch mehr Kommentare am 10.03.2017", + "dbsearch_language_s": "de", + "dbsearch_doctype_s": "Document", + "[elevated]": false + }, { + "dbsearch_mod_date_tdt": "2017-03-01T12:21:09Z", + "dbsearch_source_name_s": "Athene", + "dbsearch_source_type_s": "Athene", + "id": "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=25748005", + "dbsearch_pub_date_tdt": "2017-03-01T12:21:09Z", + "dbsearch_content_type_s": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "dbsearch_content_type_aggregated_s": "mspowerpoint", + "dbsearch_link_s": "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=25748005", + "dbsearch_content_size_l": 241520, + "dbsearch_title_s": "Umgebungen - Instanzen.pptx", + "dbsearch_extracted_title_s": "Test", + "dbsearch_extracted_pub_date_tdt": "2005-02-21T07:36:49Z", + "dbsearch_extracted_mod_date_tdt": "2017-03-01T12:20:34Z", + "dbsearch_excerpt_s": "DB Systel GmbH | PAISY-Point | I.LPA 54 | 13.07.2016 PAISY CS Umgebungen ñ Instanzen Teilweise freigegeben Hinweis: F¸r externe Pr‰sentationen bitte immer eine Titelfolie mit der Ressort-Farbe", + "dbsearch_language_s": "de", + "dbsearch_doctype_s": "Document", + "[elevated]": false + }, { + "dbsearch_mod_date_tdt": "2017-02-22T14:28:03Z", + "dbsearch_source_name_s": "Wiki-Pool DB Systel", + "dbsearch_space_id_s": "dbsystel", + "dbsearch_source_type_s": "confluence", + "id": "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=57850201", + "dbsearch_pub_date_tdt": "2017-02-22T14:14:28Z", + "dbsearch_space_name_t": "DB Systel Wiki", + "dbsearch_content_type_s": "text/html", + "dbsearch_content_type_aggregated_s": "html", + "dbsearch_keywords_txt": ["test", "protokoll"], + "dbsearch_link_s": "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=57850201", + "dbsearch_doctype_s": "Document", + "dbsearch_author_t": "Peter Markwart", + "dbsearch_content_size_l": 5798, + "dbsearch_title_s": "D.IPB 44 - Protokoll - Abteilungsmeeting 22.02.17", + "dbsearch_excerpt_s": "† † † † Seitennavigation >>† Meetings und Protokolle der D.IPB 44 † Allgemeine Daten Thema des Meetings Abteilungsmeeting Datum 22.02.2017 Zeitraum 13:30 - 14:30 Uhr Ort Silberturm J02 03 Teilnehmer", + "dbsearch_language_s": "de", + "[elevated]": false + }, { + "dbsearch_mod_date_tdt": "2017-01-26T09:52:08Z", + "dbsearch_source_name_s": "Wiki-Pool DB Systel", + "dbsearch_space_id_s": "dbsystel", + "dbsearch_source_type_s": "confluence", + "id": "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=56552993", + "dbsearch_pub_date_tdt": "2017-01-26T09:30:16Z", + "dbsearch_space_name_t": "DB Systel Wiki", + "dbsearch_content_type_s": "text/html", + "dbsearch_content_type_aggregated_s": "html", + "dbsearch_keywords_txt": ["test", "protokoll"], + "dbsearch_link_s": "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=56552993", + "dbsearch_doctype_s": "Document", + "dbsearch_author_t": "Peter Markwart", + "dbsearch_content_size_l": 6318, + "dbsearch_title_s": "D.IPB 44 - Protokoll - Abteilungsmeeting 25.01.17", + "dbsearch_excerpt_s": "† † † † Seitennavigation >>† Meetings und Protokolle der D.IPB 44 † Allgemeine Daten Thema des Meetings Abteilungsmeeting Datum 25.01.2017 Zeitraum 13:30 - 15:30 Uhr Ort Silberturm J02 03 Teilnehmer", + "dbsearch_language_s": "de", + "[elevated]": false + }, { + "dbsearch_mod_date_tdt": "2017-01-27T15:34:56Z", + "dbsearch_source_name_s": "Athene", + "dbsearch_source_type_s": "Athene", + "id": "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=27775601", + "dbsearch_pub_date_tdt": "2017-01-27T15:34:56Z", + "dbsearch_content_type_s": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "dbsearch_content_type_aggregated_s": "mspowerpoint", + "dbsearch_link_s": "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=27775601", + "dbsearch_content_size_l": 871516, + "dbsearch_title_s": "170126 - Info GBR Vertrieb Status KFMS.pptx", + "dbsearch_extracted_title_s": "Test", + "dbsearch_extracted_pub_date_tdt": "2005-02-21T07:36:49Z", + "dbsearch_extracted_mod_date_tdt": "2017-01-27T07:45:52Z", + "dbsearch_excerpt_s": "Zukunft Bahn Projekt Kundenfeedbackmanagement 30.01.2017 Hinweis: ÑVielen Dank f¸r Ihre Aufmerksamkeitì kann auch durch ein anderes Abschlusszitat oder eine Botschaft ersetzt werden. 1 ‹bergang ZuBa", + "dbsearch_language_s": "de", + "dbsearch_doctype_s": "Document", + "[elevated]": false + }, { + "dbsearch_mod_date_tdt": "2017-01-26T13:40:31Z", + "dbsearch_source_name_s": "Athene", + "dbsearch_source_type_s": "Athene", + "id": "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=27761612", + "dbsearch_pub_date_tdt": "2017-01-26T13:40:31Z", + "dbsearch_content_type_s": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "dbsearch_content_type_aggregated_s": "mspowerpoint", + "dbsearch_link_s": "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=27761612", + "dbsearch_content_size_l": 1166289, + "dbsearch_title_s": "170124 - 7. Review Board gezeigte Version.pptx", + "dbsearch_extracted_title_s": "Test", + "dbsearch_extracted_pub_date_tdt": "2005-02-21T07:36:49Z", + "dbsearch_extracted_mod_date_tdt": "2017-01-25T07:30:10Z", + "dbsearch_excerpt_s": "Zukunft Bahn Projekt Kundenfeedbackmanagement 7. Review Board Projekt Kundenfeedbackmanagement 24.01.2017 Hinweis: ÑVielen Dank f¸r Ihre Aufmerksamkeitì kann auch durch ein anderes Abschlusszitat", + "dbsearch_language_s": "de", + "dbsearch_doctype_s": "Document", + "[elevated]": false + }] + }, + "facet_counts": { + "facet_queries": { + "all": 139378, + "last_day": 11, + "last_week": 207, + "last_month": 843, + "last_year": 20724, + "older": 118468, + "missing": 186 + }, + "facet_fields": { + "dbsearch_source_path": { + "Athene": 120965, + "BahnWiki": 814, + "BahnWiki/BEAM-Wiki": 49, + "BahnWiki/BahnWiki+-+Das+Wiki+der+Bahn": 17, + "BahnWiki/CIOFernverkehr": 44, + "BahnWiki/Controlling-Fernverkehr": 23, + "BahnWiki/Corporate+Real+Estate+Wiki": 3, + "BahnWiki/DB+Cargo+Data+Management": 8, + "BahnWiki/DB+Institute+of+Technology": 29, + "BahnWiki/DBMAS-Wiki": 19, + "BahnWiki/DS.C": 275, + "BahnWiki/Dual-Studierenden+Wiki": 3, + "BahnWiki/Enterprise+Mobility+Management": 3, + "BahnWiki/FZI-PS": 8, + "BahnWiki/ISI+Wiki": 35, + "BahnWiki/ITK-Wiki": 113, + "BahnWiki/Infrastrukturstammdaten": 3, + "BahnWiki/KIT+Kommunikation.Information.Technologie": 33, + "BahnWiki/Mafo+Wiki": 29, + "BahnWiki/Microsoft+Office+2010": 6, + "BahnWiki/PSSx": 3, + "BahnWiki/Prometheus": 8, + "BahnWiki/Serviceportal": 54, + "BahnWiki/Wiki+d%27Euro+Cargo+Rail": 8, + "BahnWiki/Wiki-Safety": 1, + "BahnWiki/iTWO+Wiki": 40, + "Blogs": 141, + "Blogs/Agile+Gedanken": 2, + "Blogs/Agile+Round+Table": 2, + "Blogs/Alles+im+gr%C3%BCnen+Bereich": 1, + "Blogs/Architektur+vs.+Details": 1, + "Blogs/Bieberblog": 2, + "Blogs/Code+Zukunft+Kommunikation": 1, + "Blogs/DB+Knowledge+Core": 1, + "Blogs/DCS+Dialog": 1, + "Blogs/Datenschutz": 1, + "Blogs/Der+unbekannte+Dritte": 31, + "Blogs/Design+Thinking": 4, + "Blogs/G+1.05+Blog": 4, + "Blogs/GF+en+Blog": 5, + "Blogs/INetBlog": 1, + "Blogs/JUC%26%23039%3Bs+World": 1, + "Blogs/LVD+8+News+-+Strategy+and+Consulting": 3, + "Blogs/Logbuch+der+Ver%C3%A4nderungen": 2, + "Blogs/Mobile+Development": 12, + "Blogs/Neues+von+Q": 18, + "Blogs/Notes+on+Notes": 7, + "Blogs/Nummer+32": 2, + "Blogs/Plattform+News": 3, + "Blogs/Return+of+Developers": 10, + "Blogs/SXSW": 2, + "Blogs/Securing+the+Land+of+01": 2, + "Blogs/Sendungsbewusstsein": 4, + "Blogs/Small+Solutions": 3, + "Blogs/Technologie-+und+Innovationsmanagement": 5, + "Blogs/User+Experience": 10, + "DB+Personalportal": 12, + "DB+Systel+Forum": 12, + "DB+Systel+Forum/Biete+und+Suche": 1, + "DB+Systel+Forum/Rollout+DB+Communicator": 4, + "DB+Systel+Forum/SW-Entwicklung": 2, + "DB+Systel+Forum/Strategie": 2, + "DB+Systel+Forum/Web+2.0%3A+Neues%2C+Fragen%2C+Ideen%2C+Tipps+und+Tricks": 3, + "DB+im+Internet": 2779, + "DB+im+Internet/%D0%94%D0%91+%D0%A8%D0%B5%D0%BD%D0%BA%D0%B5%D1%80+%D0%9C%D0%B0%D0%BA%D0%B5%D0%B4%D0%BE%D0%BD%D0%B8%D1%98%D0%B0": 1, + "DB+im+Internet/%E5%BE%B7%E9%90%B5%E4%BF%A1%E5%8F%AF%E5%8F%B0%E7%81%A3": 51, + "DB+im+Internet/%E5%BE%B7%E9%93%81%E4%BF%A1%E5%8F%AF%E5%9C%A8%E4%B8%AD%E5%9B%BD": 7, + "DB+im+Internet/+DB+Schenker+Norway": 9, + "DB+im+Internet/Anlagentechnik%2C+Bautechnik+und+ITK": 11, + "DB+im+Internet/Bahnbau+Gruppe": 1, + "DB+im+Internet/BeMobility": 46, + "DB+im+Internet/CDN+Test": 10, + "DB+im+Internet/DB+-+Pressestelle+-+Berlin": 7, + "DB+im+Internet/DB+-+Pressestelle+-+D%C3%BCsseldorf": 10, + "DB+im+Internet/DB+-+Pressestelle+-+Frankfurt": 1, + "DB+im+Internet/DB+-+Pressestelle+-+Hamburg": 3, + "DB+im+Internet/DB+-+Pressestelle+-+M%C3%BCnchen": 8, + "DB+im+Internet/DB+-+Pressestelle+-+Stuttgart": 6, + "DB+im+Internet/DB+-+Pressestelle+Leipzig": 5, + "DB+im+Internet/DB+Cargo+-+Danmark": 14, + "DB+im+Internet/DB+Cargo+-+Romania": 3, + "DB+im+Internet/DB+Cargo+AG": 50, + "DB+im+Internet/DB+Cargo+BTT+GmbH": 6, + "DB+im+Internet/DB+Cargo+Polska+S.A.": 11, + "DB+im+Internet/DB+Cargo+UK": 12, + "DB+im+Internet/DB+Cargo+in+Italy": 1, + "DB+im+Internet/DB+Cargo+in+Nederland": 3, + "DB+im+Internet/DB+Dialog+GmbH": 2, + "DB+im+Internet/DB+Energie": 6, + "DB+im+Internet/DB+Engineering+%26+Consulting": 30, + "DB+im+Internet/DB+Fahrzeuginstandhaltung": 12, + "DB+im+Internet/DB+Heavy+Maintenance": 27, + "DB+im+Internet/DB+Konzern": 108, + "DB+im+Internet/DB+Management+Consulting": 2, + "DB+im+Internet/DB+Museum": 9, + "DB+im+Internet/DB+Netz+AG": 99, + "DB+im+Internet/DB+Port+Szczecin": 12, + "DB+im+Internet/DB+Schenker": 25, + "DB+im+Internet/DB+Schenker+%C3%96sterreich+-+Logistik-+und+Transportunternehmen": 3, + "DB+im+Internet/DB+Schenker+%D0%B2+%D0%91%D0%B5%D0%BB%D0%B0%D1%80%D1%83%D1%81%D0%B8": 2, + "DB+im+Internet/DB+Schenker+%D0%B2+%D0%91%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D0%B8%D1%8F": 1, + "DB+im+Internet/DB+Schenker+%D0%B2+%D0%A3%D0%BA%D1%80%D0%B0%D0%B8%D0%BD%D0%B5": 15, + "DB+im+Internet/DB+Schenker++%CE%95%CE%BB%CE%BB%CE%AC%CE%B4%CE%B1": 1, + "DB+im+Internet/DB+Schenker++Greece+": 7, + "DB+im+Internet/DB+Schenker++Macedonia": 6, + "DB+im+Internet/DB+Schenker++in+Bulgaria": 9, + "DB+im+Internet/DB+Schenker+-+P%C5%99eprava+a+logistika": 8, + "DB+im+Internet/DB+Schenker+-+Transport+and+Logistics": 17, + "DB+im+Internet/DB+Schenker+Argentina": 5, + "DB+im+Internet/DB+Schenker+Arkas+T%C3%BCrkiye": 2, + "DB+im+Internet/DB+Schenker+Arkas+Turkey": 6, + "DB+im+Internet/DB+Schenker+Australia+%2F+New+Zealand": 13, + "DB+im+Internet/DB+Schenker+Austria+-+Transportation+and+Logistics": 12, + "DB+im+Internet/DB+Schenker+BTT+GmbH": 2, + "DB+im+Internet/DB+Schenker+Chile+Countrysite": 2, + "DB+im+Internet/DB+Schenker+Corporate+Layout+2014": 1, + "DB+im+Internet/DB+Schenker+France+site": 9, + "DB+im+Internet/DB+Schenker+Hrvatska": 1, + "DB+im+Internet/DB+Schenker+Italia": 9, + "DB+im+Internet/DB+Schenker+Italy": 19, + "DB+im+Internet/DB+Schenker+Latvia+-+Transportation+and+Logistics": 5, + "DB+im+Internet/DB+Schenker+Latvia+-+Transports+un+Lo%C4%A3istika": 3, + "DB+im+Internet/DB+Schenker+Lietuva": 22, + "DB+im+Internet/DB+Schenker+Logistics+Bosnia+and+Herzegovina": 6, + "DB+im+Internet/DB+Schenker+Logistics+Romania": 7, + "DB+im+Internet/DB+Schenker+Logistics+Romania+++": 2, + "DB+im+Internet/DB+Schenker+Logistics+UK": 20, + "DB+im+Internet/DB+Schenker+Logistics+in+Croatia": 7, + "DB+im+Internet/DB+Schenker+Logistics+in+Poland": 2, + "DB+im+Internet/DB+Schenker+Malaysia": 57, + "DB+im+Internet/DB+Schenker+Norge": 5, + "DB+im+Internet/DB+Schenker+Peru": 8, + "DB+im+Internet/DB+Schenker+Poland": 4, + "DB+im+Internet/DB+Schenker+Privpak": 4, + "DB+im+Internet/DB+Schenker+Russia": 1, + "DB+im+Internet/DB+Schenker+Slovenia": 7, + "DB+im+Internet/DB+Schenker+Slovenija": 1, + "DB+im+Internet/DB+Schenker+Sverige": 22, + "DB+im+Internet/DB+Schenker+Sweden": 8, + "DB+im+Internet/DB+Schenker+Taiwan": 59, + "DB+im+Internet/DB+Schenker+Transa": 1, + "DB+im+Internet/DB+Schenker+em+Angola": 3, + "DB+im+Internet/DB+Schenker+em+Portugal": 11, + "DB+im+Internet/DB+Schenker+en+Am%C3%A9rica+Latina": 3, + "DB+im+Internet/DB+Schenker+en+Guatemala": 2, + "DB+im+Internet/DB+Schenker+en+Mexico+++": 2, + "DB+im+Internet/DB+Schenker+en+Panam%C3%A1": 3, + "DB+im+Internet/DB+Schenker+en+Venezuela+": 2, + "DB+im+Internet/DB+Schenker+i+Danmark": 5, + "DB+im+Internet/DB+Schenker+in+Angola": 6, + "DB+im+Internet/DB+Schenker+in+Asia+Pacific": 30, + "DB+im+Internet/DB+Schenker+in+Belarus": 11, + "DB+im+Internet/DB+Schenker+in+Belgi%C3%AB": 22, + "DB+im+Internet/DB+Schenker+in+Belgium": 35, + "DB+im+Internet/DB+Schenker+in+Brazil": 6, + "DB+im+Internet/DB+Schenker+in+Cambodia": 6, + "DB+im+Internet/DB+Schenker+in+Canada": 30, + "DB+im+Internet/DB+Schenker+in+China": 19, + "DB+im+Internet/DB+Schenker+in+Denmark": 18, + "DB+im+Internet/DB+Schenker+in+Egypt": 40, + "DB+im+Internet/DB+Schenker+in+Estonia": 70, + "DB+im+Internet/DB+Schenker+in+Finland": 1, + "DB+im+Internet/DB+Schenker+in+Guatemala": 4, + "DB+im+Internet/DB+Schenker+in+Hungary": 3, + "DB+im+Internet/DB+Schenker+in+India": 9, + "DB+im+Internet/DB+Schenker+in+Ireland": 1, + "DB+im+Internet/DB+Schenker+in+Kenya": 39, + "DB+im+Internet/DB+Schenker+in+Korea": 10, + "DB+im+Internet/DB+Schenker+in+Latin+America": 3, + "DB+im+Internet/DB+Schenker+in+Lithuania": 30, + "DB+im+Internet/DB+Schenker+in+Luxembourg": 7, + "DB+im+Internet/DB+Schenker+in+Mexico": 6, + "DB+im+Internet/DB+Schenker+in+Oman": 40, + "DB+im+Internet/DB+Schenker+in+Panama": 4, + "DB+im+Internet/DB+Schenker+in+Philippines": 45, + "DB+im+Internet/DB+Schenker+in+Portugal": 13, + "DB+im+Internet/DB+Schenker+in+Saudi+Arabia": 13, + "DB+im+Internet/DB+Schenker+in+Serbia": 4, + "DB+im+Internet/DB+Schenker+in+Singapore": 18, + "DB+im+Internet/DB+Schenker+in+Slovakia": 13, + "DB+im+Internet/DB+Schenker+in+Spain": 2, + "DB+im+Internet/DB+Schenker+in+Switzerland": 17, + "DB+im+Internet/DB+Schenker+in+Thailand": 47, + "DB+im+Internet/DB+Schenker+in+UAE": 4, + "DB+im+Internet/DB+Schenker+in+Ukraine": 26, + "DB+im+Internet/DB+Schenker+in+Venezuela+": 7, + "DB+im+Internet/DB+Schenker+in+Vietnam+%7C+Ocean+Freight+-+Air+Freight+-+Fairs+%26+Exhibitions": 42, + "DB+im+Internet/DB+Schenker+in+der+Schweiz": 12, + "DB+im+Internet/DB+Schenker+in+the+USA": 54, + "DB+im+Internet/DB+Schenker+na+Slovensku": 1, + "DB+im+Internet/DB+Schenker+no+Brasil": 6, + "DB+im+Internet/DB+Schenker+u+Srbiji": 2, + "DB+im+Internet/DB+Station+%26+Service": 5, + "DB+im+Internet/DB+Systel+-+Konzerntreff+2016": 2, + "DB+im+Internet/DB+Systel+GmbH": 18, + "DB+im+Internet/DB+Systel+GmbH+": 9, + "DB+im+Internet/DB+Systemtechnik": 350, + "DB+im+Internet/DB+Training%2C+Learning+%26+Consulting": 15, + "DB+im+Internet/DB+Zeitarbeit": 1, + "DB+im+Internet/Deutsche+Bahn": 55, + "DB+im+Internet/Deutsche+Bahn+-+2013+Annual+Report": 18, + "DB+im+Internet/Deutsche+Bahn+-+Gesch%C3%A4ftsbericht+2013": 8, + "DB+im+Internet/Deutsche+Bahn+-+Interim+Report+January+-+June+2014": 4, + "DB+im+Internet/Deutsche+Bahn+Stiftung": 2, + "DB+im+Internet/ESG+2014": 8, + "DB+im+Internet/Elbe-Saale-Bahn": 2, + "DB+im+Internet/Enterprise+Content+Management": 3, + "DB+im+Internet/Etihad+Rail+DB": 2, + "DB+im+Internet/Euro+Cargo+Rail": 8, + "DB+im+Internet/FB-Community": 51, + "DB+im+Internet/Home+-+DB+Schenker+in+Indonesia": 45, + "DB+im+Internet/Infrastrukturportal": 34, + "DB+im+Internet/Intermodal": 2, + "DB+im+Internet/Investor+Relations": 71, + "DB+im+Internet/Investor+Relations+DB+AG": 133, + "DB+im+Internet/L%C3%A4rmschutzportal": 8, + "DB+im+Internet/PopUpStore": 1, + "DB+im+Internet/Portal+F": 2, + "DB+im+Internet/S-Bahn+Stuttgart": 5, + "DB+im+Internet/SUSSTATION": 13, + "DB+im+Internet/Schenker+Deutschland+AG": 30, + "DB+im+Internet/Schenker+Logistics+Nederland": 9, + "DB+im+Internet/Schenker-Seino+Co.%2C+Ltd.": 7, + "DB+im+Internet/ServiceStore+DB": 1, + "DB+im+Internet/Suomen+DB+Schenker": 5, + "DB+im+Internet/Symposium+Competition+%26+Regulation": 2, + "DB+im+Internet/Transfesa": 1, + "DB+im+Internet/Vorstandsinformationsportal": 2, + "DB+im+Internet/Wettbewerbssymposium": 2, + "DB+im+Internet/Zukunft+Deutsche+Bahn": 18, + "EinkaufsWiki": 433, + "EinkaufsWiki/EinkaufsWiki": 191, + "EinkaufsWiki/Institute+of+Procurement": 6, + "EinkaufsWiki/Katalog+der+Dienstleistungen+im+DB-Konzern": 9, + "EinkaufsWiki/T-Wiki": 220, + "EinkaufsWiki/TE-MD": 7, + "Intranet": 4062, + "Intranet/%D0%98%D0%BD%D1%82%D1%80%D0%B0%D0%BD%D0%B5%D1%82+%D0%BD%D0%B0+%D0%A8%D0%B5%D0%BD%D0%BA%D0%B5%D1%80+%D0%95%D0%9E%D0%9E%D0%94": 17, + "Intranet/Archivierung": 1, + "Intranet/Bahn+Content+Management+-+Das+Dokumentenmanagement+System": 6, + "Intranet/Bereitstellungszentrum+Nord": 3, + "Intranet/Content+Management": 4, + "Intranet/DB+Akademie": 2, + "Intranet/DB+Enterprise+Cloud": 7, + "Intranet/DB+Gastronomie": 1, + "Intranet/DB+Management+Portal": 215, + "Intranet/DB+Schenker+Americas+Intranet": 149, + "Intranet/DB+Schenker+Asia+Pacific": 137, + "Intranet/DB+Schenker+Australia": 189, + "Intranet/DB+Schenker+Belgium": 82, + "Intranet/DB+Schenker+China": 49, + "Intranet/DB+Schenker+Denmark": 17, + "Intranet/DB+Schenker+France+Intranet": 14, + "Intranet/DB+Schenker+Greece": 10, + "Intranet/DB+Schenker+Head+Office": 678, + "Intranet/DB+Schenker+India": 41, + "Intranet/DB+Schenker+Intranet+Lithuania": 10, + "Intranet/DB+Schenker+Intranet+SEE": 81, + "Intranet/DB+Schenker+Italy+-+Intranet": 44, + "Intranet/DB+Schenker+Japan": 45, + "Intranet/DB+Schenker+Latvia+Intranet": 30, + "Intranet/DB+Schenker+Romania": 22, + "Intranet/DB+Schenker+Singapore": 1, + "Intranet/DB+Schenker+United+Kingdom": 133, + "Intranet/DB+Schenker+in+Thailand": 36, + "Intranet/DB+Schulkooperationen": 8, + "Intranet/DB+Systel+GmbH": 172, + "Intranet/DB+Systel+helps": 3, + "Intranet/DB+Systel+hilft": 2, + "Intranet/DB+Vertrieb": 3, + "Intranet/DB-Management+Portal": 369, + "Intranet/DB-Melder": 2, + "Intranet/DB-net": 783, + "Intranet/Digitalisierung+Netz": 3, + "Intranet/ECM+Showcase+-+Ein+Portal": 4, + "Intranet/Einfachbahn": 1, + "Intranet/FAQ+Plattform+ETCS+Programm": 6, + "Intranet/FZI+-+Produktions+IT": 6, + "Intranet/FZI+Intranet": 7, + "Intranet/GBR+DB+AG+Holding": 6, + "Intranet/GBR+DB+Fernverkehr+AG": 3, + "Intranet/GBR+DB+ProjektBau+GmbH": 1, + "Intranet/GBR+DB+Regio%2FStadtverkehr": 6, + "Intranet/GBR+DB+RegioNetz": 5, + "Intranet/GBR+DB+Services": 3, + "Intranet/GBR+DB+Sicherheit": 1, + "Intranet/H44+wird+noch+besser": 2, + "Intranet/HRconnects": 30, + "Intranet/IT+Fernverkehr": 27, + "Intranet/IT+Sicherheit": 4, + "Intranet/Ideenmanagement": 1, + "Intranet/Intranet+DB+Projekt+Stuttgart%E2%80%93Ulm+GmbH": 4, + "Intranet/Intranet+DB+ProjektBau+GmbH": 15, + "Intranet/Intranet+Ressort+Personal": 1, + "Intranet/Intranet+Schenker+Switzerland": 27, + "Intranet/Intranet+der+DB+Netz+AG": 20, + "Intranet/Nova": 50, + "Intranet/Plattformportal": 2, + "Intranet/Portal+C": 3, + "Intranet/Regionalnetze": 1, + "Intranet/SAP+Portal": 33, + "Intranet/SCHENKER+%26+CO+AG": 231, + "Intranet/SCR-Lernportal": 5, + "Intranet/SEPA": 4, + "Intranet/Schenker+Logistics+Nederland": 79, + "Intranet/Schwerbehindertenvertretungen": 3, + "Intranet/Serviceteam+Online-Media": 4, + "Intranet/Serviceteam+Online-Medien": 7, + "Intranet/Small+Solutions": 1, + "Intranet/Stephi": 8, + "Intranet/Technischer+Netzzugang": 1, + "Intranet/Trafo": 1, + "Intranet/Vertriebs-+und+Fahrplanforum": 2, + "Intranet/Werk+Krefeld": 1, + "Intranet/iMan+%E2%80%93+Identity+Management": 18, + "Intranet/iMan+%E2%80%93+zentrales+Benutzermanagement": 39, + "Intranet/youropeWORK": 17, + "KRWD": 47, + "Mediathek": 114, + "Notes+Datenbanken": 304, + "Notes+Datenbanken/Bilanzierungshandbuch": 124, + "Notes+Datenbanken/smartidee+Ideenbank": 180, + "Prozessportal": 234, + "Prozessportal/Cargo+DB+Cargo+AG": 10, + "Prozessportal/Cargo+DB+Cargo+Logistics+GmbH": 5, + "Prozessportal/Cargo+DB+Cargo+Schweiz+GmbH": 5, + "Prozessportal/Cargo+DB+Intermodal+Services+GmbH": 5, + "Prozessportal/DB+JobService": 2, + "Prozessportal/DB+Netz2": 10, + "Prozessportal/DB+Services": 21, + "Prozessportal/DB+Services+Immo": 1, + "Prozessportal/DB+Station+und+Service": 10, + "Prozessportal/DB+Systel": 104, + "Prozessportal/DB-Konzern": 55, + "Prozessportal/Ressort+FE+Beschaffung": 5, + "Prozessportal/Ressort+IQ+PrePub+SQMM": 1, + "TIKA": 506, + "TIKA/GA+SSC+-+Global+IT+Roll+Out": 45, + "TIKA/GA+SSC+Training": 2, + "TIKA/GPO+-+FBG1": 8, + "TIKA/GPO+-+Official+Documents": 185, + "TIKA/Global+Accounting+Conference": 2, + "TIKA/Global+Accounting+SSC+PMO": 17, + "TIKA/Muster+WA+%2F+WA+Example": 27, + "TIKA/SSC+EU_General+Schenker+Logistics+RO": 2, + "TIKA/SSC+EU_ISP7+Training": 9, + "TIKA/SSC+EU_LL+CZ_AP": 8, + "TIKA/SSC+EU_LL+CZ_AR": 4, + "TIKA/SSC+EU_LL+ES_AP": 4, + "TIKA/SSC+EU_LL+NL_AP": 3, + "TIKA/SSC+EU_LL+RO_AP": 6, + "TIKA/SSC+EU_LL+RO_GL": 6, + "TIKA/TIKA+Support": 3, + "TIKA/TIKA+intern": 120, + "TIKA/TIKA_Training": 1, + "TIKA/Templates_Roadmap": 15, + "Wiki-Pool+DB+Systel": 8548, + "Wiki-Pool+DB+Systel/%C2%BBMonitoring+2.0%C2%AB": 20, + "Wiki-Pool+DB+Systel/AIDA+BINA": 7, + "Wiki-Pool+DB+Systel/AVB+Wiki": 34, + "Wiki-Pool+DB+Systel/Agile+Community": 60, + "Wiki-Pool+DB+Systel/Agilisierung+Solution+Center": 3, + "Wiki-Pool+DB+Systel/Application+und+Release+Management+Infrastructure+and+Holding+II": 2, + "Wiki-Pool+DB+Systel/Applicationmanagement+Infrastructure": 1, + "Wiki-Pool+DB+Systel/Arbeits-%2C+Gesundheits-%2C+Brand-+und+Umweltschutz": 25, + "Wiki-Pool+DB+Systel/Archivierung": 3, + "Wiki-Pool+DB+Systel/Athene+Nutzer+Info": 5, + "Wiki-Pool+DB+Systel/Ausbildung": 3, + "Wiki-Pool+DB+Systel/Ausbildung+Infrastruktur": 4, + "Wiki-Pool+DB+Systel/Automatenzentrum+Wiki": 24, + "Wiki-Pool+DB+Systel/Automation": 218, + "Wiki-Pool+DB+Systel/Availability+Managment+und+Reporting": 16, + "Wiki-Pool+DB+Systel/B2.0+Haus+4+und+BiesdorfUmz%C3%BCge+%28HuBU%29": 7, + "Wiki-Pool+DB+Systel/BASE": 251, + "Wiki-Pool+DB+Systel/BAzubi-Labor": 7, + "Wiki-Pool+DB+Systel/BF+Doku+VM-Tool": 12, + "Wiki-Pool+DB+Systel/Barrierefreiheit": 8, + "Wiki-Pool+DB+Systel/Bereichs-News": 5, + "Wiki-Pool+DB+Systel/Blockchain": 2, + "Wiki-Pool+DB+Systel/Business+Innovation+Management": 73, + "Wiki-Pool+DB+Systel/Business+Intelligence": 17, + "Wiki-Pool+DB+Systel/Business+Workflow+on+Demand+%28BWoD%29": 17, + "Wiki-Pool+DB+Systel/Button+Portal": 1, + "Wiki-Pool+DB+Systel/CRM+DBS+Wiki": 12, + "Wiki-Pool+DB+Systel/CRM_SuperOffice": 19, + "Wiki-Pool+DB+Systel/Carmen+-+Tosca": 218, + "Wiki-Pool+DB+Systel/Changemanagement": 3, + "Wiki-Pool+DB+Systel/Cloud-Community": 77, + "Wiki-Pool+DB+Systel/Cockpit+Management": 7, + "Wiki-Pool+DB+Systel/Content+Management": 28, + "Wiki-Pool+DB+Systel/Continuous+Delivery+Dokumentation%3A+Architektur+und+Baupl%C3%A4ne": 41, + "Wiki-Pool+DB+Systel/Customer+Centricity+Botschafter+Community": 1, + "Wiki-Pool+DB+Systel/DB+Rapid+App": 2, + "Wiki-Pool+DB+Systel/DB+Search": 58, + "Wiki-Pool+DB+Systel/DB+Systel+Human+Resources": 7, + "Wiki-Pool+DB+Systel/DB+Systel+SAP+SRM": 26, + "Wiki-Pool+DB+Systel/DB+Systel+UK+Ltd+Wiki": 7, + "Wiki-Pool+DB+Systel/DB+Systel+Wiki": 688, + "Wiki-Pool+DB+Systel/DB+Systel+im+KundenServiceZentrum+von+DB+Schenker+Rail": 6, + "Wiki-Pool+DB+Systel/DBNetzBetriebszentralen": 3, + "Wiki-Pool+DB+Systel/DCS+-+Zentrales+Auftragsmanagement": 1, + "Wiki-Pool+DB+Systel/DCS+Infos+und+FAQ": 4, + "Wiki-Pool+DB+Systel/DCS+News": 190, + "Wiki-Pool+DB+Systel/DIPI+TEAM+WIKI": 4, + "Wiki-Pool+DB+Systel/Dataservices": 1, + "Wiki-Pool+DB+Systel/DevOps": 107, + "Wiki-Pool+DB+Systel/Dockerhub+Dokumentation": 3, + "Wiki-Pool+DB+Systel/Duales+Studium": 12, + "Wiki-Pool+DB+Systel/E-Werk+Algorithmus+Team": 1, + "Wiki-Pool+DB+Systel/E2E": 2, + "Wiki-Pool+DB+Systel/ECM+-+Know+How": 3, + "Wiki-Pool+DB+Systel/ECM+Projekte": 211, + "Wiki-Pool+DB+Systel/Einstiegsqualifizierung+DB+Systel": 3, + "Wiki-Pool+DB+Systel/Exchange+Program+JR+Group": 11, + "Wiki-Pool+DB+Systel/Fahrzeug-IT": 456, + "Wiki-Pool+DB+Systel/Flexibles+Arbeiten": 3, + "Wiki-Pool+DB+Systel/Forum+DB+Systel": 5, + "Wiki-Pool+DB+Systel/GA+SSC+SeMa": 3, + "Wiki-Pool+DB+Systel/Gr%C3%BCnes+Backlog+Test-Wiki": 3, + "Wiki-Pool+DB+Systel/HPI+Studies": 33, + "Wiki-Pool+DB+Systel/Hilfe": 27, + "Wiki-Pool+DB+Systel/I.LVD+42": 5, + "Wiki-Pool+DB+Systel/ICT+Beratung": 5, + "Wiki-Pool+DB+Systel/ICT+Management+%40+DB+Systel": 20, + "Wiki-Pool+DB+Systel/ICT-OM": 6, + "Wiki-Pool+DB+Systel/ICT-Operations+Management": 3, + "Wiki-Pool+DB+Systel/IFS1+-+Billing": 2, + "Wiki-Pool+DB+Systel/INet+BF": 1, + "Wiki-Pool+DB+Systel/ISIS-Projektwiki": 1, + "Wiki-Pool+DB+Systel/ISTP+DevOps": 8, + "Wiki-Pool+DB+Systel/IT+Common+Services": 1, + "Wiki-Pool+DB+Systel/IT+Operations+Management": 2, + "Wiki-Pool+DB+Systel/IT-Fabrik": 11, + "Wiki-Pool+DB+Systel/ImProW": 8, + "Wiki-Pool+DB+Systel/Info-Buffet": 14, + "Wiki-Pool+DB+Systel/Infrastructure+Visualisation+%26+Planning": 37, + "Wiki-Pool+DB+Systel/Insourcing+%26+Optimization": 6, + "Wiki-Pool+DB+Systel/Internet+of+Things": 3, + "Wiki-Pool+DB+Systel/IntraCloud": 40, + "Wiki-Pool+DB+Systel/JIRA+as+a+Service": 4, + "Wiki-Pool+DB+Systel/KI-Community": 7, + "Wiki-Pool+DB+Systel/Kommunikation+Services": 50, + "Wiki-Pool+DB+Systel/LCM+Transportation+%26+Logistics": 2, + "Wiki-Pool+DB+Systel/LMSlab": 83, + "Wiki-Pool+DB+Systel/Leistungsportfolio": 1, + "Wiki-Pool+DB+Systel/Lizenzierung+von+Software": 15, + "Wiki-Pool+DB+Systel/Logistic+4.0%40DB+SR+Infrastructure": 26, + "Wiki-Pool+DB+Systel/LuFV-IT%3A+Leistungs-+und+Finanzierungsvereinbarung+IT": 8, + "Wiki-Pool+DB+Systel/Managed+Environment": 46, + "Wiki-Pool+DB+Systel/Managed+Workplace": 9, + "Wiki-Pool+DB+Systel/Metis": 2, + "Wiki-Pool+DB+Systel/Minerva": 433, + "Wiki-Pool+DB+Systel/Mobile+Solutions+Community": 234, + "Wiki-Pool+DB+Systel/Mobilfunk": 5, + "Wiki-Pool+DB+Systel/Mobilfunkmigration": 2, + "Wiki-Pool+DB+Systel/Monitoring+Personenverkehr+T.SVL+1": 14, + "Wiki-Pool+DB+Systel/MyDBSystel+-+Business+Service+Provisioning+Plattform": 1, + "Wiki-Pool+DB+Systel/NVS": 39, + "Wiki-Pool+DB+Systel/NetzBF": 2, + "Wiki-Pool+DB+Systel/Open+Data": 2, + "Wiki-Pool+DB+Systel/Open+Source+Community": 4, + "Wiki-Pool+DB+Systel/Operational+Intelligence": 9, + "Wiki-Pool+DB+Systel/Operations+Planning+%26+Management": 1, + "Wiki-Pool+DB+Systel/Oracle+CRM": 9, + "Wiki-Pool+DB+Systel/PPSFR": 2, + "Wiki-Pool+DB+Systel/PSC": 1, + "Wiki-Pool+DB+Systel/PSS+Team.Wiki": 2, + "Wiki-Pool+DB+Systel/Portfolio+%26+Innovation+Team+Wiki": 1, + "Wiki-Pool+DB+Systel/PrIMa": 95, + "Wiki-Pool+DB+Systel/ProcLib+KVP": 6, + "Wiki-Pool+DB+Systel/ProduktManagement+RiM": 27, + "Wiki-Pool+DB+Systel/Programm+1+%22Order+to+Cash%22": 2, + "Wiki-Pool+DB+Systel/Projekt+%C3%9C+12": 11, + "Wiki-Pool+DB+Systel/Projekt+ECM": 529, + "Wiki-Pool+DB+Systel/Projekt+EPS": 17, + "Wiki-Pool+DB+Systel/Projekt+KVD": 17, + "Wiki-Pool+DB+Systel/Projekt+MTx": 1, + "Wiki-Pool+DB+Systel/Projekt+PlanET": 28, + "Wiki-Pool+DB+Systel/Projekt-KTR-Abl%C3%B6sung": 3, + "Wiki-Pool+DB+Systel/Projektdokumentation+des+Solution+Center+Mobile": 1, + "Wiki-Pool+DB+Systel/Projektwiki+RiM": 29, + "Wiki-Pool+DB+Systel/QT+LuP": 2, + "Wiki-Pool+DB+Systel/Redaktionsteam+Ver%C3%A4nderungskommunikation": 1, + "Wiki-Pool+DB+Systel/Reisebuddy": 25, + "Wiki-Pool+DB+Systel/Releasemanagement+Wiki": 11, + "Wiki-Pool+DB+Systel/ReportIT+Wiki": 4, + "Wiki-Pool+DB+Systel/Requestmanagement": 263, + "Wiki-Pool+DB+Systel/RunIT+2.i": 16, + "Wiki-Pool+DB+Systel/SC+Instandhaltung": 138, + "Wiki-Pool+DB+Systel/SC+Schedule+Systems": 629, + "Wiki-Pool+DB+Systel/SOA%40DB": 26, + "Wiki-Pool+DB+Systel/SVS+Wiki": 22, + "Wiki-Pool+DB+Systel/Sarasvati": 55, + "Wiki-Pool+DB+Systel/Schenker+Logistics+Wiki": 254, + "Wiki-Pool+DB+Systel/Schlog+Monitoring": 23, + "Wiki-Pool+DB+Systel/Schwerbehindertenvertretung": 6, + "Wiki-Pool+DB+Systel/Security": 10, + "Wiki-Pool+DB+Systel/Security+Operations": 14, + "Wiki-Pool+DB+Systel/Service+Delivery+Manual": 28, + "Wiki-Pool+DB+Systel/Skill-+und+Ressourcenmanagement": 16, + "Wiki-Pool+DB+Systel/Skydeck": 58, + "Wiki-Pool+DB+Systel/Small+Solutions": 3, + "Wiki-Pool+DB+Systel/Social+Business": 6, + "Wiki-Pool+DB+Systel/Solution+Center+Infrastructure+Management": 9, + "Wiki-Pool+DB+Systel/Solution+Center+Personalsysteme": 3, + "Wiki-Pool+DB+Systel/Solution+Center+Reisendeninformationssysteme": 254, + "Wiki-Pool+DB+Systel/Solution+Group+DB+Schenker+Rail": 264, + "Wiki-Pool+DB+Systel/Sourcing+Wiki": 1, + "Wiki-Pool+DB+Systel/Step42": 290, + "Wiki-Pool+DB+Systel/Strategie+DB+Systel": 71, + "Wiki-Pool+DB+Systel/Suppliermanagement": 1, + "Wiki-Pool+DB+Systel/TM1+Systeme": 22, + "Wiki-Pool+DB+Systel/Tarifwegeermittlung%3A+TWE2": 32, + "Wiki-Pool+DB+Systel/Technologietag+DB+Systel": 2, + "Wiki-Pool+DB+Systel/Technology+Board": 5, + "Wiki-Pool+DB+Systel/Test+Integration+Center": 30, + "Wiki-Pool+DB+Systel/Test-Factory+Netze+Fahrplansysteme": 3, + "Wiki-Pool+DB+Systel/Testautomation": 149, + "Wiki-Pool+DB+Systel/Testspace": 176, + "Wiki-Pool+DB+Systel/Thales+Move+2013": 32, + "Wiki-Pool+DB+Systel/Tool+Management": 91, + "Wiki-Pool+DB+Systel/Transformation": 9, + "Wiki-Pool+DB+Systel/Transformationsboard+der+DB+Systel": 5, + "Wiki-Pool+DB+Systel/UDG": 92, + "Wiki-Pool+DB+Systel/UX+Community": 7, + "Wiki-Pool+DB+Systel/UX+Space": 24, + "Wiki-Pool+DB+Systel/VIP-Service": 2, + "Wiki-Pool+DB+Systel/Value+IT+-+Projekt-Delivery": 1, + "Wiki-Pool+DB+Systel/Verbundtarifierung+und+-kontrolle": 1, + "Wiki-Pool+DB+Systel/Verfahren-MAXX": 39, + "Wiki-Pool+DB+Systel/Verteile+Fertigung+-Global+Sourcing": 6, + "Wiki-Pool+DB+Systel/WIS-Wiki": 1, + "Wiki-Pool+DB+Systel/Wiki+Personenverkehr": 14, + "Wiki-Pool+DB+Systel/WorldInsight": 1, + "Wiki-Pool+DB+Systel/ZAS+-+Zentrale+Auftragssteuerung": 5, + "Wiki-Pool+DB+Systel/ZEBRA": 8, + "Wiki-Pool+DB+Systel/Zentrales+Monitoring": 4, + "Wiki-Pool+DB+Systel/assistify": 7, + "Wiki-Pool+DB+Systel/connect": 3, + "Wiki-Pool+DB+Systel/docker+Community": 35, + "Wiki-Pool+DB+Systel/eSuite+%26+RailServer": 105, + "Wiki-Pool+DB+Systel/eTicket+%28%28%28e": 2, + "Wiki-Pool+DB+Systel/middleware": 15, + "Wiki-Pool+DB+Systel/scot-wiki": 38, + "Wiki-Pool+DB+Systel/sid": 15, + "ZRWD": 407 + }, + "dbsearch_author_ss": { + "Andrea Brandt": 407, + "Katja Lutherdt": 164, + "Michael Binzen": 130, + "Ronald Bieber": 115, + "Sven Oschelewski": 114, + "Lars Tiedermann": 111, + "Dorit Baumeister": 92, + "Sebastian Se Schmidt": 84, + "Markus Schlun": 82, + "Martin Strunk": 79, + "Natascha Brosche": 79, + "Ulrich Heinl": 78, + "Stefan Hook": 77, + "Marianne Girchott": 76, + "Chris Walter": 75, + "Toni Krevet": 73, + "Unbekannter Benutzer (florianhuster)": 71, + "Detlef Winkler": 70, + "Oliver J‰gle": 70, + "Markus Schuch": 69, + "": 127097 + }, + "dbsearch_keywords_ss": { + "test": 122, + "release": 103, + "meeting-notes": 98, + "news": 96, + "Allgemein": 83, + "eip": 69, + "kb-how-to-article": 57, + "lpa": 54, + "lpa8": 50, + "ilpa8": 48, + "retrospective": 30, + "protokoll": 29, + "sap": 23, + "performancetest": 21, + "tmt": 21, + "Qualit‰t": 20, + "prcbb": 20, + "dcs_news": 18, + "maxx": 18, + "qatc": 17 + }, + "dbsearch_content_type_aggregated_s": { + "msword": 64189, + "msexcel": 30134, + "pdf": 18290, + "mspowerpoint": 12074, + "html": 10209, + "text": 2749, + "compressed": 1009, + "richtext": 436, + "notes": 254, + "msproject": 34 + } + }, + "facet_dates": {}, + "facet_ranges": {} + }, + "highlighting": { + "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=57855715": {}, + "https://m00014.sharepoint.noncd.rz.db.de/workingareas/musterwawaexample/Documents/Projekt%202/Test%20Anna%2023.pdf": {"dbsearch_highlight_t_de": ["Test Anna 23 Anna 01.03.2017 Mit Kommentaren am 10.03.2017 Und noch mehr Kommentare am 10.03.2017"]}, + "https://m00014.sharepoint.noncd.rz.db.de/workingareas/musterwawaexample/Documents/Projekt%202/Test%20Anna%2023.docx": {"dbsearch_highlight_t_de": ["Test Anna 23 Anna 01.03.2017 Mit Kommentaren am 10.03.2017 Und noch mehr Kommentare am 10.03.2017"]}, + "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=25748005": {"dbsearch_highlight_t_de": ["f¸r das Testen der Funktionsf‰higkeit nach einem Betriebssystem-Update angelegt. tempor‰r f¸r Test Para", "/app/paisycs/archiv/bin/create_baseline P14B N+10y # TMT 697 Testlauf Verzeichnispflege Output_SST_archiv"]}, + "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=57850201": {"dbsearch_highlight_t_de": ["Teilnehmer † Agenda des Meetings Begr¸flung CA Test ZPM Test Vorstellung neuer MA (Alexander Hitz) Offene", "Protokollpunkt ToDos Verantwortlich Endtermin 1 Stand CA Test Bereitstellen Markwart 22.02.2017 2 Stand ZPM Test"]}, + "https://dbsystel.wiki.intranet.deutschebahn.com/wiki/pages/viewpage.action?pageId=56552993": {"dbsearch_highlight_t_de": ["Kurzinfo MAB Folgeworkshops (Christina Herweg) CA Test Vorstellung Neuer Mitarbeiter (Hossein Rabighomi", "nkt ToDos Verantwortlich Endtermin 1 Roadmap CA Test 2017 Bereitstellen Markwart 26.01.2017 2 Informationen"]}, + "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=27775601": {}, + "https://athene.intranet.deutschebahn.com/athene/livelink?func=ll&objAction=overview&objID=27761612": {} + }, + "spellcheck": {"suggestions": {"correctlySpelled": true}}, + "debug": { + "timing": { + "time": 1430.0, + "prepare": { + "time": 13.0, + "query": {"time": 1.0}, + "facet": {"time": 0.0}, + "mlt": {"time": 0.0}, + "highlight": {"time": 0.0}, + "stats": {"time": 0.0}, + "spellcheck": {"time": 0.0}, + "dynamicElevator": {"time": 1.0}, + "elevator": {"time": 0.0}, + "manifoldCFSecurity": {"time": 11.0}, + "dbsearchPreFilter": {"time": 0.0}, + "dbsearchTimeFilter": {"time": 0.0}, + "debug": {"time": 0.0} + }, + "process": { + "time": 1417.0, + "query": {"time": 1139.0}, + "facet": {"time": 261.0}, + "mlt": {"time": 0.0}, + "highlight": {"time": 17.0}, + "stats": {"time": 0.0}, + "spellcheck": {"time": 0.0}, + "dynamicElevator": {"time": 0.0}, + "elevator": {"time": 0.0}, + "manifoldCFSecurity": {"time": 0.0}, + "dbsearchPreFilter": {"time": 0.0}, + "dbsearchTimeFilter": {"time": 0.0}, + "debug": {"time": 0.0} + } + } + } +}; diff --git a/packages/dbs-ai/client/lib/collections.js b/packages/dbs-ai/client/lib/collections.js new file mode 100644 index 000000000000..6dbccd7e5657 --- /dev/null +++ b/packages/dbs-ai/client/lib/collections.js @@ -0,0 +1,5 @@ +/** + * This collection serves as buffer for inline results which are retrieved on the client-side + * @type {Meteor.Collection} + */ +InlineResultsCache = new Meteor.Collection(null); diff --git a/packages/dbs-ai/client/redlink_ui.js b/packages/dbs-ai/client/redlink_ui.js new file mode 100755 index 000000000000..53635b673a50 --- /dev/null +++ b/packages/dbs-ai/client/redlink_ui.js @@ -0,0 +1,12 @@ +RocketChat.TabBar.removeButton('external-search'); + +RocketChat.TabBar.addButton({ + groups: ['live', 'channel'], + id: 'external-search', + i18nTitle: 'Knowledge_Base', + icon: 'icon-lightbulb', + template: 'dbsAI_externalSearch', + order: 0, + initialOpen: true +}); + diff --git a/packages/dbs-ai/client/views/app/tabbar/externalSearch.html b/packages/dbs-ai/client/views/app/tabbar/externalSearch.html new file mode 100755 index 000000000000..6e38ea17b0d1 --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/externalSearch.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/dbs-ai/client/views/app/tabbar/externalSearch.js b/packages/dbs-ai/client/views/app/tabbar/externalSearch.js new file mode 100755 index 000000000000..88ab9c8b6796 --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/externalSearch.js @@ -0,0 +1,387 @@ +for (var tpl in Template) { + if (Template.hasOwnProperty(tpl) && tpl.startsWith('dynamic_redlink_')) { + Template[tpl].onRendered(function () { + this.$('.field-with-label').each(function(indx, wrapperItem) { + const inputField = $(wrapperItem).find(".knowledge-base-value"); + $(wrapperItem).find(".icon-cancel").data("initValue", inputField.val()); + }); + this.$('.datetime-field').each(function(indx, inputFieldItem) { + $.datetimepicker.setDateFormatter({ + parseDate: function (date, format) { + var d = moment(date, format); + return d.isValid() ? d.toDate() : false; + }, + formatDate: function (date, format) { + return moment(date).format(format); + } + }); + $(inputFieldItem).datetimepicker({ + dayOfWeekStart: 1, + format: 'L LT', + formatTime: 'LT', + formatDate: 'L', + validateOnBlur:false // prevent validation to use questionmark as placeholder + }); + }); + }); + } +} + +Template.dbsAI_externalSearch.helpers({ + messages() { + return RocketChat.models.LivechatExternalMessage.findByRoomId(this.rid, {ts: 1}); + }, + dynamicTemplateExists() { + return !!Template['dynamic_redlink_' + this.queryType]; + }, + queryTemplate() { + return 'dynamic_redlink_' + this.queryType; + }, + filledQueryTemplate() { + var knowledgebaseSuggestions = RocketChat.models.LivechatExternalMessage.findByRoomId(Template.currentData().rid, + {ts: -1}).fetch(), filledTemplate = []; + if (knowledgebaseSuggestions.length > 0) { + const tokens = knowledgebaseSuggestions[0].prepareResult.tokens; + $(knowledgebaseSuggestions[0].prepareResult.queryTemplates).each(function (indexTpl, queryTpl) { + let extendedQueryTpl = queryTpl, filledQuerySlots = []; + + /* tokens und queryTemplates mergen */ + $(queryTpl.querySlots).each(function (indxSlot, slot) { + if (slot.tokenIndex != -1) { + const currentToken = tokens[slot.tokenIndex]; + if (currentToken.type === "Date" && typeof currentToken.value === "object") { + slot.clientValue = moment(currentToken.value.date).format("L LT"); + } else { + slot.clientValue = currentToken.value; + } + slot.tokenVal = currentToken; + } else { + slot.clientValue = "?"; + } + filledQuerySlots.push(slot); + }); + + extendedQueryTpl.filledQuerySlots = filledQuerySlots.filter( (slot) => {slot.role != 'topic'}); //topic represents the template itself + extendedQueryTpl.forItem = function (itm) { + let returnValue = { + htmlId: Meteor.uuid(), + item: "?", + itemStyle: "empty-style", + inquiryStyle: "disabled", + label: 'topic_' + itm, + parentTplIndex: indexTpl //todo replace with looping index in html + }; + if (typeof extendedQueryTpl.filledQuerySlots === "object") { + const slot = extendedQueryTpl.filledQuerySlots.find((ele) => ele.role === itm); + if (slot) { + returnValue = _.extend(slot, returnValue); + returnValue.item = slot.clientValue; + if (!_.isEmpty(slot.inquiryMessage)) { + returnValue.inquiryStyle = ''; + } + if (returnValue.item !== "" && returnValue.item !== "?") { + returnValue.itemStyle = ""; + } + if (returnValue.tokenType === "Date") { + returnValue.itemStyle = returnValue.itemStyle + " datetime-field"; + } + } + } + return returnValue; + }; + + extendedQueryTpl.dummyEinstiegshilfe = function() { //todo: Entfernen, wenn Redlink die Hilfeart erkennt + return { + htmlId: Meteor.uuid(), + item: "Einstiegshilfe", + itemStyle: "", + inquiryStyle: "", + label: t('topic_supportType'), + parentTplIndex: 0 //todo replace with looping index in html + }; + }; + filledTemplate.push(extendedQueryTpl); + }); + } + return filledTemplate; + }, + queriesContext(queries, templateIndex){ + const instance = Template.instance(); + $(queries).each(function (indx, queryItem) { + if(queries[indx].creator && typeof queries[indx].creator == "string") { + queries[indx].replacedCreator = queries[indx].creator.replace(/\.|-/g, "_"); + } else { + queries[indx].replacedCreator = ""; + } + }); + return { + queries: queries, + roomId: instance.data.rid, + templateIndex: templateIndex + } + } + , + helpRequestByRoom(){ + const instance = Template.instance(); + return instance.helpRequest.get(); + } +}); + + +Template.dbsAI_externalSearch.events({ + /** + * Notifies that a query was confirmed by an agent (aka. clicked) + */ + 'click .knowledge-queries-wrapper .query-item a ': function (event, instance) { + const query = $(event.target).closest('.query-item'); + let externalMsg = instance.externalMessages.get(); + externalMsg.prepareResult.queryTemplates[query.data('templateIndex')].queries[query.data('queryIndex')].state = 'Confirmed'; + instance.externalMessages.set(externalMsg); + Meteor.call('updateKnowledgeProviderResult', instance.externalMessages.get(),(err) => { + if (err) {//TODO logging error + } + }); + }, + /** + * Hide datetimepicker when right mouse clicked + */ + 'mousedown .field-with-label': function(event, instance) { + if(event.button === 2) { + $("body").addClass("suppressDatetimepicker"); + setTimeout(() => { + $('.datetime-field').datetimepicker("hide"); + $("body").removeClass("suppressDatetimepicker"); + }, 500); + } + }, + /* + * open contextmenu with "-edit, -delete and -nachfragen" + * */ + 'contextmenu .field-with-label': function (event, instance) { + event.preventDefault(); + instance.$(".knowledge-input-wrapper.active").removeClass("active"); + instance.$(event.currentTarget).find(".knowledge-input-wrapper").addClass("active"); + $(document).off("mousedown.contextmenu").on("mousedown.contextmenu", function (e) { + if (!$(e.target).parent(".knowledge-base-tooltip").length > 0) { + $(".knowledge-input-wrapper.active").removeClass("active"); + } + }); + }, + 'click .query-template-tools-wrapper .icon-up-open': function (event) { + $(event.currentTarget).closest(".query-template-wrapper").toggleClass("collapsed"); + }, + /** + * Mark a template as confirmed + */ + 'click .query-template-tools-wrapper .icon-ok': function (event, instance) { + const query = $(event.target).closest('.query-template-wrapper'); + let externalMsg = instance.externalMessages.get(); + externalMsg.prepareResult.queryTemplates[query.data('templateIndex')].state = 'Confirmed'; + instance.externalMessages.set(externalMsg); + Meteor.call('updateKnowledgeProviderResult', instance.externalMessages.get(), (err) => { + if (err) {//TODO logging error + } + }); + }, + /** + * Mark a template as rejected. + */ + 'click .query-template-tools-wrapper .icon-cancel': function (event, instance) { + const query = $(event.target).closest('.query-template-wrapper'); + let externalMsg = instance.externalMessages.get(); + externalMsg.prepareResult.queryTemplates[query.data('templateIndex')].state = 'Rejected'; + instance.externalMessages.set(externalMsg); + Meteor.call('updateKnowledgeProviderResult', instance.externalMessages.get(), (err) => { + if (err) {//TODO logging error + } + }); + }, + + 'keydup .knowledge-base-value, keydown .knowledge-base-value': function (event, inst) { + const inputWrapper = $(event.currentTarget).closest(".field-with-label"), + ENTER_KEY = 13, + ESC_KEY = 27, + TAB_KEY = 9, + keycode = event.keyCode; + if (inputWrapper.hasClass("editing")) { + switch (keycode) { + case ENTER_KEY: + inputWrapper.find(".icon-floppy").click(); + break; + case ESC_KEY: + case TAB_KEY: + inputWrapper.find(".icon-cancel").click(); + break; + } + } else if(keycode != TAB_KEY) { + $(".field-with-label.editing").removeClass("editing"); + inputWrapper.addClass('editing'); + } + }, + 'click .knowledge-input-wrapper .icon-cancel': function (event, instance) { + const inputWrapper = $(event.currentTarget).closest(".field-with-label"), + inputField = inputWrapper.find(".knowledge-base-value"); + inputWrapper.removeClass("editing"); + inputField.val($(event.currentTarget).data("initValue")); + }, + 'click .knowledge-input-wrapper .icon-floppy': function (event, instance) { + event.preventDefault(); + const inputWrapper = $(event.currentTarget).closest(".field-with-label"), + templateWrapper = $(event.currentTarget).closest(".query-template-wrapper"), + inputField = inputWrapper.find(".knowledge-base-value"); + inputWrapper.removeClass("editing"); + templateWrapper.addClass("spinner"); + const saveValue = inputField.val(); + inputWrapper.find(".icon-cancel").data("initValue", saveValue); + + let externalMsg = instance.externalMessages.get(); + const newToken = { + confidence: 0.95, + messageIdx: -1, + start: -1, + end: -1, + state: "Confirmed", + hints: [], + type: _.isEmpty(inputWrapper.data('tokenType')) ? 'Unknown' : inputWrapper.data('tokenType'), + origin: "Agent", + value: inputField.hasClass('datetime-field') ? + { + grain: 'minute', + date: moment(saveValue, "L LT").toISOString() + } : + saveValue + }; + + externalMsg.prepareResult.tokens.push(newToken); + externalMsg.prepareResult.queryTemplates[inputWrapper.data('parentTplIndex')].querySlots = _.map(externalMsg.prepareResult.queryTemplates[inputWrapper.data('parentTplIndex')].querySlots, + (query) => { + if (query.role === inputWrapper.data('slotRole')) { + query.tokenIndex = externalMsg.prepareResult.tokens.length - 1; + } + return query; + }); + instance.externalMessages.set(externalMsg); + Meteor.call('updateKnowledgeProviderResult', instance.externalMessages.get(), (err) => { + templateWrapper.removeClass("spinner"); + instance.$(".knowledge-input-wrapper.active").removeClass("active"); + if (err) {//TODO logging error + } + }); + }, + 'click .knowledge-base-tooltip .edit-item, click .knowledge-base-value, click .knowledge-base-label': function (event, instance) { + event.preventDefault(); + const inputWrapper = $(event.currentTarget).closest(".field-with-label"), + inputField = inputWrapper.find(".knowledge-base-value"); + + if (!inputWrapper.hasClass('editing')) { + $(".field-with-label.editing").removeClass("editing"); + inputField.focus().select(); + inputWrapper.addClass('editing'); + } + }, + /** + * Deletes a token from a queryTemplate and mark it as rejected. + */ + 'click .knowledge-base-tooltip .delete-item': function (event, instance) { + event.preventDefault(); + const field = $(event.target).closest('.field-with-label'), + templateIndex = field.attr('data-parent-tpl-index'), + slotRole = field.attr('data-slot-role'); + let externalMsg = instance.externalMessages.get(); + externalMsg.prepareResult.queryTemplates[templateIndex].querySlots = _.map(externalMsg.prepareResult.queryTemplates[templateIndex].querySlots, + (query) => { + if (query.role === slotRole) { + query.tokenIndex = -1; + } + return query; + }); + externalMsg.prepareResult.tokens[field.attr('data-token-index')].state = "Rejected"; + instance.externalMessages.set(externalMsg); + Meteor.call('updateKnowledgeProviderResult', instance.externalMessages.get(), (err) => { + instance.$(".knowledge-input-wrapper.active").removeClass("active"); + if (err) {//TODO logging error + } + }); + + }, + /** + * Writes the inqury of an queryTemplateSlot to the chatWindowInputField. + */ + 'click .knowledge-base-tooltip .chat-item:not(.disabled)': function (event, inst) { + event.preventDefault(); + const rlData = _.first(RocketChat.models.LivechatExternalMessage.findByRoomId(inst.roomId, {ts: -1}).fetch()); + if (rlData && rlData.prepareResult) { + const input = inst.$(event.target).closest('.field-with-label'), + slotRole = input.attr('data-slot-role'); + const qSlot = _.find(rlData.prepareResult.queryTemplates[input.attr('data-parent-tpl-index')].querySlots, (slot) => { + return slot.role == slotRole; + }); + if (qSlot && qSlot.inquiryMessage) { + const inputBox = $('#chat-window-' + inst.roomId + ' .input-message'); + const initialInputBoxValue = inputBox.val() ? inputBox.val() + ' ' : ''; + inputBox.val(initialInputBoxValue + qSlot.inquiryMessage).focus().trigger('keyup'); + inst.$(".knowledge-input-wrapper.active").removeClass("active"); + } + } + }, + /** + * Switches the tokens between two slots within a query template. + */ + 'click .external-message .icon-wrapper .icon-exchange': function(event, instance) { + const changeBtn = $(event.target).parent().closest('.icon-wrapper'), + left = changeBtn.prevAll('.field-with-label'), + right = changeBtn.nextAll('.field-with-label'), + leftTokenIndex = parseInt(left.attr('data-token-index')), + rightTokenIndex = parseInt(right.attr('data-token-index')); + if(changeBtn.hasClass("spinner")) { + return; + } + changeBtn.addClass("spinner"); + let externalMsg = instance.externalMessages.get(); + externalMsg.prepareResult.queryTemplates[left.data('parentTplIndex')].querySlots = _.map(externalMsg.prepareResult.queryTemplates[left.data('parentTplIndex')].querySlots, + (query) => { + if (query.tokenIndex === leftTokenIndex) { + query.tokenIndex = rightTokenIndex; + } else if (query.tokenIndex === rightTokenIndex) { + query.tokenIndex = leftTokenIndex; + } + return query; + }); + instance.externalMessages.set(externalMsg); + Meteor.call('updateKnowledgeProviderResult', instance.externalMessages.get(),(err) => { + changeBtn.removeClass("spinner"); + if (err) {//TODO logging error + } + }); + } +}); + +Template.dbsAI_externalSearch.onCreated(function () { + this.externalMessages = new ReactiveVar([]); + this.helpRequest = new ReactiveVar(null); + + const instance = this; + this.autorun(() => { + instance.subscribe('livechat:externalMessages', instance.data.rid); + const extMsg = RocketChat.models.LivechatExternalMessage.findByRoomId(instance.data.rid, {ts: -1}).fetch(); + if (extMsg.length > 0) { + instance.externalMessages.set(extMsg[0]); + } + + if(instance.data.rid){ + // instance.subscribe('assistify:helpRequest', instance.data.rid); + // const helpRequest = RocketChat.models.HelpRequests.findOneByRoomId(instance.roomId); + // instance.helpRequest.set(helpRequest); + + if(!instance.helpRequest.get()){ //todo remove after PoC: Non-reactive method call + Meteor.call('assistify:helpRequestByRoomId', instance.data.rid,(err, result) => { + if(!err){ + instance.helpRequest.set(result); + } else { + console.log(err); + } + }); + } + } + }); +}); diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkInlineResult.html b/packages/dbs-ai/client/views/app/tabbar/redlinkInlineResult.html new file mode 100755 index 000000000000..55b8de77e6c1 --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkInlineResult.html @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkInlineResult.js b/packages/dbs-ai/client/views/app/tabbar/redlinkInlineResult.js new file mode 100755 index 000000000000..2ba2a8491d6d --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkInlineResult.js @@ -0,0 +1,319 @@ +import toastr from 'toastr'; +Template.redlinkInlineResult._copyReplySuggestion = function (event, instance) { + if (instance.data.result.replySuggestion) { + $('#chat-window-' + instance.data.roomId + ' .input-message').val(instance.data.result.replySuggestion); + } +}; + +Template.redlinkInlineResult.helpers({ + templateName(){ + const instance = Template.instance(); + + let templateSuffix = "generic"; + switch (instance.data.creator) { + case 'bahn.de': + templateSuffix = "bahn_de"; + break; + case 'community.bahn.de': + templateSuffix = "VKL_community"; + break; + case 'VKL': + templateSuffix = "VKL_community"; + break; + case 'Hasso-MLT': + templateSuffix = "Hasso"; + break; + case 'Hasso-Search': + templateSuffix = "Hasso"; + break; + default: + if (!!Template['redlinkInlineResult_' + instance.data.creator]) { + templateSuffix = instance.data.creator; + } else { + templateSuffix = "generic"; + } + break; + } + return 'redlinkInlineResult_' + templateSuffix; + }, + templateData(){ + const instance = Template.instance(); + return { + result: instance.data.result, + roomId: instance.data.roomId + } + } +}); + +Template.redlinkInlineResult.events({ + 'click .js-copy-reply-suggestion': function (event, instance) { + return Template.redlinkInlineResult._copyReplySuggestion(event, instance) + } +}); + +//----------------------------------- Generic helper as fallback ------------------------------ + +Template.redlinkInlineResult_generic.helpers({ + relevantKeyValues(){ + const instance = Template.instance(); + + let keyValuePairs = []; + for (key in instance.data.result) { + keyValuePairs.push({key: key, value: instance.data.result[key]}); + } + + return keyValuePairs; + } +}); + +//------------------------------------- Bahn.de ----------------------------------------------- + +Template.redlinkInlineResult_bahn_de.events({ + 'click .js-copy-reply-suggestion': function (event, instance) { + return Template.redlinkInlineResult._copyReplySuggestion(event, instance) + } +}); + +Template.redlinkInlineResult_bahn_de.helpers({ + durationformat(val){ + return new _dbs.Duration(val * 60 * 1000).toHHMMSS(); + } +}); + +//----------------------------------- VKL and community --------------------------------------- +Template.redlinkInlineResult_VKL_community.helpers({ + classExpanded(){ + const instance = Template.instance(); + return instance.state.get('expanded') ? 'expanded' : 'collapsed'; + } +}); + +Template.redlinkInlineResult_VKL_community.events({ + 'click .result-item-wrapper .js-toggle-result-preview-expanded': function (event, instance) { + const current = instance.state.get('expanded'); + instance.state.set('expanded', !current); + }, +}); + +Template.redlinkInlineResult_VKL_community.onCreated(function () { + const instance = this; + + this.state = new ReactiveDict(); + this.state.setDefault({ + expanded: false + }); +}); + +//-------------------------------------- Assistify -------------------------------- +Template.inlineResultMessage.helpers({ + getOriginatorClass(message){ + if(message.user){ + switch(message.user.displayName.toLowerCase()){ + case 'seeker': + return 'seeker'; + case 'provider': + return 'provider'; + default: + return 'unknown'; + } + } + }, + getSelectedClass(){ + const instance = Template.instance(); + + if(instance.selected.get()){ + return 'selected'; + } + } +}); + +Template.inlineResultMessage.events({ + 'click .conversationMessage': function(event, instance) { + const current = instance.selected.get(); + + instance.selected.set(!current); + + if (instance.selected.get()) { + Template.redlinkQueries.utilities.addCleanupActivity(() => { + instance.selected.set(false) + }); + + } + } +}); + +Template.inlineResultMessage.onCreated(function(){ + const instance = this; + + instance.selected = new ReactiveVar(false); +}); + + +Template.redlinkInlineResult_Hasso.events({ + 'click .result-item-wrapper .js-toggle-result-preview-expanded': function (event, instance) { + const current = instance.state.get('expanded'); + instance.state.set('expanded', !current); + + if(!instance.state.get('expanded')){ + Template.redlinkQueries.utilities.resultsInteractionCleanup(); + } + }, + 'click .js-send-message': function(event, instance){ + + /* buffer metadata of messages which are _about to be sent_ + * This is necessary as the results or queries displayed may be entered into the message-area, + * but only one the message is actually sent, the metadata becomes effective for this new message + * - and only by then we know the message-id for which this metadata is actually valid + */ + Session.set('messageMetadata', { + user: Meteor.user(), + room: instance.data.roomId, + metadata: { + origin: "historicConversation", + conversationId: instance.data.result.conversationId + } + }); + + //create a text-response + let textToInsert = ""; + const selectedMessages = instance.findAll('.selected'); + if(selectedMessages.length > 0){ + textToInsert = selectedMessages.reduce(function(concat, elem) { + return concat + " " + elem.textContent; + }, + ''); + } else { + //translate GUID of the conversation provided into a link + const originRoom = RocketChat.models.Rooms.findOne({_id: instance.data.result.conversationId}); + if(originRoom) { + const routeLink = RocketChat.roomTypes.getRouteLink(originRoom.t, originRoom); + const roomLink = Meteor.absoluteUrl() + routeLink.slice(1, routeLink.length); + textToInsert = TAPi18n.__('Link_provided') + " " + roomLink; + } else { + return toastr.info(TAPi18n.__('No_room_link_possible')); + } + } + + $('#chat-window-' + instance.data.roomId + ' .input-message').val(textToInsert).focus(); + } +}); + +Template.redlinkInlineResult_Hasso.helpers({ + classExpanded(){ + const instance = Template.instance(); + return instance.state.get('expanded') ? 'expanded' : 'collapsed'; + }, + getResultTitle(){ + const instance = Template.instance(); + if(instance.state.get('expanded') && instance.state.get('conversationLoaded')){ + return instance.state.get('conversation').messages[0].content; + } else { + return instance.data.result.content + } + }, + originQuestion(){ + const instance = Template.instance(); + if(instance.state.get('conversation') && instance.state.get('conversationLoaded')){ + return instance.state.get('conversation').messages[0].content; + } + }, + latestResponse(){ + const instance = Template.instance(); + if(instance.state.get('conversation') && instance.state.get('conversationLoaded')){ + return instance.state.get('conversation').messages.filter((message) => message.user && message.user.displayName === 'Provider').pop().text; + } + }, + + subsequentCommunication(){ + const instance = Template.instance(); + if(instance.state.get('conversation') && instance.state.get('conversationLoaded')) { + return instance.state.get('conversation').messages.slice(1); + } + } +}); + +Template.redlinkInlineResult_Hasso.onCreated(function (){ + let instance = this; + + this.state = new ReactiveDict(); + this.state.setDefault({ + expanded: false, + conversation: {}, + conversationLoaded: false + }); + + + instance.autorun(()=> { + + if(instance.state.get('expanded')){ + Meteor.call('redlink:getStoredConversation', instance.data.result.conversationId, + (err, conversation)=>{ + if(!err){ + instance.state.set('conversation', conversation); + instance.state.set('conversationLoaded', true); + } else { + console.error(err); + }} + ); + } + + }); +}); + +Template.redlinkInlineResult_dbsearch.onCreated(function (){ + + this.state = new ReactiveDict(); + this.state.setDefault({ + expanded: false, + }); +}); + +Template.redlinkInlineResult_dbsearch.helpers({ + classExpanded(){ + const instance = Template.instance(); + return instance.state.get('expanded') ? 'expanded' : 'collapsed'; + } +}); + +Template.redlinkInlineResult_dbsearch.events({ + + 'click .result-item-wrapper .js-toggle-result-preview-expanded': function (event, instance) { + const current = instance.state.get('expanded'); + instance.state.set('expanded', !current); + + if(!instance.state.get('expanded')){ + Template.redlinkQueries.utilities.resultsInteractionCleanup(); + } + }, + 'click .js-send-message': function(event, instance){ + + /* buffer metadata of messages which are _about to be sent_ + * This is necessary as the results or queries displayed may be entered into the message-area, + * but only one the message is actually sent, the metadata becomes effective for this new message + * - and only by then we know the message-id for which this metadata is actually valid + */ + Session.set('messageMetadata', { + user: Meteor.user(), + room: instance.data.roomId, + metadata: { + origin: "historicConversation", + searchLink: instance.data.result.dbsearch_link_s + } + }); + + //create a text-response + let textToInsert = ""; + const selectedMessages = instance.findAll('.selected'); + if(selectedMessages.length > 0){ + textToInsert = selectedMessages.reduce(function(concat, elem) { + return concat + " " + elem.textContent; + }, + ''); + } else { + textToInsert = instance.data.result.dbsearch_link_s; + } + + $('#chat-window-' + instance.data.roomId + ' .input-message').val(textToInsert).focus(); + } + +}); diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkQueries.html b/packages/dbs-ai/client/views/app/tabbar/redlinkQueries.html new file mode 100755 index 000000000000..4a449931a03b --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkQueries.html @@ -0,0 +1,19 @@ + diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkQueries.js b/packages/dbs-ai/client/views/app/tabbar/redlinkQueries.js new file mode 100755 index 000000000000..4cad36646d92 --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkQueries.js @@ -0,0 +1,54 @@ +Template.redlinkQueries.helpers({ + queryContext(query, queryIndex){ + const instance = Template.instance(); + + function hasInlineSupport(item) { + return item.inlineResultSupport === true; + } + + const queriesWithInlineSupport = instance.data.queries + .filter(hasInlineSupport); + + return { + query: query, + maxConfidence: Math.max(...queriesWithInlineSupport.map((query) => query.confidence)), + roomId: instance.data.roomId, + templateIndex: instance.data.templateIndex, + queryIndex: queryIndex + } + } +}); + +Template.redlinkQueries.events({}); + +Template.redlinkQueries.onCreated(function () { + Template.redlinkQueries.utilities.addCleanupActivity(()=>Session.set('messageMetadata', null)); + + RocketChat.callbacks.add('afterSaveMessage', (message) => { + const bufferedMetadata = Session.get('messageMetadata'); + if (bufferedMetadata && Meteor.user()._id == bufferedMetadata.user._id && bufferedMetadata.room == message.rid) { + Meteor.call('addMessageMetadata', message, bufferedMetadata.metadata, (err, result) => { + if (err) { + console.error(err) + } else { + console.log(result) + } + });//, message, bufferedMetadata.metadata); + } + + Template.redlinkQueries.utilities.resultsInteractionCleanup(); + + }); +}); + +Template.redlinkQueries.utilities = { + cleanupCallbacks:[], + resultsInteractionCleanup: function(){ + if(this.cleanupCallbacks) { + this.cleanupCallbacks.forEach((cb)=>cb()) + } + }, + addCleanupActivity: function(cb){ + this.cleanupCallbacks.push(cb); + }, +}; diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkQuery.html b/packages/dbs-ai/client/views/app/tabbar/redlinkQuery.html new file mode 100755 index 000000000000..4f7e778751d5 --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkQuery.html @@ -0,0 +1,58 @@ + diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkQuery.js b/packages/dbs-ai/client/views/app/tabbar/redlinkQuery.js new file mode 100755 index 000000000000..7c1962f58568 --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkQuery.js @@ -0,0 +1,224 @@ +import {ClientResultFactory} from '../../../lib/ClientResultProvider.js' + +Template.redlinkQuery._hasResult = function (instance) { + const results = instance.state.get('results') || []; + if (results) { + return results.length > 0; + } else { + return false; + } +}; + +Template.redlinkQuery._fetched = function (instance) { + return instance.state.get('status') === 'fetched'; +}; + +Template.redlinkQuery.helpers({ + hasError(){ + const instance = Template.instance(); + return Template.redlinkQuery._fetched(instance) && instance.state.get('error'); + }, + + errorText(){ + const instance = Template.instance(); + const error = instance.state.get('error'); + if (error) { + const translatedText = TAPi18n.__(error); + if (translatedText === error) { + //translation was not succesful - provide a nicer but generic message + return TAPi18n.__('oops_error'); + } else { + return translatedText; + } + } + }, + + hasResult(){ + const instance = Template.instance(); + return Template.redlinkQuery._hasResult(instance); + }, + + isDirty(){ + return Template.instance().state.get('status') === 'dirty'; + }, + + fetched(){ + const instance = Template.instance(); + Template.redlinkQuery._fetched(instance); + }, + + noResultFetched(){ + const instance = Template.instance(); + return Template.redlinkQuery._fetched(instance) && !Template.redlinkQuery._hasResult(instance) + }, + + classExpanded(){ + const instance = Template.instance(); + return instance.state.get('resultsExpanded') ? 'expanded' : 'collapsed'; + }, + + queryPreviewHeadline(){ + const instance = Template.instance(); + const results = instance.state.get('results'); + if (results) { + const creator = instance.state.get('creator'); //all results have got the same creator + switch (creator) { + case 'community.bahn.de': + return t('results_community_bahn_de'); + case 'bahn.de': + return t('results_bahn_de'); + case 'dbsearch': + return t('dbsearch'); + default: + return t(results); + } + } + }, + + navigationOptions(){ + const instance = Template.instance(); + const results = instance.state.get('results'); + if (results) { + const creator = instance.state.get('creator'); //all results have got the same creator + let options = { + results: results, + roomId: instance.data.roomId, + creator: instance.state.get('creator') + }; + + switch (creator) { + case 'bahn.de': + options.template = 'redlinkResultContainer_Slider'; + options.stepping = 2; + break; + case 'VKL': + options.template = 'redlinkResultContainer_Slider'; + options.stepping = 3; + break; + default: + options.template = 'redlinkResultContainer_Slider'; + options.stepping = 5; + } + return options; + } + }, + getCreatorText(){ + const instance = Template.instance(); + let text = { + full: '', + shortened: '' + }; + switch (instance.data.query.creator) { + case 'Hasso-MLT': + text.full = TAPi18n.__('similar_requests'); + break; + case 'Hasso-Search': + const keywordsStart = instance.data.query.displayTitle.search(new RegExp("\\[","g")) + 1; + const keywordsEnd = instance.data.query.displayTitle.search(new RegExp("\\]","g")); + let keywords = instance.data.query.displayTitle.substr(keywordsStart, keywordsEnd - keywordsStart).split(','); + let uniqueKeywords = []; + if(Array.isArray(keywords)) { + $.each(keywords, function(i, el){ //use jQuery instead of ES Array.Each() due to better compatibility + if($.inArray(el, uniqueKeywords) === -1) uniqueKeywords.push(el); + }); + } + text.full = TAPi18n.__('similar_to') + ' "' + uniqueKeywords.reduce((act, val)=>{ + if(act){ + return act + ", " + val; + } else { + return val; + } + }, "") + '"'; + break; + default: + text.full = TAPi18n.__(instance.data.query.replacedCreator); + } + + text.short = text.full.slice(0, 28); + if(text.short !== text.full){ + text.short += '...'; + } + + return text; + }, + getQueryDisplayTitle() + { + const instance = Template.instance(); + if (instance.data.query.creator === 'Hasso-MLT') { + return ''; + } + if (instance.data.query.creator === 'Hasso-Search') { + return ''; + } + + // else + return instance.data.query.displayTitle; + } +}); + +Template.redlinkQuery.events({ + 'click .js-toggle-results-expanded': function (event, instance) { + const current = instance.state.get('resultsExpanded'); + instance.state.set('resultsExpanded', !current); + } +}); + +Template.redlinkQuery.clientResult = function (creator) { + switch (creator) { + case 'dbsearch': + return true; + default: + return false; + } +}; + +Template.redlinkQuery.onCreated(function () { + const instance = this; + + this.state = new ReactiveDict(); + this.state.setDefault({ + resultsExpanded: instance.data.query.inlineResultSupport && ( instance.data.maxConfidence === instance.data.query.confidence ), + results: [], + status: 'initial', + error: '' + }); + + // Asynchronously load the results. + instance.autorun(() => { + if (instance.data && instance.data.query && instance.data.roomId) { + //subscribe to the external messages for the room in order to re-fetch the results once the result + // of the knowledge provider changes + this.subscribe('livechat:externalMessages', Template.currentData().roomId); + instance.state.set('creator', instance.data.query.creator); + + //issue a request to the redlink results-service and buffer the potential results in a reactive variable + //which then can be forwarded to the results-template + if (instance.data.query.inlineResultSupport) { + instance.state.set('status', 'dirty'); + Meteor.call('redlink:retrieveResults', instance.data.roomId, instance.data.templateIndex, instance.data.query.creator, (err, results) => { + instance.state.set('results', results); + instance.state.set('status', 'fetched'); + }); + } + if (Template.redlinkQuery.clientResult(instance.data.query.creator)) { + instance.state.set('status', 'dirty'); + let crf = new ClientResultFactory().getInstance(instance.data.query.creator, instance.data.query.url); + this.roomId = Template.currentData().roomId; + this.instance = instance; //in order to pass the actual template instance to the callback in the next call + + crf.executeSearch([]) + .then(function (response) { + Meteor.setTimeout(() => { + instance.state.set('results', response.response.docs); + instance.state.set('status', 'fetched'); + instance.state.set('error', null); + }, 500); + }) + .catch(function (err) { + instance.state.set('error', "cannot-retrieve-" + instance.data.query.creator + "-results"); + instance.state.set('status', 'fetched'); + }); + } + } + }) +}); diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkResultContainers.html b/packages/dbs-ai/client/views/app/tabbar/redlinkResultContainers.html new file mode 100755 index 000000000000..cd6eebea7f0e --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkResultContainers.html @@ -0,0 +1,16 @@ + diff --git a/packages/dbs-ai/client/views/app/tabbar/redlinkResultContainers.js b/packages/dbs-ai/client/views/app/tabbar/redlinkResultContainers.js new file mode 100755 index 000000000000..970df78b902b --- /dev/null +++ b/packages/dbs-ai/client/views/app/tabbar/redlinkResultContainers.js @@ -0,0 +1,95 @@ +/** + * Generic helper for all containers displaying results + * Might serve as superclass + */ +class redlinkResultContainerHelpers { + visibleResults() { + const instance = Template.instance(); + const results = instance.data.results; + const stepping = instance.data.stepping; + const creator = instance.data.creator; + const totalLength = results.length; + + let offset = instance.state.get('currentOffset'); + if(offset >= totalLength){ + //start over immediately + offset = 0; + offset = instance.state.set('currentOffset', 0); + } + let lastElement = totalLength - 1; + if (stepping) { + lastElement = offset + stepping; + } + return results.slice(offset, lastElement); + } + + visiblePage() { + const instance = Template.instance(); + const results = instance.data.results; + const totalLength = results.length; + const offset = instance.state.get('currentOffset'); + const stepping = instance.data.stepping; + + return Math.ceil((offset/(totalLength))*(totalLength/stepping)) + 1 + } + + totalPages() { + const instance = Template.instance(); + const results = instance.data.results; + const stepping = instance.data.stepping; + + return Math.ceil(results.length/stepping) + } + + resultsCount(){ + const instance = Template.instance(); + const results = instance.data.results; + if (results) return results.length; + } + + needsNavigation(){ + const instance = Template.instance(); + const results = instance.data.results; + const stepping = instance.data.stepping; + + return (stepping < results.length); + } +} + +//----------------------------------- Slider --------------------------------------- + +Template.redlinkResultContainer_Slider.helpers(new redlinkResultContainerHelpers()); + +Template.redlinkResultContainer_Slider.events({ + + 'click .js-next-result': function (event, instance) { + const currentOffset = instance.state.get('currentOffset'); + if (currentOffset < instance.data.results.length - 1) { + instance.state.set('currentOffset', currentOffset + instance.data.stepping); + } else { + instance.state.set('currentOffset', 0); + } + }, + + 'click .js-previous-result': function (event, instance) { + const currentOffset = instance.state.get('currentOffset'); + if (currentOffset > 0) { + if (currentOffset >= instance.data.stepping) { + instance.state.set('currentOffset', currentOffset - instance.data.stepping); + } else { + instance.state.set('currentOffset', 0); + } + } else { + instance.state.set('currentOffset', instance.data.results.length - 1); + } + } +}); + +Template.redlinkResultContainer_Slider.onCreated(function () { + const instance = this; + this.state = new ReactiveDict(); + + this.state.setDefault({ + currentOffset: 0 + }); +}); diff --git a/packages/dbs-ai/dbs-ai.js b/packages/dbs-ai/dbs-ai.js new file mode 100644 index 000000000000..e925d8912bb2 --- /dev/null +++ b/packages/dbs-ai/dbs-ai.js @@ -0,0 +1,5 @@ +// Write your package code here! + +// Variables exported by this module can be imported by other packages and +// applications. See dbs-ai-tests.js for an example of importing. +export const name = 'dbs-ai'; diff --git a/packages/dbs-ai/methods/addMessageMetadata.js b/packages/dbs-ai/methods/addMessageMetadata.js new file mode 100644 index 000000000000..5356391027bb --- /dev/null +++ b/packages/dbs-ai/methods/addMessageMetadata.js @@ -0,0 +1,6 @@ +Meteor.methods({ + 'addMessageMetadata': function(message, metadata){//(message, metadata){ + console.log('Adding metadata to message', message._id); + return RocketChat.models.Messages.addMetadata(message, metadata); + } +}); diff --git a/packages/dbs-ai/methods/getStoredConversation.js b/packages/dbs-ai/methods/getStoredConversation.js new file mode 100755 index 000000000000..3a03ade7e028 --- /dev/null +++ b/packages/dbs-ai/methods/getStoredConversation.js @@ -0,0 +1,10 @@ +Meteor.methods({ + 'redlink:getStoredConversation'(conversationId){ + if(Meteor.isServer) { + const adapter = _dbs.RedlinkAdapterFactory.getInstance(); + const conversation = adapter.getStoredConversation(conversationId); + + return conversation; + } + } +}); diff --git a/packages/dbs-ai/methods/retrieveResults.js b/packages/dbs-ai/methods/retrieveResults.js new file mode 100755 index 000000000000..bfb4892aaefa --- /dev/null +++ b/packages/dbs-ai/methods/retrieveResults.js @@ -0,0 +1,85 @@ +Meteor.methods({ + 'redlink:retrieveResults'(roomId, templateIndex, creator){ + + if(Meteor.isServer) { + const adapter = _dbs.RedlinkAdapterFactory.getInstance(); + results = adapter.getQueryResults(roomId, templateIndex, creator); + + return results; + } + // + // ein paar offline-fähige Testdaten + // return [ + // { + // "replySuggestion": "Sie können sich eine neue Bahncard zusenden lassen", + // "offer": "38210", + // "title": "Jugend BahnCard 25 ", + // "categories": [ + // "BahnCard" + // ], + // "body": "e Angaben in der endgültigen Jugend BahnCard 25 BC-Service anrufen und Zusendung einer neuen Jugend BC 25 veranlassen. Auffinden einer Jugend BahnCard 25 Gefundene Jugend BahnCard 25 an BahnCard Service senden Umtausch/Erstattung Ausgeschlossen Auch der Umtausch einer BahnCard 25 Zusatz (Kind) in eine Jugend BahnCard 25 ist ausgeschlossen. Vergessene Jugend BahnCard 25 Verfahren analog BahnCard 25 Verlust der vorläufigen bzw. endgültigen Jugend BC 25 Keine Ersatzausstellung Kunde kann eine neue Jugend BahnCard 25 zum Preis von 10 EUR erwerben Hintergrundinfo Jugend BahnCards 25 werden ohne Passfoto ausgestellt und sind für Inhaber ab 16 Jahre im Zug nur mit einem amtlichen Lichtbildausweis gültig. Die BahnCard Jugend 25 wird bei der Bestellung einer BahnCard 25 für Familien (Haupt- und Zusatzkarten) nicht als BahnCard 25 Zusatzkarte Kind anerkannt. Der Jugend BahnCard 25-Rabatt wird für Fahrkarten 1. und 2. Klasse gewährt Eine Kündigung ...
.... Jugend BahnCard 25 4398 Muster Jugend BahnCard 25 ", + // "link": "http://www.dbportal.db.de/scripts/cgiip.exe/VKL/Sichten/XMLAusgabe.w?RegelwerkNr=38210&AufrufVon=VKL&User=VKL", + // "score": 221.85854, + // "creator": "VKL", + // "topic": "Produkt" + // }, + // { + // "replySuggestion": null, + // "offer": "37974", + // "title": "Probe BahnCard 25 ", + // "categories": [ + // "BahnCard" + // ], + // "body": "nCard 25 als Folge-BahnCard ist bereits beim Kauf der Probe BahnCard 25 der jeweils notwendige Nachweis erforderlich und entsprechend im Probe BC 25-Bestellschein anzukreuzen Umtausch/Erstattung Ausgeschlossen Kündigung Analog BahnCard 25 Vergessene Probe BahnCard 25 Analog BahnCard 25 Verlust der vorläufigen / endgültigen Probe BahnCard 25 Analog der regulärenBahnCard 25 über den BahnCard-Service gegen ein Entgelt von 15 EUR zugelassen ERV Verkauf der Versicherung nur zeitgleich beim Kauf der Probe BahnCard 25 zugelassen Ein nachträglicher Verkauf der Aktions- und Probe BahnCard-Versicherung ist ausgeschlossen Eingabehilfe Probe BahnCard 25 Leistungskatalog Klasse Leistungs-ID Vorl. Probe BahnCard 25 2./1.Klasse 4369 Muster 2. Klasse 1. Klasse", + // "link": "http://www.dbportal.db.de/scripts/cgiip.exe/VKL/Sichten/XMLAusgabe.w?RegelwerkNr=37974&AufrufVon=VKL&User=VKL", + // "score": 221.68828, + // "creator": "VKL", + // "topic": "Produkt" + // } + // ]; + // return [ + // { + // "replySuggestion": "Um 22:25  Uhr gibt es eine Verbindung von Frankfurt(Main)Hbf nach Paris Est. Du wärst dann um 07:50  dort.", + // "departure": { + // "location": "Frankfurt(Main)Hbf", + // "time": "22:25 " + // }, + // "arrival": { + // "location": "Paris Est", + // "time": "07:50 " + // }, + // "dateChange": "+ 1 Tag", + // "travelDuration": 565, + // "travelChanges": 3, + // "travelProducts": [ + // "RE", + // "RE", + // "TER", + // "TGV" + // ], + // "creator": "bahn.de", + // "topic": "Reiseplanung" + // }, + // { + // "replySuggestion": "Um 02:48  Uhr gibt es eine Verbindung von Frankfurt(Main)Hbf nach Paris Est. Du wärst dann um 09:07  dort.", + // "departure": { + // "location": "Frankfurt(Main)Hbf", + // "time": "02:48 " + // }, + // "arrival": { + // "location": "Paris Est", + // "time": "09:07 " + // }, + // "travelDuration": 379, + // "travelChanges": 2, + // "travelProducts": [ + // "IC", + // "SWE", + // "TGV" + // ], + // "creator": "bahn.de", + // "topic": "Reiseplanung" + // } + // ]; + } +}); diff --git a/packages/dbs-ai/methods/updateKnowledgeProviderResult.js b/packages/dbs-ai/methods/updateKnowledgeProviderResult.js new file mode 100755 index 000000000000..ee946a144b56 --- /dev/null +++ b/packages/dbs-ai/methods/updateKnowledgeProviderResult.js @@ -0,0 +1,16 @@ +Meteor.methods({ + 'updateKnowledgeProviderResult': function (modifiedKnowledgeProviderResult) { + if (Meteor.isServer) { + if (!modifiedKnowledgeProviderResult) { + return; + } + + const knowledgeAdapter = _dbs.getKnowledgeAdapter(); + + if (knowledgeAdapter instanceof _dbs.RedlinkAdapterFactory.getInstance().constructor && + modifiedKnowledgeProviderResult.knowledgeProvider === 'redlink') { + return knowledgeAdapter.onResultModified(modifiedKnowledgeProviderResult); + } + } + } +}); diff --git a/packages/dbs-ai/models/Messages.js b/packages/dbs-ai/models/Messages.js new file mode 100755 index 000000000000..26cc8efce241 --- /dev/null +++ b/packages/dbs-ai/models/Messages.js @@ -0,0 +1,19 @@ +/** + * Enhance the Messages model to capture metadata containing information about how the message + * was created and is to be handled + */ +// import {_} from 'underscore'; + +_.extend(RocketChat.models.Messages, { + addMetadata: function (message, metadata) { + const query = { _id: message._id }; + + const update = { + $set: { + meta: metadata + } + }; + + return this.update(query, update); + } +}); diff --git a/packages/dbs-ai/package.js b/packages/dbs-ai/package.js new file mode 100755 index 000000000000..f767e7cb8386 --- /dev/null +++ b/packages/dbs-ai/package.js @@ -0,0 +1,56 @@ +Package.describe({ + name: 'dbs:ai', + version: '0.0.1', + summary: 'Integration of artifical knowledge', + git: '', //not hosted on separaete git repo yet - use http://github.com/mrsimpson/Rocket.Chat + documentation: 'README.md' +}); + +function addDirectory(api, pathInPackage, environment) { + const PACKAGE_PATH = 'packages/dbs-ai/'; + const _ = Npm.require('underscore'); + const fs = Npm.require('fs'); + const files = _.compact(_.map(fs.readdirSync(PACKAGE_PATH + pathInPackage), function (filename) { + return pathInPackage + '/' + filename + })); + api.addFiles(files, environment); +} + +Package.onUse(function (api) { + + api.use(['ecmascript', 'underscore']); + api.use('templating', 'client'); + api.use('less@2.5.1'); + api.use('rocketchat:lib'); //in order to make general setting load earlier + api.use('dbs:common'); + + api.addAssets('assets/stylesheets/redlink.less', 'server'); + + api.addAssets('assets/icons/sapTransaction.png', 'client'); + api.addAssets('assets/icons/assistify.png', 'client'); + api.addAssets('assets/icons/communication.png', 'client'); + api.addAssets('assets/icons/Hasso_MLT.png', 'client'); + api.addAssets('assets/icons/Hasso_Search.png', 'client'); + api.addAssets('assets/icons/dbsearch.png', 'client'); + api.addAssets('assets/icons/deselected-circle.png', 'client'); + api.addAssets('assets/icons/selected-circle.png', 'client'); + + //Common business logic + addDirectory(api, 'methods'); + api.addFiles('models/Messages.js'); + + //Server business logic + api.addFiles('server/config.js', 'server'); + addDirectory(api, 'server/lib', 'server'); + addDirectory(api, 'server/hooks', 'server'); + + //Client business logic + api.addFiles('client/redlink_ui.js', 'client'); + api.addFiles('client/lib/ClientResultProvider.js', 'client'); + api.addFiles('client/lib/collections.js', 'client'); + + //client views + addDirectory(api,'client/views/app/tabbar', 'client'); + + //i18n in Rocket.Chat-package (packages/rocketchat-i18n/i18n +}); diff --git a/packages/dbs-ai/server/config.js b/packages/dbs-ai/server/config.js new file mode 100755 index 000000000000..51e6745464d0 --- /dev/null +++ b/packages/dbs-ai/server/config.js @@ -0,0 +1,63 @@ +Meteor.startup(function () { + RocketChat.settings.addGroup('dbsAI'); + + RocketChat.settings.add('DBS_AI_Enabled', true, { + type: 'boolean', + group: 'dbsAI', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'Enabled' + }); + + RocketChat.settings.add('DBS_AI_Source', '1', { + type: 'select', + group: 'dbsAI', + section: 'Knowledge_Base', + values: [ + { key: '0', i18nLabel: 'DBS_AI_Source_APIAI'}, + { key: '1', i18nLabel: 'DBS_AI_Source_Redlink'} + ], + public: true, + i18nLabel: 'DBS_AI_Source' + }); + + RocketChat.settings.add('DBS_AI_Redlink_URL', '', { + type: 'string', + group: 'dbsAI', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'DBS_AI_Redlink_URL' + }); + + /* Currently, Redlink does not offer hashed API_keys, but uses simple password-auth + * This is of course far from perfect and is hopeully going to change sometime later */ + RocketChat.settings.add('DBS_AI_Redlink_Auth_Token', '', { + type: 'string', + group: 'dbsAI', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'DBS_AI_Redlink_Auth_Token' + }); + + + let domain = RocketChat.settings.get('Site_Url'); + if(domain){ + domain = domain + .replace("https://", "") + .replace("http://", ""); + while(domain.charAt(domain.length - 1) === '/'){ + domain = domain.substr(0, domain.length - 1); + } + } + RocketChat.settings.add('DBS_AI_Redlink_Domain', domain, { + type: 'string', + group: 'dbsAI', + section: 'Knowledge_Base', + public: true, + i18nLabel: 'DBS_AI_Redlink_Domain' + }); +}); + +RocketChat.theme.addPackageAsset(() => { + return Assets.getText('assets/stylesheets/redlink.less'); +}); diff --git a/packages/dbs-ai/server/hooks/closeLivechatKnowledgeAdapter.js b/packages/dbs-ai/server/hooks/closeLivechatKnowledgeAdapter.js new file mode 100755 index 000000000000..d632b2233ad5 --- /dev/null +++ b/packages/dbs-ai/server/hooks/closeLivechatKnowledgeAdapter.js @@ -0,0 +1,23 @@ +/** + * Notifies the knowledgeProvider about the end of a livechat conversation + */ + +const _callbackOnClose = function (room, closeProps={}) { + try { + const knowledgeAdapter = _dbs.getKnowledgeAdapter(); + if (knowledgeAdapter && knowledgeAdapter.onClose) { + knowledgeAdapter.onClose(room); + } else { + SystemLogger.warn('No knowledge provider configured'); + } + } catch (e) { + SystemLogger.error('Error submitting closed conversation to knowledge provider ->', e); + } + + let updatedRBInfo = room.rbInfo ? room.rbInfo : {}; + updatedRBInfo.knowledgeProviderUsage = closeProps.knowledgeProviderUsage; + +}; + +RocketChat.callbacks.add('livechat.closeRoom', _callbackOnClose, RocketChat.callbacks.priority.LOW); +RocketChat.callbacks.add('assistify.closeRoom', _callbackOnClose, RocketChat.callbacks.priority.LOW); diff --git a/packages/dbs-ai/server/hooks/sendMessageToKnowledgeAdapter.js b/packages/dbs-ai/server/hooks/sendMessageToKnowledgeAdapter.js new file mode 100755 index 000000000000..4ebbee77d302 --- /dev/null +++ b/packages/dbs-ai/server/hooks/sendMessageToKnowledgeAdapter.js @@ -0,0 +1,42 @@ +/* globals SystemLogger */ + +RocketChat.callbacks.add('afterSaveMessage', function (message, room) { + // skips this callback if the message was edited + if (message.editedAt) { + return message; + } + + let knowledgeEnabled = false; + RocketChat.settings.get('DBS_AI_Enabled', function (key, value) { + knowledgeEnabled = value; + }); + + if (!knowledgeEnabled) { + return message; + } + + if (!(typeof room.t !== 'undefined' && room.v && room.v.token)) { + return message; + } + + // if the message hasn't a token, it was not sent by the visitor, so ignore it + if (!message.token) { + return message; + } + + const knowledgeAdapter = _dbs.getKnowledgeAdapter(); + if (!knowledgeAdapter) { + return; + } + + Meteor.defer(() => { + try { + knowledgeAdapter.onMessage(message); + } + catch (e) { + SystemLogger.error('Error using knowledge provider ->', e); + } + }); + + return message; +}, RocketChat.callbacks.priority.LOW); diff --git a/packages/dbs-ai/server/lib/AiApiAdapter.js b/packages/dbs-ai/server/lib/AiApiAdapter.js new file mode 100755 index 000000000000..f2ce45bf38cd --- /dev/null +++ b/packages/dbs-ai/server/lib/AiApiAdapter.js @@ -0,0 +1,33 @@ +class ApiAiAdapter { + constructor(adapterProps) { + this.properties = adapterProps; + this.headers = { + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': 'Bearer ' + this.properties.token + } + } + + onMessage(message) { + const responseAPIAI = HTTP.post(this.properties.url, { + data: { + query: message.msg, + lang: this.properties.language + }, + headers: this.headers + }); + if (responseAPIAI.data && responseAPIAI.data.status.code === 200 && !_.isEmpty(responseAPIAI.data.result.fulfillment.speech)) { + RocketChat.models.LivechatExternalMessage.insert({ + rid: message.rid, + msg: responseAPIAI.data.result.fulfillment.speech, + orig: message._id, + ts: new Date() + }); + } + } + + onClose() { + //do nothing, api.ai does not learn from us. + } +} + +_dbs.ApiAiAdapterClass = ApiAiAdapter; diff --git a/packages/dbs-ai/server/lib/KnowledgeAdapterProvider.js b/packages/dbs-ai/server/lib/KnowledgeAdapterProvider.js new file mode 100755 index 000000000000..25c8011ebe69 --- /dev/null +++ b/packages/dbs-ai/server/lib/KnowledgeAdapterProvider.js @@ -0,0 +1,54 @@ +_dbs.getKnowledgeAdapter = function () { + var knowledgeSource = ''; + + const KNOWLEDGE_SRC_APIAI = "0"; + const KNOWLEDGE_SRC_REDLINK = "1"; + + RocketChat.settings.get('DBS_AI_Source', function (key, value) { + knowledgeSource = value; + }); + + let adapterProps = { + url: '', + token: '', + language: '' + }; + + switch (knowledgeSource) { + case KNOWLEDGE_SRC_APIAI: + adapterProps.url = 'https://api.api.ai/api/query?v=20150910'; + + RocketChat.settings.get('Assistify_AI_Apiai_Key', function (key, value) { + adapterProps.token = value; + }); + RocketChat.settings.get('Assistify_AI_Apiai_Language', function (key, value) { + adapterProps.language = value; + }); + + if (!_dbs.apiaiAdapter) { + _dbs.apiaiAdapter = new _dbs.ApiAiAdapterClass(adapterProps); + } + return _dbs.apiaiAdapter; + break; + case KNOWLEDGE_SRC_REDLINK: + return _dbs.RedlinkAdapterFactory.getInstance(); // buffering done inside the factory method + break; + } +}; + +/** + * Refreshes the adapter instances on change of the configuration - the redlink-adapter factory does that on its own + */ +Meteor.autorun(()=> { + RocketChat.settings.get('DBS_AI_Source', function (key, value) { + _dbs.apiaiAdapter = undefined; + }); + + RocketChat.settings.get('Assistify_AI_Apiai_Key', function (key, value) { + _dbs.apiaiAdapter = undefined; + }); + + RocketChat.settings.get('Assistify_AI_Apiai_Language', function (key, value) { + _dbs.apiaiAdapter = undefined; + }); +}); diff --git a/packages/dbs-ai/server/lib/Redlink.js b/packages/dbs-ai/server/lib/Redlink.js new file mode 100755 index 000000000000..3ecaabcc036b --- /dev/null +++ b/packages/dbs-ai/server/lib/Redlink.js @@ -0,0 +1,349 @@ +class RedlinkAdapter { + constructor(adapterProps) { + this.properties = adapterProps; + this.properties.url = this.properties.url.toLowerCase(); + + this.options = {}; + this.options.headers = {}; + this.options.headers['content-Type'] = 'application/json; charset=utf-8'; + if (this.properties.token) { + this.options.headers['authorization'] = 'basic ' + this.properties.token; + } + if (this.properties.url.substring(0, 4) === 'https') { + this.options.cert = '~/.nodeCaCerts/' + this.properties.url.replace('https', ''); + } + } + + createRedlinkStub(rid, latestKnowledgeProviderResult) { + const latestRedlinkResult = (latestKnowledgeProviderResult && latestKnowledgeProviderResult.knowledgeProvider === 'redlink') + ? latestKnowledgeProviderResult.prepareResult + : {}; + return { + id: latestRedlinkResult.id ? latestRedlinkResult.id : rid, + meta: latestRedlinkResult.meta ? latestRedlinkResult.meta : {}, + user: latestRedlinkResult.user ? latestRedlinkResult.user : {}, + messages: latestRedlinkResult.messages ? latestRedlinkResult.messages : [], + tokens: latestRedlinkResult.tokens ? latestRedlinkResult.tokens : [], + queryTemplates: latestRedlinkResult.queryTemplates ? latestRedlinkResult.queryTemplates : [] + } + } + + getConversation(rid, latestKnowledgeProviderResult) { + + let analyzedUntil = 0; + let conversation = []; + + if (latestKnowledgeProviderResult && latestKnowledgeProviderResult.knowledgeProvider === 'redlink') { + //there might have been another provider configures, e. g. if API.ai was entered earlier + // therefore we need to validate we're operating with a Redlink-result + + analyzedUntil = latestKnowledgeProviderResult.originMessage ? latestKnowledgeProviderResult.originMessage.ts : 0; + conversation = latestKnowledgeProviderResult.prepareResult.messages ? latestKnowledgeProviderResult.prepareResult.messages : []; + } + + const room = RocketChat.models.Rooms.findOneById(rid); + const owner = room.v || room.u; //livechat or regular room + RocketChat.models.Messages.find({ + rid: rid, + _hidden: {$ne: true}, + ts: {$gt: new Date(analyzedUntil)} + }).forEach(visibleMessage => { + conversation.push({ + content: visibleMessage.msg, + time: visibleMessage.ts, + origin: (owner._id === visibleMessage.u._id) ? 'User' : 'Agent' //in livechat, the owner of the room is the user + }); + }); + return conversation; + } + + onResultModified(modifiedRedlinkResult) { + try { + SystemLogger.debug("sending update to redlinkk with: " + JSON.stringify(modifiedRedlinkResult)); + let options = this.options; + options.data = modifiedRedlinkResult.prepareResult; + const responseRedlinkQuery = HTTP.post(this.properties.url + '/query', options); + SystemLogger.debug("recieved update to redlinkk with: " + JSON.stringify(responseRedlinkQuery)); + RocketChat.models.LivechatExternalMessage.update( + { + _id: modifiedRedlinkResult._id + }, + { + $set: { + result: responseRedlinkQuery.data + }, + $unset: { + inlineResults: "" + } + }); + + } catch (err) { + console.error('Updating redlink results (via QUERY) did not succeed -> ', JSON.stringify(err)); + } + } + + onMessage(message, context = {}) { + + //private methods + /** This method adapts the service response. + * It is intended to make it easier for the consumer to digest the results provided by the AI + * @param prepareResponse + * @returns prepareResponse + * @private + */ + const _postprocessPrepare = function (prepareResponse) { + return prepareResponse; + }; + + const knowledgeProviderResultCursor = this.getKnowledgeProviderCursor(message.rid); + const latestKnowledgeProviderResult = knowledgeProviderResultCursor.fetch()[0]; + + const requestBody = this.createRedlinkStub(message.rid, latestKnowledgeProviderResult); + requestBody.messages = this.getConversation(message.rid, latestKnowledgeProviderResult); + + requestBody.context = context; + try { + let options = this.options; + this.options.data = requestBody; + + if (RocketChat.settings.get('DBS_AI_Redlink_Domain')) { + options.data.context.domain = RocketChat.settings.get('DBS_AI_Redlink_Domain'); + } + const responseRedlinkPrepare = HTTP.post(this.properties.url + '/prepare', options); + + if (responseRedlinkPrepare.data && responseRedlinkPrepare.statusCode === 200) { + + this.purgePreviousResults(knowledgeProviderResultCursor); + + const externalMessageId = RocketChat.models.LivechatExternalMessage.insert({ + rid: message.rid, + knowledgeProvider: "redlink", + originMessage: {_id: message._id, ts: message.ts}, + prepareResult: _postprocessPrepare(responseRedlinkPrepare.data), + ts: new Date() + }); + + const externalMessage = RocketChat.models.LivechatExternalMessage.findOneById(externalMessageId); + + Meteor.defer(() => RocketChat.callbacks.run('afterExternalMessage', externalMessage)); + } + } catch (e) { + console.error('Redlink-Prepare/Query with results from prepare did not succeed -> ', e); + } + } + + getQueryResults(roomId, templateIndex, creator) { + // ---------------- private methods + const _getKeyForBuffer = function (templateIndex, creator) { + return templateIndex + '-' + creator.replace(/\./g, '_'); + }; + + const _getBufferedResults = function (latestKnowledgeProviderResult, templateIndex, creator) { + + if (latestKnowledgeProviderResult && latestKnowledgeProviderResult.knowledgeProvider === 'redlink' && latestKnowledgeProviderResult.inlineResults) { + return latestKnowledgeProviderResult.inlineResults[_getKeyForBuffer(templateIndex, creator)]; + } + }; + + /** + * We might have modified a prepare resonse earlier. + * If we want to revert this adaptation + * @param queryTemplates + * @private + */ + const _preprocessTemplates = function (queryTemplates) { + return queryTemplates; + }; + + const _postprocessResultResponse = function (results) { + return results; + }; + // ---------------- private methods + + var results = []; + + const latestKnowledgeProviderResult = this.getKnowledgeProviderCursor(roomId).fetch()[0]; + + if (latestKnowledgeProviderResult) { + results = _getBufferedResults(latestKnowledgeProviderResult, templateIndex, creator); + } else { + return []; // If there was no knowledge-provider-result, there cannot be any results either + } + + if (!results) { + try { + + let options = this.options; + this.options.data = this.options; + + options.data = { + messages: latestKnowledgeProviderResult.prepareResult.messagescl, + tokens: latestKnowledgeProviderResult.prepareResult.tokens, + queryTemplates: _preprocessTemplates(latestKnowledgeProviderResult.prepareResult.queryTemplates), + context: latestKnowledgeProviderResult.prepareResult.context + }; + + + if (RocketChat.settings.get('DBS_AI_Redlink_Domain')) { + options.data.context.domain = RocketChat.settings.get('DBS_AI_Redlink_Domain'); + } + const responseRedlinkResult = HTTP.post(this.properties.url + '/result/' + creator + '/?templateIdx=' + templateIndex, options); + if (responseRedlinkResult.data && responseRedlinkResult.statusCode === 200) { + results = responseRedlinkResult.data; + + if (creator === 'conversation') { + results.forEach(function (result) { + // Some dirty string operations to convert the snippet to javascript objects + let transformedSnippet = JSON.stringify(result.snippet); + transformedSnippet = transformedSnippet.slice(1, transformedSnippet.length - 1); //remove quotes in the beginning and at the end + + if (transformedSnippet) { + transformedSnippet = '[' + transformedSnippet; + transformedSnippet = transformedSnippet.replace(/\\n/g, ''); + transformedSnippet = transformedSnippet.replace(/
/g, '{"origin": "seeker", "text": "'); + transformedSnippet = transformedSnippet.replace(/
/g, '{"origin": "provider", "text": "'); + transformedSnippet = transformedSnippet.replace(/<\/div>/g, '"},'); + transformedSnippet = transformedSnippet.trim(); + if (transformedSnippet.endsWith(',')) { + transformedSnippet = transformedSnippet.slice(0, transformedSnippet.length - 1); + } + transformedSnippet = transformedSnippet + ']'; + } + try { + const messages = JSON.parse(transformedSnippet); + result.messages = messages; + } catch (err) { + console.error('Error parsing conversation', err) + } + }); + results.reduce((result) => !!result.messages); + } + + results = _postprocessResultResponse(results); + + //buffer the results + let inlineResultsMap = latestKnowledgeProviderResult.inlineResults || {}; + inlineResultsMap[_getKeyForBuffer(templateIndex, creator)] = results; + + RocketChat.models.LivechatExternalMessage.update( + { + _id: latestKnowledgeProviderResult._id + }, + { + $set: { + inlineResults: inlineResultsMap + } + }); + + } else { + console.error("Couldn't read result from Redlink"); + } + } catch (err) { + console.error('Retrieving Query-resuls from Redlink did not succeed -> ', err); + } + } + return results; + } + + purgePreviousResults(knowledgeProviderResultCursor) { + //delete suggestions proposed so far - Redlink will always analyze the complete conversation + knowledgeProviderResultCursor.forEach((oldSuggestion) => { + RocketChat.models.LivechatExternalMessage.remove(oldSuggestion._id); + }); + } + + getKnowledgeProviderCursor(roomId) { + return RocketChat.models.LivechatExternalMessage.findByRoomId(roomId, {ts: -1}); + } + + getStoredConversation(conversationId) { + let options = this.options; + + const response = HTTP.get(this.properties.url + '/store/' + conversationId, options); + if (response.statusCode === 200) { + return response.data; + } + } + + onClose(room) { //async + + const knowledgeProviderResultCursor = this.getKnowledgeProviderCursor(room._id); + let latestKnowledgeProviderResult = knowledgeProviderResultCursor.fetch()[0]; + if (latestKnowledgeProviderResult) { + // latestKnowledgeProviderResult.helpful = room.rbInfo.knowledgeProviderUsage; + + let options = this.options; + options.data = latestKnowledgeProviderResult.prepareResult; + if (RocketChat.settings.get('DBS_AI_Redlink_Domain')) { + if(!options.data.context){ + options.data.context = {}; + } + options.data.context.domain = RocketChat.settings.get('DBS_AI_Redlink_Domain'); + } + try { + const responseStore = HTTP.post(this.properties.url + '/store', options); + if (responseStore.statusCode === 200) { + return responseStore.data; + } + } catch (err) { + console.error('Error on Store', err); + } + } + } +} + +class RedlinkMock extends RedlinkAdapter { + constructor(adapterProps) { + super(adapterProps); + + this.properties.url = 'http://localhost:8080'; + delete this.headers.authorization; + } +} + +class RedlinkAdapterFactory { + constructor() { + this.singleton = undefined; + + /** + * Refreshes the adapter instances on change of the configuration + */ + var factory = this; + this.settingsHandle = RocketChat.models.Settings.findByIds(['DBS_AI_Source', 'DBS_AI_Redlink_URL', 'DBS_AI_Redlink_Auth_Token']).observeChanges({ + added(id, fields) { + factory.singleton = undefined; + }, + changed(id, fields) { + factory.singleton = undefined; + }, + removed(id) { + factory.singleton = undefined; + } + }); + }; + + static getInstance() { + if (this.singleton) { + return this.singleton + } else { + var adapterProps = { + url: '', + token: '', + language: '' + }; + + adapterProps.url = RocketChat.settings.get('DBS_AI_Redlink_URL'); + + adapterProps.token = RocketChat.settings.get('DBS_AI_Redlink_Auth_Token'); + + if (_dbs.mockInterfaces()) { //use mock + this.singleton = new RedlinkMock(adapterProps); + } else { + this.singleton = new RedlinkAdapter(adapterProps); + } + return this.singleton; + } + } +} + +_dbs.RedlinkAdapterFactory = RedlinkAdapterFactory; diff --git a/packages/dbs-common/README.md b/packages/dbs-common/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/dbs-common/client/lib/globalTemplateHelpers.js b/packages/dbs-common/client/lib/globalTemplateHelpers.js new file mode 100755 index 000000000000..6833462ff9b2 --- /dev/null +++ b/packages/dbs-common/client/lib/globalTemplateHelpers.js @@ -0,0 +1,19 @@ +Template.registerHelper('and', (a, b)=> a && b); +Template.registerHelper('or', (a, b)=> a || b); + +/** + * Allows to access reactive dict components in Blaze-templates: {{instance.state.get "foo"}} + */ +Template.registerHelper('instance', ()=> Template.instance()); + +Template.registerHelper('arrayLength', (array) => array.length); + +Template.registerHelper('add', (a, b) => a + b); + +Template.registerHelper('text', (i18n_alias) => t(i18n_alias)); + +Template.registerHelper('formatDateMilliseconds', (val) => new _dbs.Duration(val).toHHMMSS()); + +Template.registerHelper('templateExists', (val) => !!Template[val]); + +Template.registerHelper('floatToFixed', (size, val) => val ? val.toFixed(size) : ''); diff --git a/packages/dbs-common/dbs-common.js b/packages/dbs-common/dbs-common.js new file mode 100644 index 000000000000..191e37714fd5 --- /dev/null +++ b/packages/dbs-common/dbs-common.js @@ -0,0 +1,5 @@ +// Write your package code here! + +// Variables exported by this module can be imported by other packages and +// applications. See dbs-common-tests.js for an example of importing. +export const name = 'dbs-common'; diff --git a/packages/dbs-common/lib/core.js b/packages/dbs-common/lib/core.js new file mode 100755 index 000000000000..b5fff1362e22 --- /dev/null +++ b/packages/dbs-common/lib/core.js @@ -0,0 +1,2 @@ +/* exported _dbs */ +_dbs = {}; diff --git a/packages/dbs-common/lib/duration.js b/packages/dbs-common/lib/duration.js new file mode 100755 index 000000000000..c9b758b0df17 --- /dev/null +++ b/packages/dbs-common/lib/duration.js @@ -0,0 +1,23 @@ +/* globals _dbs */ + +class Duration { + constructor(ms) { + this.ms = ms; + this.date = new Date(ms); + } + + static padZero(i) { + return ( i < 10 ? "0" + i : i ); + } + + toHHMMSS() { + return Math.floor(this.ms / 3600000) + ':' + Duration.padZero(this.date.getMinutes()) + ':' + + Duration.padZero(this.date.getSeconds()) + } + + toMM() { + return Duration.padZero(Math.floor(this.ms / 60000)); + } +} + +_dbs.Duration = Duration; diff --git a/packages/dbs-common/lib/testing.js b/packages/dbs-common/lib/testing.js new file mode 100755 index 000000000000..136f0fc0760a --- /dev/null +++ b/packages/dbs-common/lib/testing.js @@ -0,0 +1,3 @@ +_dbs.mockInterfaces = function(){ + return false; +}; diff --git a/packages/dbs-common/package.js b/packages/dbs-common/package.js new file mode 100755 index 000000000000..7d0a4bf6deed --- /dev/null +++ b/packages/dbs-common/package.js @@ -0,0 +1,20 @@ +Package.describe({ + name: 'dbs:common', + version: '0.0.1', + summary: 'Basic customizing for db', // Brief, one-line summary of the package. + git: '', + documentation: '' +}); + + +Package.onUse(function (api) { + api.use(['ecmascript', 'underscore']); + api.use('templating', 'client'); //needed in order to be able to register global helpers on the Template-object + + api.addFiles('lib/core.js'); + api.addFiles('lib/duration.js', 'client'); + api.addFiles('lib/testing.js', 'server'); + api.addFiles('client/lib/globalTemplateHelpers.js', 'client'); + + api.export('_dbs'); +}); diff --git a/packages/rocketchat-i18n/i18n/assistify.de.i18n.yml b/packages/rocketchat-i18n/i18n/assistify.de.i18n.yml new file mode 100644 index 000000000000..3876718f6e7e --- /dev/null +++ b/packages/rocketchat-i18n/i18n/assistify.de.i18n.yml @@ -0,0 +1,3 @@ +Assistify_Show_Standard_Features: Standard RocketChat Features +Create_new_standard_channel: Neuen einfachen Kanal erstellen +General: Allgemeines diff --git a/packages/rocketchat-i18n/i18n/assistify.en.i18n.yml b/packages/rocketchat-i18n/i18n/assistify.en.i18n.yml new file mode 100644 index 000000000000..31817a0a82cf --- /dev/null +++ b/packages/rocketchat-i18n/i18n/assistify.en.i18n.yml @@ -0,0 +1,3 @@ +Assistify_Show_Standard_Features: Standard RocketChat features +Create_new_standard_channel: Create new simple channel +General: General diff --git a/packages/rocketchat-i18n/i18n/assistifyBot.de.i18n.yml b/packages/rocketchat-i18n/i18n/assistifyBot.de.i18n.yml new file mode 100644 index 000000000000..a28587834009 --- /dev/null +++ b/packages/rocketchat-i18n/i18n/assistifyBot.de.i18n.yml @@ -0,0 +1,2 @@ +Assistify_Bot_Username: Bot Benutzername +Assistify_Bot_Automated_Response_Threshold: Schwellwert für Bot-Antwort diff --git a/packages/rocketchat-i18n/i18n/assistifyBot.en.i18n.yml b/packages/rocketchat-i18n/i18n/assistifyBot.en.i18n.yml new file mode 100644 index 000000000000..cbced16e6597 --- /dev/null +++ b/packages/rocketchat-i18n/i18n/assistifyBot.en.i18n.yml @@ -0,0 +1,2 @@ +Assistify_Bot_Username: Bot username +Assistify_Bot_Automated_Response_Threshold: Bot response threshhold diff --git a/packages/rocketchat-i18n/i18n/assistifyHelpRequest.de.i18n.yml b/packages/rocketchat-i18n/i18n/assistifyHelpRequest.de.i18n.yml new file mode 100755 index 000000000000..60e1f21c12e4 --- /dev/null +++ b/packages/rocketchat-i18n/i18n/assistifyHelpRequest.de.i18n.yml @@ -0,0 +1,19 @@ +SystemContext: System-Kontext +system: System +release: Version +transaction: Transaktion +application: Anwendung +gui_title: GUI-Titel +Close_HelpRequest: Hilfe beenden +Expertise_needs_experts: Bitte Experten auswählen, um eine Expertise zu definieren +No_expertise_yet: Noch in keiner Expertengruppe +Request: Anfrage +Requests: Anfragen +New_request: Neue Anfrage +More_requests: Weitere Anfragen +No_requests_yet: Keine Anfragen bisher +Assistify_room_count: Letzte Raumnummer +New_request_for_expertise: Neue Anfrage zum Thema +Experts: Experten +Experts_channel: Expertenkanal + diff --git a/packages/rocketchat-i18n/i18n/assistifyHelpRequest.en.i18n.yml b/packages/rocketchat-i18n/i18n/assistifyHelpRequest.en.i18n.yml new file mode 100755 index 000000000000..71f063c73778 --- /dev/null +++ b/packages/rocketchat-i18n/i18n/assistifyHelpRequest.en.i18n.yml @@ -0,0 +1,18 @@ +SystemContext: System context +system: System +release: Version +transaction: Transaction +application: Application +gui_title: GUI-Title +Close_HelpRequest: Help completed +Expertise_needs_experts: Select some experts in order to define an expertise +No_expertise_yet: No expertise yet +Request: Request +Requests: Requests +New_request: New request +More_requests: Other requests +No_requests_yet: No requests yet +Asssitify_room_count: Roomcount +New_request_for_expertise: New request for topic +Experts: Experts +Experts_channel: Experts channel diff --git a/packages/rocketchat-i18n/i18n/dbsAI.de.i18n.yml b/packages/rocketchat-i18n/i18n/dbsAI.de.i18n.yml new file mode 100755 index 000000000000..32d3391b933f --- /dev/null +++ b/packages/rocketchat-i18n/i18n/dbsAI.de.i18n.yml @@ -0,0 +1,67 @@ +topic_from: Von +topic_to: Nach +topic_depart: Abfahrt +topic_arrive: Ankunft +topic_product: Produkt +topic_what: Was +topic_train: Zug +topic_when: Wann +topic_start: Von +topic_end: Bis +topic_location: Ort +topic_date: Datum +topic_card: BahnCard +topic_class: Klasse +bahnDe: Bahn.de +bahn_de: Bahn.de +community_bahn_de: Service-Community +expedia: Expedia +google: Google +quixxit: Qixxit +yelp: Yelp +VKL: VKL +googleMap: Google Maps +bahnDeSearchbox: Bahn.de Suche +communityBahnDe: Service-Community +dbsearch: DB Search +topAnsweredQuestions: Top beantwortete Fragen +maps_google-FoodAndBeverages: Googe Maps Speisen und Getränke +maps_google-WasTun: Googe Maps Suche +ApplicationHelp: Hilfe +sapTransaction: SAP Transaktionen +conversation: Vorherige Konversationen +topic_supportType: Art der Hilfe +topic_keyword: Stichwort +help: Hilfe! +oops_error: Oops, da hat etwas nicht geklappt. Sorry! +cannot-retrieve-dbsearch-results: Konnte DB Search nicht erreichen. Bist Du im Intranet? +no_results: Leider keine Ergebnisse +knowledge_provider_usage_unknown: Die Wissensbasis... +knowledge_provider_usage_perfect: hat alles perfekt erkannt +knowledge_provider_usage_helpful: war hilfreich +knowledge_provider_usage_not_used: wurde nicht verwendet +knowledge_provider_usage_useless: war nicht hilfreich +results_community_bahn_de: Top Community Ergebnisse +results_bahn_de: Mögliche Verbindungen +dbsearch: DB Search +results_dbsearch: DB Search +results: Top Ergebnisse +duration: Dauer +track_changes: Umstiege +products: Produkte +price_low: Sparpreis +price_standard: Flexpreis +connection_details: Details +copy_to_message: Senden +connection_not_available: Verbindung nicht buchbar +DBS_AI_Source: Wissensquelle +DBS_AI_Source_APIAI: API.ai +DBS_AI_Source_Redlink: Redlink +DBS_AI_Redlink_URL: URL des Redlink-Service +DBS_AI_Redlink_Auth_Token: Basic-Auth Token +DBS_AI_Redlink_Domain: Domäne für Nachrichten +Assistify_AI_DBSearch_Suffix: Eigener Such-Suffix +similar_to: Ähnliches zu +similar_requests: Ähnliche Anfragen +Link_provided: Ich habe hier eine ähnliche Konversation gefunden +No_room_link_possible: Die originale Konversation fand nicht über diesen Chat statt. Daher kann ich sie leider nicht verlinken. Bitte wähle einzelne Nachtichten aus. diff --git a/packages/rocketchat-i18n/i18n/dbsAI.en.i18n.yml b/packages/rocketchat-i18n/i18n/dbsAI.en.i18n.yml new file mode 100755 index 000000000000..c4b1dde5e524 --- /dev/null +++ b/packages/rocketchat-i18n/i18n/dbsAI.en.i18n.yml @@ -0,0 +1,63 @@ +topic_from: from +topic_to: to +topic_depart: depart +topic_arrive: arrive +topic_product: product +topic_what: what +topic_train: train +topic_when: when +topic_start: start +topic_end: end +topic_location: location +topic_date: date +topic_card: BahnCard +topic_class: Class +bahnDe: Bahn.de +bahn_de: Bahn.de +community_bahn_de: Service-Community +expedia: Expedia +google: Google +quixxit: Qixxit +yelp: Yelp +VKL: VKL +googleMap: Google Maps +bahnDeSearchbox: Bahn.de Search +communityBahnDe: Service-Community +dbsearch: DB Search +topAnsweredQuestions: Top answered questions +maps_google-FoodAndBeverages: Googe Maps Food and Beverage +maps_google-WasTun: Googe Maps Suche +ApplicationHelp: Support +sapTransaction: SAP Transactions +conversation: Previous conversation +topic_supportType: Support type +topic_keyword: Keyword +knowledge_provider_usage_unknown: Unknown +knowledge_provider_usage_perfect: Perfectly recognized +knowledge_provider_usage_helpful: Helpful +knowledge_provider_usage_not_used: Not used +knowledge_provider_usage_useless: Not helpful +results_community_bahn_de: Top community results +results_bahn_de: Possible connections +dbsearch: DB Search +results_dbsearch: DB Search +results: Top results +duration: Duration +track_changes: Track changes +products: Products +price_low: Standard price +price_standard: Flex price +connection_details: Details +copy_to_message: Send +connection_not_available: Connection not available +DBS_AI_Source: Knowledge source +DBS_AI_Source_APIAI: API.ai +DBS_AI_Source_Redlink: Redlink +DBS_AI_Redlink_URL: URL of Redlink service +DBS_AI_Redlink_Auth_Token: Basic-Auth token +DBS_AI_Redlink_Domain: Domain for messages +Assistify_AI_DBSearch_Suffix: Custom search suffix +similar_to: Similar to +similar_requests: Similar requests +Link_provided: Similar question at +No_room_link_possible: Origin conversation was not provided within this chat. Cannot insert a link. Please pick individual messages. diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index cd6ac9f2ab15..0f702cb951db 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -864,7 +864,7 @@ "Only_you_can_see_this_message": "Nur Sie können diese Nachricht sehen.", "Oops!": "Hoppla", "Open": "Öffnen", - "Open_Livechats": "Öffne Livechats", + "Open_Livechats": "Offene Livechats", "Opened": "Geöffnet", "Opens_a_channel_group_or_direct_message": "Eröffnet einen Kanal, eine Gruppe oder Direktnachrichten", "optional": "optional", @@ -1341,4 +1341,4 @@ "your_message_optional": "ihre optionale Nachricht", "Your_password_is_wrong": "Falsches Passwort", "Your_push_was_sent_to_s_devices": "Die Push-Nachricht wurde an %s Geräte gesendet." -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/package.js b/packages/rocketchat-i18n/package.js index d7d520a85f54..43452f5496d6 100644 --- a/packages/rocketchat-i18n/package.js +++ b/packages/rocketchat-i18n/package.js @@ -11,7 +11,7 @@ Package.onUse(function(api) { var fs = Npm.require('fs'); var workingDir = process.env.PWD || '.'; fs.readdirSync(workingDir + '/packages/rocketchat-i18n/i18n').forEach(function(filename) { - if (filename.indexOf('.json') > -1 && fs.statSync(workingDir + '/packages/rocketchat-i18n/i18n/' + filename).size > 16) { + if ((filename.indexOf('.json') > -1 || filename.indexOf('.yml') > -1) && fs.statSync(workingDir + '/packages/rocketchat-i18n/i18n/' + filename).size > 16) { api.addFiles('i18n/' + filename); } }); diff --git a/packages/rocketchat-lib/server/functions/settings.coffee b/packages/rocketchat-lib/server/functions/settings.coffee index f8348261c5f0..1098cd7941da 100644 --- a/packages/rocketchat-lib/server/functions/settings.coffee +++ b/packages/rocketchat-lib/server/functions/settings.coffee @@ -17,8 +17,9 @@ RocketChat.settings._sorter = {} RocketChat.settings.add = (_id, value, options = {}) -> # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - if not _id or not value? - return false + if not _id or + not value? and not process?.env?['OVERWRITE_SETTING_' + _id]? + return false RocketChat.settings._sorter[options.group] ?= 0 diff --git a/packages/rocketchat-livechat/config.js b/packages/rocketchat-livechat/config.js index d9f8810d8fcd..7a9f8f3e2cb1 100644 --- a/packages/rocketchat-livechat/config.js +++ b/packages/rocketchat-livechat/config.js @@ -145,7 +145,7 @@ Meteor.startup(function() { i18nLabel: 'Apiai_Key' }); - RocketChat.settings.add('Livechat_Knowledge_Apiai_Language', 'en', { + RocketChat.settings.add('Livechat_Knowledge_Language', 'en', { type: 'string', group: 'Livechat', section: 'Knowledge_Base', diff --git a/packages/rocketchat-livechat/server/hooks/externalMessage.js b/packages/rocketchat-livechat/server/hooks/externalMessage.js index be655a524a46..6f0df90dfa13 100644 --- a/packages/rocketchat-livechat/server/hooks/externalMessage.js +++ b/packages/rocketchat-livechat/server/hooks/externalMessage.js @@ -3,13 +3,13 @@ var knowledgeEnabled = false; var apiaiKey = ''; var apiaiLanguage = 'en'; -RocketChat.settings.get('Livechat_Knowledge_Enabled', function(key, value) { +RocketChat.settings.get('DBS_AI_Enabled', function(key, value) { knowledgeEnabled = value; }); -RocketChat.settings.get('Livechat_Knowledge_Apiai_Key', function(key, value) { +RocketChat.settings.get('Assistify_AI_Apiai_Key', function(key, value) { apiaiKey = value; }); -RocketChat.settings.get('Livechat_Knowledge_Apiai_Language', function(key, value) { +RocketChat.settings.get('Assistify_AI_Apiai_Language', function(key, value) { apiaiLanguage = value; }); diff --git a/packages/rocketchat-ui-flextab/client/tabs/membersList.coffee b/packages/rocketchat-ui-flextab/client/tabs/membersList.coffee index 12561961069b..8aa7cf5d0355 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/membersList.coffee +++ b/packages/rocketchat-ui-flextab/client/tabs/membersList.coffee @@ -3,7 +3,7 @@ Template.membersList.helpers return t('Add_users') isGroupChat: -> - return ChatRoom.findOne(this.rid, { reactive: false })?.t in ['c', 'p'] + return ChatRoom.findOne(this.rid, { reactive: false })?.t in ['c', 'p', 'r', 'e'] isDirectChat: -> return ChatRoom.findOne(this.rid, { reactive: false })?.t is 'd' @@ -68,6 +68,8 @@ Template.membersList.helpers return switch roomData.t when 'p' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-p-room', 'add-user-to-joined-room'], this._id when 'c' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-c-room', 'add-user-to-joined-room'], this._id + when 'e' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-p-room', 'add-user-to-joined-room'], this._id + when 'r' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-c-room', 'add-user-to-joined-room'], this._id else false autocompleteSettingsAddUser: -> @@ -103,8 +105,8 @@ Template.membersList.helpers tabBar: Template.currentData().tabBar username: Template.instance().userDetail.get() clear: Template.instance().clearUserDetail - showAll: room?.t in ['c', 'p'] - hideAdminControls: room?.t in ['c', 'p', 'd'] + showAll: room?.t in ['c', 'p', 'r', 'e'] + hideAdminControls: room?.t in ['c', 'p', 'd', 'r'] video: room?.t in ['d'] } @@ -120,7 +122,7 @@ Template.membersList.events roomData = Session.get('roomData' + template.data.rid) - if roomData.t in ['c', 'p'] + if roomData.t in ['c', 'p', 'r', 'e'] Meteor.call 'addUserToRoom', { rid: roomData._id, username: doc.username }, (error, result) -> if error return handleError(error) diff --git a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.coffee b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.coffee index ff306898959b..04188237dee1 100644 --- a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.coffee +++ b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.coffee @@ -1,4 +1,7 @@ Template.createCombinedFlex.helpers + showStandardFeatures: -> + return RocketChat.settings.get('Assistify_Show_Standard_Features') + selectedUsers: -> return Template.instance().selectedUsers.get() diff --git a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html index acc8e3230984..2c6eee7e5af6 100644 --- a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html +++ b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html @@ -5,65 +5,71 @@

{{_ "Channels"}}

-
-

{{_ "Create_new" }}

-
- {{_ "Name"}} - + {{> AssistifyCreateChannel}} + {{#if showStandardFeatures}} +
+

{{_ "Create_new_standard_channel" }}

+
+ {{_ "Name"}} + +
+
+ {{_ "Private"}} +
+ + +
+
+
+ {{_ "Read_only_channel"}} +
+ + +
+
+
+ + {{> inputAutocomplete settings=autocompleteSettings id="channel-members" class="search" placeholder=(_ "Search_by_username") autocomplete="off"}} +
    + {{#each selectedUsers}} +
  • {{.}} +
  • + {{/each}} +
+
+ {{#if error.fields}} +
+ {{_ "Oops!"}} + {{#each error.fields}} +

{{_ "The_field_is_required" .}}

+ {{/each}} +
+ {{/if}} + {{#if error.invalid}} +
+ {{_ "Oops!"}} + {{{_ "Invalid_room_name" roomName}}} +
+ {{/if}} + {{#if error.duplicate}} +
+ {{_ "Oops!"}} + {{{_ "Duplicate_channel_name" roomName}}} +
+ {{/if}} + {{#if error.archivedduplicate}} +
+ {{_ "Oops!"}} + {{{_ "Duplicate_archived_channel_name" roomName}}} +
+ {{/if}} +
+ + +
-
- {{_ "Private"}} -
- - -
-
-
- {{_ "Read_only_channel"}} -
- - -
-
-
- - {{> inputAutocomplete settings=autocompleteSettings id="channel-members" class="search" placeholder=(_ "Search_by_username") autocomplete="off"}} -
    - {{#each selectedUsers}} -
  • {{.}}
  • - {{/each}} -
-
- {{#if error.fields}} -
- {{_ "Oops!"}} - {{#each error.fields}} -

{{_ "The_field_is_required" .}}

- {{/each}} -
- {{/if}} - {{#if error.invalid}} -
- {{_ "Oops!"}} - {{{_ "Invalid_room_name" roomName}}} -
- {{/if}} - {{#if error.duplicate}} -
- {{_ "Oops!"}} - {{{_ "Duplicate_channel_name" roomName}}} -
- {{/if}} - {{#if error.archivedduplicate}} -
- {{_ "Oops!"}} - {{{_ "Duplicate_archived_channel_name" roomName}}} -
- {{/if}} -
- - -
-
+ {{/if}}
diff --git a/packages/rocketchat-ui/views/app/room.coffee b/packages/rocketchat-ui/views/app/room.coffee index aa501bc39a35..f629abd57e4d 100644 --- a/packages/rocketchat-ui/views/app/room.coffee +++ b/packages/rocketchat-ui/views/app/room.coffee @@ -301,7 +301,7 @@ Template.room.events e.stopPropagation() return - if roomData.t in ['c', 'p', 'd'] + if roomData.t in ['c', 'p', 'd', 'r', 'e'] instance.setUserDetail this._arguments[1].u.username instance.tabBar.setTemplate('membersList')