+ basic ai chatting.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = '去调试';
|
||||
|
||||
@@ -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';?>
|
||||
@@ -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);
|
||||
?>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = '浏览提词';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user