New chat widget basically works

This commit is contained in:
Kovid Goyal 2025-09-06 08:22:36 +05:30
parent c08e37168b
commit 444a3069a8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 29 additions and 31 deletions

View File

@ -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'''<table width="100%" {style} cellpadding="2"><tr><td>{html}</td></tr></table>'''
# 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'<div>{header.as_html}</div>'
html += f'<div>{body_html}</div>'
bg = self.alternate_color if is_alternate else self.base_color
html = f'''<table width="100%" style="background-color: {bg}" cellpadding="2"><tr><td>{html}</td></tr></table>'''
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'<div style="{style}">{msg_html}</div>'
if details:
html += f"<pre>{_('Details:')}\n{escape(details)}</pre>"
self.current_message = f'<table width="100%" cellpadding="2"><tr><td>{html}</td></tr></table>'
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()

View File

@ -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

View File

@ -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()