From 444a3069a8d9c8d8a4bd0f323408470be9663cb3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 6 Sep 2025 08:22:36 +0530 Subject: [PATCH] New chat widget basically works --- src/calibre/gui2/chat_widget.py | 13 ++++++++---- src/calibre/gui2/viewer/llm.py | 12 ++++++----- src/calibre/gui2/viewer/lookup.py | 35 ++++++++++++------------------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/chat_widget.py b/src/calibre/gui2/chat_widget.py index 173fe2ee0d..31d851077b 100644 --- a/src/calibre/gui2/chat_widget.py +++ b/src/calibre/gui2/chat_widget.py @@ -98,6 +98,7 @@ class ChatWidget(QWidget): def __init__(self, parent: QWidget = None, placeholder_text: str = ''): super().__init__(parent) l = QVBoxLayout(self) + l.setContentsMargins(0, 0, 0, 0) self.browser = b = Browser(self) b.anchorClicked.connect(self.link_clicked) l.addWidget(b) @@ -110,6 +111,10 @@ class ChatWidget(QWidget): self.alternate_color = pal.color(QPalette.ColorRole.Window).name() self.base_color = pal.color(QPalette.ColorRole.Base).name() + def wrap_content_in_padding_table(self, html: str, background_color: str = '') -> str: + style = f'style="background-color: {background_color}"' if background_color else '' + return f'''
{html}
''' + # API {{{ def add_block(self, body_html: str, header: Header = Header(), is_alternate: bool = False) -> None: self.current_message = '' @@ -118,8 +123,7 @@ class ChatWidget(QWidget): html += f'
{header.as_html}
' html += f'
{body_html}
' bg = self.alternate_color if is_alternate else self.base_color - html = f'''
{html}
''' - self.blocks.append(html) + self.blocks.append(self.wrap_content_in_padding_table(html, bg)) def replace_last_block(self, body_html: str, header: Header = Header(), is_alternate: bool = False) -> None: if self.blocks: @@ -136,7 +140,7 @@ class ChatWidget(QWidget): html = f'
{msg_html}
' if details: html += f"
{_('Details:')}\n{escape(details)}
" - self.current_message = f'
{html}
' + self.current_message = self.wrap_content_in_padding_table(html) self.re_render() def clear(self) -> None: @@ -158,7 +162,8 @@ class ChatWidget(QWidget): def re_render(self) -> None: if self.current_message: self.browser.setHtml(self.current_message) - return + else: + self.browser.setHtml('\n\n'.join(self.blocks)) def on_input(self) -> None: text = self.input.toPlainText() diff --git a/src/calibre/gui2/viewer/llm.py b/src/calibre/gui2/viewer/llm.py index 369ca38e34..ce23fe5650 100644 --- a/src/calibre/gui2/viewer/llm.py +++ b/src/calibre/gui2/viewer/llm.py @@ -353,17 +353,19 @@ class LLMPanel(QWidget): return self.ai_provider_plugin.human_readable_model_name(self.conversation_history.model_used) or _('Assistant') def show_ai_conversation(self): + self.result_display.clear() assistant = self.assistant_name for i, message in enumerate(self.conversation_history): content_for_display = for_display_to_human(message) if not content_for_display: continue - if you_block := not message.from_assistant: - header = Header() - else: + header = Header() + alternate = False + if message.from_assistant: + alternate = True header = Header(assistant, (Button( _('Save'), f'http://{self.save_note_hostname}/{i}', _('Save this specific response as the note')),)) - self.result_display.add_block(content_for_display, header, not you_block) + self.result_display.add_block(content_for_display, header, alternate) if self.conversation_history.api_call_active: content_for_display = for_display_to_human(ChatMessage(self.conversation_history.accumulator.all_content)) self.result_display.add_block(content_for_display, Header(_('{} thinking…').format(assistant))) @@ -399,7 +401,6 @@ class LLMPanel(QWidget): self.conversation_history.new_api_call() Thread(name='LLMAPICall', daemon=True, target=self.do_api_call, args=( self.conversation_history.copy(), self.current_api_call_number, self.ai_provider_plugin)).start() - self.show_ai_conversation() def do_api_call( self, conversation_history: ConversationHistory, current_api_call_number: int, ai_plugin: AIProviderPlugin @@ -661,6 +662,7 @@ def develop(show_initial_messages: bool = False): # return LLMSettingsDialog().exec() d = QDialog() l = QVBoxLayout(d) + l.setContentsMargins(0, 0, 0, 0) llm = LLMPanel(d) llm.update_with_text('developing') h = llm.conversation_history diff --git a/src/calibre/gui2/viewer/lookup.py b/src/calibre/gui2/viewer/lookup.py index 6dd43bbaab..9aa88466a0 100644 --- a/src/calibre/gui2/viewer/lookup.py +++ b/src/calibre/gui2/viewer/lookup.py @@ -414,31 +414,22 @@ class Lookup(QTabWidget): return panel def _activate_llm_panel(self): - if self.llm_panel is None: - # Deferred import to avoid circular dependencies and improve startup time; import may be redundant - from calibre.live import start_worker - start_worker() # needed for live loading of AI backends - from calibre.gui2.viewer.llm import LLMPanel - self.llm_panel = LLMPanel(self) - self.llm_container.layout().addWidget(self.llm_panel) - - if self.current_book_metadata: - self.llm_panel.update_book_metadata(self.current_book_metadata) - - try: - self.llm_panel.add_note_requested.disconnect(self.llm_add_note_requested) - except TypeError: - pass - self.llm_panel.add_note_requested.connect(self.llm_add_note_requested) - - self.removeTab(self.llm_tab_index) - self.llm_tab_index = self.addTab(self.llm_panel, QIcon.ic('ai.png'), _('Ask &AI')) - self.setCurrentIndex(self.llm_tab_index) - self.llm_panel.update_with_text(self.selected_text, self.current_highlight_data) + ' Only load LLM code when actually requested by the user ' + if self.llm_panel is not None: + return + from calibre.live import start_worker + start_worker() # needed for live loading of AI backends + from calibre.gui2.viewer.llm import LLMPanel + self.llm_panel = LLMPanel(self) + self.llm_container.layout().addWidget(self.llm_panel) + if self.current_book_metadata: + self.llm_panel.update_book_metadata(self.current_book_metadata) + self.llm_panel.add_note_requested.connect(self.llm_add_note_requested) + self.llm_panel.update_with_text(self.selected_text, self.current_highlight_data) def _tab_changed(self, index): vprefs.set('llm_lookup_tab_index', index) - if index == self.llm_tab_index and self.llm_panel is None: + if index == self.llm_tab_index: self._activate_llm_panel() self.update_query()