index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /**
  2. * ChatGPT工具
  3. * @author zhaoxianlie
  4. */
  5. import AI from './fh.ai.js';
  6. new Vue({
  7. el: '#pageContainer',
  8. data: {
  9. prompt: '',
  10. demos: [
  11. '用Js写一个冒泡排序的Demo',
  12. 'Js里的Fetch API是怎么用的',
  13. '帮我写一个单网页版的俄罗斯方块游戏',
  14. '我开发了一个浏览器插件,是专门为HR自动找简历的,现在请你帮我用SVG绘制一个插件的ICON,不需要问我细节,直接生成'
  15. ],
  16. initMessage: {
  17. id:'id-test123',
  18. sendTime:'2022/12/20 12:12:12',
  19. message: '你好,可以告诉我你是谁吗?我该怎么和你沟通?',
  20. respTime: '2022/12/20 12:12:13',
  21. respContent: '你好,我是FeHelper智能助理,由OpenAI提供技术支持;你可以在下面的输入框向我提问,我会尽可能回答你~~~'
  22. },
  23. respResult:{
  24. id: '',
  25. sendTime:'',
  26. message:'',
  27. respTime:'',
  28. respContent:''
  29. },
  30. currentSession: [],
  31. history:[],
  32. tempId:'',
  33. hideDemo: false,
  34. undergoing: false,
  35. messages: [],
  36. showHistoryPanel: false
  37. },
  38. computed: {
  39. groupedHistory() {
  40. // 按日期分组,主题为message前20字
  41. const groups = {};
  42. this.history.forEach(item => {
  43. const date = item.sendTime ? item.sendTime.split(' ')[0] : '未知日期';
  44. if (!groups[date]) groups[date] = [];
  45. groups[date].push({
  46. ...item,
  47. theme: item.message ? item.message.slice(0, 20) : ''
  48. });
  49. });
  50. return groups;
  51. }
  52. },
  53. watch: {
  54. history: {
  55. handler(val) {
  56. localStorage.setItem('fh-aiagent-history', JSON.stringify(val));
  57. },
  58. deep: true
  59. }
  60. },
  61. mounted: function () {
  62. this.$refs.prompt.focus();
  63. this.hideDemo = !!(new URL(location.href)).searchParams.get('hideDemo');
  64. // 加载本地历史
  65. const local = localStorage.getItem('fh-aiagent-history');
  66. if(local){
  67. try {
  68. this.history = JSON.parse(local);
  69. } catch(e) {}
  70. }
  71. this.loadPatchHotfix();
  72. },
  73. methods: {
  74. loadPatchHotfix() {
  75. // 页面加载时自动获取并注入页面的补丁
  76. chrome.runtime.sendMessage({
  77. type: 'fh-dynamic-any-thing',
  78. thing: 'fh-get-tool-patch',
  79. toolName: 'aiagent'
  80. }, patch => {
  81. if (patch) {
  82. if (patch.css) {
  83. const style = document.createElement('style');
  84. style.textContent = patch.css;
  85. document.head.appendChild(style);
  86. }
  87. if (patch.js) {
  88. try {
  89. if (window.evalCore && window.evalCore.getEvalInstance) {
  90. window.evalCore.getEvalInstance(window)(patch.js);
  91. }
  92. } catch (e) {
  93. console.error('aiagent补丁JS执行失败', e);
  94. }
  95. }
  96. }
  97. });
  98. },
  99. // 这个代码,主要用来判断大模型返回的内容是不是包含完整的代码块
  100. validateCodeBlocks(content) {
  101. let backticksCount = 0;
  102. let inCodeBlock = false;
  103. let codeBlockStartIndex = -1;
  104. for (let i = 0; i < content.length; i++) {
  105. // 检查当前位置是否是三个连续的反引号
  106. if (content.startsWith('```', i)) {
  107. backticksCount++;
  108. i += 2; // 跳过接下来的两个字符,因为它们也是反引号的一部分
  109. // 如果我们遇到了奇数个反引号序列,那么我们进入了代码块
  110. if (backticksCount % 2 !== 0) {
  111. inCodeBlock = true;
  112. codeBlockStartIndex = i - 2;
  113. } else { // 否则,我们离开了代码块
  114. inCodeBlock = false;
  115. if (codeBlockStartIndex === -1 || codeBlockStartIndex > i) {
  116. return false; // 这意味着有不匹配的反引号
  117. }
  118. codeBlockStartIndex = -1;
  119. }
  120. }
  121. }
  122. // 如果最终 backticksCount 是偶数,则所有代码块都正确关闭
  123. return backticksCount % 2 === 0 && !inCodeBlock;
  124. },
  125. sendMessage(prompt){
  126. if(this.undergoing) return;
  127. if(this.respResult.id){
  128. // 先存储上一轮对话到历史
  129. this.history.push({
  130. id: this.respResult.id,
  131. sendTime: this.respResult.sendTime,
  132. message: this.respResult.message,
  133. respTime: this.respResult.respTime,
  134. respContent: this.respResult.respContent
  135. });
  136. }
  137. this.undergoing = true;
  138. let sendTime = (new Date()).format('yyyy/MM/dd HH:mm:ss');
  139. this.$nextTick(() => {
  140. this.scrollToBottom();
  141. });
  142. this.tempId = '';
  143. let respContent = '';
  144. // 1. 先把用户输入 push 到 messages
  145. this.messages.push({ role: 'user', content: prompt });
  146. // 新增:用户消息push到currentSession
  147. this.currentSession.push({
  148. role: 'user',
  149. id: 'user-' + Date.now(),
  150. time: sendTime,
  151. content: prompt
  152. });
  153. AI.askCoderLLM(this.messages, (respJson, done) => {
  154. if(done){
  155. this.undergoing = false;
  156. if(this.respResult.id && this.respResult.respContent){
  157. const tempDiv = document.createElement('div');
  158. tempDiv.innerHTML = this.respResult.respContent;
  159. const plainText = tempDiv.textContent || tempDiv.innerText || '';
  160. this.messages.push({ role: 'assistant', content: plainText });
  161. this.history.push({
  162. id: this.respResult.id,
  163. sendTime: this.respResult.sendTime,
  164. message: this.respResult.message,
  165. respTime: this.respResult.respTime,
  166. respContent: this.respResult.respContent
  167. });
  168. }
  169. this.$nextTick(() => {
  170. document.querySelectorAll('.x-xcontent pre code').forEach((block) => {
  171. hljs.highlightBlock(block);
  172. insertCodeToolbar(block);
  173. });
  174. this.scrollToBottom();
  175. });
  176. return;
  177. }
  178. let id = respJson.id;
  179. let rawContent = respJson.content || '';
  180. // 检查多轮代码补全场景
  181. const lastAssistantMsg = this.currentSession.slice().reverse().find(m => m.role === 'assistant');
  182. const lastIsCodeBlock = lastAssistantMsg && /```\s*$/.test(lastAssistantMsg.content.trim());
  183. const thisIsCodeBlock = /^```/.test(rawContent.trim());
  184. if (lastIsCodeBlock && !thisIsCodeBlock) {
  185. rawContent = '```js\n' + rawContent.trim() + '\n```';
  186. }
  187. respContent = rawContent;
  188. if(!this.validateCodeBlocks(respContent)) {
  189. respContent += '\n```';
  190. }
  191. respContent = marked(respContent);
  192. if(this.tempId !== id) {
  193. this.tempId = id;
  194. let dateTime = new Date(respJson.created * 1000);
  195. let respTime = dateTime.format('yyyy/MM/dd HH:mm:ss');
  196. this.respResult = { id,sendTime,message:prompt,respTime,respContent };
  197. // 新增:助手回复push到currentSession
  198. this.currentSession.push({
  199. role: 'assistant',
  200. id,
  201. time: respTime,
  202. content: respContent
  203. });
  204. }else{
  205. this.respResult.respContent = respContent;
  206. // 更新最后一条助手消息内容
  207. if(this.currentSession.length && this.currentSession[this.currentSession.length-1].role==='assistant'){
  208. this.currentSession[this.currentSession.length-1].content = respContent;
  209. }
  210. }
  211. this.$nextTick(() => this.scrollToBottom());
  212. });
  213. },
  214. scrollToBottom(){
  215. this.$refs.boxResult.scrollTop = this.$refs.boxResult.scrollHeight;
  216. },
  217. goChat(){
  218. this.sendMessage(this.prompt);
  219. this.$nextTick(() => this.prompt='');
  220. },
  221. openOptionsPage: function(event) {
  222. event.preventDefault();
  223. event.stopPropagation();
  224. chrome.runtime.openOptionsPage();
  225. },
  226. openDonateModal: function(event ){
  227. event.preventDefault();
  228. event.stopPropagation();
  229. chrome.runtime.sendMessage({
  230. type: 'fh-dynamic-any-thing',
  231. thing: 'open-donate-modal',
  232. params: { toolName: 'aiagent' }
  233. });
  234. },
  235. loadHistory(item) {
  236. // 渲染到主面板
  237. this.respResult = {
  238. id: item.id,
  239. sendTime: item.sendTime,
  240. message: item.message,
  241. respTime: item.respTime,
  242. respContent: item.respContent
  243. };
  244. this.showHistoryPanel = false;
  245. this.$nextTick(() => this.scrollToBottom());
  246. },
  247. startNewChat(event) {
  248. event && event.preventDefault();
  249. this.messages = [];
  250. this.respResult = {
  251. id: '',
  252. sendTime: '',
  253. message: '',
  254. respTime: '',
  255. respContent: ''
  256. };
  257. this.currentSession = [];
  258. this.showHistoryPanel = false;
  259. this.$nextTick(() => {
  260. this.$forceUpdate();
  261. this.scrollToBottom();
  262. });
  263. },
  264. onHistoryClick(event) {
  265. event.preventDefault();
  266. event.stopPropagation();
  267. this.showHistoryPanel = !this.showHistoryPanel;
  268. },
  269. onPromptKeydown(e) {
  270. if (e.key === 'Enter') {
  271. if (e.shiftKey) {
  272. // 允许换行
  273. return;
  274. } else {
  275. // 阻止默认换行,发送消息
  276. e.preventDefault();
  277. this.goChat();
  278. }
  279. }
  280. }
  281. }
  282. });
  283. // 工具函数:复制和运行
  284. function copyCode(code) {
  285. if (navigator.clipboard) {
  286. navigator.clipboard.writeText(code);
  287. } else {
  288. const textarea = document.createElement('textarea');
  289. textarea.value = code;
  290. document.body.appendChild(textarea);
  291. textarea.select();
  292. document.execCommand('copy');
  293. document.body.removeChild(textarea);
  294. }
  295. }
  296. // 新增:为代码块插入工具栏
  297. function insertCodeToolbar(block) {
  298. // 检查是否已插入按钮,避免重复
  299. if (block.parentNode.querySelector('.fh-code-toolbar')) return;
  300. // 创建工具栏
  301. const toolbar = document.createElement('div');
  302. toolbar.className = 'fh-code-toolbar';
  303. toolbar.style.cssText = 'position:absolute;bottom:6px;right:12px;z-index:10;display:flex;gap:8px;';
  304. // 复制按钮
  305. const btnCopy = document.createElement('button');
  306. btnCopy.innerText = '复制';
  307. btnCopy.className = 'fh-btn-copy';
  308. btnCopy.style.cssText = 'padding:2px 8px;font-size:12px;cursor:pointer;';
  309. btnCopy.onclick = (e) => {
  310. e.stopPropagation();
  311. copyCode(block.innerText);
  312. // 复制成功反馈
  313. const oldText = btnCopy.innerText;
  314. btnCopy.innerText = '已复制';
  315. btnCopy.disabled = true;
  316. setTimeout(() => {
  317. btnCopy.innerText = oldText;
  318. btnCopy.disabled = false;
  319. }, 1000);
  320. };
  321. // 运行按钮
  322. const lang = (block.className || '').toLowerCase();
  323. let btnRun = document.createElement('button');
  324. btnRun.className = 'fh-btn-run';
  325. btnRun.style.cssText = 'padding:2px 8px;font-size:12px;cursor:pointer;';
  326. let shouldAppendBtnRun = true;
  327. if (lang.includes('lang-javascript') || lang.includes('lang-js')) {
  328. btnRun.innerText = 'Console运行';
  329. btnRun.onclick = (e) => {
  330. e.stopPropagation();
  331. copyCode(block.innerText);
  332. btnRun.innerText = '已复制到剪贴板';
  333. btnRun.disabled = true;
  334. setTimeout(() => {
  335. btnRun.innerText = 'Console运行';
  336. btnRun.disabled = false;
  337. }, 1200);
  338. showToast('代码已复制到剪贴板,请按F12打开开发者工具,切换到Console粘贴回车即可运行!');
  339. };
  340. } else if (lang.includes('lang-html')) {
  341. btnRun.innerText = '下载并运行';
  342. btnRun.onclick = (e) => {
  343. e.stopPropagation();
  344. const blob = new Blob([block.innerText], {type: 'text/html'});
  345. const url = URL.createObjectURL(blob);
  346. const a = document.createElement('a');
  347. a.href = url;
  348. a.download = 'fehelper-demo.html';
  349. document.body.appendChild(a);
  350. a.click();
  351. document.body.removeChild(a);
  352. URL.revokeObjectURL(url);
  353. btnRun.innerText = '已下载';
  354. btnRun.disabled = true;
  355. setTimeout(() => {
  356. btnRun.innerText = '下载并运行';
  357. btnRun.disabled = false;
  358. }, 1200);
  359. showToast('HTML文件已下载,请双击打开即可运行!');
  360. };
  361. } else if (lang.includes('lang-xml') || lang.includes('lang-svg')) {
  362. // 检查内容是否为svg
  363. const codeText = block.innerText.trim();
  364. if (/^<svg[\s\S]*<\/svg>$/.test(codeText)) {
  365. btnRun.innerText = '点击预览';
  366. btnRun.onclick = (e) => {
  367. e.stopPropagation();
  368. // 弹窗预览svg
  369. const modal = document.createElement('div');
  370. modal.style.cssText = 'position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);z-index:999999;display:flex;align-items:center;justify-content:center;';
  371. const inner = document.createElement('div');
  372. inner.style.cssText = 'background:#fff;width:400px;height:400px;border-radius:10px;box-shadow:0 2px 16px rgba(0,0,0,0.18);position:relative;display:flex;align-items:center;justify-content:center;';
  373. const closeBtn = document.createElement('button');
  374. closeBtn.innerText = '×';
  375. closeBtn.style.cssText = 'position:absolute;top:8px;right:12px;width:32px;height:32px;font-size:22px;line-height:28px;background:transparent;border:none;cursor:pointer;color:#888;z-index:2;';
  376. closeBtn.onclick = () => document.body.removeChild(modal);
  377. const img = document.createElement('img');
  378. img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(codeText)));
  379. img.alt = 'SVG预览';
  380. img.style.cssText = 'max-width:90%;max-height:90%;display:block;border:1px solid #eee;background:#fafbfc;';
  381. inner.appendChild(closeBtn);
  382. inner.appendChild(img);
  383. modal.appendChild(inner);
  384. document.body.appendChild(modal);
  385. };
  386. } else {
  387. btnRun.remove();
  388. shouldAppendBtnRun = false;
  389. }
  390. } else {
  391. btnRun.remove();
  392. shouldAppendBtnRun = false;
  393. }
  394. toolbar.appendChild(btnCopy);
  395. if (shouldAppendBtnRun) {
  396. toolbar.appendChild(btnRun);
  397. }
  398. // 让pre相对定位,插入工具栏到底部
  399. const pre = block.parentNode;
  400. pre.style.position = 'relative';
  401. pre.appendChild(toolbar);
  402. }
  403. // 页面内Toast提示
  404. function showToast(msg) {
  405. let toast = document.createElement('div');
  406. toast.className = 'fh-toast';
  407. toast.innerText = msg;
  408. toast.style.cssText = `
  409. position:fixed;left:50%;top:80px;transform:translateX(-50%);
  410. background:rgba(0,0,0,0.85);color:#fff;padding:10px 24px;
  411. border-radius:6px;font-size:16px;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,0.2);
  412. transition:opacity 0.3s;opacity:1;
  413. `;
  414. document.body.appendChild(toast);
  415. setTimeout(() => {
  416. toast.style.opacity = '0';
  417. setTimeout(() => document.body.removeChild(toast), 300);
  418. }, 1800);
  419. }