promptmenu.html.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <?php
  2. /**
  3. * The ai prompt menu ui view file of ai module of ZenTaoPMS.
  4. *
  5. * This view file is used to print the prompt menu, acts just like header php files.
  6. * Prompt menus are generated with php and injected with javascript. A lot of hacking
  7. * went into this, so please don't touch it unless you know what you are doing.
  8. *
  9. * @copyright Copyright 2009-2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  10. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  11. * @author Wenrui LI <liwenrui@easycorp.ltd>
  12. * @package ai
  13. * @link https://www.zentao.net
  14. */
  15. namespace zin;
  16. h::css("
  17. .detail-view > .detail-body > .detail-main > .detail-sections:first-child > .detail-section {position: relative; z-index: 1;}
  18. .detail-view > .detail-body > .detail-main > .detail-sections:first-child > .detail-section:first-child {z-index: 2;}
  19. ");
  20. $generateAgents = function($agents)
  21. {
  22. $agentIds = array();
  23. $agentCodes = array();
  24. $agentList = explode(',', $agents);
  25. foreach($agentList as $agent)
  26. {
  27. $agent = trim($agent);
  28. if($agent === '') continue;
  29. if(strpos($agent, 'zt_') === 0)
  30. {
  31. $agentCodes[] = $agent;
  32. }
  33. elseif(is_numeric($agent))
  34. {
  35. $agentIds[] = $agent;
  36. }
  37. }
  38. if(!empty($agentCodes))
  39. {
  40. $codeAgents = $this->ai->getAgentsByCodes($agentCodes, 'active');
  41. foreach($codeAgents as $codeAgent) $agentIds[] = $codeAgent->id;
  42. }
  43. return $agentIds;
  44. };
  45. $promptMenuInject = function() use ($generateAgents)
  46. {
  47. if(isInModal()) return;
  48. $this->loadModel('ai');
  49. if(!commonModel::hasPriv('ai', 'promptExecute')) return;
  50. $this->app->loadLang('ai');
  51. $this->app->loadConfig('ai');
  52. $module = $this->app->getModuleName();
  53. $method = $this->app->getMethodName();
  54. $isDocApp = $module === 'doc' && $method === 'app';
  55. $useMethod = $method;
  56. if($isDocApp) $method = 'view';
  57. $showOnList = in_array($module, ['product', 'project', 'projectstory', 'execution']) && in_array($useMethod, ['browse', 'story', 'task']);
  58. $isOtherDoc = $isDocApp && $this->app->tab != 'doc';
  59. $showOnList = $showOnList || ($isOtherDoc);
  60. $isProjectStory = $module === 'product' && $useMethod === 'browse' && $this->app->tab == 'project';
  61. if($showOnList) $method = 'view';
  62. if($isOtherDoc || $isProjectStory)
  63. {
  64. $module = $this->app->tab;
  65. $useMethod = 'view';
  66. }
  67. if(!isset($this->config->ai->menuPrint->locations[$module][$method])) return;
  68. $menuOptions = $this->config->ai->menuPrint->locations[$module][$method];
  69. $prompts = $this->ai->getPromptsForUser($menuOptions->module);
  70. $prompts = $this->ai->filterPromptsForExecution($prompts, true);
  71. $btnName = $this->lang->ai->prompts->common;
  72. $promptIds = array_column($prompts, 'id');
  73. $canAssign = !empty($this->config->enableAITeammate) && hasPriv('aiteammate', 'assignagent') && $this->config->edition != 'open';
  74. $teammates = array();
  75. if(!empty($prompts) && $canAssign) $teammates = $this->loadModel('aiteammate')->browse('0');
  76. $showTeammates = array_filter($teammates, function($item) use ($promptIds, $generateAgents)
  77. {
  78. if(empty($item->agents)) return false;
  79. $agents = $generateAgents($item->agents);
  80. return !empty(array_intersect($promptIds, $agents));
  81. });
  82. if(!empty($showTeammates)) $showTeammates = array_values($showTeammates);
  83. $assignedBtnName = '';
  84. if($canAssign) $assignedBtnName = sprintf($this->lang->ai->promptMenu->assignedTo, $this->lang->aiteammate->common);
  85. if($isDocApp && $this->app->tab == 'doc')
  86. {
  87. h::globalJS('window.docAIPrompts = ' . json_encode($prompts) . ";\n", 'window.docAIPromptLang = ' . json_encode(array('dropdownTitle' => $btnName, 'statuses' => $this->lang->ai->prompts->statuses)) . ";\n", 'window.docAITeammates = ' . json_encode($showTeammates) . ";\n", 'window.docAITeammateLang = ' . json_encode(array('dropdownTitle' => $assignedBtnName, 'nameLabel' => $this->lang->ai->promptMenu->assignedTo)) . ";\n");
  88. return;
  89. }
  90. if($module === 'productplan' && $method === 'view')
  91. {
  92. /* 子计划不显示“拆分子计划智能体” */
  93. $plan = data('plan');
  94. if(is_object($plan) && $plan->parent > 0)
  95. {
  96. $prompts = array_filter($prompts, function($prompt)
  97. {
  98. return $prompt->module !== 'productplan' || $prompt->targetForm !== 'productplan.create';
  99. });
  100. }
  101. }
  102. if(empty($prompts)) return;
  103. $aiSuggestions = [];
  104. $isReleaseView = $module == 'release' && $method == 'view';
  105. $className = $isReleaseView ? 'mt-1' : '';
  106. $html = "<div class='flex gap-2 inline-block pull-right ml-2 $className'>";
  107. $objectVarName = empty($menuOptions->objectVarName) ? $menuOptions->module : $menuOptions->objectVarName;
  108. $currentObjectId = !empty($this->view->$objectVarName) ? $this->view->$objectVarName->id : 0;
  109. $pageInfo = "$module,$method";
  110. if(!$showOnList)
  111. {
  112. $html .= '<div class="prompts dropdown' . ((isset($menuOptions->class) ? ' ' . $menuOptions->class : '') . (isset($menuOptions->dropdownClass) ? ' ' . $menuOptions->dropdownClass : '')) . '"><button class="btn ai-styled size-sm size-sm font-medium' . (isset($menuOptions->buttonClass) ? ' ' . $menuOptions->buttonClass : '') . '" type="button" data-toggle="dropdown" data-placement="' . zget($menuOptions, 'buttonPlacement', 'bottom-end') . '"><i class="icon icon-lightning"></i>' . $btnName . '<span class="caret-down"></span></button><menu class="dropdown-menu menu">';
  113. foreach($prompts as $prompt)
  114. {
  115. $html .= '<li class="menu-item">';
  116. $html .= html::a
  117. (
  118. helper::createLink('ai', 'promptExecute', "promptId=$prompt->id&objectId=$currentObjectId&auto=0"),
  119. $prompt->name . ($prompt->status != 'active' ? '<span class="label size-sm gray-500-pale ring-gray-500" style="margin-left: 4px; white-space: nowrap;">' . $this->lang->ai->prompts->statuses[$prompt->status] . '</span>' : ''),
  120. '',
  121. "class='prompt ajax-submit' style='width: 100%;'" . (empty($prompt->unauthorized) ? '' : ' disabled') . (empty($prompt->desc) ? '' : " title='$prompt->desc'"),
  122. 'btn ghost size-sm font-medium text-left'
  123. );
  124. $html .= '</li>';
  125. }
  126. $html .= '</menu></div>';
  127. if($canAssign && !empty($showTeammates))
  128. {
  129. $html .= '<div class="prompts dropdown inline-block"><button class="btn ai-styled size-sm size-sm font-medium" type="button" data-toggle="dropdown" data-placement="' . zget($menuOptions, 'buttonPlacement', 'bottom-end') . '"><i class="icon icon-hand-right"></i>' . $assignedBtnName . '<span class="caret-down"></span></button><menu class="dropdown-menu menu">';
  130. foreach($showTeammates as $teammate)
  131. {
  132. $avatar = html::avatar(array('avatar' => $teammate->avatar, 'account' => $teammate->name), '20', 'rounded-full');
  133. $name = sprintf($this->lang->ai->promptMenu->assignedTo, $teammate->name);
  134. $html .= '<li class="menu-item">';
  135. $html .= html::a
  136. (
  137. helper::createLink('aiteammate', 'assignagent', "teammateID=$teammate->id&objectType=$objectVarName&objectID=$currentObjectId&pageInfo=$pageInfo"),
  138. $avatar . $name,
  139. '',
  140. "style='justify-content: flex-start;' data-placement='left' data-toggle='modal' data-size='sm' title='$name'",
  141. 'btn ghost size-sm font-medium text-left'
  142. );
  143. $html .= '</li>';
  144. }
  145. $html .= '</menu></div>';
  146. }
  147. $html .= '</div>';
  148. }
  149. else
  150. {
  151. $page = "$module-$useMethod";
  152. $objectID = $currentObjectId;
  153. if($isDocApp)
  154. {
  155. $objectID = $this->view->objectID;
  156. $page = "doc-app";
  157. }
  158. if($isProjectStory) $page = "product-browse";
  159. if($canAssign && !empty($showTeammates))
  160. {
  161. foreach($showTeammates as $teammate)
  162. {
  163. $name = sprintf($this->lang->ai->promptMenu->assignedTo, $teammate->name);
  164. $url = helper::createLink('aiteammate', 'assignagent', "teammateID=$teammate->id&objectType=$objectVarName&objectID=$objectID&pageInfo=$pageInfo&from=global");
  165. $btnProps = (object)['data-url' => $url, 'data-toggle' => 'modal', 'data-size' => 'sm'];
  166. $aiSuggestions[] = (object)['id' => "zt_teammate_$teammate->id", 'title' => $name, 'hint' => $teammate->desc, 'page' => $page, 'btnProps' => $btnProps];
  167. }
  168. }
  169. foreach($prompts as $prompt)
  170. {
  171. $zentaoAgent = (object)['agentID' => $prompt->id, 'objectID' => $objectID];
  172. $aiSuggestions[] = (object)['id' => "zt_agent_$prompt->id", 'title' => $prompt->name, 'hint' => $prompt->desc, 'page' => $page, 'zentaoAgent' => $zentaoAgent];
  173. }
  174. $html = '';
  175. }
  176. /* Assemble injector script. */
  177. $script = <<<JAVASCRIPT
  178. (() => {
  179. if(!window.top.zai) return;
  180. const container = window.frameElement?.closest('.load-indicator');
  181. if(container && container.dataset.loading)
  182. {
  183. delete container.dataset.loading;
  184. container.classList.remove('loading');
  185. container.classList.remove('no-delay');
  186. }
  187. JAVASCRIPT;
  188. $script .= 'window.aiSuggestions =' . json_encode($aiSuggestions) . ';';
  189. $script .= 'let $aiMenu = $("' . $menuOptions->targetContainer . '").first();';
  190. $script .= 'if(!$aiMenu.length) $aiMenu = $("#mainContent .ai-menu-box").empty();';
  191. $script .= 'else $aiMenu.find(".prompts.dropdown").remove();';
  192. $script .= '$aiMenu.' . (!empty($menuOptions->injectMethod) ? $menuOptions->injectMethod : 'append') . "(`$html`).css('z-index', 20);\n";
  193. $script .= <<<JAVASCRIPT
  194. \$('[data-toggle="popover"]').popover({template: '<div class="popover"><h3 class="popover-title"></h3><div class="popover-content"></div></div>'});
  195. JAVASCRIPT;
  196. $script .= count($prompts) > 1 ? "$('.prompts.dropdown .dropdown-menu').on('click', 'a', e =>" : "$('.prompt').on('click', e =>";
  197. $script .= <<<JAVASCRIPT
  198. {
  199. if(!container) return;
  200. container.dataset.loading = e.target.querySelector('.label') ? '{$this->lang->ai->execute->auditing}' : '{$this->lang->ai->execute->loading}';
  201. container.classList.add('loading');
  202. container.classList.add('no-delay');
  203. /* Checks for session storage to cancel loading status (see inputinject.html.php). */
  204. sessionStorage.removeItem('ai-prompt-data-injected');
  205. const loadCheckInterval = setInterval(() =>
  206. {
  207. if(sessionStorage.getItem('ai-prompt-data-injected'))
  208. {
  209. if(container && container.dataset.loading)
  210. {
  211. delete container.dataset.loading;
  212. container.classList.remove('loading');
  213. container.classList.remove('no-delay');
  214. }
  215. sessionStorage.removeItem('ai-prompt-data-injected');
  216. clearInterval(loadCheckInterval);
  217. }
  218. }, 200);
  219. });
  220. })();
  221. JAVASCRIPT;
  222. /* Perform injection. */
  223. if(isset($menuOptions->stylesheet)) pageCSS($menuOptions->stylesheet);
  224. h::globalJS($script);
  225. };
  226. $promptMenuInject();