diff --git a/src/css/ttyg/chat-panel.css b/src/css/ttyg/chat-panel.css index 7c0c423bb..f8e3cf250 100644 --- a/src/css/ttyg/chat-panel.css +++ b/src/css/ttyg/chat-panel.css @@ -65,6 +65,7 @@ } .chat-panel .messages-hint { + padding-top: 1rem; text-align: center; } @@ -128,7 +129,6 @@ .chat-panel .new-question { width: 100%; - padding: 0 1rem; } .chat-panel .new-question .question-input { @@ -137,7 +137,7 @@ } .chat-panel .chat-loading { - margin-top: 100px; + margin: auto; } .chat-panel .text-warning { diff --git a/src/css/ttyg/ttyg.css b/src/css/ttyg/ttyg.css index 0316c1475..0af4c2304 100644 --- a/src/css/ttyg/ttyg.css +++ b/src/css/ttyg/ttyg.css @@ -3,7 +3,9 @@ --toolbar-height: 37px; /* the gap between user and agent as well as between agent and agent messages */ --message-gap: 1.5rem; - --chat-panel-height-offset: 13em; + --chat-height-vh-reduce: 12rem; + --chat-min-height: 300px; + --chat-panel-vh-reduce: 4rem; } @media (max-width: 1440px) { @@ -125,25 +127,22 @@ chat area } .ttyg-view .chat-content .toolbar { - margin: auto; + /*margin: auto;*/ padding: 0 1rem; } .ttyg-view .chat-content .chat { max-width: calc(800px + 2rem); margin: auto; - height: calc(100vh - var(--chat-panel-height-offset)); + height: calc(100vh - var(--chat-height-vh-reduce)); + min-height: var(--chat-min-height); display: flex; flex-direction: column; - justify-content: space-between; } .ttyg-view .chat-content .chat .selected-chat-panel { - height: calc(100vh - var(--chat-panel-height-offset)); -} - -.ttyg-view.help-visible .chat-content .chat .selected-chat-panel { - height: calc(100vh - var(--chat-panel-height-offset) + 3em); + height: calc(100vh - var(--chat-height-vh-reduce) - var(--chat-panel-vh-reduce)); + min-height: calc(var(--chat-min-height) - var(--chat-panel-vh-reduce)) ; } .ttyg-view .controls { @@ -154,3 +153,15 @@ chat area .ttyg-view agent-select-menu { width: 300px; } + +.ttyg-view .chat-list-panel-loader { + display: flex; + align-items: center; + justify-content: center; + height: calc(100% - var(--toolbar-height)) +} + +.ttyg-view .chat-list-panel-loader > div { + display: flex; + font-weight: 400; +} diff --git a/src/js/angular/core/services/ttyg.service.js b/src/js/angular/core/services/ttyg.service.js index a503715b6..57b029066 100644 --- a/src/js/angular/core/services/ttyg.service.js +++ b/src/js/angular/core/services/ttyg.service.js @@ -69,6 +69,17 @@ function TTYGService(TTYGRestService, $repositories) { .then((response) => chatAnswerModelMapper(response.data)); }; + /** + * Continues a chat run. In essence continue means "fetch more answers". + * @param {ContinueChatRun} continueData . + * @return {Promise} the next answer in the run. + */ + const continueChatRun = (continueData) => { + const payload = continueData.toContinueRunRequestPayload(); + return TTYGRestService.continueChatRun(payload) + .then((response) => chatAnswerModelMapper(response.data)); + }; + /** * Deletes a conversation by its ID. * @param {string} id @@ -174,6 +185,7 @@ function TTYGService(TTYGRestService, $repositories) { renameConversation, exportConversation, askQuestion, + continueChatRun, getConversations, deleteConversation, createConversation, diff --git a/src/js/angular/models/ttyg/chat-answer.js b/src/js/angular/models/ttyg/chat-answer.js index ff5aa62b8..f1cbed528 100644 --- a/src/js/angular/models/ttyg/chat-answer.js +++ b/src/js/angular/models/ttyg/chat-answer.js @@ -19,6 +19,11 @@ export class ChatAnswerModel { * @type {ChatMessageModel[]} */ this._messages = data.messages || []; + + /** + * @type {string} + */ + this._continueRunId = data.continueRunId; } get chatId() { @@ -52,4 +57,36 @@ export class ChatAnswerModel { set messages(value) { this._messages = value; } + + get continueRunId() { + return this._continueRunId; + } + + set continueRunId(value) { + this._continueRunId = value; + } +} + +/** + * Represents information on continuing the chat run, i.e., fetching answers iteratively after + * asking until the last answer is received. + */ +export class ContinueChatRun { + constructor(chatItem, runId) { + this._chatItem = chatItem; + this._runId = runId; + } + + get chatId() { + return this._chatItem.chatId; + } + + toContinueRunRequestPayload() { + return { + conversationId: this.chatId, + runId: this._runId, + lastMessageId: this._chatItem.answers[this._chatItem.answers.length - 1].id, + agentId: this._chatItem.agentId + }; + } } diff --git a/src/js/angular/models/ttyg/chats.js b/src/js/angular/models/ttyg/chats.js index 6e9872ec8..1f9135275 100644 --- a/src/js/angular/models/ttyg/chats.js +++ b/src/js/angular/models/ttyg/chats.js @@ -1,6 +1,15 @@ import {ChatItemsListModel} from "./chat-item"; +import {md5HashGenerator} from "../../utils/hash-utils"; export class ChatModel { + static getEmptyChat() { + const data = { + name: "\u00B7 \u00B7 \u00B7", + timestamp: Math.floor(Date.now() / 1000) + }; + return new ChatModel(data, md5HashGenerator()); + } + constructor(data, hashGenerator) { this.hashGenerator = hashGenerator; @@ -148,9 +157,21 @@ export class ChatsListModel { const chat = this._chats.find((c) => c.id === chatId); if (chat) { chat.timestamp = timestamp; + this.sortByTime(); + this.updateChatsByDay(); + } + } + + /** + * Updates the name of a chat in the list. + * @param {string} chatId + * @param {string} name + */ + updateChatName(chatId, name) { + const chat = this._chats.find((c) => c.id === chatId); + if (chat) { + chat.name = name; } - this.sortByTime(); - this.updateChatsByDay(); } /** diff --git a/src/js/angular/rest/ttyg.rest.service.fake.backend.js b/src/js/angular/rest/ttyg.rest.service.fake.backend.js index 028bb7042..8eefa4c38 100644 --- a/src/js/angular/rest/ttyg.rest.service.fake.backend.js +++ b/src/js/angular/rest/ttyg.rest.service.fake.backend.js @@ -94,6 +94,10 @@ export class TtygRestServiceFakeBackend { // return new Promise((resolve, reject) => setTimeout(() => reject(''), ASK_DELAY)); } + continueChatRun(data) { + alert("continueChatRun() not implemented"); + } + deleteConversation(id) { this.conversations = this.conversations.filter((conversation) => conversation.id !== id); return new Promise((resolve) => setTimeout(() => resolve(), DELETE_DELAY)); diff --git a/src/js/angular/rest/ttyg.rest.service.js b/src/js/angular/rest/ttyg.rest.service.js index c2e6e53e0..38460bf5f 100644 --- a/src/js/angular/rest/ttyg.rest.service.js +++ b/src/js/angular/rest/ttyg.rest.service.js @@ -75,6 +75,18 @@ function TTYGRestService($http) { return $http.post(`${CONVERSATIONS_ENDPOINT}`, data); }; + /** + * Calls the REST API to continue a chat run. + * @param {*} data + * @return {*} + */ + const continueChatRun = (data) => { + if (DEVELOPMENT) { + return _fakeBackend.continueChatRun(data); + } + return $http.post(`${CONVERSATIONS_ENDPOINT}/continue`, data); + }; + /** * Deletes a conversation by its ID. * @param {string} id @@ -186,6 +198,7 @@ function TTYGRestService($http) { renameConversation, exportConversation, askQuestion, + continueChatRun, getConversations, deleteConversation, createConversation, diff --git a/src/js/angular/ttyg/controllers/ttyg-view.controller.js b/src/js/angular/ttyg/controllers/ttyg-view.controller.js index cc526ca50..703c5e90b 100644 --- a/src/js/angular/ttyg/controllers/ttyg-view.controller.js +++ b/src/js/angular/ttyg/controllers/ttyg-view.controller.js @@ -18,7 +18,7 @@ import {saveAs} from 'lib/FileSaver-patch'; import {AgentSettingsModal} from "../model/agent-settings-modal"; import {decodeHTML} from "../../../../app"; import {status as httpStatus} from "../../models/http-status"; -import {md5HashGenerator} from "../../utils/hash-utils"; +import {ContinueChatRun} from "../../models/ttyg/chat-answer"; const modules = [ 'toastr', @@ -182,12 +182,7 @@ function TTYGViewCtrl( * Creates a new chat and selects it. */ $scope.startNewChat = () => { - let nonPersistedChat = TTYGContextService.getChats().getNonPersistedChat(); - if (!nonPersistedChat) { - nonPersistedChat = getEmptyChat(); - TTYGContextService.addChat(nonPersistedChat); - } - TTYGContextService.selectChat(nonPersistedChat); + TTYGContextService.deselectChat(); }; $scope.onopen = $scope.onclose = () => angular.noop(); @@ -503,35 +498,34 @@ function TTYGViewCtrl( } }; - const getEmptyChat = () => { - const data = { - name: "\u00B7 \u00B7 \u00B7", - timestamp: Math.floor(Date.now() / 1000) - }; - return new ChatModel(data, md5HashGenerator()); - }; - /** * @param {ChatItemModel} chatItem */ const onCreateNewChat = (chatItem) => { - $scope.startNewChat(); + let nonPersistedChat = TTYGContextService.getChats().getNonPersistedChat(); + if (!nonPersistedChat) { + nonPersistedChat = ChatModel.getEmptyChat(); + TTYGContextService.addChat(nonPersistedChat); + } + TTYGContextService.selectChat(nonPersistedChat); TTYGService.createConversation(chatItem) .then((chatAnswer) => { TTYGContextService.emit(TTYGEventName.CREATE_CHAT_SUCCESSFUL); - // TODO: To discus: If we have all answer ids, can we update selected chats without loading it? - return TTYGService.getConversation(chatAnswer.chatId); - }) - .then((chat) => { const selectedChat = TTYGContextService.getSelectedChat(); // If the selected chat is not changed during the creation process. if (selectedChat && !selectedChat.id) { + // Give the newly created things the real IDs + selectedChat.id = chatAnswer.chatId; + chatItem.chatId = chatAnswer.chatId; + + // Replace the placeholder with the newly created chat const nonPersistedChat = TTYGContextService.getChats().getNonPersistedChat(); - TTYGContextService.updateSelectedChat(chat); - TTYGContextService.replaceChat(chat, nonPersistedChat); + TTYGContextService.replaceChat(selectedChat, nonPersistedChat); + + // Process the messages + updateChatAnswersFirstResponse(selectedChat, chatItem, chatAnswer); } - TTYGContextService.emit(TTYGEventName.LOAD_CHATS); }) .catch(() => { TTYGContextService.emit(TTYGEventName.CREATE_CHAT_FAILURE); @@ -543,22 +537,13 @@ function TTYGViewCtrl( * @param {ChatItemModel} chatItem */ const onAskQuestion = (chatItem) => { - chatItem.question.timestamp = Date.now(); TTYGService.askQuestion(chatItem) .then((chatAnswer) => { const selectedChat = TTYGContextService.getSelectedChat(); + // If still the same chat selected if (selectedChat && selectedChat.id === chatItem.chatId) { - selectedChat.timestamp = chatAnswer.timestamp; - const item = chatItem; - chatItem.answers = chatAnswer.messages; - selectedChat.chatHistory.appendItem(item); - TTYGContextService.updateSelectedChat(selectedChat); - // TODO reorder the list of chats - // Update the timestamp of the chat to which the last question was added in the chats list and - // update the list so that the chat is moved to the top. - const chats = TTYGContextService.getChats(); - chats.updateChatTimestamp(selectedChat.id, chatAnswer.timestamp); - TTYGContextService.updateChats(chats); + // just process the messages + updateChatAnswersFirstResponse(selectedChat, chatItem, chatAnswer); } }) .catch((error) => { @@ -567,6 +552,55 @@ function TTYGViewCtrl( }); }; + const onContinueChatRun = (continueData) => { + TTYGService.continueChatRun(continueData) + .then((chatAnswer) => { + const chatId = continueData.chatId; + const selectedChat = TTYGContextService.getSelectedChat(); + // If still the same chat selected + if (selectedChat && selectedChat.id === chatId) { + // just process the additional answers (with a recovered ChatItemModel) + const items = selectedChat.chatHistory.items; + const lastItem = items[items.length - 1]; + updateChatAnswers(selectedChat, lastItem, chatAnswer); + } + }) + .catch((error) => { + // TODO failure event + TTYGContextService.emit(TTYGEventName.ASK_QUESTION_FAILURE); + toastr.error(getError(error, 0, TTYG_ERROR_MSG_LENGTH)); + }); + }; + + const updateChatAnswers = (selectedChat, chatItem, chatAnswer) => { + selectedChat.timestamp = chatAnswer.timestamp; + chatItem.answers = chatItem.answers || []; + chatItem.answers.push(...chatAnswer.messages); + TTYGContextService.updateSelectedChat(selectedChat); + + if (chatAnswer.continueRunId) { + TTYGContextService.emit(TTYGEventName.CONTINUE_CHAT_RUN, + new ContinueChatRun(chatItem, chatAnswer.continueRunId)); + } else { + // Last message - update the timestamp and the name of the chat in the chat list + const chats = TTYGContextService.getChats(); + // Updating the timestamp in the list gets the chat moved to the top. + chats.updateChatTimestamp(selectedChat.id, chatAnswer.timestamp); + // Strictly speaking the chat name update is needed only for new chats, + // but it doesn't hurt for every chat. + chats.updateChatName(selectedChat.id, chatAnswer.chatName); + // and also in the chat - doesn't seem to matter atm + selectedChat.name = chatAnswer.name; + TTYGContextService.updateChats(chats); + TTYGContextService.emit(TTYGEventName.LAST_MESSAGE_RECEIVED, selectedChat); + } + }; + + const updateChatAnswersFirstResponse = (selectedChat, chatItem, chatAnswer) => { + selectedChat.chatHistory.appendItem(chatItem); + updateChatAnswers(selectedChat, chatItem, chatAnswer); + }; + /** * Handles the renaming of a chat by calling the service and updating the chats list. * Events are fired for success and failure cases. @@ -716,7 +750,7 @@ function TTYGViewCtrl( TTYGContextService.emit(TTYGEventName.LOAD_CHAT_FAILURE, selectedChat); } }); - } else { + } else if (selectedChat) { TTYGContextService.updateSelectedChat(selectedChat); } }; @@ -899,6 +933,7 @@ function TTYGViewCtrl( subscriptions.push(TTYGContextService.subscribe(TTYGEventName.DELETE_CHAT, onDeleteChat)); subscriptions.push(TTYGContextService.subscribe(TTYGEventName.CHAT_EXPORT, onExportChat)); subscriptions.push(TTYGContextService.subscribe(TTYGEventName.ASK_QUESTION, onAskQuestion)); + subscriptions.push(TTYGContextService.subscribe(TTYGEventName.CONTINUE_CHAT_RUN, onContinueChatRun)); subscriptions.push(TTYGContextService.subscribe(TTYGEventName.LOAD_CHATS, loadChats)); subscriptions.push(TTYGContextService.subscribe(TTYGEventName.OPEN_AGENT_SETTINGS, $scope.onOpenNewAgentSettings)); subscriptions.push(TTYGContextService.subscribe(TTYGEventName.EDIT_AGENT, $scope.onOpenAgentSettings)); diff --git a/src/js/angular/ttyg/directives/chat-panel.directive.js b/src/js/angular/ttyg/directives/chat-panel.directive.js index 36f3e8e52..b76485018 100644 --- a/src/js/angular/ttyg/directives/chat-panel.directive.js +++ b/src/js/angular/ttyg/directives/chat-panel.directive.js @@ -4,6 +4,7 @@ import {CHAT_MESSAGE_ROLE, ChatMessageModel} from "../../models/ttyg/chat-messag import {ChatItemModel} from "../../models/ttyg/chat-item"; import {cloneDeep} from "lodash"; import {decodeHTML} from "../../../../app"; +import {ChatModel} from "../../models/ttyg/chats"; const modules = [ 'graphdb.framework.ttyg.directives.chat-item-detail' @@ -59,11 +60,17 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { */ $scope.askingChatItem = undefined; + /** + * True while a question is being handled. It may involve multiple requests until it turns back to false. + * @type {boolean} + */ + $scope.waitingForLastMessage = false; + /** * Flag that indicates that the chat is about to be changed. * @type {boolean} */ - $scope.loadingChat = false; + $scope.loadingChat = true; // ========================= // Private variables @@ -77,6 +84,7 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { * Handles the ask question action. */ $scope.ask = () => { + $scope.chatItem.question.timestamp = Date.now(); $scope.askingChatItem = cloneDeep($scope.chatItem); if (!$scope.chatItem.chatId) { createNewChat(); @@ -136,10 +144,12 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { // ========================= const createNewChat = () => { + $scope.waitingForLastMessage = true; TTYGContextService.emit(TTYGEventName.CREATE_CHAT, $scope.chatItem); }; const askQuestion = (chatItem) => { + $scope.waitingForLastMessage = true; TTYGContextService.emit(TTYGEventName.ASK_QUESTION, chatItem); }; @@ -165,6 +175,10 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { focusQuestionInput(); }; + const onLastMessageReceived = () => { + $scope.waitingForLastMessage = false; + }; + /** * Handles the failure of loading the chat and the server returns 404. This might happen if the chat does * not exist anymore because it was deleted by another user for example. @@ -176,15 +190,20 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { }; const onSelectedChatChanged = (chat) => { - // Skip the loading indication if it is a new (dummy) chat that has not been created yet. - $scope.loadingChat = chat && chat.id; - $scope.chatItem = getEmptyChatItem(); - focusQuestionInput(); + if (chat) { + // Skip the loading indication if it is a new (dummy) chat that has not been created yet. + $scope.loadingChat = chat && chat.id; + $scope.chatItem = getEmptyChatItem(); + focusQuestionInput(); + } else { + reset(); + } }; const onQuestionFailure = () => { $scope.chatItem = cloneDeep($scope.askingChatItem); $scope.askingChatItem = undefined; + $scope.waitingForLastMessage = false; }; /** @@ -220,7 +239,10 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { }; const focusQuestionInput = () => { - element.find('.question-input')[0].focus(); + const els = element.find('.question-input'); + if (els.length) { + els[0].focus(); + } }; const scrollToBottom = () => { @@ -234,14 +256,16 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { }; const reset = () => { - $scope.chat = undefined; + $scope.chat = ChatModel.getEmptyChat(); $scope.loadingChat = false; $scope.chatItem = getEmptyChatItem(); $scope.askingChatItem = undefined; + $scope.waitingForLastMessage = false; focusQuestionInput(); }; const init = () => { + $scope.chat = ChatModel.getEmptyChat(); $scope.chatItem = getEmptyChatItem(); focusQuestionInput(); }; @@ -257,6 +281,7 @@ function ChatPanelComponent(toastr, $translate, TTYGContextService) { subscriptions.push($scope.$watchCollection('chat.chatHistory.items', scrollToBottom)); subscriptions.push(TTYGContextService.onSelectedChatUpdated(onSelectedChatUpdated)); + subscriptions.push(TTYGContextService.onLastMessageReceived(onLastMessageReceived)); subscriptions.push(TTYGContextService.onSelectedAgentChanged(onSelectedAgentChanged)); subscriptions.push(TTYGContextService.onSelectedChatChanged(onSelectedChatChanged)); subscriptions.push(TTYGContextService.subscribe(TTYGEventName.LOAD_CHAT_FAILURE, onLoadChatFailure)); diff --git a/src/js/angular/ttyg/services/chat-message.mapper.js b/src/js/angular/ttyg/services/chat-message.mapper.js index 193fb77b4..86caafb10 100644 --- a/src/js/angular/ttyg/services/chat-message.mapper.js +++ b/src/js/angular/ttyg/services/chat-message.mapper.js @@ -53,13 +53,16 @@ export const chatItemsModelMapper = (data = []) => { // i.e. it doesn't group user + assistant messages together. // In essence, there may be multiple consecutive user messages as well as // multiple consecutive assistant messages. + const chatId = message.conversationId; if (CHAT_MESSAGE_ROLE.USER === message.role) { if (currentItem) { items.push(currentItem); } - const chatId = message.conversationId; currentItem = new ChatItemModel(chatId, chatMessageModelMapper(message)); } else { + if (!currentItem) { + currentItem = new ChatItemModel(chatId, null); + } currentItem.answers.push(chatMessageModelMapper(message)); currentItem.agentId = message.agentId; } @@ -101,6 +104,7 @@ export const chatAnswerModelMapper = (data) => { chatId: data.id, chatName: data.name, timestamp: data.timestamp, - messages: chatMessageModelListMapper(data.messages) + messages: chatMessageModelListMapper(data.messages), + continueRunId: data.continueRunId }); }; diff --git a/src/js/angular/ttyg/services/ttyg-context.service.js b/src/js/angular/ttyg/services/ttyg-context.service.js index 890ef4411..56359288c 100644 --- a/src/js/angular/ttyg/services/ttyg-context.service.js +++ b/src/js/angular/ttyg/services/ttyg-context.service.js @@ -153,6 +153,11 @@ function TTYGContextService(EventEmitterService) { } }; + const deselectChat = () => { + _selectedChat = undefined; + emit(TTYGEventName.SELECT_CHAT, getSelectedChat()); + }; + /** * Subscribes to the 'selectChat' event. * @param {function} callback - The callback to be called when the event is fired. @@ -160,7 +165,7 @@ function TTYGContextService(EventEmitterService) { * @return {function} unsubscribe function. */ const onSelectedChatChanged = (callback) => { - if (_selectedChat && angular.isFunction(callback)) { + if (angular.isFunction(callback)) { callback(getSelectedChat()); } return subscribe(TTYGEventName.SELECT_CHAT, (selectedChat) => callback(selectedChat)); @@ -195,6 +200,18 @@ function TTYGContextService(EventEmitterService) { return subscribe(TTYGEventName.SELECTED_CHAT_UPDATED, (selectedChat) => callback(selectedChat)); }; + /** Subscribes to the 'lastMessageReceived' event. + * @param {function} callback - The callback to be called when the event is fired. + * + * @return {function} unsubscribe function. + */ + const onLastMessageReceived = (callback) => { + if (_selectedChat && angular.isFunction(callback)) { + callback(getSelectedChat()); + } + return subscribe(TTYGEventName.LAST_MESSAGE_RECEIVED, (selectedChat) => callback(selectedChat)); + }; + /** * @param {AgentListModel} agents */ @@ -354,10 +371,12 @@ function TTYGContextService(EventEmitterService) { onChatsListChanged, getSelectedChat, selectChat, + deselectChat, deleteChat, onSelectedChatChanged, updateSelectedChat, onSelectedChatUpdated, + onLastMessageReceived, // agents updateAgents, onAgentsListChanged, @@ -410,6 +429,11 @@ export const TTYGEventName = { */ SELECTED_CHAT_UPDATED: 'selectChatUpdated', + /** + * Emitting the "lastMessageReceived" event when the final answer message has been received. + */ + LAST_MESSAGE_RECEIVED: 'lastMessageReceived', + /** * This event will be emitted when the chat delete request is in progress. The payload will contain the chat ID * and a boolean indicating if the deletion is in progress. @@ -450,6 +474,11 @@ export const TTYGEventName = { */ ASK_QUESTION_FAILURE: 'askQuestionFailure', + /** + * Emitting the "continueChatRun" event triggers a request to the backend to retrieve more remaining answers from the same chat run. + */ + CONTINUE_CHAT_RUN: 'continueChatRun', + /** * Emitting the "loadChats" event will trigger an action to loads all chats from backend server. */ diff --git a/src/js/angular/ttyg/templates/chat-panel.html b/src/js/angular/ttyg/templates/chat-panel.html index 55d522b79..67412d277 100644 --- a/src/js/angular/ttyg/templates/chat-panel.html +++ b/src/js/angular/ttyg/templates/chat-panel.html @@ -20,11 +20,12 @@ + asking="!askingChatItem && waitingForLastMessage && $last" + disabled="waitingForLastMessage"> @@ -36,19 +37,19 @@ -
+
-
+
diff --git a/src/js/angular/ttyg/templates/ttyg.html b/src/js/angular/ttyg/templates/ttyg.html index 6ac33cd58..9e79f4ae1 100644 --- a/src/js/angular/ttyg/templates/ttyg.html +++ b/src/js/angular/ttyg/templates/ttyg.html @@ -52,8 +52,10 @@

ps-size="80vw">