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
« 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
4import os
5import sys
6import time
7import json
8import queue
9import base64
10import mimetypes
11import traceback
12import threading
13from typing import Dict, Any
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
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
31ChatEvent, EVT_CHAT = NewEvent()
32matplotlib.use('Agg')
34def image_to_base64(file_path):
35 mime_type, _ = mimetypes.guess_type(file_path)
36 if mime_type is None:
37 return None
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
45 data_url = f"data:{mime_type};base64,{encoded_string}"
46 return data_url
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')
59 def stop_task(self):
60 if self._task:
61 self._task.stop()
62 else:
63 self.log.warning("没有正在进行的任务")
65 def has_task(self):
66 return self._task is not None
68 def can_done(self):
69 return not self._busy.is_set() and self.has_task()
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''
76 evt = ChatEvent(user=user, msg=content)
77 wx.PostEvent(self.gui, evt)
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']
87 msg = f''
88 evt = ChatEvent(user=user, msg=msg)
89 wx.PostEvent(self.gui, evt)
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)
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)
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)
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)
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)
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
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
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))
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"): '🐙'}
173 icon = wx.Icon(str(__respath__ / "aipy.ico"), wx.BITMAP_TYPE_ICO)
174 self.SetIcon(icon)
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)
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()
187 def make_input_panel(self, panel):
188 self.container = wx.Panel(panel)
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)
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
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)
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)
223 hbox.Add(vbox, 0, wx.ALIGN_CENTER)
224 container.SetSizer(hbox)
225 self.container = container
226 return container
228 def make_panel(self):
229 panel = wx.Panel(self)
230 vbox = wx.BoxSizer(wx.VERTICAL)
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)
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()
247 vbox.Add(input_panel, proportion=0, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=12)
249 panel.SetSizer(vbox)
250 self.panel = panel
252 def make_menu_bar(self):
253 menubar = wx.MenuBar()
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"))
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"))
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"))
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"))
291 self.SetMenuBar(menubar)
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)
304 def on_exit(self, event):
305 self.task_queue.put('exit')
306 self.aipython.join()
307 self.Close()
309 def on_stop_task(self, event):
310 self.aipython.stop_task()
312 def on_done(self, event):
313 self.task_queue.put('/done')
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()
322 def on_container_resize(self, event):
323 container_size = event.GetSize()
324 self.input.SetSize(container_size)
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))
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()
339 event.Skip()
341 def on_clear_chat(self, event):
342 self.webview.LoadURL(f'file://{self.html_file_path}')
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)
355 def on_open_website(self, event):
356 """打开网站"""
357 menu_item = self.GetMenuBar().FindItemById(event.GetId())
358 if not menu_item:
359 return
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)
372 def on_about(self, event):
373 about_dialog = AboutDialog(self)
374 about_dialog.ShowModal()
375 about_dialog.Destroy()
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")
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
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}")
397 def on_key_down(self, event):
398 keycode = event.GetKeyCode()
399 send_shortcut = (event.ControlDown() or event.CmdDown()) and keycode == wx.WXK_RETURN
401 if send_shortcut:
402 self.send_message()
403 else:
404 event.Skip()
406 def on_send(self, event):
407 self.send_message()
409 def get_task(self):
410 return self.task_queue.get()
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)
430 self.panel.Layout()
431 self.panel.Refresh()
433 def send_message(self):
434 text = self.input.GetValue().strip()
435 if not text:
436 return
438 if not self.aipython.has_task():
439 self.SetTitle(f"[{T('Current task')}] {text}")
441 self.append_message(T("Me"), text)
442 self.input.Clear()
444 self.toggle_input()
445 self.task_queue.put(text)
447 def on_chat(self, event):
448 user = event.user
449 text = event.msg
450 self.append_message(user, text)
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)
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()
466 def on_api_market(self, event):
467 """打开API配置对话框"""
468 dialog = ApiMarketDialog(self, self.tm.config_manager)
469 dialog.ShowModal()
470 dialog.Destroy()
472 def on_llm_config(self, event):
473 """打开LLM配置向导"""
474 show_provider_config(self.tm.llm_config, parent=self)
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
484Note: Click the "**Help**" link in the menu bar to contact the **AIPy** official and join the group chat."""))
485 self.welcomed = True
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}")
495 event.Skip()
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()
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)
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)
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)
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)
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)
547 # 添加确定按钮
548 ok_button = wx.Button(self, wx.ID_OK, T("OK"))
549 vbox.Add(ok_button, 0, wx.ALL | wx.ALIGN_CENTER, 10)
551 self.SetSizer(vbox)
552 self.Centre()
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
565 # 初始化显示效果管理器
566 display_config = settings.get('display', {})
567 display_manager = DisplayManager(display_config)
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()