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

1#!/usr/bin/env python

2#coding: utf-8

3

4 import os

5 import sys

6 import time

7 import json

8 import queue

9 import base64

10 import mimetypes

11 import traceback

12 import threading

13 from typing import Dict, Any

14

15 import wx

16 import wx.html2

17 import matplotlib

18 import matplotlib.pyplot as plt

19 from loguru import logger

20 from wx.lib.newevent import NewEvent

21 from wx.lib.agw.hyperlink import HyperLinkCtrl

22 from wx import FileDialog, FD_SAVE, FD_OVERWRITE_PROMPT

23

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

25 from ..aipy.config import CONFIG_DIR

26 from ..aipy import TaskManager

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

28 from ..config import LLMConfig

29 from ..display import DisplayManager

30

31 ChatEvent, EVT_CHAT = NewEvent()

32 matplotlib.use('Agg')

33

34 def 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

48 class 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

142 class 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

159 class 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!

480 Please 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

484 Note: 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

516 class 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

555 def 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()