inputinject.html.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. /**
  3. * The ai input inject ui view file of ai module of ZenTaoPMS.
  4. *
  5. * This view file is used to inject ai generated result into
  6. * input fields. Include this view file in the target forms.
  7. *
  8. * @copyright Copyright 2009-2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  9. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  10. * @author Wenrui LI <liwenrui@easycorp.ltd>
  11. * @package ai
  12. * @link https://www.zentao.net
  13. */
  14. namespace zin;
  15. $this->app->loadLang('ai');
  16. $this->app->loadConfig('ai');
  17. $module = $this->app->getModuleName();
  18. $method = $this->app->getMethodName();
  19. /* Inject audit menu, called from within $inputInject(), be aware of javascript assembling abuse. */
  20. $auditInject = function() use($module, $method)
  21. {
  22. if(!isset($_SESSION['aiPrompt']['prompt']) || empty($_SESSION['aiPrompt']['objectId'])) return;
  23. $prompt = $_SESSION['aiPrompt']['prompt'];
  24. $objectId = $_SESSION['aiPrompt']['objectId'];
  25. $isAudit = isset($_SESSION['auditPrompt']) && time() - $_SESSION['auditPrompt']['time'] < 10 * 60;
  26. $auditScript = '';
  27. if(isset($this->config->ai->injectAuditButton->locations[$module][$method]))
  28. {
  29. $publishBtn = html::commonButton($this->lang->ai->promptPublish, "id='promptPublish' data-promptId=$prompt->id", 'btn btn-primary btn-wide ajax-submit');
  30. $targetContainer = $this->config->ai->injectAuditButton->locations[$module][$method]['action']->targetContainer;
  31. $injectMethod = $this->config->ai->injectAuditButton->locations[$module][$method]['action']->injectMethod;
  32. $auditScript = <<<JAVASCRIPT
  33. window.injectAuditAction = () =>
  34. {
  35. JAVASCRIPT;
  36. if($module == 'doc')
  37. {
  38. $actionContainerStyles = empty($this->config->ai->injectAuditButton->locations[$module][$method]['action']->containerStyles) ? '{}' : $this->config->ai->injectAuditButton->locations[$module][$method]['action']->containerStyles;
  39. $exitAuditButton = html::commonButton($this->lang->ai->audit->exit, "id='promptAuditExit'", 'btn');
  40. $auditScript .= <<<JAVASCRIPT
  41. const actionContainerStyles = JSON.parse('{$actionContainerStyles}');
  42. \$('#mainContent .titleBox').children('button,a').remove()
  43. \$('#mainContent .titleBox').append(`{$publishBtn}`);
  44. \$('#mainContent .titleBox').css(actionContainerStyles);
  45. \$('#mainContent .titleBox').prepend(`{$exitAuditButton}`);
  46. JAVASCRIPT;
  47. }
  48. else
  49. {
  50. $exitAuditButton = html::commonButton($this->lang->ai->audit->exit, "id='promptAuditExit'", 'btn btn-wide');
  51. $auditScript .= <<<JAVASCRIPT
  52. const injectHTML = `{$publishBtn} {$exitAuditButton}`;
  53. \$(`{$targetContainer}`)[`{$injectMethod}`](injectHTML);
  54. JAVASCRIPT;
  55. }
  56. $auditScript .= <<<JAVASCRIPT
  57. };
  58. window.injectAuditToolbar = () =>
  59. {
  60. JAVASCRIPT;
  61. $regenButton = html::linkButton('<i class="icon icon-refresh muted"></i> ' . $this->lang->ai->audit->regenerate, helper::createLink('ai', 'promptexecute', "promptId=$prompt->id&objectId=$objectId"), 'self', "id='promptRegenerate'", 'btn ghost');
  62. $auditButton = html::commonButton($this->lang->ai->audit->designPrompt, 'id="promptAudit" data-toggle="modal" data-type="iframe" data-url="' . helper::createLink('ai', 'promptaudit', "promptId=$prompt->id&objectId=$objectId") . '"', 'btn btn-info iframe');
  63. $targetContainer = $this->config->ai->injectAuditButton->locations[$module][$method]['toolbar']->targetContainer;
  64. $injectMethod = $this->config->ai->injectAuditButton->locations[$module][$method]['toolbar']->injectMethod;
  65. $buttonHTML = $isAudit ? "<div class='btn-group'>$regenButton $auditButton</div>" : $regenButton;
  66. $auditScript .= <<<JAVASCRIPT
  67. const buttonHTML = `{$buttonHTML}`;
  68. JAVASCRIPT;
  69. $toolbarContainerStyles = empty($this->config->ai->injectAuditButton->locations[$module][$method]['toolbar']->containerStyles) ? '{}' : $this->config->ai->injectAuditButton->locations[$module][$method]['toolbar']->containerStyles;
  70. $auditScript .= "$(`$targetContainer`).first().$injectMethod(buttonHTML);";
  71. $auditScript .= <<<JAVASCRIPT
  72. \$('#promptAudit').attr('data-size', '{"height": 600}');
  73. const toolbarContainerStyles = JSON.parse('{$toolbarContainerStyles}');
  74. \$(`{$targetContainer}`).css(toolbarContainerStyles);
  75. };
  76. JAVASCRIPT;
  77. if($isAudit) $auditScript .= 'window.injectAuditAction();';
  78. $auditScript .= "if(typeof window.injectData !== 'undefined') {window.injectAuditToolbar();}";
  79. $loadingText = $this->lang->ai->execute->loading;
  80. $auditScript .= <<<JAVASCRIPT
  81. const publishButton = document.getElementById('promptPublish');
  82. if(publishButton)
  83. {
  84. \$(document).off('click', '#promptPublish').on('click', '#promptPublish', function(e)
  85. {
  86. e.preventDefault();
  87. const promptId = publishButton.dataset.promptid;
  88. const publishLink = document.createElement('a');
  89. publishLink.href = \$.createLink('ai', 'promptPublish', 'promptId=' + promptId + '&backToTestingLocation=true') + '#app=admin';
  90. publishLink.className = 'ajax-submit';
  91. publishLink.style.display = 'none';
  92. document.body.appendChild(publishLink);
  93. if(\$.apps.currentCode != 'admin')
  94. {
  95. window.addEventListener('popstate', function()
  96. {
  97. \$.apps.closeApp(\$.apps.currentCode);
  98. });
  99. }
  100. publishLink.click();
  101. });
  102. }
  103. const auditExitButton = document.getElementById('promptAuditExit');
  104. if(auditExitButton)
  105. {
  106. \$(document).off('click', '#promptAuditExit').on('click', '#promptAuditExit', function(e)
  107. {
  108. e.preventDefault();
  109. const promptId = publishButton.dataset.promptid;
  110. const exitLink = document.createElement('a');
  111. exitLink.href = \$.createLink('ai', 'promptAudit', 'promptId=' + promptId + '&objectId=0' + '&exit=true') + '#app=admin';
  112. exitLink.style.display = 'none';
  113. document.body.appendChild(exitLink);
  114. exitLink.click();
  115. /* TODO: find a way to close app. */
  116. if(\$.apps.currentCode != 'admin') \$.apps.closeApp(\$.apps.currentCode);
  117. });
  118. }
  119. const regenButton = document.getElementById('promptRegenerate');
  120. if(regenButton)
  121. {
  122. regenButton.addEventListener('click', e =>
  123. {
  124. \$('body').attr('data-loading', `{$loadingText}`);
  125. \$('body').addClass('load-indicator loading');
  126. window.sessionStorage.setItem('ai-prompt-regen', true);
  127. });
  128. }
  129. JAVASCRIPT;
  130. }
  131. h::globalJS("(() => {requestAnimationFrame(() => {{$auditScript}});})();");
  132. };
  133. /* Inject input data. */
  134. $inputInject = function() use($module, $method, &$auditInject)
  135. {
  136. if(!isset($this->config->ai->availableForms[$module]) || !in_array($method, $this->config->ai->availableForms[$module])) return;
  137. if(isset($_SESSION['aiInjectData']) && isset($_SESSION['aiInjectData'][$module]) && isset($_SESSION['aiInjectData'][$module][$method])) $injectData = $_SESSION['aiInjectData'][$module][$method];
  138. if(empty($injectData)) return;
  139. jsVar('window.injectData', $injectData);
  140. unset($_SESSION['aiInjectData'][$module]);
  141. $this->app->loadLang('ai');
  142. h::globalJS(<<<JAVASCRIPT
  143. (() => {
  144. window.injectToInputElement = (inputName, data, index, tries = 0) =>
  145. {
  146. let name = inputName;
  147. if(typeof index !== 'undefined') name = name + '[' + index + ']';
  148. const inputEl = \$('[name="' + name + '"]');
  149. /* Retry if input is not found, sometimes form renders late. */
  150. if(!inputEl.length && tries < 5) return setTimeout(() => window.injectToInputElement(inputName, data, index, ++tries), 1000);
  151. const inputType = inputEl.prop('nodeName');
  152. switch(inputType) // Contains case fallthroughs, on purpose.
  153. {
  154. case 'ZEN-EDITOR':
  155. /* Set Zen Editor content with its exposed method, loop till editor is ready. */
  156. const zenEditorEl = inputEl.get().pop();
  157. const setZenEditorData = () =>
  158. {
  159. try
  160. {
  161. zenEditorEl.setHTML(data).catch(_ => setTimeout(setZenEditorData, 100));
  162. }
  163. catch(_)
  164. {
  165. setTimeout(setZenEditorData, 100);
  166. }
  167. };
  168. setZenEditorData();
  169. break;
  170. case 'INPUT':
  171. /* Watch out for zui date pickers. */
  172. if(inputEl.hasClass('pick-value'))
  173. {
  174. if(inputEl.parent().hasClass('date-picker'))
  175. {
  176. if(inputEl.parent().parent().data().hasOwnProperty('zuiDatetimepicker'))
  177. {
  178. inputEl.zui('dateTimePicker').\$.setValue(data);
  179. break;
  180. }
  181. if(inputEl.parent().parent().data().hasOwnProperty('zuiDatepicker'))
  182. {
  183. inputEl.zui('datePicker').\$.setValue(data);
  184. break;
  185. }
  186. }
  187. }
  188. case 'TEXTAREA':
  189. /* Textareas might be controlled by KindEditors. */
  190. if(typeof KindEditor !== 'undefined' && KindEditor.instances.length)
  191. {
  192. const editorInstance = KindEditor.instances.find(e => e.srcElement.attr('name') == name);
  193. if(editorInstance)
  194. {
  195. if(data === null) data = '';
  196. /* Set editor content and sync with textarea. */
  197. editorInstance.html(data);
  198. editorInstance.sync();
  199. break;
  200. }
  201. }
  202. default:
  203. /* For normal inputs, just set the value is enough. */
  204. inputEl.val(data);
  205. break;
  206. }
  207. }
  208. window.inject = (obj) =>
  209. {
  210. for(const key in obj)
  211. {
  212. window.dealWithSpecialInputType(key, obj[key]);
  213. if(Array.isArray(obj[key]))
  214. {
  215. const isStartWith0 = \$('[name="' + key + '[0]' + '"]').length;
  216. const arr = obj[key];
  217. for(let i = 0; i < arr.length; i++)
  218. {
  219. for(const key in arr[i])
  220. {
  221. window.injectToInputElement(key, arr[i][key], isStartWith0 ? i : i + 1);
  222. }
  223. }
  224. }
  225. else if(typeof obj[key] === 'Object')
  226. {
  227. inject(obj[key]);
  228. }
  229. else
  230. {
  231. window.injectToInputElement(key, obj[key])
  232. }
  233. }
  234. }
  235. window.dealWithSpecialInputType = (key, value) =>
  236. {
  237. if(key === 'steps')
  238. {
  239. const steps = document.getElementById('steps');
  240. if(!steps) return;
  241. if(value.length > steps.children.length)
  242. {
  243. const gap = value.length - steps.children.length;
  244. for(let i = 0; i < gap; i++)
  245. {
  246. const addButton = document.querySelector('#steps > tr:last-child > .step-actions button:first-child');
  247. if(!addButton) return;
  248. addButton.click();
  249. }
  250. }
  251. }
  252. }
  253. requestAnimationFrame(() =>
  254. {
  255. try
  256. {
  257. const data = JSON.parse(window.injectData);
  258. if(!data) return;
  259. window.inject(data);
  260. zui.Messager.show({content: '{$this->lang->ai->dataInject->success}', type: 'success'});
  261. }
  262. catch(e)
  263. {
  264. zui.Messager.show({content:'{$this->lang->ai->dataInject->fail}', type: 'danger'});
  265. console.error(e);
  266. }
  267. finally
  268. {
  269. /* Set injected in oreder to cancel loading class on object view (see ./promptmenu.html.php). */
  270. sessionStorage.setItem('ai-prompt-data-injected', true);
  271. const container = window.frameElement?.closest('.load-indicator');
  272. if(container && container.dataset.loading)
  273. {
  274. sessionStorage.removeItem('ai-prompt-data-injected');
  275. delete container.dataset.loading;
  276. container.classList.remove('loading');
  277. container.classList.remove('no-delay');
  278. }
  279. }
  280. });
  281. })();
  282. JAVASCRIPT
  283. );
  284. $auditInject();
  285. };
  286. $inputInject();