diff --git a/module/ai/control.php b/module/ai/control.php index 8fccd5a9f2..859a72b9ff 100644 --- a/module/ai/control.php +++ b/module/ai/control.php @@ -694,4 +694,41 @@ class ai extends control $this->view->roleTemplates = $this->ai->getRoleTemplates(); $this->display(); } + + /** + * Chat with LLMs. + * + * @access public + * @return void + */ + public function chat() + { + $messages = array(); + + if(!empty($_POST)) + { + $history = $this->post->history; + $message = $this->post->message; + $isRetry = $this->post->retry == 'true'; + + $messages = json_decode($history); + if(empty($messages)) $messages[] = (object)array('role' => 'system', 'content' => $this->lang->ai->chatSystemMessage); + + if(!$isRetry) $messages[] = (object)array('role' => 'user', 'content' => $message); + + $response = $this->ai->converse($messages); + if(empty($response)) + { + $this->view->error = $this->lang->ai->chatNoResponse; + } + else + { + $messages[] = (object)array('role' => 'assistant', 'content' => is_array($response) ? current($response) : $response); + } + } + + $this->view->title = $this->lang->ai->chat; + $this->view->messages = $messages; + $this->display(); + } } diff --git a/module/ai/css/chat.css b/module/ai/css/chat.css new file mode 100644 index 0000000000..01d39d3dc2 --- /dev/null +++ b/module/ai/css/chat.css @@ -0,0 +1,35 @@ +body {background-color: #fff; background-image: url('data:image/svg+xml;utf8,'); background-size: 140px 140px; background-repeat: repeat;} + +.chat {width: calc(100% - 8px); height: calc(100vh - 8px); display: flex; flex-direction: column; margin: 4px;} +.messages {height: 100%; padding: 8px; margin-bottom: 48px; display: flex; flex-direction: column-reverse; overflow: auto;} + +.message-container-user, .message-container-assistant, .message-container-error {display: flex; margin: 8px 0;} +.message-container-user {justify-content: flex-end;} +.message-container-assistant {justify-content: flex-start;} +.message-container-error {justify-content: center;} +.message-content {white-space: pre-wrap;} +.message-container-user .message-content {background: #d5ecff; padding: 8px; border-radius: 8px 8px 0 8px;} +.message-container-assistant .message-content {background: #f1f1f1; padding: 8px; border-radius: 8px 8px 8px 0;} +.message-container-error .message-content {background: #ffdddd; padding: 2px 8px; border-radius: 8px;} + +.chat-input {display: flex; justify-content: space-between; position: fixed; bottom: 0; width: calc(100% - 24px); margin: 8px;} +.chat-input textarea {flex: 1; margin-right: 4px; padding: 4px; border: 1px solid #ccc; border-radius: 4px; outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; resize: none;} +.chat-input input[type=submit] {flex: 0; padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; color: #333; background: #fff; cursor: pointer; background: -webkit-linear-gradient(45deg, #1183fb 10%, #0a48d1 20%, #a828f0 50%); background-size: 200% 200%; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent;} +.chat-input textarea.busy, .chat-input input[type=submit].busy {cursor: wait; opacity: 0.5;} + +@keyframes gradient +{ + 0% {background-position: 0% 0%;} + 25% {background-position: 100% 50%;} + 50% {background-position: 0% 100%;} + 75% {background-position: -100% 50%;} + 100% {background-position: 0% 0%;} +} +.chat-input input[type=submit]:hover {animation: gradient 3s ease infinite;} +#retry.busy {cursor: wait; opacity: 0.5;} + +::-webkit-scrollbar-thumb {border-radius: 5px;} +::-webkit-scrollbar {width: 10px; height: 10px;} +::-webkit-scrollbar-thumb:vertical {box-shadow: inset 1px 1px 0 rgb(0 0 0 / 10%), inset 0 -1px 0 rgb(0 0 0 / 7%); background-color: rgba(0, 0, 0, 0.2); border-radius: 10px; opacity: 0; transition: opacity 0.1s;} +::-webkit-scrollbar, ::-webkit-scrollbar-button, ::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb {visibility: hidden;opacity: 0;} +:hover ::-webkit-scrollbar, :hover ::-webkit-scrollbar-button, :hover ::-webkit-scrollbar-track, :hover ::-webkit-scrollbar-thumb {visibility: visible;opacity: 1;} diff --git a/module/ai/js/chat.js b/module/ai/js/chat.js new file mode 100644 index 0000000000..ffd3ad983e --- /dev/null +++ b/module/ai/js/chat.js @@ -0,0 +1,82 @@ +$(function() +{ + function markBusy() + { + $('input[type=submit]').attr('readonly', true).addClass('busy'); + $('textarea[name=message]').attr('readonly', true).addClass('busy'); + $('#retry').addClass('busy'); + } + + function reset() + { + markBusy(); + $.ajax({ + url: createLink('ai', 'chat'), + dataType: 'html', + success: function(response) + { + $('.chat').html($($.parseHTML(response)).filter('.chat').html()); + init(); + } + }); + } + + function retry() + { + markBusy(); + $.ajax({ + url: createLink('ai', 'chat'), + type: 'POST', + dataType: 'html', + data: {retry: 'true', history: $('input[name=history]').val()}, + success: function(response) + { + $('.chat').html($($.parseHTML(response)).filter('.chat').html()); + init(); + } + }); + } + + function init() + { + $('textarea[name=message]').keydown(function(e) + { + /* Send message on enter, allowing linebreak inputs. */ + if((e.keyCode == 13 || e.keyCode == 10)) + { + if(e.ctrlKey || e.metaKey) + { + $('textarea[name=message]').val($('textarea[name=message]').val() + '\n'); + } + else + { + $('form').submit(); + } + } + }); + $('form').on('submit', function(e) + { + /* Disable form and add message. */ + markBusy(); + $('.messages').prepend("
'); + + /* Send message to server and re-render the chat. */ + e.preventDefault(); + $.ajax({ + url: createLink('ai', 'chat'), + type: 'POST', + data: {message: $('textarea[name=message]').val(), history: $('input[name=history]').val()}, + dataType: 'html', + success: function(response) + { + $('.chat').html($($.parseHTML(response)).filter('.chat').html()); + init(); + } + }); + }); + $('#reset').click(reset); + $('#retry').click(retry); + $('textarea[name=message]').focus(); + } + init(); +}); diff --git a/module/ai/lang/de.php b/module/ai/lang/de.php index 79e258181d..b9c1b2b259 100644 --- a/module/ai/lang/de.php +++ b/module/ai/lang/de.php @@ -49,6 +49,13 @@ $lang->ai->promptView = 'View Prompt'; $lang->ai->promptExecute = 'Execute Prompt'; $lang->ai->promptExecutionReset = 'Reset Execute Prompt'; $lang->ai->roleTemplates = 'Manage Role Templates'; +$lang->ai->chat = 'Chat'; + +$lang->ai->chatPlaceholderMessage = 'Hi, I\'m Adao.'; +$lang->ai->chatPlaceholderInput = 'type here...'; +$lang->ai->chatSystemMessage = 'You\'re Adao, the mascot of ZenTao. You can answer questions and chat with users. You\'re currently within the ZenTao project management software.'; +$lang->ai->chatSend = 'Send'; +$lang->ai->chatNoResponse = 'Something went wrong, click here to retry.'; $lang->ai->nextStep = 'Next'; $lang->ai->goTesting = 'Go testing'; diff --git a/module/ai/lang/en.php b/module/ai/lang/en.php index 79e258181d..b9c1b2b259 100644 --- a/module/ai/lang/en.php +++ b/module/ai/lang/en.php @@ -49,6 +49,13 @@ $lang->ai->promptView = 'View Prompt'; $lang->ai->promptExecute = 'Execute Prompt'; $lang->ai->promptExecutionReset = 'Reset Execute Prompt'; $lang->ai->roleTemplates = 'Manage Role Templates'; +$lang->ai->chat = 'Chat'; + +$lang->ai->chatPlaceholderMessage = 'Hi, I\'m Adao.'; +$lang->ai->chatPlaceholderInput = 'type here...'; +$lang->ai->chatSystemMessage = 'You\'re Adao, the mascot of ZenTao. You can answer questions and chat with users. You\'re currently within the ZenTao project management software.'; +$lang->ai->chatSend = 'Send'; +$lang->ai->chatNoResponse = 'Something went wrong, click here to retry.'; $lang->ai->nextStep = 'Next'; $lang->ai->goTesting = 'Go testing'; diff --git a/module/ai/lang/fr.php b/module/ai/lang/fr.php index 79e258181d..b9c1b2b259 100644 --- a/module/ai/lang/fr.php +++ b/module/ai/lang/fr.php @@ -49,6 +49,13 @@ $lang->ai->promptView = 'View Prompt'; $lang->ai->promptExecute = 'Execute Prompt'; $lang->ai->promptExecutionReset = 'Reset Execute Prompt'; $lang->ai->roleTemplates = 'Manage Role Templates'; +$lang->ai->chat = 'Chat'; + +$lang->ai->chatPlaceholderMessage = 'Hi, I\'m Adao.'; +$lang->ai->chatPlaceholderInput = 'type here...'; +$lang->ai->chatSystemMessage = 'You\'re Adao, the mascot of ZenTao. You can answer questions and chat with users. You\'re currently within the ZenTao project management software.'; +$lang->ai->chatSend = 'Send'; +$lang->ai->chatNoResponse = 'Something went wrong, click here to retry.'; $lang->ai->nextStep = 'Next'; $lang->ai->goTesting = 'Go testing'; diff --git a/module/ai/lang/zh-cn.php b/module/ai/lang/zh-cn.php index b7eefa07e5..85ea797014 100644 --- a/module/ai/lang/zh-cn.php +++ b/module/ai/lang/zh-cn.php @@ -49,6 +49,13 @@ $lang->ai->promptView = '查看提词详情'; $lang->ai->promptExecute = '执行提词'; $lang->ai->promptExecutionReset = '重置调试'; $lang->ai->roleTemplates = '管理角色模板'; +$lang->ai->chat = '聊天'; + +$lang->ai->chatPlaceholderMessage = '你好,我是阿道。'; +$lang->ai->chatPlaceholderInput = '在此输入…'; +$lang->ai->chatSystemMessage = '你叫阿道,是禅道的吉祥物,你可以回答用户的问题和与用户聊天。你当前所处的环境是禅道项目管理软件。'; +$lang->ai->chatSend = '发送'; +$lang->ai->chatNoResponse = '会话发生了错误,点击这里重试。'; $lang->ai->nextStep = '下一步'; $lang->ai->goTesting = '去调试'; diff --git a/module/ai/view/chat.html.php b/module/ai/view/chat.html.php new file mode 100644 index 0000000000..bf64dcaf21 --- /dev/null +++ b/module/ai/view/chat.html.php @@ -0,0 +1,42 @@ + + * @package ai + * @link https://www.zentao.net + */ +?> + +