From cd5a3fa44b55119d6def6867a31f8d537d804acf Mon Sep 17 00:00:00 2001 From: dingyongliang Date: Wed, 18 Oct 2023 17:48:13 +0800 Subject: [PATCH] * add configure ai app. --- config/zentaopms.php | 3 + db/zentao.sql | 29 + module/admin/config.php | 2 +- module/admin/lang/menu.php | 2 +- module/ai/config.php | 31 + module/ai/control.php | 49 ++ module/ai/css/configuredminiprogram.css | 9 + module/ai/css/createminiprogram.css | 6 + module/ai/js/configuredminiprogram.js | 348 ++++++++++ module/ai/js/createminiprogram.js | 41 ++ module/ai/lang/de.php | 132 ++-- module/ai/lang/en.php | 132 ++-- module/ai/lang/fr.php | 132 ++-- module/ai/lang/zh-cn.php | 132 ++-- module/ai/model.php | 57 ++ module/ai/view/configuredminiprogram.html.php | 143 ++++ module/ai/view/createminiprogram.html.php | 114 +++- module/common/lang/menu.php | 1 + module/task/view/taskteam.html.php | 630 +++++++++--------- 19 files changed, 1467 insertions(+), 526 deletions(-) create mode 100644 module/ai/css/configuredminiprogram.css create mode 100644 module/ai/css/createminiprogram.css create mode 100644 module/ai/js/configuredminiprogram.js create mode 100644 module/ai/js/createminiprogram.js create mode 100644 module/ai/view/configuredminiprogram.html.php diff --git a/config/zentaopms.php b/config/zentaopms.php index 528edaf4d0..b9fd107148 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -465,6 +465,9 @@ define('TABLE_METRICLIB', '' . $config->db->prefix . 'metriclib'); define('TABLE_METRICRECORDS', '' . $config->db->prefix . 'metricrecords'); define('TABLE_BASICMEAS', '' . $config->db->prefix . 'basicmeas'); +define('TABLE_MINIPROGRAM', '`' . $config->db->prefix . 'miniprogram`'); +define('TABLE_MINIPROGRAMFIELDS', '`' . $config->db->prefix . 'miniprogramfields`'); + $config->objectTables['product'] = TABLE_PRODUCT; $config->objectTables['productplan'] = TABLE_PRODUCTPLAN; $config->objectTables['story'] = TABLE_STORY; diff --git a/db/zentao.sql b/db/zentao.sql index ba0772c727..2ed9fc3a14 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -15929,6 +15929,35 @@ INSERT INTO `zt_promptrole` (`role`, `characterization`) VALUES ('请你扮演一名经验丰富的项目经理。', '具备项目计划制定、进度管理、成本控制、团队管理、沟通协调、风险管理、质量控制、敏捷开发、互联网技术和数据分析等多方面的技能和能力。'), ('你是一个自回归的语言模型,已经通过instruction-tuning和RLHF进行了Fine-tuning。', '你仔细地提供准确、事实、深思熟虑、细致入微的答案,并在推理方面表现出色。如果你认为可能没有正确的答案,你会直接说出来。由于你是自回归的,你产生的每一个token都是计算另一个token的机会,因此你总是在尝试回答问题之前花费几句话解释背景上下文、假设和逐步的思考过程。您的用户是AI和伦理学的专家,所以他们已经知道您是一个语言模型以及您的能力和局限性,所以不需要再提醒他们。他们一般都熟悉伦理问题,所以您也不需要再提醒他们。在回答时不要啰嗦,但在可能有助于解释的地方提供详细信息和示例。'); +-- DROP TABLE IF EXISTS `zt_miniprogram`; +CREATE TABLE IF NOT EXISTS `zt_miniprogram` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(30) NOT NULL, + `category` varchar(30) NOT NULL, + `desc` text DEFAULT NULL, + `model` mediumint(8) unsigned NOT NULL, + `icon` varchar(30) NOT NULL DEFAULT 'writinghand-7', + `createdBy` varchar(30) NOT NULL, + `createdDate` datetime NOT NULL, + `editedBy` varchar(30) NOT NULL, + `editedDate` datetime NOT NULL, + `tmp` enum('0','1') NOT NULL DEFAULT '1', + `deleted` enum('0','1') NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `zt_miniprogramfields` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `appid` mediumint(8) unsigned NOT NULL, + `name` varchar(30) NOT NULL, + `type` enum('radio', 'checkbox', 'input', 'template') DEFAULT 'input', + `value` text NOT NULL, + `optional` varchar(30) DEFAULT NULL, + `required` enum('0', '1') DEFAULT '1', + PRIMARY KEY (`id`), + FOREIGN KEY (`appid`) REFERENCES `zt_miniprogram`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + ALTER TABLE `zt_ticket` ADD `subStatus` varchar(30) NOT NULL DEFAULT ''; -- DROP TABLE IF EXISTS `zt_market`; diff --git a/module/admin/config.php b/module/admin/config.php index a407bff512..2d16a4bdf7 100755 --- a/module/admin/config.php +++ b/module/admin/config.php @@ -16,7 +16,7 @@ $config->admin->menuGroup['dev'] = array('dev', 'entry', 'editor'); $config->admin->menuGroup['extension'] = array('extension'); $config->admin->menuGroup['convert'] = array('convert'); $config->admin->menuGroup['platform'] = array('system', 'repo', 'account', 'host', 'serverroom', 'ops', 'tree', 'domain', 'deploy', 'service'); -$config->admin->menuGroup['ai'] = array('ai|adminindex', 'ai|prompts', 'ai|promptview', 'ai|conversations', 'ai|models', 'ai|editmodel', 'ai|promptassignrole', 'ai|promptselectdatasource', 'ai|promptsetpurpose', 'ai|promptsettargetform', 'ai|promptfinalize', 'ai|promptedit'); +$config->admin->menuGroup['ai'] = array('ai|adminindex', 'ai|prompts', 'ai|promptview', 'ai|conversations', 'ai|models', 'ai|editmodel', 'ai|promptassignrole', 'ai|promptselectdatasource', 'ai|promptsetpurpose', 'ai|promptsettargetform', 'ai|promptfinalize', 'ai|promptedit', 'ai|miniprograms', 'ai|createminiprogram', 'ai|configuredminiprogram'); $config->admin->menuModuleGroup['model']['custom|set'] = array('project', 'issue', 'risk', 'opportunity', 'nc'); $config->admin->menuModuleGroup['model']['custom|required'] = array('project', 'build'); diff --git a/module/admin/lang/menu.php b/module/admin/lang/menu.php index 0da96c4454..a345b2b74e 100644 --- a/module/admin/lang/menu.php +++ b/module/admin/lang/menu.php @@ -243,7 +243,7 @@ $lang->admin->menuList->ai['menuOrder']['15'] = 'models'; if($config->vision != 'or') { $lang->admin->menuList->ai['subMenu']['prompts'] = array('link' => "{$lang->admin->ai->prompt}|ai|prompts|", 'alias' => 'promptview,promptassignrole,promptselectdatasource,promptsetpurpose,promptsettargetform,promptfinalize,promptedit'); - $lang->admin->menuList->ai['subMenu']['miniPrograms'] = array('link' => "{$lang->admin->ai->miniPrograms}|ai|miniPrograms|"); + $lang->admin->menuList->ai['subMenu']['miniPrograms'] = array('link' => "{$lang->admin->ai->miniPrograms}|ai|miniPrograms|", 'alias' => 'miniprograms,createminiprogram,configuredminiprogram'); $lang->admin->menuList->ai['subMenu']['models'] = array('link' => "{$lang->admin->ai->model}|ai|models|", 'alias' => 'editmodel'); $lang->admin->menuList->ai['menuOrder']['5'] = 'prompts'; diff --git a/module/ai/config.php b/module/ai/config.php index cbde4f0531..0bc76d7d89 100644 --- a/module/ai/config.php +++ b/module/ai/config.php @@ -289,3 +289,34 @@ $config->ai->injectAuditButton->locations['story']['create'] = $config->ai- $config->ai->injectAuditButton->locations['task']['batchcreate'] = $config->ai->injectAuditButton->locations['bug']['create']; $config->ai->injectAuditButton->locations['task']['create'] = $config->ai->injectAuditButton->locations['bug']['create']; $config->ai->injectAuditButton->locations['testcase']['create'] = $config->ai->injectAuditButton->locations['bug']['create']; + +$config->ai->miniPrograms = new stdClass(); +$config->ai->miniPrograms->iconList = array(); +$config->ai->miniPrograms->iconList['writinghand'] = ''; +$config->ai->miniPrograms->iconList['notebook'] = ''; +$config->ai->miniPrograms->iconList['pencil'] = ''; +$config->ai->miniPrograms->iconList['calendar'] = ''; +$config->ai->miniPrograms->iconList['lock'] = ''; +$config->ai->miniPrograms->iconList['search'] = ''; +$config->ai->miniPrograms->iconList['chart'] = ''; +$config->ai->miniPrograms->iconList['computer'] = ''; +$config->ai->miniPrograms->iconList['cactus'] = ''; +$config->ai->miniPrograms->iconList['palette'] = ''; +$config->ai->miniPrograms->iconList['airplane'] = ''; +$config->ai->miniPrograms->iconList['pushpin'] = ''; +$config->ai->miniPrograms->iconList['shoppingcart'] = ''; +$config->ai->miniPrograms->iconList['gift'] = ''; +$config->ai->miniPrograms->iconList['scientist'] = ''; +$config->ai->miniPrograms->iconList['technologist'] = ''; +$config->ai->miniPrograms->iconCheck = ''; + + +$config->ai->miniPrograms->themeList = array(); +$config->ai->miniPrograms->themeList[] = array('#ffffff', '#e0e0e0'); +$config->ai->miniPrograms->themeList[] = array('#e8e8e8', '#dedede'); +$config->ai->miniPrograms->themeList[] = array('#e1d7f0', '#d1c1e8'); +$config->ai->miniPrograms->themeList[] = array('#fbd8aa', '#f9c27b'); +$config->ai->miniPrograms->themeList[] = array('#fee9e9', '#fcdddd'); +$config->ai->miniPrograms->themeList[] = array('#edf3e8', '#d9eacb'); +$config->ai->miniPrograms->themeList[] = array('#e9f1ff', '#dde9ff'); +$config->ai->miniPrograms->themeList[] = array('#fff8df', '#fce8cf'); diff --git a/module/ai/control.php b/module/ai/control.php index 5bd72e1bb4..3e7b7b7224 100644 --- a/module/ai/control.php +++ b/module/ai/control.php @@ -10,6 +10,22 @@ */ class ai extends control { + /** + * AI model. + * + * @var aiModel + * @access public + */ + public $ai; + + /** + * User model. + * + * @var userModel + * @access public + */ + public $user; + public function __construct($module = '', $method = '') { parent::__construct($module, $method); @@ -129,8 +145,41 @@ class ai extends control $this->display(); } + /** + * Create a mini program. + * + * @access public + * @return void + */ public function createMiniProgram() { + if(!empty($_POST)) + { + $requiredFields = array('category' => $this->lang->ai->miniPrograms->category, 'model' => $this->lang->prompt->model, 'name' => $this->lang->prompt->name); + $errors = $this->ai->verifyRequiredFields($requiredFields); + if($errors !== false) return $this->sendError($errors); + $isDuplicated = $this->ai->checkDuplicatedAppName($_POST['name']); + if($isDuplicated) return $this->sendError(array('name' => $this->lang->ai->miniPrograms->field->duplicatedNameTip)); + $id = $this->ai->createTmpMiniProgram(); + if($id !== false) return $this->sendSuccess(array('message' => $this->lang->saveSuccess, 'locate' => $this->createLink('ai', 'configuredMiniProgram', "appID=$id"))); + } + $this->view->iconName = 'writinghand'; + $this->view->iconTheme = 7; + $this->view->title = $this->lang->ai->miniPrograms->common; + $this->display(); + } + + /** + * Configured a mini program. + * + * @param string $appID + * @access public + * @return void + */ + public function configuredMiniProgram($appID) + { + if(!is_numeric($appID)) return $this->locate('ai', 'createMiniProgram'); + $this->view->title = $this->lang->ai->miniPrograms->common; $this->display(); } diff --git a/module/ai/css/configuredminiprogram.css b/module/ai/css/configuredminiprogram.css new file mode 100644 index 0000000000..df8d123a55 --- /dev/null +++ b/module/ai/css/configuredminiprogram.css @@ -0,0 +1,9 @@ +.field-configuration .drag-area {cursor: move; text-align: right;} +.field-configuration .drag-area > div {display: inline-flex; align-items: center; gap: 4px;} +.options-container {display: flex; flex-direction: column; gap: 16px;} + +#mainContent {position: fixed; left: 20px; right: 20px; top: 70px; bottom: 0; display: flex; background: #f0f2f5; padding: 0;} +#mainContent > div {height: 100%; background: #fff; display: flex; flex-direction: column;} +#mainContent > div > header {background: #f8f8fa; height: 40px; display: flex; align-items: center; padding-left: 24px;} +#mainContent > div > header > strong {font-size: 16px;} +#mainContent > div > main {flex-grow: 1; overflow-y: auto;} diff --git a/module/ai/css/createminiprogram.css b/module/ai/css/createminiprogram.css new file mode 100644 index 0000000000..25600fa82a --- /dev/null +++ b/module/ai/css/createminiprogram.css @@ -0,0 +1,6 @@ +#ai-edit-icon {position: relative; overflow: hidden; display: flex; justify-content: center; align-items: center;} +#ai-edit-icon #edit-icon {display: none; position: absolute; inset: 0; background-color: rgba(230, 240, 255, 0.7);} +#ai-edit-icon:hover {border: 1px solid #EDEEF1;} +#ai-edit-icon:hover #edit-icon {display: flex; justify-content: center; align-items: center;} +#icon-buttons svg {cursor: pointer;} +.theme-checked {display: flex; justify-content: center; align-items: center;} diff --git a/module/ai/js/configuredminiprogram.js b/module/ai/js/configuredminiprogram.js new file mode 100644 index 0000000000..31282c3d36 --- /dev/null +++ b/module/ai/js/configuredminiprogram.js @@ -0,0 +1,348 @@ +let $editingField = null; + +/** + * Handle the field type change events. + * + * @param {Event} event + * @returns {void} + */ +function handleFieldTypeChange(event) +{ + const fieldType = $(event.target).prop('value'); + if(fieldType === 'text' || fieldType === 'textarea') + { + $('input[name="placeholder"]').closest('tr').removeClass('hidden'); + $('.field-options').addClass('hidden'); + revertOptions(); + } + else + { + revertOptions(); + $('input[name="placeholder"]').closest('tr').addClass('hidden'); + $('.field-options').removeClass('hidden'); + } +} + +/** + * Fix options order. + * + * @returns {void} + */ +function fixOptionsOrder() +{ + const forEach = function() + { + const index = $(this).children('span.input-group-addon').html().replace(/\d+$/, $(this).index() + 1); + $(this).children('span.input-group-addon').html(index); + } + $('.options-container').children().each(forEach); +} + +/** + * Handle the remove option button Click event. + * + * @param {Event} event + * @returns {void} + */ +function handleRemoveOptionClick(event) +{ + $(event.target).closest('.input-group').remove(); + fixOptionsOrder(); +} + +/** + * Handle the add option button Click event. + * + * @param {Event} event + * @returns {void} + */ +function handleAddOptionClick(event) +{ + const $newField = $(event.target).closest('.input-group').clone(); + $newField.find('input').prop('value', ''); + $(event.target).closest('.options-container').append($newField); + fixOptionsOrder(); +} + +/** + * Revert options. + * + * @returns {void} + */ +function revertOptions() +{ + const template = document.getElementById('option-template'); + const $optionsContainer = $('
'); + const $option = $(document.importNode(template.content, true)); + $optionsContainer.append($option); + const $optionStr = $optionsContainer.html(); + $optionsContainer.append($optionStr); + $optionsContainer.append($optionStr); + + $('.field-options') + .children('td') + .empty() + .append($optionsContainer); + + fixOptionsOrder(); +} + +/** + * Handle the new field button click event. + * + * @returns {void} + */ +function handleAddFieldClick() +{ + const $modal = $('#add-field-modal'); + const $modalBody = $modal.find('.modal-body'); + $modalBody.empty(); + const template = document.getElementById('field-template'); + const clone = document.importNode(template.content, true); + $modalBody.append(clone); + $('#add-field-modal .text-danger').remove(); + $modal.modal('show', 'fit'); +} + +/** + * Get the editing field. + * + * @param {*} $fieldTr + * @returns {[boolean, *]} + */ +function getEditingField($fieldTr) +{ + const name = $fieldTr.find('.field-name').html(); + const isTextType = $fieldTr.find('.field-type').length; + if(isTextType) + { + const type = $fieldTr.find('input.field-type').length ? 'text' : 'textarea'; + const placeholder = $fieldTr.find('input[type="text"]').prop('placeholder'); + const required = $fieldTr.find('input[type="text"]').closest('td').hasClass('required'); + return [true, {name, type, placeholder, required}]; + } + + const $picker = $fieldTr.find('.picker'); + const type = $picker.hasClass('picker-multi') ? 'checkbox' : 'radio'; + const options = $picker.attr('data-options').split(','); + const required = $picker.closest('td').hasClass('required'); + return [false, {name, type, options, required}]; +} + +/** + * Handle the edit field button click event. + * + * @param {Event} event + * @returns {void} + */ +function handleEditFieldClick(event) +{ + const $fieldTr = $(event.target).closest('tr'); + $editingField = $fieldTr; + const [isTextType, field] = getEditingField($fieldTr); + + const $modal = $('#edit-field-modal'); + const $modalBody = $modal.find('.modal-body'); + $modalBody.empty(); + const template = document.getElementById('field-template'); + const $clone = $(document.importNode(template.content, true)); + $modalBody.append($clone); + $modalBody.find('input[name="field-name"]').prop('value', field.name); + $modalBody.find('select[name="field-type"]').prop('value', field.type); + + if(isTextType) + { + $modalBody.find('input[name="placeholder"]').prop('value', field.placeholder); + $('input[name="placeholder"]').closest('tr').removeClass('hidden'); + $('.field-options').addClass('hidden'); + } + else + { + const $optionsWrapper = $modalBody.find('.field-options > td') + $optionsWrapper.empty(); + const $optionsContainer = $(`
`) + $optionsWrapper.append($optionsContainer); + + field.options.forEach((option) => + { + const template = document.getElementById('option-template'); + const $clone = $(document.importNode(template.content, true)); + $optionsContainer.append($clone); + $optionsContainer.children('.input-group').last().find('input[name="option[]"]').prop('value', option); + }); + + fixOptionsOrder(); + $('input[name="placeholder"]').closest('tr').addClass('hidden'); + $('.field-options').removeClass('hidden'); + } + + if(field.required) + { + $modalBody.find('input[value="1"]').attr('checked', 'checked'); + $modalBody.find('input[value="0"]').removeAttr('checked'); + } + else + { + $modalBody.find('input[value="0"]').attr('checked', 'checked'); + $modalBody.find('input[value="1"]').removeAttr('checked'); + } + $('#edit-field-modal .text-danger').remove(); + $modal.modal('show', 'fit'); +} + +/** + * Handle the remove field button click event. + * + * @param {Event} event + * @returns {void} + */ +function handleRemoveFieldClick(event) +{ + const result = window.confirm(deleteTip); + if(result) $(event.target).closest('tr').remove(); + const $button = $('.field-configuration-main > div'); + const $table = $('.field-configuration-main > table'); + if($button.hasClass('hidden')) $button.removeClass('hidden'); + if(!$table.hasClass('hidden')) $table.addClass('hidden'); +} + +/** + * Handle the save field button click event. + * + * @returns {void} + */ +function handleSaveFieldClick() +{ + const formData = new FormData(document.querySelector('#add-field-modal form')); + const name = formData.get('field-name'); + if(!name) + { + const template = document.getElementById('text-danger-template'); + const $clone = $(document.importNode(template.content, true)); + $('#add-field-modal input[name="field-name"]') + .closest('.required') + .append($clone); + return; + } + + $('#add-field-modal').modal('hide'); + const $fieldView = createFieldView(formData); + $('#sortable-list').append($fieldView); + const $button = $('.field-configuration-main > div'); + const $table = $('.field-configuration-main > table'); + if(!$button.hasClass('hidden')) $button.addClass('hidden'); + if($table.hasClass('hidden')) $table.removeClass('hidden'); +} + +/** + * Handle save edited field button click event. + * + * @returns {void} + */ +function handleSaveEditedFieldClick() +{ + const formData = new FormData(document.querySelector('#edit-field-modal form')); + const name = formData.get('field-name'); + if(!name) + { + const template = document.getElementById('text-danger-template'); + const $clone = $(document.importNode(template.content, true)); + $('#edit-field-modal input[name="field-name"]') + .closest('.required') + .append($clone); + return; + } + + $('#edit-field-modal').modal('hide'); + const $fieldView = createFieldView(formData); + $editingField.replaceWith($fieldView); +} + +/** + * Create field view. + * + * @param {FormData} formData + * @returns {*} + */ +function createFieldView(formData) +{ + const $tr = $(``); + $tr.append(createFieldNameView(formData)); + $tr.append(createFieldTypeView(formData)); + $tr.append(createFieldButtons(formData)); + return $tr; +} + +/** + * Create field name view. + * + * @param {FormData} formData + * @returns {*} + */ +function createFieldNameView(formData) +{ + const name = formData.get('field-name'); + return $(` +
+ + ${name} +
+ `); +} + +/** + * Create field type view. + * + * @param {FormData} formData + * @returns {*} + */ +function createFieldTypeView(formData) +{ + const type = formData.get('field-type'); + const required = formData.get('required') === '1'; + const $td = $(''); + if(required) $td.addClass('required'); + if(type === 'text') + { + const placeholder = formData.get('placeholder') || pleaseInput; + $td.append(``); + return $td; + } + + if(type === 'textarea') + { + const placeholder = formData.get('placeholder') || pleaseInput; + $td.append(`