+ basic ai chatting.

This commit is contained in:
liwenrui
2023-11-01 14:14:26 +08:00
parent 75819b9f01
commit d8134f3cb0
16 changed files with 237 additions and 0 deletions
+37
View File
@@ -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();
}
}
+35
View File
@@ -0,0 +1,35 @@
body {background-color: #fff; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" style="opacity: 0.05; filter: grayscale(0.05);"><text x="0em" y="1em" font-size="60">🐚</text><text x="1.25em" y="2em" font-size="60">🐚</text></svg>'); 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;}
+82
View File
@@ -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("<div class='message-container-user'><div class='message-content'>" + $('textarea[name=message]').val() + '</div></div>');
/* 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();
});
+7
View File
@@ -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, <a id="retry" class="text-blue">click here to retry</a>.';
$lang->ai->nextStep = 'Next';
$lang->ai->goTesting = 'Go testing';
+7
View File
@@ -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, <a id="retry" class="text-blue">click here to retry</a>.';
$lang->ai->nextStep = 'Next';
$lang->ai->goTesting = 'Go testing';
+7
View File
@@ -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, <a id="retry" class="text-blue">click here to retry</a>.';
$lang->ai->nextStep = 'Next';
$lang->ai->goTesting = 'Go testing';
+7
View File
@@ -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 = '会话发生了错误,<a id="retry" class="text-blue">点击这里重试</a>。';
$lang->ai->nextStep = '下一步';
$lang->ai->goTesting = '去调试';
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* Chat view of ai module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Wenrui LI <liwenrui@easycorp.ltd>
* @package ai
* @link https://www.zentao.net
*/
?>
<?php include '../../common/view/header.lite.html.php';?>
<div class="chat">
<div class="messages">
<?php
if(!empty($error))
{
echo "<div class='message-container-error'><div class='message-content'>$error</div></div>";
}
$shownMessages = array_reverse(array_filter($messages, function($message) { return $message->role != 'system'; }));
if(!empty($shownMessages))
{
foreach($shownMessages as $message)
{
if($message->role != 'system') echo "<div class='message-container-$message->role'><div class='message-content'>" . htmlspecialchars($message->content) . '</div></div>';
}
}
else
{
echo "<div class='message-container-assistant'><div class='message-content'>{$lang->ai->chatPlaceholderMessage}</div></div>";
}
?>
</div>
<form method="post">
<div class="chat-input">
<textarea name="message" placeholder="<?php echo $lang->ai->chatPlaceholderInput; ?>" autocomplete="off"></textarea>
<input type="submit" value="<?php echo $lang->ai->chatSend; ?>" />
</div>
<input type="hidden" name="history" value="<?php echo htmlspecialchars(json_encode($messages)); ?>" />
</form>
</div>
<?php include '../../common/view/footer.lite.html.php';?>
+1
View File
@@ -13,6 +13,7 @@
<?php
$currentVendor = empty($modelConfig->vendor) ? key($lang->ai->models->vendorList->{empty($modelConfig->type) ? key($lang->ai->models->typeList) : $modelConfig->type}) : $modelConfig->vendor;
$requiredFields = $config->ai->vendorList[$currentVendor]['requiredFields'];
if(empty($requiredFields)) $requiredFields = array();
js::set('vendorList', $config->ai->vendorList);
js::set('vendorListLang', $lang->ai->models->vendorList);
?>
+1
View File
@@ -13,6 +13,7 @@
<?php
$currentVendor = empty($modelConfig->vendor) ? key($lang->ai->models->vendorList->{empty($modelConfig->type) ? key($lang->ai->models->typeList) : $modelConfig->type}) : $modelConfig->vendor;
$requiredFields = $config->ai->vendorList[$currentVendor]['requiredFields'];
if(empty($requiredFields)) $requiredFields = array();
?>
<div id='mainContent' class='main-content'>
<table class='table table-form mw-600px'>
+1
View File
@@ -554,6 +554,7 @@ $lang->group->package->deleteRoadmap = 'Delete Roadmap';
$lang->group->package->deleteDemandPool = 'Delete Demand Pool';
$lang->group->package->exportDatatable = 'Export Datatable';
$lang->group->package->ai = 'AI';
$lang->group->package->aiChatting = 'AI Chat';
$lang->group->package->executePrompt = 'Execute Prompts';
$lang->group->package->manageLLM = 'Manage Models';
$lang->group->package->browsePrompt = 'Browse Prompts';
+1
View File
@@ -558,6 +558,7 @@ $lang->group->package->deleteRoadmap = 'Delete Roadmap';
$lang->group->package->deleteDemandPool = 'Delete Demand Pool';
$lang->group->package->exportDatatable = 'Export Datatable';
$lang->group->package->ai = 'AI';
$lang->group->package->aiChatting = 'AI Chat';
$lang->group->package->executePrompt = 'Execute Prompts';
$lang->group->package->manageLLM = 'Manage Models';
$lang->group->package->browsePrompt = 'Browse Prompts';
+1
View File
@@ -554,6 +554,7 @@ $lang->group->package->deleteRoadmap = 'Delete Roadmap';
$lang->group->package->deleteDemandPool = 'Delete Demand Pool';
$lang->group->package->exportDatatable = 'Export Datatable';
$lang->group->package->ai = 'AI';
$lang->group->package->aiChatting = 'AI Chat';
$lang->group->package->executePrompt = 'Execute Prompts';
$lang->group->package->manageLLM = 'Manage Models';
$lang->group->package->browsePrompt = 'Browse Prompts';
+1
View File
@@ -1860,6 +1860,7 @@ $lang->resource->ai->prompts = 'promptBrowse';
$lang->resource->ai->promptView = 'promptView';
$lang->resource->ai->promptExecute = 'promptExecute';
$lang->resource->ai->promptExecutionReset = 'promptExecutionReset';
$lang->resource->ai->chat = 'chat';
$lang->api->methodOrder[0] = 'index';
$lang->api->methodOrder[5] = 'createLib';
+1
View File
@@ -558,6 +558,7 @@ $lang->group->package->deleteRoadmap = '删除产品路标';
$lang->group->package->deleteDemandPool = '删除需求池';
$lang->group->package->exportDatatable = '导出数据表';
$lang->group->package->ai = 'AI';
$lang->group->package->aiChatting = 'AI 聊天';
$lang->group->package->executePrompt = '执行提词';
$lang->group->package->manageLLM = '语言模型管理';
$lang->group->package->browsePrompt = '浏览提词';
+6
View File
@@ -3774,6 +3774,12 @@ $config->group->package->exportDatatable->subset = 'dataview';
$config->group->package->exportDatatable->privs = array();
$config->group->package->exportDatatable->privs['dataview-export'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd', 'order' => 30, 'depend' => array('dataview-browse', 'dataview-query'), 'recommend' => array());
$config->group->package->aiChatting = new stdclass();
$config->group->package->aiChatting->order = 2020;
$config->group->package->aiChatting->subset = 'ai';
$config->group->package->aiChatting->privs = array();
$config->group->package->aiChatting->privs['ai-chat'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 10, 'depend' => array(), 'recommend' => array());
$config->group->package->executePrompt = new stdclass();
$config->group->package->executePrompt->order = 2040;
$config->group->package->executePrompt->subset = 'ai';