From fb6c36630040ec24ac8effca36f784ddf07c46b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Dec 2025 05:52:43 +0530 Subject: [PATCH] Add an Ask AI about selected books action to the view button --- manual/gui.rst | 2 ++ src/calibre/gui2/actions/view.py | 13 ++++++++++++ src/calibre/gui2/dialogs/llm_book.py | 31 +++++++++++++++++++++------- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/manual/gui.rst b/manual/gui.rst index 93bc972ad3..d635105276 100644 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -927,6 +927,8 @@ calibre has several keyboard shortcuts to save you time and mouse movement. Thes - View * - :kbd:`Shift+V` - View last read book + * - :kbd:`Ctrl+Alt+A` + - Ask AI about the currently selected books * - :kbd:`Alt+V/Cmd+V for macOS` - View specific format * - :kbd:`Alt+Shift+J` diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 2203853981..08534c56fc 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -62,6 +62,7 @@ class ViewAction(InterfaceAction): cm = partial(self.create_menu_action, self.view_menu) self.view_specific_action = cm('specific', _('View specific format'), shortcut='Alt+V', triggered=self.view_specific_format) + self.llm_action = cm('llm-book', _('Ask AI about the selected book(s)'), shortcut='Ctrl+Alt+A', triggered=self.ask_ai, icon='ai.png') self.internal_view_action = cm('internal', _('View with calibre E-book viewer'), icon='viewer.png', triggered=self.view_internal) self.action_pick_random = cm('pick random', _('Read a random book'), icon='random.png', triggered=self.view_random) @@ -206,6 +207,18 @@ class ViewAction(InterfaceAction): internal = self.force_internal_viewer or ext in config['internally_viewed_formats'] or open_at is not None self._launch_viewer(name, viewer, internal, calibre_book_data=calibre_book_data, open_at=open_at) + def ask_ai(self): + rows = list(self.gui.library_view.selectionModel().selectedRows()) + if not rows or len(rows) == 0: + d = error_dialog(self.gui, _('Cannot ask AI'), _('No book selected')) + d.exec() + return + db = self.gui.library_view.model().db + rows = [r.row() for r in rows] + book_ids = [db.id(r) for r in rows] + from calibre.gui2.dialogs.llm_book import LLMBookDialog + LLMBookDialog([db.new_api.get_metadata(bid) for bid in book_ids], parent=self.gui).exec() + def view_specific_format(self, triggered): rows = list(self.gui.library_view.selectionModel().selectedRows()) if not rows or len(rows) == 0: diff --git a/src/calibre/gui2/dialogs/llm_book.py b/src/calibre/gui2/dialogs/llm_book.py index 49896cada8..2cd5af6b1d 100644 --- a/src/calibre/gui2/dialogs/llm_book.py +++ b/src/calibre/gui2/dialogs/llm_book.py @@ -5,7 +5,7 @@ from collections.abc import Iterator from functools import lru_cache from typing import Any -from qt.core import QAbstractItemView, QDialog, QDialogButtonBox, QLabel, QListWidget, QListWidgetItem, Qt, QUrl, QVBoxLayout, QWidget +from qt.core import QAbstractItemView, QDialog, QDialogButtonBox, QLabel, QListWidget, QListWidgetItem, QSize, Qt, QUrl, QVBoxLayout, QWidget from calibre.ai import ChatMessage, ChatMessageType from calibre.db.cache import Cache @@ -13,6 +13,7 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.gui2 import Application, gprefs from calibre.gui2.llm import ActionData, ConverseWidget, LLMActionsSettingsWidget, LLMSettingsDialogBase, LocalisedResults from calibre.gui2.ui import get_gui +from calibre.gui2.widgets2 import Dialog from calibre.utils.icu import primary_sort_key from polyglot.binary import from_hex_unicode @@ -233,17 +234,31 @@ class LLMPanel(ConverseWidget): return action.prompt_text(self.books) +class LLMBookDialog(Dialog): + + def __init__(self, books: list[Metadata], parent: QWidget | None = None): + self.books = books + super().__init__( + name='llm-book-dialog', title=_('Ask AI about {}').format(books[0].title) if len(books) < 2 else _( + 'Ask AI about {} books').format(len(books)), + parent=parent, default_buttons=QDialogButtonBox.StandardButton.Close) + + def setup_ui(self): + l = QVBoxLayout(self) + l.setContentsMargins(0, 0, 0, 0) + self.llm = llm = LLMPanel(self.books, parent=self) + l.addWidget(llm) + l.addWidget(self.bb) + + def sizeHint(self): + return QSize(600, 750) + + def develop(): from calibre.library import db get_current_db.ans = db() app = Application([]) - d = QDialog() - l = QVBoxLayout(d) - l.setContentsMargins(0, 0, 0, 0) - - llm = LLMPanel([Metadata('The Trials of Empire', ['Richard Swan'])], parent=d) - l.addWidget(llm) - d.exec() + LLMBookDialog([Metadata('The Trials of Empire', ['Richard Swan'])]).exec() del app