* fix ai prompts and stuff.

This commit is contained in:
liwenrui
2024-02-27 17:04:46 +08:00
parent 2b5c612d23
commit 51ebcc1eb7
30 changed files with 313 additions and 13 deletions
+8 -9
View File
@@ -193,9 +193,8 @@ $config->ai->menuPrint = new stdclass();
$config->ai->menuPrint->locations = array();
$config->ai->menuPrint->locations['story']['view'] = (object)array(
'module' => 'story',
'targetContainer' => '#mainContent .main-col .cell:first-of-type .detail:first-of-type .detail-title',
'class' => 'pull-right',
'stylesheet' => '#mainContent .cell:first-of-type .detail:first-of-type .detail-title>button {margin-left: 10px;} #mainContent .cell:first-of-type .detail:first-of-type .detail-content {margin-top: 12px;}'
'targetContainer' => '#mainContent .detail-body .section:first-of-type > div:first-of-type',
'stylesheet' => '#mainContent .detail-body .section:first-of-type > div:first-of-type {width: 100%; justify-content: space-between;}'
);
$config->ai->menuPrint->locations['task']['view'] = clone $config->ai->menuPrint->locations['story']['view'];
$config->ai->menuPrint->locations['task']['view']->module = 'task';
@@ -206,7 +205,7 @@ $config->ai->menuPrint->locations['bug']['view']->module = 'bug';
$config->ai->menuPrint->locations['projectstory']['view'] = clone $config->ai->menuPrint->locations['story']['view'];
$config->ai->menuPrint->locations['execution']['storyView'] = $config->ai->menuPrint->locations['story']['view'];
$config->ai->menuPrint->locations['execution']['view'] = (object)array(
$config->ai->menuPrint->locations['execution']['view'] = (object)array( // TODO: fix this.
'module' => 'execution',
'injectMethod' => 'prepend',
'targetContainer' => '#mainContent.main-row > .col-4.side-col .detail:first-child > .detail-title',
@@ -218,7 +217,7 @@ $config->ai->menuPrint->locations['project']['view']->module = 'project';
$config->ai->menuPrint->locations['project']['view'] = clone $config->ai->menuPrint->locations['execution']['view'];
$config->ai->menuPrint->locations['project']['view']->module = 'project';
$config->ai->menuPrint->locations['product']['view'] = (object)array(
$config->ai->menuPrint->locations['product']['view'] = (object)array( // TODO: fix this.
'module' => 'product',
'injectMethod' => 'append',
'targetContainer' => '#mainContent.main-row > .col-8.main-col .detail:first-child > .detail-title',
@@ -227,7 +226,8 @@ $config->ai->menuPrint->locations['product']['view'] = (object)array(
$config->ai->menuPrint->locations['productplan']['view'] = (object)array(
'module' => 'productplan',
'targetContainer' => '#mainMenu>.btn-toolbar.pull-right',
'injectMethod' => 'prepend',
'targetContainer' => '#mainContent .toolbar',
'objectVarName' => 'plan'
);
$config->ai->menuPrint->locations['projectplan']['view'] = $config->ai->menuPrint->locations['productplan']['view'];
@@ -239,11 +239,10 @@ $config->ai->menuPrint->locations['projectrelease']['view']->objectVarName = nul
$config->ai->menuPrint->locations['doc']['view'] = (object)array(
'module' => 'doc',
'injectMethod' => 'prepend',
'targetContainer' => '#mainMenu>.btn-toolbar.pull-right',
'targetContainer' => '#docPanel > .panel-heading > .panel-actions > .toolbar',
);
$config->ai->injectAuditButton = new stdclass();
$config->ai->injectAuditButton = new stdclass(); // TODO: fix this.
$config->ai->injectAuditButton->locations = array();
$config->ai->injectAuditButton->locations['task']['edit'] = array(
'toolbar' => (object)array(
+3 -2
View File
@@ -789,8 +789,9 @@ class ai extends control
if($prompt->status == 'draft') $_SESSION['auditPrompt']['time'] = time();
$promptViewLink = $this->inlink('promptview', "promptID=$promptId");
echo js::start() . "const currentApp = parent.$.apps.getLastApp().code; parent.$.apps.open('$location'); if(currentApp == 'admin') {location.href = '$promptViewLink';} else if(currentApp != parent.$.apps.getLastApp().code) {parent.$.apps.close(currentApp);}" . js::end();
$this->view->formLocation = $location;
$this->view->promptViewLink = $this->inlink('promptview', "promptID=$promptId");
$this->display();
}
/**
+2 -1
View File
@@ -2019,7 +2019,8 @@ class aiModel extends model
$schema = $this->getFunctionCallSchema($prompt->targetForm);
if(empty($schema)) return -5;
$response = $this->{$this->config->ai->models[$this->modelConfig->type] == 'ernie' ? 'converseTwiceForJSON' : 'converseForJSON'}($prompt->model , array((object)array('role' => 'user', 'content' => $wholePrompt)), $schema);
$this->useLanguageModel($prompt->model);
$response = $this->{$this->config->ai->models[$this->modelConfig->type] == 'ernie' ? 'converseTwiceForJSON' : 'converseForJSON'}($prompt->model, array((object)array('role' => 'user', 'content' => $wholePrompt)), $schema);
if(empty($response)) return -6;
return current($response);
+148
View File
@@ -0,0 +1,148 @@
<?php
/**
* The ai input inject ui view file of ai module of ZenTaoPMS.
*
* This view file is used to inject ai generated result into
* input fields. Include this view file in the target forms.
*
* @copyright Copyright 2009-2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://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
*/
namespace zin;
$inputInject = function()
{
$this->app->loadConfig('ai');
$this->app->loadLang('ai');
$module = $this->app->getModuleName();
$method = $this->app->getMethodName();
if(!isset($this->config->ai->availableForms[$module]) || !in_array($method, $this->config->ai->availableForms[$module])) return;
if(isset($_SESSION['aiInjectData']) && isset($_SESSION['aiInjectData'][$module]) && isset($_SESSION['aiInjectData'][$module][$method])) $injectData = $_SESSION['aiInjectData'][$module][$method];
if(empty($injectData)) return;
jsVar('window.injectData', $injectData);
unset($_SESSION['aiInjectData'][$module]);
$this->app->loadLang('ai');
js(<<< JAVASCRIPT
window.injectToInputElement = (inputName, data, index) =>
{
if(typeof index !== 'undefined') inputName = inputName + '[' + index + ']';
const inputEl = $('[name="' + inputName + '"]');
if(!inputEl.length) return;
const inputType = inputEl.prop('nodeName');
switch(inputType) // Contains case fallthroughs, on purpose.
{
case 'TEXTAREA':
/* Textareas might be controlled by KindEditors. */
if(typeof KindEditor !== 'undefined' && KindEditor.instances.length)
{
const editorInstance = KindEditor.instances.find(e => e.srcElement.attr('name') == inputName);
if(editorInstance)
{
if(data === null) data = '';
/* Set editor content and sync with textarea. */
editorInstance.html(data);
editorInstance.sync();
break;
}
}
default:
/* For normal inputs, just set the value is enough. */
inputEl.val(data);
break;
}
}
window.inject = (obj) =>
{
for(const key in obj)
{
window.dealWithSpecialInputType(key, obj[key]);
if(Array.isArray(obj[key]))
{
const isStartWith0 = $('[name="' + key + '[0]' + '"]').length;
const arr = obj[key];
for(let i = 0; i < arr.length; i++)
{
for(const key in arr[i])
{
window.injectToInputElement(key, arr[i][key], isStartWith0 ? i : i + 1);
}
}
}
else if(typeof obj[key] === 'Object')
{
inject(obj[key]);
}
else
{
window.injectToInputElement(key, obj[key])
}
}
}
window.dealWithSpecialInputType = (key, value) =>
{
if(key === 'steps')
{
const steps = document.getElementById('steps');
if(!steps) return;
if(value.length > steps.children.length)
{
const gap = value.length - steps.children.length;
for(let i = 0; i < gap; i++)
{
const addButton = document.querySelector('#steps > tr:last-child > .step-actions button:first-child');
if(!addButton) return;
addButton.click();
}
}
}
}
requestAnimationFrame(() =>
{
try
{
const data = JSON.parse(window.injectData);
if(!data) return;
window.inject(data);
zui.Messager.show({content: '{$this->lang->ai->dataInject->success}', type: 'success'});
}
catch(e)
{
zui.Messager.show({content:'{$this->lang->ai->dataInject->fail}', type: 'danger'});
console.error(e);
}
finally
{
/* Set injected in oreder to cancel loading class on object view (see ./promptmenu.html.php). */
sessionStorage.setItem('ai-prompt-data-injected', true);
const container = window.frameElement?.closest('.load-indicator');
if(container && container.dataset.loading)
{
sessionStorage.removeItem('ai-prompt-data-injected');
delete container.dataset.loading;
container.classList.remove('loading');
container.classList.remove('no-delay');
}
}
});
JAVASCRIPT);
/* TODO: handle prompt audit, see original ../view/inputinject.html.php. */
};
$inputInject();
+6
View File
@@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
namespace zin;
js("requestAnimationFrame(() => setTimeout(() => openUrl(`{$formLocation}`), 1000));");
+95
View File
@@ -0,0 +1,95 @@
<?php
/**
* The ai prompt menu ui view file of ai module of ZenTaoPMS.
*
* This view file is used to print the prompt menu, acts just like header php files.
* Prompt menus are generated with php and injected with javascript. A lot of hacking
* went into this, so please don't touch it unless you know what you are doing.
*
* @copyright Copyright 2009-2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://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
*/
namespace zin;
$promptMenuInject = function()
{
$this->loadModel('ai');
if(!$this->ai->hasModelsAvailable() || !commonModel::hasPriv('ai', 'promptExecute')) return;
$this->app->loadConfig('ai');
$module = $this->app->getModuleName();
$method = $this->app->getMethodName();
if(!isset($this->config->ai->menuPrint->locations[$module][$method])) return;
$menuOptions = $this->config->ai->menuPrint->locations[$module][$method];
$prompts = $this->ai->getPromptsForUser($menuOptions->module);
$prompts = $this->ai->filterPromptsForExecution($prompts, true);
if(empty($prompts)) return;
$html = '';
$objectVarName = empty($menuOptions->objectVarName) ? $menuOptions->module : $menuOptions->objectVarName;
$currentObjectId = !empty($this->view->$objectVarName) ? $this->view->$objectVarName->id : 0;
if(count($prompts) > 1)
{
$html .= '<div class="prompts dropdown' . ((isset($menuOptions->class) ? ' ' . $menuOptions->class : '') . (isset($menuOptions->dropdownClass) ? ' ' . $menuOptions->dropdownClass : '')) . '"><button class="btn ghost size-sm font-medium' . (isset($menuOptions->buttonClass) ? ' ' . $menuOptions->buttonClass : '') . '" type="button" data-toggle="dropdown">' . $this->lang->ai->promptMenu->dropdownTitle . ' <i class="icon-caret-down"></i></button><menu class="dropdown-menu menu">';
foreach($prompts as $prompt) $html .= '<li class="menu-item">' . html::linkButton($prompt->name . ($prompt->status != 'active' ? '<span class="label label-info label-badge" style="margin-left: 4px;">' . $this->lang->ai->prompts->statuses[$prompt->status] . '</span>' : ''), helper::createLink('ai', 'promptExecute', "promptId=$prompt->id&objectId=$currentObjectId"), 'self', "style='width: 100%;'" . (empty($prompt->unauthorized) ? '' : ' disabled') . (empty($prompt->desc) ? '' : " data-toggle='popover' data-container='body' data-trigger='hover' data-content='$prompt->desc' data-title='$prompt->name' data-placement='left'"), 'btn ghost size-sm font-medium text-left') . '</li>';
$html .= '</menu></div>';
}
else
{
$prompt = current($prompts);
$html .= html::linkButton($prompt->name . ($prompt->status != 'active' ? '<span class="label label-info label-badge" style="margin-left: 4px;">' . $this->lang->ai->prompts->statuses[$prompt->status] . '</span>' : ''), helper::createLink('ai', 'promptExecute', "promptId=$prompt->id&objectId=$currentObjectId"), 'self', (empty($prompt->unauthorized) ? '' : 'disabled') . (empty($prompt->desc) ? '' : " data-toggle='popover' data-container='body' data-trigger='hover' data-content='$prompt->desc' data-title='$prompt->name' data-placement='bottom'"), 'prompt btn ghost size-sm font-medium' . ((isset($menuOptions->class) ? ' ' . $menuOptions->class : '') . (isset($menuOptions->buttonClass) ? ' ' . $menuOptions->buttonClass : '')));
}
/* Assemble injector script. */
$script = <<< JAVASCRIPT
if(window.location.search.includes('onlybody')) return;
const container = window.frameElement?.closest('.load-indicator');
if(container && container.dataset.loading)
{
delete container.dataset.loading;
container.classList.remove('loading');
container.classList.remove('no-delay');
}
JAVASCRIPT;
$script .= "$(`$menuOptions->targetContainer`)." . (!empty($menuOptions->injectMethod) ? $menuOptions->injectMethod : 'append') . "(`$html`);\n";
$script .= <<< JAVASCRIPT
$('[data-toggle="popover"]').popover({template: '<div class="popover"><h3 class="popover-title"></h3><div class="popover-content"></div></div>'});
JAVASCRIPT;
$script .= count($prompts) > 1 ? "$('.prompts.dropdown .dropdown-menu').on('click', 'a', e =>" : "$('.prompt').on('click', e =>";
$script .= <<< JAVASCRIPT
{
if(!container) return;
container.dataset.loading = e.target.querySelector('.label') ? '{$this->lang->ai->execute->auditing}' : '{$this->lang->ai->execute->loading}';
container.classList.add('loading');
container.classList.add('no-delay');
/* Checks for session storage to cancel loading status (see inputinject.html.php). */
sessionStorage.removeItem('ai-prompt-data-injected');
const loadCheckInterval = setInterval(() =>
{
if(sessionStorage.getItem('ai-prompt-data-injected'))
{
if(container && container.dataset.loading)
{
delete container.dataset.loading;
container.classList.remove('loading');
container.classList.remove('no-delay');
}
sessionStorage.removeItem('ai-prompt-data-injected');
clearInterval(loadCheckInterval);
}
}, 200);
});
JAVASCRIPT;
/* Perform injection. */
if(isset($menuOptions->stylesheet)) h('style', $menuOptions->stylesheet);
js($script);
};
$promptMenuInject();
+2
View File
@@ -11,6 +11,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
$fields = useFields('bug.create');
if(!empty($executionType) && $executionType == 'kanban') $fields->merge('bug.kanban');
+2
View File
@@ -11,6 +11,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('bug', $bug);
jsVar('confirmChangeProduct', $lang->bug->notice->confirmChangeProduct);
jsVar('moduleID', $bug->module);
+3
View File
@@ -10,6 +10,9 @@ declare(strict_types=1);
* @link https://www.zentao.net
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
jsVar('bugID', $bug->id);
jsVar('productID', $bug->product);
jsVar('branchID', $bug->branch);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
featureBar
(
li(backBtn(setClass('ghost'), set::icon('back'), $lang->goback)),
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
$progress = ($execution->totalConsumed + $execution->totalLeft) ? floor($execution->totalConsumed / ($execution->totalConsumed + $execution->totalLeft) * 1000) / 1000 * 100 : 0;
$isKanban = isset($execution->type) && $execution->type == 'kanban';
$chartURL = createLink('execution', $isKanban ? 'ajaxGetCFD' : 'ajaxGetBurn', "executionID={$execution->id}");
+2
View File
@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
/* Flag variable for hiding product code. */
$hiddenCode = (!isset($config->setCode) || $config->setCode == 0 || empty($product->code));
$allStoryCount = array_sum($product->stories);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('weekend', $config->execution->weekend);
jsVar('productID', $product->id);
jsVar('lastLang', $lang->productplan->last);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('today', helper::today());
jsVar('weekend', $config->execution->weekend);
jsVar('productID', $productID);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
$unlinkURL['story'] = helper::createLink('productplan', 'unlinkStory', "storyID=%s&planID={$plan->id}");
$unlinkURL['bug'] = helper::createLink('productplan', 'unlinkBug', "bugID=%s&planID={$plan->id}");
+2
View File
@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
$fields = $this->config->programplan->form->create;
/* Generate title that is tailored to specific situation. */
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
jsVar('confirmDeleteTip', $lang->project->confirmDelete);
$programDom = null;
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
$releaseModule = $app->rawModule == 'projectrelease' ? 'projectrelease' : 'release';
jsVar('initLink', $link);
+2
View File
@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
data('activeMenuID', $type);
data('storyType', $type);
jsVar('storyType', $type);
+2
View File
@@ -9,6 +9,8 @@ declare(strict_types=1);
* @link https://www.zentao.net
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
include './affected.html.php';
data('activeMenuID', $story->type);
+2
View File
@@ -11,6 +11,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
data('storyType', $type);
data('activeMenuID', $type);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
$confirmDelete = $this->lang->story->confirmDelete;
if($story->type == 'requirement')
{
+2
View File
@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('window.executionID', $execution->id);
jsVar('vision', $config->vision);
jsVar('window.lifetime', $execution->lifetime);
+1
View File
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
include './taskteam.html.php';
/* ====== Preparing and processing page data ====== */
+3
View File
@@ -9,6 +9,9 @@ declare(strict_types=1);
* @link https://www.zentao.net
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
global $lang;
/* Join mailto. */
+3
View File
@@ -10,6 +10,9 @@ declare(strict_types=1);
* @link https://www.zentao.net
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('tab', $this->app->tab);
if($app->tab == 'execution') jsVar('objectID', $executionID);
if($app->tab == 'project') jsVar('objectID', $projectID);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('tab', $this->app->tab);
jsVar('isLibCase', $isLibCase);
jsVar('caseBranch', $case->branch);
+2
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
$canCreateCase = hasPriv('testcase', 'create');
jsVar('viewParams', "caseID={$case->id}&version={$version}&from={$from}&taskID={$taskID}&stepsType=");
+2 -1
View File
@@ -10,6 +10,8 @@ declare(strict_types=1);
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
foreach($reports as $report)
{
$taskName = '';
@@ -63,4 +65,3 @@ dtable
);
render();
+3
View File
@@ -9,6 +9,9 @@ declare(strict_types=1);
* @link https://www.zentao.net
*/
namespace zin;
include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php');
jsVar('goalTip', $lang->testreport->goalTip);
jsVar('foundBugTip', $lang->testreport->foundBugTip);
jsVar('legacyBugTip', $lang->testreport->legacyBugTip);