Merge branch 'ai-next/chat' into 18.x

This commit is contained in:
liwenrui
2023-11-08 14:44:47 +08:00
27 changed files with 482 additions and 30 deletions
+4 -4
View File
@@ -15,8 +15,8 @@ $config->ai->openai->api->openai->version = 'v1'; /
$config->ai->openai->api->openai->format = 'https://api.openai.com/%s/%s'; // OpenAI API format, args: API version, API name.
$config->ai->openai->api->openai->authFormat = 'Authorization: Bearer %s'; // OpenAI API auth header format.
$config->ai->openai->api->azure = new stdclass();
$config->ai->openai->api->azure->resource = 'openai-at-zentao'; // your Azure OpenAI resource name, required.
$config->ai->openai->api->azure->deployment = 'the-first-gpt'; // your Azure OpenAI deployment name, required.
$config->ai->openai->api->azure->resource = ''; // Azure OpenAI resource name, required.
$config->ai->openai->api->azure->deployment = ''; // Azure OpenAI deployment name, required.
$config->ai->openai->api->azure->apiVersion = '2023-07-01-preview'; // Azure OpenAI API version, required.
$config->ai->openai->api->azure->format = 'https://%s.openai.azure.com/openai/deployments/%s/%s?api-version=%s'; // Azure API format, args: resource name, deployment name, API name, API version.
$config->ai->openai->api->azure->authFormat = 'api-key: %s'; // Azure API auth header format.
@@ -57,9 +57,9 @@ $config->ai->ernie->params = new stdclass();
$config->ai->ernie->params->chat = new stdclass();
$config->ai->ernie->params->function = new stdclass();
$config->ai->ernie->params->chat->required = array('messages');
$config->ai->ernie->params->chat->optional = array('temperature', 'top_p', 'penalty_score', 'stream', 'user_id');
$config->ai->ernie->params->chat->optional = array('temperature', 'top_p', 'penalty_score', 'stream', 'system', 'user_id');
$config->ai->ernie->params->function->required = array('messages', 'functions');
$config->ai->ernie->params->function->optional = array('temperature', 'top_p', 'penalty_score', 'stream', 'user_id');
$config->ai->ernie->params->function->optional = array('temperature', 'top_p', 'penalty_score', 'stream', 'system', 'user_id');
$config->ai->ernie->model = new stdclass();
$config->ai->ernie->model->chat = 'ernie-bot-turbo';
+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();
}
}
+33
View File
@@ -0,0 +1,33 @@
body {background-color: #fff; background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='164' height='164' style='opacity: 0.15; filter: grayscale(0.05);'%3e%3cdefs%3e%3clinearGradient id='b' x1='100%25' x2='25%25' y1='100%25' y2='25%25'%3e%3cstop offset='0%25' stop-color='%23FF2070'/%3e%3cstop offset='50%25' stop-color='%239670FF'/%3e%3cstop offset='100%25' stop-color='%23258DFF' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='c' x1='0%25' x2='75%25' y1='0%25' y2='75%25'%3e%3cstop offset='0%25' stop-color='%23258DFF'/%3e%3cstop offset='50%25' stop-color='%239670FF'/%3e%3cstop offset='100%25' stop-color='%23FF2070' stop-opacity='0'/%3e%3c/linearGradient%3e%3cpath id='a' fill='none' fill-rule='evenodd' stroke-linejoin='round' d='M41 .5c11.178 0 21.303 4.536 28.633 11.867C76.964 19.697 81.5 29.822 81.5 41s-4.536 21.303-11.867 28.633C62.303 76.964 52.178 81.5 41 81.5c-11.184 0-21.314-4.528-28.646-11.86C5.031 62.317.5 52.197.5 41c0-11.178 4.536-21.303 11.867-28.633C19.697 5.036 29.822.5 41 .5ZM30.628 10.897c-7.712 0-13.615 2.316-17.681 6.995-3.396 3.906-5.508 9.44-6.359 16.585 1.426-4.143 3.089-7.394 4.98-9.76 2.603-3.256 5.642-4.859 9.098-4.859 1.857 0 3.141.58 4.002 1.44 1.052 1.052 1.48 2.55 1.48 4.04 0 2.068-1.527 3.379-3.514 4.952-2.81 2.223-6.448 5.088-6.448 12.507 0 2.572.7 4.106 1.347 5.532.61 1.347 1.169 2.605 1.169 4.43 0 1.248-.605 2.502-1.62 3.421a5.443 5.443 0 0 1-3.66 1.407c-1.598 0-3.422-.682-5.191-2.496 1.758 4.658 5.443 8.964 10.246 12.229 5.475 3.722 12.408 6.094 19.648 6.094 7.125 0 13.102-2.01 17.364-5.21 4.462-3.348 7.04-8.001 7.04-12.98 0-4.66-1.687-8.478-4.387-11.177-2.892-2.892-6.938-4.498-11.288-4.498-4.077 0-7.316 1.11-9.57 2.95-2.374 1.938-3.64 4.685-3.64 7.744 0 2.25.686 3.991 1.604 5.281 1.009 1.418 2.3 2.307 3.216 2.704-1.299-3.083-1.11-5.497-.13-7.262.658-1.186 1.682-2.09 2.903-2.706 1.296-.654 2.814-.982 4.333-.982 1.997 0 3.274.898 4.095 2.132 1.206 1.816 1.377 4.418 1.386 5.862 0 .832-.527 4.904-3.333 7.71-1.568 1.57-3.84 2.754-7.129 2.754-6.08 0-10.244-1.92-13.025-4.702-3.67-3.67-4.933-8.87-4.933-13.256 0-3.992 1.74-8.001 4.67-10.933 2.719-2.718 6.457-4.51 10.772-4.51 6.504 0 11.005 2.325 15.212 4.599l.744.402c3.579 1.935 7.012 3.729 11.464 3.729 2.687 0 4.747-.579 6.292-1.576 1.728-1.116 2.8-2.753 3.397-4.64 1.233-3.893.486-8.835-.823-12.676.27 2.515.067 4.706-.515 6.452-.496 1.488-1.267 2.661-2.252 3.464-.991.807-2.2 1.247-3.583 1.247-6.809 0-10.378-4.46-14.326-9.507-4.744-6.063-10.084-12.932-23.055-12.932Z'/%3e%3c/defs%3e%3cuse x='82' y='82' stroke='url(%23b)' href='%23a'/%3e%3cuse stroke='url(%23c)' href='%23a'/%3e%3c/svg%3e"); background-position: -20px -20px; 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 8px 0 8px; 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; align-items: flex-end; bottom: 0; width: calc(100% - 16px); margin: 8px;}
.chat-input textarea {flex: 1; margin-right: 4px; line-height: 1.5; height: 40px; min-height: 40px; padding: 9px 36px 9px 12px; border: 1px solid transparent; border-radius: 8px; background: linear-gradient(#fff, #fff) padding-box, linear-gradient(225deg, #ff2070, #9670ff, #258dff) border-box; outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; resize: none;}
.chat-input input[type=submit] {position: absolute; left: 236px; bottom: 12px; width: 40px; height: 40px; padding: 0; border: none; border-radius: 4px; outline: none; background: linear-gradient(225deg, #ff2070, #9670ff, #258dff); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><path fill="black" d="M3.788 6.995H8.5c.301 0 .502.201.502.502 0 .3-.2.501-.502.501H3.788c0 .1-.1.201-.1.402L.98 14.016l14.04-6.52L.98.979l2.707 5.616c.1.2.1.301.1.401ZM15.422 8.4 1.38 14.919c-.502.2-1.104 0-1.304-.502-.1-.3-.1-.602 0-.902l2.708-5.617c.1-.3.1-.602 0-.903L.077 1.38C-.123.878.077.276.578.075c.2-.1.602-.1.803 0l14.04 6.52c.502.2.703.802.502 1.303-.1.2-.3.401-.501.502Z"/></svg>'); -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><path fill="black" d="M3.788 6.995H8.5c.301 0 .502.201.502.502 0 .3-.2.501-.502.501H3.788c0 .1-.1.201-.1.402L.98 14.016l14.04-6.52L.98.979l2.707 5.616c.1.2.1.301.1.401ZM15.422 8.4 1.38 14.919c-.502.2-1.104 0-1.304-.502-.1-.3-.1-.602 0-.902l2.708-5.617c.1-.3.1-.602 0-.903L.077 1.38C-.123.878.077.276.578.075c.2-.1.602-.1.803 0l14.04 6.52c.502.2.703.802.502 1.303-.1.2-.3.401-.501.502Z"/></svg>'); mask-position: center; -webkit-mask-position: center; mask-origin: padding-box; -webkit-mask-origin: padding-box; background-repeat: no-repeat; background-position: center; background-size: 16px 15px; mask-size: 16px 15px; -webkit-mask-size: 16px 15px; cursor: pointer;}
.chat-input input[type=submit].disabled {cursor: unset; opacity: 0.5;}
.chat-input input[type=submit]:not(.disabled):hover {transform: scale(1.1);}
.chat-input textarea.has-scrollbar + input[type=submit] {left: 228px; width: 36px; mask-position: 20px 12.5px; -webkit-mask-position: 20px 12.5px; background-position: 20px 12.5px;}
.chat-input textarea.busy, .chat-input input[type=submit].busy {cursor: wait; opacity: 0.5;}
#reset {height: 40px; border: none; background: none; color: #9ea3b0; border-radius: 4px;}
#reset.disabled {cursor: unset; opacity: 0.5;}
#reset:not(.disabled):hover {color: #737a8c;}
#reset.busy, #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;}
.messages::-webkit-scrollbar-track {margin: -30px 0;}
.chat-input textarea::-webkit-scrollbar {width: 8px;}
+115
View File
@@ -0,0 +1,115 @@
$(function()
{
function markBusy()
{
$('input[type=submit]').attr('readonly', true).addClass('busy');
$('textarea[name=message]').attr('readonly', true).addClass('busy');
$('#retry').addClass('busy');
$('#reset').addClass('busy');
}
function reset(e)
{
if($(e.target).hasClass('busy') || $(e.target).hasClass('disabled')) return;
markBusy();
$.ajax({
url: createLink('ai', 'chat'),
dataType: 'html',
success: function(response)
{
$('.chat').html($($.parseHTML(response)).filter('.chat').html());
init();
}
});
}
function retry(e)
{
if($(e.target).hasClass('busy')) return;
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($(this).hasClass('busy')) return;
if(e.ctrlKey || e.metaKey)
{
$('textarea[name=message]').val($('textarea[name=message]').val() + '\n');
$('textarea[name=message]').trigger('input');
}
else
{
if($('textarea[name=message]').val().trim().length == 0)
{
e.preventDefault();
return;
}
$('form').submit();
}
}
});
$('form').on('submit', function(e)
{
/* Disable form and add message. */
markBusy();
const message = $('textarea[name=message]').val().trim();
$('.messages').prepend("<div class='message-container-user'><div class='message-content'>" + message + '</div></div>');
/* Send message to server and re-render the chat. */
e.preventDefault();
$.ajax({
url: createLink('ai', 'chat'),
type: 'POST',
data: {message, 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();
$('textarea[name=message]').on('input', function()
{
/* Toggle send button. */
const hasMessage = $(this).val().trim().length > 0;
$('input[type=submit]').attr('disabled', !hasMessage).toggleClass('disabled', !hasMessage);
/* Resize textarea to fit content. */
const clientHeight = +$(this).prop('clientHeight');
const scrollHeight = +$(this).prop('scrollHeight');
if(clientHeight < scrollHeight)
{
if(clientHeight < 150)
{
$(this).css('height', (scrollHeight + 2) + 'px');
}
else
{
$(this).addClass('has-scrollbar');
}
}
});
}
init();
});
+8
View File
@@ -49,6 +49,14 @@ $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, your AI assistant at ZenTao!';
$lang->ai->chatPlaceholderInput = 'type here...';
$lang->ai->chatSystemMessage = 'You\'re Adao, the AI assistant and 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->chatReset = 'Reset';
$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';
+8
View File
@@ -49,6 +49,14 @@ $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, your AI assistant at ZenTao!';
$lang->ai->chatPlaceholderInput = 'type here...';
$lang->ai->chatSystemMessage = 'You\'re Adao, the AI assistant and 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->chatReset = 'Reset';
$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';
+8
View File
@@ -49,6 +49,14 @@ $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, your AI assistant at ZenTao!';
$lang->ai->chatPlaceholderInput = 'type here...';
$lang->ai->chatSystemMessage = 'You\'re Adao, the AI assistant and 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->chatReset = 'Reset';
$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';
+8
View File
@@ -49,6 +49,14 @@ $lang->ai->promptView = '查看提词详情';
$lang->ai->promptExecute = '执行提词';
$lang->ai->promptExecutionReset = '重置调试';
$lang->ai->roleTemplates = '管理角色模板';
$lang->ai->chat = '聊天';
$lang->ai->chatPlaceholderMessage = 'Hi,我是 AI 助手阿道,您可以问我任何问题。';
$lang->ai->chatPlaceholderInput = '问问阿道…';
$lang->ai->chatSystemMessage = '你叫阿道,是禅道的 AI 助手兼吉祥物,你可以回答用户的问题和与用户聊天。你当前所处的环境是禅道项目管理软件。';
$lang->ai->chatSend = '发送';
$lang->ai->chatReset = '清空';
$lang->ai->chatNoResponse = '会话发生了错误,<a id="retry" class="text-blue">点击这里重试</a>。';
$lang->ai->nextStep = '下一步';
$lang->ai->goTesting = '去调试';
+11 -2
View File
@@ -65,7 +65,7 @@ class aiModel extends model
$modelConfig = new stdclass();
$storedModelConfig = $this->loadModel('setting')->getItems('owner=system&module=ai');
foreach($storedModelConfig as $item) $modelConfig->{$item->key} = $item->value;
return !empty($modelConfig->key);
return $modelConfig->status == 'on' && !empty($modelConfig->key);
}
/**
@@ -96,7 +96,7 @@ class aiModel extends model
{
if($modelVendor == 'azure')
{
$url = sprintf($this->config->ai->openai->api->azure->format, $this->config->ai->openai->api->azure->resource, $this->config->ai->openai->api->azure->deployment, $this->config->ai->openai->api->methods[$type], $this->config->ai->openai->api->azure->apiVersion);
$url = sprintf($this->config->ai->openai->api->azure->format, $this->modelConfig->resource, $this->modelConfig->deployment, $this->config->ai->openai->api->methods[$type], $this->config->ai->openai->api->azure->apiVersion);
}
else
{
@@ -469,6 +469,15 @@ class aiModel extends model
*/
public function converse($messages, $options = array())
{
/* Filter system message out for ERNIE. */
if($this->config->ai->models[$this->modelConfig->type] == 'ernie')
{
$systemMessage = current(array_filter($messages, function($message) { return $message->role == 'system'; }));
if(!empty($systemMessage)) $options['system'] = $systemMessage->content;
$messages = array_values(array_filter($messages, function($message) { return $message->role == 'assistant' || $message->role == 'user'; }));
}
$data = compact('messages');
if(!empty($options))
+43
View File
@@ -0,0 +1,43 @@
<?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 class="disabled" disabled />
<input type="button" id="reset" <?php if(empty($shownMessages)) echo 'class="disabled"'; ?> value="<?php echo $lang->ai->chatReset; ?>" />
</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
@@ -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
@@ -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
@@ -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
@@ -1862,6 +1862,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
@@ -3775,6 +3775,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';
+8
View File
@@ -15,3 +15,11 @@ $lang->index->upgrade = 'Upgrade';
$lang->index->log = 'Log';
$lang->index->detailed = 'Details';
$lang->index->website = 'Please visit the official website';
$lang->index->chat = new stdclass();
$lang->index->chat->chat = 'Chat';
$lang->index->chat->ai = 'AI';
$lang->index->chat->unconfiguredFormat = '%s is currently not configured. %s.';
$lang->index->chat->goConfigureFormat = 'To configure the function, click to navigate to <a class="text-primary" href="%s">%s configuration</a>';
$lang->index->chat->contactAdminForHelp = 'Please contact an administrator for assistance';
$lang->index->chat->unauthorized = 'You are not authorized to access AI chatting. Please contact an administrator for permission.';
+8
View File
@@ -15,3 +15,11 @@ $lang->index->upgrade = 'Upgrade';
$lang->index->log = 'Log';
$lang->index->detailed = 'Details';
$lang->index->website = 'Visit ZenTao official website';
$lang->index->chat = new stdclass();
$lang->index->chat->chat = 'Chat';
$lang->index->chat->ai = 'AI';
$lang->index->chat->unconfiguredFormat = '%s is currently not configured. %s.';
$lang->index->chat->goConfigureFormat = 'To configure the function, click to navigate to <a class="text-primary" href="%s">%s configuration</a>';
$lang->index->chat->contactAdminForHelp = 'Please contact an administrator for assistance';
$lang->index->chat->unauthorized = 'You are not authorized to access AI chatting. Please contact an administrator for permission.';
+8
View File
@@ -15,3 +15,11 @@ $lang->index->upgrade = 'Upgrade';
$lang->index->log = 'Log';
$lang->index->detailed = 'Details';
$lang->index->website = 'Please visit the official website';
$lang->index->chat = new stdclass();
$lang->index->chat->chat = 'Chat';
$lang->index->chat->ai = 'AI';
$lang->index->chat->unconfiguredFormat = '%s is currently not configured. %s.';
$lang->index->chat->goConfigureFormat = 'To configure the function, click to navigate to <a class="text-primary" href="%s">%s configuration</a>';
$lang->index->chat->contactAdminForHelp = 'Please contact an administrator for assistance';
$lang->index->chat->unauthorized = 'You are not authorized to access AI chatting. Please contact an administrator for permission.';
+8
View File
@@ -15,3 +15,11 @@ $lang->index->upgrade = 'Upgrade';
$lang->index->log = 'Log';
$lang->index->detailed = 'Details';
$lang->index->website = 'Please visit the official website';
$lang->index->chat = new stdclass();
$lang->index->chat->chat = 'Chat';
$lang->index->chat->ai = 'AI';
$lang->index->chat->unconfiguredFormat = '%s is currently not configured. %s.';
$lang->index->chat->goConfigureFormat = 'To configure the function, click to navigate to <a class="text-primary" href="%s">%s configuration</a>';
$lang->index->chat->contactAdminForHelp = 'Please contact an administrator for assistance';
$lang->index->chat->unauthorized = 'You are not authorized to access AI chatting. Please contact an administrator for permission.';
+8
View File
@@ -15,3 +15,11 @@ $lang->index->upgrade = '升级';
$lang->index->log = '查看版本更新日志 >';
$lang->index->detailed = '详情';
$lang->index->website = '请访问官网';
$lang->index->chat = new stdclass();
$lang->index->chat->chat = '聊天';
$lang->index->chat->ai = 'AI 对话';
$lang->index->chat->unconfiguredFormat = '尚未配置 %s 功能,%s。';
$lang->index->chat->goConfigureFormat = '点击前往 <a class="text-primary" href="%s">%s 配置页面</a> 进行功能配置';
$lang->index->chat->contactAdminForHelp = '请联系管理员进行功能配置';
$lang->index->chat->unauthorized = '没有权限访问 AI 聊天功能,请联系管理员进行权限分配。';
+8
View File
@@ -15,3 +15,11 @@ $lang->index->upgrade = '升級';
$lang->index->log = '查看版本更新日誌 >';
$lang->index->detailed = '詳情';
$lang->index->website = '請訪問官網';
$lang->index->chat = new stdclass();
$lang->index->chat->chat = '聊天';
$lang->index->chat->ai = 'AI 對話';
$lang->index->chat->unconfiguredFormat = '尚未配置 %s 功能,%s。';
$lang->index->chat->goConfigureFormat = '點擊前往 <a class="text-primary" href="%s">%s 配置頁面</a> 進行功能配置';
$lang->index->chat->contactAdminForHelp = '請聯繫管理員進行功能配置';
$lang->index->chat->unauthorized = '沒有權限訪問 AI 聊天功能,請聯繫管理員進行權限分配。';
+133
View File
@@ -0,0 +1,133 @@
<?php
/**
* Chat container view of index 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 index
* @link https://www.zentao.net
*/
?>
<?php
$isXuanAvailable = isset($this->config->xuanxuan->turnon) && $this->config->xuanxuan->turnon && $this->loadModel('im')->getXxdStatus() == 'online';
$isAIConfigured = $this->loadModel('ai')->isModelConfigured();
$hasAIChatPriv = commonModel::hasPriv('ai', 'chat');
js::set('isXuanAvailable', $isXuanAvailable);
js::set('isAIConfigured', $isAIConfigured);
js::set('hasAIChatPriv', $hasAIChatPriv);
?>
<style>
#chat-container {position: fixed; left: 96px; right: 0; height: calc(100% - 40px); display: none;}
.menu-hide #chat-container {left: 40px;}
#chat-switch {position: absolute; width: 330px; height: 52px; right: 0; background: #fff; display: flex; justify-content: center; align-items: center; border-bottom: 1px solid #eee; z-index: 20;}
.chat-switch-bg {display: flex; justify-content: center; align-items: center; background-color: #eff5ff; border-radius: 16px;}
.chat-switch-item {width: 96px; padding: 4px 0; border-radius: 16px; text-align: center; color: #838a9c; position: relative; user-select: none;}
.chat-switch-item:hover {color: #838a9c;}
.chat-switch-item.active {font-weight: bold; color: #fff; background-color: #5999fc;}
.chat-switch-item.has-notice::after {content: ''; position: absolute; right: 26px; top: 4px; width: 6px; height: 6px; border-radius: 50%; background-color: #ff535d;}
#xuan-chat-view {position: absolute; width: 100%; height: 100%; z-index: 10; display: none;}
#xuan-chat-view #xx-embed-container {position: absolute; bottom: 0; left: 0; right: 0; top: 0;}
#ai-chat-view {position: fixed; right: 0; width: 330px; bottom: 40px; top: 50px; outline: 1px solid #eee;}
#ai-chat-frame {height: 100%; width: 100%;}
.unconfigured {position: absolute; width: 330px; padding: 20px; right: 0; top: 0; bottom: 0; background: #fff; outline: 1px solid #eee;}
#xuan-chat-view .unconfigured {top: 50px;}
</style>
<div id="chat-container">
<div id="chat-switch">
<div class="chat-switch-bg">
<a class="chat-switch-item" data-value="chat"><?php echo $lang->index->chat->chat; ?></a>
<a class="chat-switch-item active" data-value="ai"><?php echo $lang->index->chat->ai; ?></a>
</div>
</div>
<div id="xuan-chat-view">
<?php if(!$isXuanAvailable): ?>
<div class="unconfigured text-gray"><?php echo sprintf($lang->index->chat->unconfiguredFormat, $lang->index->chat->chat, (common::hasPriv('setting', 'xuanxuan') ? sprintf($lang->index->chat->goConfigureFormat, helper::createLink('setting', 'xuanxuan'), $lang->index->chat->chat) : $lang->index->chat->contactAdminForHelp)); ?></div>
<?php endif; ?>
</div>
<div id="ai-chat-view">
<?php if(!$isAIConfigured): ?>
<div class="unconfigured text-gray"><?php echo sprintf($lang->index->chat->unconfiguredFormat, $lang->index->chat->ai, (common::hasPriv('ai', 'models') ? sprintf($lang->index->chat->goConfigureFormat, helper::createLink('ai', 'models') . '#app=admin', $lang->index->chat->ai) : $lang->index->chat->contactAdminForHelp)); ?></div>
<?php elseif(!$hasAIChatPriv): ?>
<div class="unconfigured text-gray"><?php echo $lang->index->chat->unauthorized; ?></div>
<?php else: ?>
<iframe id="ai-chat-frame" src="<?php echo helper::createLink('ai', 'chat'); ?>" frameborder="no" allowtransparency="true" scrolling="auto" hidefocus></iframe>
<?php endif; ?>
</div>
</div>
<script>
$(function()
{
/* Move xuan web client into #xuan-chat-view. */
if(window.xuan)
{
document.querySelector('#xuan-chat-view').prepend(document.querySelector('#xx-embed-container'));
/* Update badge on chat. */
window.xuan._options.onNotice = function(notice)
{
window.handleXuanNoticeChange(notice);
$('.chat-switch-item[data-value="chat"]').toggleClass('has-notice', !!notice.count);
}
/* Set style into xuan frame. */
let tries = 0;
const xuanFrameWaitLoop = setInterval(function()
{
if(document.querySelector('#xx-embed-container iframe'))
{
clearInterval(xuanFrameWaitLoop);
tries = 0;
const chatsViewWaitLoop = setInterval(function()
{
if(document.querySelector('#xx-embed-container iframe').contentDocument.querySelector('.app-chats-menu'))
{
clearInterval(chatsViewWaitLoop);
document.querySelector('#xx-embed-container iframe').contentDocument.querySelector('.app-chats-menu').style.cssText = 'width: 330px !important; top: 48px;';
document.querySelector('#xx-embed-container iframe').contentDocument.querySelector('.app-chats-cache').style.cssText = 'right: 330px !important;';
document.querySelector('#xx-embed-container iframe').contentDocument.querySelector('.app-chats-menu-search').style.cssText = 'padding: 10px 8px !important;';
document.querySelector('#xx-embed-container iframe').contentDocument.querySelector('.app-chats-menu-search .btn:first-child').style.cssText = 'display: none;';
}
else
{
if(++tries > 20) clearInterval(chatsViewWaitLoop);
}
}, 200);
}
else
{
if(++tries > 20) clearInterval(xuanFrameWaitLoop);
}
}, 200);
}
/* Create ai chat button */
const $chatBtn = $('<a href="javascript:void(0)" id="chatBtn" class="btn btn-link"><span class="badge bg-red" id="chatNoticeBadge"></span></a>');
const $aiChatIcon = $('<svg xmlns="http://www.w3.org/2000/svg" width="28" height="20"><defs><linearGradient id="a" x1="-130%" x2="135%" y1="136.25%" y2="8.75%"><stop offset="0%" stop-color="#2E7FFF"/><stop offset="100%" stop-color="#5DBDFF"/></linearGradient><linearGradient id="b" x1="-11.364%" x2="100.45%" y1="88.636%" y2="9.99%"><stop offset="0%" stop-color="#2E7FFF"/><stop offset="100%" stop-color="#6CDCFF"/></linearGradient><linearGradient id="c" x1="-847.379%" x2="115.023%" y1="460.717%" y2="-128.435%"><stop offset="0%" stop-color="#2E7FFF"/><stop offset="100%" stop-color="#6CDCFF"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" d="M24.281 7.75h1.172L22.778.433a.279.279 0 0 0-.262-.183H20.99a.279.279 0 0 0-.262.181L18 7.75h1.16a.837.837 0 0 0 .795-.577l.373-1.142h2.777l.382 1.146a.837.837 0 0 0 .794.573Zm-2.428-5.554.868 2.6h-2.038l.874-2.619a2.73 2.73 0 0 0 .133-.634h.044c.023.27.063.489.118.653h.001ZM28 6.913V.25h-1.628v7.5h.791A.837.837 0 0 0 28 6.913Z"/><path fill="url(#b)" d="M18.849 3.239A4.57 4.57 0 0 0 17.387 3H4.613C2.07 3 0 5.08 0 7.636v10.105c0 .773.355 1.486 1.005 1.902.355.238.77.357 1.183.357.355 0 .65-.119.946-.238l3.963-1.783a9.711 9.711 0 0 1 4.08-.892h6.21c2.543 0 4.613-2.08 4.613-4.636V7.636c0-.28-.025-.556-.073-.823h-1.032l-.198.603a1.618 1.618 0 0 1-1.538 1.115H18a.781.781 0 0 1-.732-1.054l1.58-4.238h.001Zm-13.64 7.47a1.015 1.015 0 0 1 .544-1.32 1 1 0 0 1 1.314.547 1.015 1.015 0 0 1-.544 1.32 1 1 0 0 1-1.314-.546v-.001Zm4.516 0a1.015 1.015 0 0 1 .544-1.32 1.001 1.001 0 0 1 1.314.547 1.016 1.016 0 0 1-.544 1.32 1 1 0 0 1-1.314-.546v-.001Zm4.94-.386a1.005 1.005 0 0 0 2.01 0c0-.268-.106-.525-.295-.715a1.003 1.003 0 0 0-1.716.715h.001Z"/><path fill="url(#c)" fill-rule="nonzero" d="M19.16 7.75a.837.837 0 0 0 .795-.577l.373-1.142h1.387-1.387l-.373 1.142a.837.837 0 0 1-.796.577h.001Z"/></g></svg>');
$chatBtn.prepend($aiChatIcon).insertBefore('#globalSearchDiv').on('click', function()
{
/* Make sure xuan is shown. */
if(window.xuan) window.xuan[window.xuan.shown ? 'expand' : 'show']();
$('#chat-container').toggle();
});
/* Handle switch events. */
$('#chat-switch .chat-switch-item').on('click', function()
{
$(this).addClass('active').siblings().removeClass('active');
$('#ai-chat-view').toggle($(this).data('value') == 'ai');
$('#xuan-chat-view').toggle($(this).data('value') == 'chat');
});
/* Switch to xuan chat view if AI is unavailable. */
if(isXuanAvailable && !(isAIConfigured && hasAIChatPriv)) $('#chat-switch .chat-switch-item[data-value="chat"]').trigger('click');
/* Handle backdrop click, hide chat view. */
$(document).on('click', function(e)
{
if($('#chat-container').is(':hidden')) return;
if($(e.target).closest('#chat-switch,#ai-chat-view,#xx-embed,#chatBtn').length) return;
$('#chat-container').hide();
});
});
</script>
+1
View File
@@ -152,6 +152,7 @@ js::set('showFeatures', $showFeatures);
</div>
</div>
</div>
<?php include './chatview.html.php';?>
<?php js::set('searchAB', $lang->searchAB);?>
<?php js::set('searchObjectList', ',' . implode(',', array_keys($lang->searchObjects)) . ',');?>
<?php js::set('searchCommon', $lang->index->search);?>
@@ -50,13 +50,13 @@ if(isset($this->config->xuanxuan->turnon) && $this->config->xuanxuan->turnon &&
<style>
#chatBtn {padding: 3px; position: relative;}
#chatBtn .icon {font-size: 24px;}
#chatNoticeBadge {position: absolute; top: -4px; right: -2px; line-height: 14px; height: 14px; min-width: 14px; text-align: center; display: inline-block; font-size: 12px; border-radius: 7px; opacity: 0; transform: scale(0); transition: .2s; transition-property: transform, opacity; padding: 0 2px;}
#chatNoticeBadge {position: absolute; bottom: 4px; right: 0; line-height: 14px; height: 14px; min-width: 14px; text-align: center; display: inline-block; font-size: 12px; border-radius: 7px; opacity: 0; transform: scale(0); transition: .2s; transition-property: transform, opacity; padding: 0 2px;}
#chatNoticeBadge.show {opacity: 1; transform: scale(1);}
#xx-embed-container {bottom: 40px!important; z-index: 1010!important;}
#xx-embed-container {z-index: 1010!important;}
#xx-embed-container .xx-embed-has-animation {transition: min-width .5s ease-out, min-height .5s ease-out, transform, opacity!important;}
#xx-embed-container .xx-embed {width: 280px; height: 100%; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06)!important;}
#xx-embed-container .xx-embed {width: 330px; height: 100%; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06)!important;}
#xx-embed-container .xx-embed.xx-embed-hidden,
#xx-embed-container .xx-embed.xx-embed-collapsed {width: 280px!important; height: 100%!important; opacity: .7; pointer-events: none; transform: translateY(100%); display: block!important;}
#xx-embed-container .xx-embed.xx-embed-collapsed {width: 330px!important; height: 100%!important; opacity: .7; pointer-events: none; transform: translateY(100%); display: block!important;}
#xx-embed-container .xx-embed.has-chat-view {min-width: 1000px!important;}
#xx-embed-container .xx-embed-body {min-height: initial!important;}
</style>
@@ -87,30 +87,18 @@ function handleXuanRouteChange(route)
/* Set client global options*/
Xuanxuan.setGlobalOptions(
{
position: 'right',
width: 280,
preload: true,
showHeader: false,
onNotice: handleXuanNoticeChange,
onRouteChange: handleXuanRouteChange,
lang: lang
});
$(function() {
/* Create chat button */
var $chatBtn = $('<a href="javascript:void(0)" id="chatBtn" class="btn btn-link"><i class="text-primary icon icon-chat-solid"></i><span class="badge bg-red" id="chatNoticeBadge"></span></a>');
$chatBtn.insertBefore('#globalSearchDiv').on('click', toggleXuanClient);
position: 'right',
width: 330,
preload: true,
showHeader: false,
showCollapseButtons: false,
onNotice: handleXuanNoticeChange,
onRouteChange: handleXuanRouteChange,
lang: lang,
});
/* Create client instance */
window.xuan = new Xuanxuan(xuanConfig);
/* Hide xuan popover on click page */
$(document).on('click', function(e)
{
if(!window.xuan.shown || $(e.target).closest('#xx-embed-container,#chatBtn').length) return;
window.xuan.hide();
});
</script>
<?php endif; ?>
<?php endif; ?>