Coverage for aipyapp/gui/main.py: 0%

441 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-11 12:02 +0200

1#!/usr/bin/env python 

2#coding: utf-8 

3 

4import os 

5import sys 

6import time 

7import json 

8import queue 

9import base64 

10import mimetypes 

11import traceback 

12import threading 

13from typing import Dict, Any 

14 

15import wx 

16import wx.html2 

17import matplotlib 

18import matplotlib.pyplot as plt 

19from loguru import logger 

20from wx.lib.newevent import NewEvent 

21from wx.lib.agw.hyperlink import HyperLinkCtrl 

22from wx import FileDialog, FD_SAVE, FD_OVERWRITE_PROMPT 

23 

24from .. import __version__, T, get_lang, __respath__ 

25from ..aipy.config import CONFIG_DIR 

26from ..aipy import TaskManager 

27from . import ConfigDialog, ApiMarketDialog, show_provider_config, AboutDialog, CStatusBar 

28from ..config import LLMConfig 

29from ..display import DisplayManager 

30 

31ChatEvent, EVT_CHAT = NewEvent() 

32matplotlib.use('Agg') 

33 

34def image_to_base64(file_path): 

35 mime_type, _ = mimetypes.guess_type(file_path) 

36 if mime_type is None: 

37 return None 

38 

39 try: 

40 with open(file_path, "rb") as image_file: 

41 encoded_string = base64.b64encode(image_file.read()).decode("utf-8") 

42 except Exception as e: 

43 return None 

44 

45 data_url = f"data:{mime_type};base64,{encoded_string}" 

46 return data_url 

47 

48class AIPython(threading.Thread): 

49 def __init__(self, gui): 

50 super().__init__(daemon=True) 

51 self.gui = gui 

52 self.tm = gui.tm 

53 self._task = None 

54 self._busy = threading.Event() 

55 plt.show = self.plt_show 

56 sys.modules["matplotlib.pyplot"] = plt 

57 self.log = logger.bind(src='aipython') 

58 

59 def stop_task(self): 

60 if self._task: 

61 self._task.stop() 

62 else: 

63 self.log.warning("没有正在进行的任务") 

64 

65 def has_task(self): 

66 return self._task is not None 

67 

68 def can_done(self): 

69 return not self._busy.is_set() and self.has_task() 

70 

71 def plt_show(self, *args, **kwargs): 

72 filename = f'{time.strftime("%Y%m%d_%H%M%S")}.png' 

73 plt.savefig(filename) 

74 user = 'BB-8' 

75 content = f'![{filename}]({filename})' 

76 evt = ChatEvent(user=user, msg=content) 

77 wx.PostEvent(self.gui, evt) 

78 

79 def on_show_image(self, event): 

80 user = T("Turing") 

81 if event.data['path']: 

82 base64_data = image_to_base64(event.data['path']) 

83 content = base64_data if base64_data else event.data['path'] 

84 else: 

85 content = event.data['url'] 

86 

87 msg = f'![图片]({content})' 

88 evt = ChatEvent(user=user, msg=msg) 

89 wx.PostEvent(self.gui, evt) 

90 

91 def on_stream(self, event): 

92 user = T("Turing") 

93 content = '\n'.join(event.data['lines']) 

94 evt = ChatEvent(user=user, msg=content) 

95 wx.PostEvent(self.gui, evt) 

96 

97 def on_round_end(self, event): 

98 user = T("AIPy") 

99 summary = event.data.get('summary') 

100 evt = ChatEvent(user=user, msg=f'{T("End processing instruction")} {summary.get("summary")}') 

101 wx.PostEvent(self.gui, evt) 

102 

103 def on_exec(self, event ): 

104 user = 'BB-8' 

105 content = f"```{event.data['block'].lang}\n{event.data['block'].code}\n```" 

106 evt = ChatEvent(user=user, msg=content) 

107 wx.PostEvent(self.gui, evt) 

108 

109 def on_exec_result(self, event): 

110 user = 'BB-8' 

111 result = event.data.get('result') 

112 content = json.dumps(result, indent=4, ensure_ascii=False) 

113 content = f'{T("Run result")}\n```json\n{content}\n```' 

114 evt = ChatEvent(user=user, msg=content) 

115 wx.PostEvent(self.gui, evt) 

116 

117 def run(self): 

118 while True: 

119 instruction = self.gui.get_task() 

120 if instruction in ('/done', 'done'): 

121 if self._task: 

122 self._task.done() 

123 self._task = None 

124 else: 

125 self.log.warning("没有正在进行的任务") 

126 wx.CallAfter(self.gui.on_task_done) 

127 elif instruction in ('/exit', 'exit'): 

128 break 

129 else: 

130 self._busy.set() 

131 try: 

132 if not self._task: 

133 self._task = self.tm.new_task() 

134 self._task.register_listener(self) 

135 self._task.run(instruction) 

136 except Exception as e: 

137 self.log.exception('Error running task') 

138 finally: 

139 self._busy.clear() 

140 wx.CallAfter(self.gui.toggle_input) 

141 

142class FileDropTarget(wx.FileDropTarget): 

143 def __init__(self, text_ctrl, gui_frame): 

144 super().__init__() 

145 self.text_ctrl = text_ctrl 

146 self.gui_frame = gui_frame 

147 

148 def OnDropFiles(self, x, y, filenames): 

149 # 直接在光标处插入@文件路径 

150 current_text = self.text_ctrl.GetValue() 

151 cursor_pos = self.text_ctrl.GetInsertionPoint() 

152 insert_text = ' '.join([f"@{f}" for f in filenames]) 

153 new_text = current_text[:cursor_pos] + insert_text + current_text[cursor_pos:] 

154 self.text_ctrl.SetValue(new_text) 

155 self.text_ctrl.SetInsertionPoint(cursor_pos + len(insert_text)) 

156 wx.MessageBox(f"已添加 {len(filenames)} 个文件: {', '.join(filenames)}", "文件添加成功") 

157 return True 

158 

159class ChatFrame(wx.Frame): 

160 def __init__(self, tm): 

161 title = T("🐙 AIPY - Your AI Assistant 🐂 🐎") 

162 super().__init__(None, title=title, size=(1024, 768)) 

163 

164 self.tm = tm 

165 self.title = title 

166 self.log = logger.bind(src='gui') 

167 self.task_queue = queue.Queue() 

168 self.aipython = AIPython(self) 

169 self.welcomed = False # 添加初始化标志 

170 self.html_file_path = os.path.abspath(__respath__ / f"chatroom_{get_lang()}.html") 

171 self.avatars = {T("Me"): '🧑', 'BB-8': '🤖', T("Turing"): '🧠', T("AIPy"): '🐙'} 

172 

173 icon = wx.Icon(str(__respath__ / "aipy.ico"), wx.BITMAP_TYPE_ICO) 

174 self.SetIcon(icon) 

175 

176 self.make_menu_bar() 

177 self.make_panel() 

178 self.statusbar = CStatusBar(self) 

179 self.SetStatusBar(self.statusbar) 

180 self.statusbar.SetStatusText(T("Press Ctrl/Cmd+Enter to send message"), 0) 

181 

182 self.Bind(EVT_CHAT, self.on_chat) 

183 self.webview.Bind(wx.html2.EVT_WEBVIEW_TITLE_CHANGED, self.on_webview_title_changed) 

184 self.aipython.start() 

185 self.Show() 

186 

187 def make_input_panel(self, panel): 

188 self.container = wx.Panel(panel) 

189 

190 self.input = wx.TextCtrl(self.container, style=wx.TE_MULTILINE) 

191 self.input.SetMinSize((-1, 60)) 

192 self.input.SetWindowStyleFlag(wx.BORDER_SIMPLE) 

193 self.input.Bind(wx.EVT_KEY_DOWN, self.on_key_down) 

194 

195 self.done_button = wx.Button(self.container, label=T("End"), size=(50, -1)) 

196 self.done_button.Hide() 

197 self.done_button.Bind(wx.EVT_BUTTON, self.on_done) 

198 self.send_button = wx.Button(self.container, label=T("Send"), size=(50, -1)) 

199 self.send_button.Bind(wx.EVT_BUTTON, self.on_send) 

200 self.container.Bind(wx.EVT_SIZE, self.on_container_resize) 

201 return self.container 

202 

203 def make_input_panel2(self, panel): 

204 container = wx.Panel(panel) 

205 hbox = wx.BoxSizer(wx.HORIZONTAL) 

206 self.input = wx.TextCtrl(container, style=wx.TE_MULTILINE) 

207 self.input.SetMinSize((-1, 80)) 

208 self.input.SetWindowStyleFlag(wx.BORDER_SIMPLE) 

209 self.input.Bind(wx.EVT_KEY_DOWN, self.on_key_down) 

210 hbox.Add(self.input, proportion=1, flag=wx.EXPAND | wx.ALL, border=5) 

211 

212 vbox = wx.BoxSizer(wx.VERTICAL) 

213 self.done_button = wx.Button(container, label=T("End")) 

214 self.done_button.Hide() 

215 self.done_button.Bind(wx.EVT_BUTTON, self.on_done) 

216 self.done_button.SetBackgroundColour(wx.Colour(255, 230, 230)) 

217 self.send_button = wx.Button(container, label=T("Send")) 

218 self.send_button.Bind(wx.EVT_BUTTON, self.on_send) 

219 vbox.Add(self.done_button, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) 

220 vbox.AddSpacer(10) 

221 vbox.Add(self.send_button, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT | wx.TOP, 10) 

222 

223 hbox.Add(vbox, 0, wx.ALIGN_CENTER) 

224 container.SetSizer(hbox) 

225 self.container = container 

226 return container 

227 

228 def make_panel(self): 

229 panel = wx.Panel(self) 

230 vbox = wx.BoxSizer(wx.VERTICAL) 

231 

232 self.webview = wx.html2.WebView.New(panel) 

233 self.webview.LoadURL(f'file://{self.html_file_path}') 

234 self.webview.SetWindowStyleFlag(wx.BORDER_NONE) 

235 vbox.Add(self.webview, proportion=1, flag=wx.EXPAND | wx.ALL, border=12) 

236 

237 if sys.platform == 'darwin': 

238 input_panel = self.make_input_panel(panel) 

239 else: 

240 input_panel = self.make_input_panel2(panel) 

241 drop_target = FileDropTarget(self.input, self) 

242 self.input.SetDropTarget(drop_target) 

243 font = wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) 

244 self.input.SetFont(font) 

245 self.input.SetFocus() 

246 

247 vbox.Add(input_panel, proportion=0, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=12) 

248 

249 panel.SetSizer(vbox) 

250 self.panel = panel 

251 

252 def make_menu_bar(self): 

253 menubar = wx.MenuBar() 

254 

255 # 文件菜单 

256 file_menu = wx.Menu() 

257 save_item = file_menu.Append(wx.ID_SAVE, T("Save chat history as HTML")) 

258 clear_item = file_menu.Append(wx.ID_CLEAR, T("Clear chat")) 

259 file_menu.AppendSeparator() 

260 exit_item = file_menu.Append(wx.ID_EXIT, T("Exit")) 

261 menubar.Append(file_menu, T("File")) 

262 

263 # 编辑菜单 

264 edit_menu = wx.Menu() 

265 config_item = edit_menu.Append(wx.ID_ANY, T("Configuration")) 

266 api_market_item = edit_menu.Append(wx.ID_ANY, T("API Market")) 

267 menubar.Append(edit_menu, T("Edit")) 

268 

269 # 任务菜单 

270 task_menu = wx.Menu() 

271 self.new_task_item = task_menu.Append(wx.ID_NEW, T("Start new task")) 

272 self.stop_task_item = task_menu.Append(wx.ID_ANY, T("Stop task")) 

273 self.stop_task_item.Enable(False) 

274 self.Bind(wx.EVT_MENU, self.on_stop_task, self.stop_task_item) 

275 self.share_task_item = task_menu.Append(wx.ID_ANY, T("Share task")) 

276 self.share_task_item.Enable(False) 

277 self.Bind(wx.EVT_MENU, self.on_share_task, self.share_task_item) 

278 menubar.Append(task_menu, T("Task")) 

279 

280 # 帮助菜单 

281 help_menu = wx.Menu() 

282 website_item = help_menu.Append(wx.ID_ANY, T("Website")) 

283 #forum_item = help_menu.Append(wx.ID_ANY, T('Forum')) 

284 if get_lang() == 'zh': 

285 wechat_item = help_menu.Append(wx.ID_ANY, T("WeChat Group")) 

286 self.Bind(wx.EVT_MENU, self.on_open_website, wechat_item) 

287 help_menu.AppendSeparator() 

288 about_item = help_menu.Append(wx.ID_ABOUT, T("About")) 

289 menubar.Append(help_menu, T("Help")) 

290 

291 self.SetMenuBar(menubar) 

292 

293 # 绑定事件 

294 self.Bind(wx.EVT_MENU, self.on_save_html, save_item) 

295 self.Bind(wx.EVT_MENU, self.on_clear_chat, clear_item) 

296 self.Bind(wx.EVT_MENU, self.on_exit, exit_item) 

297 self.Bind(wx.EVT_MENU, self.on_done, self.new_task_item) 

298 self.Bind(wx.EVT_MENU, self.on_config, config_item) 

299 self.Bind(wx.EVT_MENU, self.on_api_market, api_market_item) 

300 self.Bind(wx.EVT_MENU, self.on_open_website, website_item) 

301 #self.Bind(wx.EVT_MENU, self.on_open_website, forum_item) 

302 self.Bind(wx.EVT_MENU, self.on_about, about_item) 

303 

304 def on_exit(self, event): 

305 self.task_queue.put('exit') 

306 self.aipython.join() 

307 self.Close() 

308 

309 def on_stop_task(self, event): 

310 self.aipython.stop_task() 

311 

312 def on_done(self, event): 

313 self.task_queue.put('/done') 

314 

315 def on_task_done(self): 

316 self.done_button.Hide() 

317 self.SetStatusText(T("Current task has ended"), 0) 

318 self.new_task_item.Enable(False) 

319 self.SetTitle(self.title) 

320 self.clear_chat() 

321 

322 def on_container_resize(self, event): 

323 container_size = event.GetSize() 

324 self.input.SetSize(container_size) 

325 

326 overlap = -20 

327 send_button_size = self.send_button.GetSize() 

328 button_pos_x = container_size.width - send_button_size.width + overlap 

329 button_pos_y = container_size.height - send_button_size.height - 10 

330 self.send_button.SetPosition((button_pos_x, button_pos_y)) 

331 

332 if self.aipython.can_done(): 

333 done_button_size = self.done_button.GetSize() 

334 button_pos_x = container_size.width - done_button_size.width + overlap 

335 button_pos_y = 10 

336 self.done_button.SetPosition((button_pos_x, button_pos_y)) 

337 self.done_button.Show() 

338 

339 event.Skip() 

340 

341 def on_clear_chat(self, event): 

342 self.webview.LoadURL(f'file://{self.html_file_path}') 

343 

344 def clear_chat(self): 

345 """清空聊天记录""" 

346 js_code = """ 

347 const chatContainer = document.querySelector('.chat-container'); 

348 chatContainer.innerHTML = ''; 

349 lastUser = ''; 

350 lastMessage = null; 

351 lastRawContent = ''; 

352 """ 

353 self.webview.RunScript(js_code) 

354 

355 def on_open_website(self, event): 

356 """打开网站""" 

357 menu_item = self.GetMenuBar().FindItemById(event.GetId()) 

358 if not menu_item: 

359 return 

360 

361 label = menu_item.GetItemLabel() 

362 if label == T("Website"): 

363 url = T("https://aipy.app") 

364 elif label == T("Forum"): 

365 url = T("https://d.aipy.app") 

366 elif label == T("WeChat Group"): 

367 url = T("https://d.aipy.app/d/13") 

368 else: 

369 return 

370 wx.LaunchDefaultBrowser(url) 

371 

372 def on_about(self, event): 

373 about_dialog = AboutDialog(self) 

374 about_dialog.ShowModal() 

375 about_dialog.Destroy() 

376 

377 def on_save_html(self, event): 

378 try: 

379 html_content = self.webview.GetPageSource() 

380 self.save_html_content(html_content) 

381 except Exception as e: 

382 wx.MessageBox(f"save html error: {e}", "Error") 

383 

384 def save_html_content(self, html_content): 

385 with FileDialog(self, T("Save chat history as HTML file"), wildcard="HTML file (*.html)|*.html", 

386 style=FD_SAVE | FD_OVERWRITE_PROMPT) as dialog: 

387 if dialog.ShowModal() == wx.ID_CANCEL: 

388 return 

389 

390 path = dialog.GetPath() 

391 try: 

392 with open(path, 'w', encoding='utf-8') as file: 

393 file.write(html_content) 

394 except IOError: 

395 wx.LogError(f"{T('Failed to save file')}: {path}") 

396 

397 def on_key_down(self, event): 

398 keycode = event.GetKeyCode() 

399 send_shortcut = (event.ControlDown() or event.CmdDown()) and keycode == wx.WXK_RETURN 

400 

401 if send_shortcut: 

402 self.send_message() 

403 else: 

404 event.Skip() 

405 

406 def on_send(self, event): 

407 self.send_message() 

408 

409 def get_task(self): 

410 return self.task_queue.get() 

411 

412 def toggle_input(self): 

413 if self.container.IsShown(): 

414 self.container.Hide() 

415 self.done_button.Hide() 

416 wx.BeginBusyCursor() 

417 self.SetStatusText(T("Operation in progress, please wait..."), 0) 

418 self.new_task_item.Enable(False) 

419 self.stop_task_item.Enable(True) 

420 self.share_task_item.Enable(False) 

421 else: 

422 self.container.Show() 

423 self.done_button.Show() 

424 wx.EndBusyCursor() 

425 self.SetStatusText(T("Operation completed. If you start a new task, please click the `End` button"), 0) 

426 self.new_task_item.Enable(self.aipython.can_done()) 

427 self.stop_task_item.Enable(False) 

428 self.share_task_item.Enable(True) 

429 

430 self.panel.Layout() 

431 self.panel.Refresh() 

432 

433 def send_message(self): 

434 text = self.input.GetValue().strip() 

435 if not text: 

436 return 

437 

438 if not self.aipython.has_task(): 

439 self.SetTitle(f"[{T('Current task')}] {text}") 

440 

441 self.append_message(T("Me"), text) 

442 self.input.Clear() 

443 

444 self.toggle_input() 

445 self.task_queue.put(text) 

446 

447 def on_chat(self, event): 

448 user = event.user 

449 text = event.msg 

450 self.append_message(user, text) 

451 

452 def append_message(self, user, text): 

453 avatar = self.avatars[user] 

454 js_code = f'appendMessage("{avatar}", "{user}", {repr(text)});' 

455 self.webview.RunScript(js_code) 

456 

457 def on_config(self, event): 

458 dialog = ConfigDialog(self, self.tm.settings) 

459 if dialog.ShowModal() == wx.ID_OK: 

460 values = dialog.get_values() 

461 if values['timeout'] == 0: 

462 del values['timeout'] 

463 self.tm.config_manager.update_sys_config(values) 

464 dialog.Destroy() 

465 

466 def on_api_market(self, event): 

467 """打开API配置对话框""" 

468 dialog = ApiMarketDialog(self, self.tm.config_manager) 

469 dialog.ShowModal() 

470 dialog.Destroy() 

471 

472 def on_llm_config(self, event): 

473 """打开LLM配置向导""" 

474 show_provider_config(self.tm.llm_config, parent=self) 

475 

476 def on_webview_title_changed(self, event): 

477 """WebView 标题改变时的处理""" 

478 if not self.welcomed: 

479 wx.CallLater(100, self.append_message, T("AIPy"), T("""Hello! I am **AIPy**, your intelligent task assistant! 

480Please allow me to introduce the other members of the team: 

481- Turing: The strongest artificial intelligence, complex task analysis and planning 

482- BB-8: The strongest robot, responsible for executing tasks 

483 

484Note: Click the "**Help**" link in the menu bar to contact the **AIPy** official and join the group chat.""")) 

485 self.welcomed = True 

486 

487 # 检查更新 

488 try: 

489 update = self.tm.get_update() 

490 if update and update.get('has_update'): 

491 wx.CallLater(1000, self.append_message, T("AIPy"), f"\n🔔 **{T('Update available')}❗**: `v{update.get('latest_version')}`") 

492 except Exception as e: 

493 self.log.error(f"检查更新时出错: {e}") 

494 

495 event.Skip() 

496 

497 def on_share_task(self, event): 

498 """分享当前任务记录""" 

499 try: 

500 html_content = self.webview.GetPageSource() 

501 result = self.tm.diagnose.report_data(html_content, 'task_record.html') 

502 self.log.info(f"分享任务记录: {result}") 

503 if result.get('success'): 

504 dialog = ShareResultDialog(self, result['url']) 

505 dialog.ShowModal() 

506 dialog.Destroy() 

507 else: 

508 dialog = ShareResultDialog(self, None, result.get('error')) 

509 dialog.ShowModal() 

510 dialog.Destroy() 

511 except Exception as e: 

512 dialog = ShareResultDialog(self, None, str(e)) 

513 dialog.ShowModal() 

514 dialog.Destroy() 

515 

516class ShareResultDialog(wx.Dialog): 

517 def __init__(self, parent, url, error=None): 

518 super().__init__(parent, title=T("Share result"), size=(400, 200)) 

519 logger.info(f"ShareResultDialog: {url}, {error}") 

520 vbox = wx.BoxSizer(wx.VERTICAL) 

521 

522 if error: 

523 # 显示错误信息 

524 error_text = wx.StaticText(self, label=T("Share failed")) 

525 error_text.SetForegroundColour(wx.Colour(255, 0, 0)) 

526 vbox.Add(error_text, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

527 

528 error_msg = wx.StaticText(self, label=error) 

529 error_msg.Wrap(350) 

530 vbox.Add(error_msg, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

531 else: 

532 # 显示成功信息 

533 success_text = wx.StaticText(self, label=T("Share success")) 

534 success_text.SetForegroundColour(wx.Colour(0, 128, 0)) 

535 vbox.Add(success_text, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

536 

537 # 添加提示文本 

538 hint_text = wx.StaticText(self, label=T("Click the link below to view the task record")) 

539 vbox.Add(hint_text, 0, wx.ALL | wx.ALIGN_CENTER, 5) 

540 

541 # 添加可点击的链接 

542 link = HyperLinkCtrl(self, -1, T("View task record"), URL=url) 

543 link.EnableRollover(True) 

544 link.SetUnderlines(False, False, True) 

545 vbox.Add(link, 0, wx.ALL | wx.ALIGN_CENTER, 5) 

546 

547 # 添加确定按钮 

548 ok_button = wx.Button(self, wx.ID_OK, T("OK")) 

549 vbox.Add(ok_button, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

550 

551 self.SetSizer(vbox) 

552 self.Centre() 

553 

554 

555def main(settings): 

556 app = wx.App(False) 

557 llm_config = LLMConfig(CONFIG_DIR / "config") 

558 if settings.get('llm_need_config'): 

559 if llm_config.need_config(): 

560 show_provider_config(llm_config) 

561 if llm_config.need_config(): 

562 return 

563 settings["llm"] = llm_config.config 

564 

565 # 初始化显示效果管理器 

566 display_config = settings.get('display', {}) 

567 display_manager = DisplayManager(display_config) 

568 

569 try: 

570 tm = TaskManager(settings, display_manager=display_manager) 

571 except Exception as e: 

572 traceback.print_exc() 

573 return 

574 tm.config_manager = settings['config_manager'] 

575 tm.llm_config = llm_config 

576 ChatFrame(tm) 

577 app.MainLoop()