mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-09 14:45:01 -05:00
Use the new AI config widget in viewer Ask AI panel
This commit is contained in:
parent
ca8653bba4
commit
7c595c932b
42
imgsrc/ai.svg
Normal file
42
imgsrc/ai.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2"
|
||||||
|
sodipodi:docname="ai.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview2"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.8561553"
|
||||||
|
inkscape:cx="31.786133"
|
||||||
|
inkscape:cy="31.516759"
|
||||||
|
inkscape:window-width="976"
|
||||||
|
inkscape:window-height="581"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg2" />
|
||||||
|
<!-- First (large) star -->
|
||||||
|
<path
|
||||||
|
d="M 26.222656 3.8691406 C 25.308656 3.8691406 24.394656 4.3789844 23.972656 5.3964844 L 21.697266 10.878906 C 19.732266 15.613906 16.068844 19.3935 11.464844 21.4375 L 5.4746094 24.097656 C 3.5086094 24.969656 3.5086094 27.828172 5.4746094 28.701172 L 11.658203 31.447266 C 16.146203 33.439266 19.745141 37.083109 21.744141 41.662109 L 23.990234 46.808594 C 24.530234 48.044844 25.802187 48.508281 26.890625 48.199219 C 27.543688 48.013781 28.131078 47.550344 28.455078 46.808594 L 30.701172 41.662109 C 32.700172 37.083109 36.298109 33.439266 40.787109 31.447266 L 46.970703 28.701172 C 47.462203 28.483172 47.830422 28.142672 48.076172 27.738281 C 48.199047 27.536086 48.292078 27.317299 48.353516 27.091797 C 48.414953 26.866295 48.445312 26.633672 48.445312 26.400391 C 48.445312 26.167109 48.414953 25.932564 48.353516 25.707031 C 48.169203 25.030432 47.707953 24.425031 46.970703 24.097656 L 40.980469 21.4375 C 36.376469 19.3935 32.713047 15.613906 30.748047 10.878906 L 28.472656 5.3964844 C 28.050656 4.3789844 27.136656 3.8691406 26.222656 3.8691406 z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#2caf45;fill-opacity:1" />
|
||||||
|
<!-- Second (small) star -->
|
||||||
|
<path
|
||||||
|
d="M 49.75 39.640625 C 49.26 39.640625 48.770922 39.913484 48.544922 40.458984 L 47.894531 42.023438 C 46.787531 44.693438 44.722953 46.825516 42.126953 47.978516 L 40.289062 48.794922 C 39.237062 49.261922 39.237063 50.791766 40.289062 51.259766 L 42.234375 52.125 C 44.765375 53.25 46.795875 55.304719 47.921875 57.886719 L 48.552734 59.335938 C 48.842109 59.998438 49.523857 60.245703 50.107422 60.080078 C 50.457561 59.980703 50.772062 59.733438 50.945312 59.335938 L 51.578125 57.886719 C 52.704125 55.304719 54.732672 53.25 57.263672 52.125 L 59.210938 51.259766 C 59.474187 51.143016 59.671109 50.960563 59.802734 50.744141 C 59.868547 50.63593 59.918266 50.519115 59.951172 50.398438 C 60.016984 50.157082 60.016984 49.899621 59.951172 49.658203 C 59.885359 49.416785 59.752125 49.190699 59.554688 49.015625 C 59.455969 48.928088 59.342562 48.853422 59.210938 48.794922 L 57.373047 47.978516 C 56.399547 47.545766 55.500857 46.975572 54.699219 46.291016 C 54.432006 46.06283 54.174469 45.823031 53.929688 45.570312 C 53.195344 44.812156 52.566889 43.945994 52.064453 42.998047 C 51.896975 42.682064 51.743844 42.357062 51.605469 42.023438 L 50.955078 40.458984 C 50.729078 39.913484 50.24 39.640625 49.75 39.640625 z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#2271d5;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
6
imgsrc/srv/ai.svg
Normal file
6
imgsrc/srv/ai.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<!-- First (large) star -->
|
||||||
|
<path d="M 26.222656 3.8691406 C 25.308656 3.8691406 24.394656 4.3789844 23.972656 5.3964844 L 21.697266 10.878906 C 19.732266 15.613906 16.068844 19.3935 11.464844 21.4375 L 5.4746094 24.097656 C 3.5086094 24.969656 3.5086094 27.828172 5.4746094 28.701172 L 11.658203 31.447266 C 16.146203 33.439266 19.745141 37.083109 21.744141 41.662109 L 23.990234 46.808594 C 24.530234 48.044844 25.802187 48.508281 26.890625 48.199219 C 27.543688 48.013781 28.131078 47.550344 28.455078 46.808594 L 30.701172 41.662109 C 32.700172 37.083109 36.298109 33.439266 40.787109 31.447266 L 46.970703 28.701172 C 47.462203 28.483172 47.830422 28.142672 48.076172 27.738281 C 48.199047 27.536086 48.292078 27.317299 48.353516 27.091797 C 48.414953 26.866295 48.445312 26.633672 48.445312 26.400391 C 48.445312 26.167109 48.414953 25.932564 48.353516 25.707031 C 48.169203 25.030432 47.707953 24.425031 46.970703 24.097656 L 40.980469 21.4375 C 36.376469 19.3935 32.713047 15.613906 30.748047 10.878906 L 28.472656 5.3964844 C 28.050656 4.3789844 27.136656 3.8691406 26.222656 3.8691406 z"/>
|
||||||
|
<!-- Second (small) star -->
|
||||||
|
<path d="M 49.75 39.640625 C 49.26 39.640625 48.770922 39.913484 48.544922 40.458984 L 47.894531 42.023438 C 46.787531 44.693438 44.722953 46.825516 42.126953 47.978516 L 40.289062 48.794922 C 39.237062 49.261922 39.237063 50.791766 40.289062 51.259766 L 42.234375 52.125 C 44.765375 53.25 46.795875 55.304719 47.921875 57.886719 L 48.552734 59.335938 C 48.842109 59.998438 49.523857 60.245703 50.107422 60.080078 C 50.457561 59.980703 50.772062 59.733438 50.945312 59.335938 L 51.578125 57.886719 C 52.704125 55.304719 54.732672 53.25 57.263672 52.125 L 59.210938 51.259766 C 59.474187 51.143016 59.671109 50.960563 59.802734 50.744141 C 59.868547 50.63593 59.918266 50.519115 59.951172 50.398438 C 60.016984 50.157082 60.016984 49.899621 59.951172 49.658203 C 59.885359 49.416785 59.752125 49.190699 59.554688 49.015625 C 59.455969 48.928088 59.342562 48.853422 59.210938 48.794922 L 57.373047 47.978516 C 56.399547 47.545766 55.500857 46.975572 54.699219 46.291016 C 54.432006 46.06283 54.174469 45.823031 53.929688 45.570312 C 53.195344 44.812156 52.566889 43.945994 52.064453 42.998047 C 51.896975 42.682064 51.743844 42.357062 51.605469 42.023438 L 50.955078 40.458984 C 50.729078 39.913484 50.24 39.640625 49.75 39.640625 z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
resources/images/ai.png
Normal file
BIN
resources/images/ai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@ -42,14 +42,28 @@ class ConfigureAI(QWidget):
|
|||||||
s.addWidget()
|
s.addWidget()
|
||||||
v.addWidget(self.gb)
|
v.addWidget(self.gb)
|
||||||
|
|
||||||
def commit(self) -> bool:
|
@property
|
||||||
|
def is_ready_for_use(self) -> bool:
|
||||||
|
if not self.available_plugins:
|
||||||
|
return False
|
||||||
|
return self.plugin_config_widgets[self.current_idx].is_ready_for_use
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_idx(self) -> int:
|
||||||
|
if len(self.available_plugins) < 2:
|
||||||
|
return 0
|
||||||
|
return self.provider_combo.currentIndex()
|
||||||
|
|
||||||
|
def validate(self) -> bool:
|
||||||
if not self.available_plugins:
|
if not self.available_plugins:
|
||||||
error_dialog(self, _('No AI providers'), self.none_label.text(), show=True)
|
error_dialog(self, _('No AI providers'), self.none_label.text(), show=True)
|
||||||
return False
|
return False
|
||||||
if len(self.available_plugins) == 1:
|
return self.plugin_config_widgets[self.current_idx].validate()
|
||||||
idx = 0
|
|
||||||
else:
|
def commit(self) -> bool:
|
||||||
idx = self.provider_combo.currentIndex()
|
if not self.validate():
|
||||||
|
return False
|
||||||
|
idx = self.current_idx
|
||||||
p, w = self.available_plugins[idx], self.plugin_config_widgets[idx]
|
p, w = self.available_plugins[idx], self.plugin_config_widgets[idx]
|
||||||
if not w.validate():
|
if not w.validate():
|
||||||
return False
|
return False
|
||||||
|
|||||||
@ -41,6 +41,7 @@ from calibre.gui2 import Application, error_dialog, gprefs, safe_open_url
|
|||||||
from calibre.gui2.widgets2 import Dialog
|
from calibre.gui2.widgets2 import Dialog
|
||||||
from calibre.utils.date import qt_from_dt
|
from calibre.utils.date import qt_from_dt
|
||||||
from calibre.utils.icu import primary_sort_key
|
from calibre.utils.icu import primary_sort_key
|
||||||
|
from polyglot.binary import as_hex_unicode, from_hex_unicode
|
||||||
|
|
||||||
pref = partial(pref_for_provider, OpenRouterAI.name)
|
pref = partial(pref_for_provider, OpenRouterAI.name)
|
||||||
|
|
||||||
@ -161,7 +162,7 @@ class ModelDetails(QTextBrowser):
|
|||||||
<p>{_('Pick an AI model to use. Generally, newer models are more capable but also more expensive.')}</p>
|
<p>{_('Pick an AI model to use. Generally, newer models are more capable but also more expensive.')}</p>
|
||||||
<p>{_('By default, an appropriate AI model is chosen automatically based on the query being made.'
|
<p>{_('By default, an appropriate AI model is chosen automatically based on the query being made.'
|
||||||
' By picking a model explicitly, you have more control over this process.')}</p>
|
' By picking a model explicitly, you have more control over this process.')}</p>
|
||||||
<p>{_('Another critera to look for is if the model is <i>moderated</i> (that is, its output is filtered by the provider).')}</p>
|
<p>{_('Another criterion to look for is if the model is <i>moderated</i> (that is, its output is filtered by the provider).')}</p>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
def show_model_details(self, m: 'AIModel'):
|
def show_model_details(self, m: 'AIModel'):
|
||||||
@ -378,7 +379,7 @@ class ConfigWidget(QWidget):
|
|||||||
a.setPlaceholderText(_('An API key is required to use OpenRouter'))
|
a.setPlaceholderText(_('An API key is required to use OpenRouter'))
|
||||||
l.addRow(_('API &key:'), a)
|
l.addRow(_('API &key:'), a)
|
||||||
if key := pref('api_key'):
|
if key := pref('api_key'):
|
||||||
a.setText(key)
|
a.setText(from_hex_unicode(key))
|
||||||
self.text_model = tm = Model(parent=self)
|
self.text_model = tm = Model(parent=self)
|
||||||
tm.select_model.connect(self.select_model)
|
tm.select_model.connect(self.select_model)
|
||||||
l.addRow(_('Model for &text tasks:'), tm)
|
l.addRow(_('Model for &text tasks:'), tm)
|
||||||
@ -396,7 +397,11 @@ class ConfigWidget(QWidget):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def settings(self) -> dict[str, Any]:
|
def settings(self) -> dict[str, Any]:
|
||||||
return {'api_key': self.api_key}
|
return {'api_key': as_hex_unicode(self.api_key)}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ready_for_use(self) -> bool:
|
||||||
|
return bool(self.api_key)
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
if self.api_key:
|
if self.api_key:
|
||||||
|
|||||||
@ -853,6 +853,11 @@ class AIProviderPlugin(Plugin): # {{{
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
def config_widget(self):
|
def config_widget(self):
|
||||||
|
'''
|
||||||
|
The config widget for an AI plugin must support validate() and additionally the property
|
||||||
|
is_ready_for_use which must be true iff the plugin is ready to be used,
|
||||||
|
i.e. it does not require configuration such as an API key or the API key is already set.
|
||||||
|
'''
|
||||||
if self.builtin_live_module_name:
|
if self.builtin_live_module_name:
|
||||||
return self.builtin_live_module.config_widget()
|
return self.builtin_live_module.config_widget()
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@ -27,12 +27,14 @@ from qt.core import (
|
|||||||
QPushButton,
|
QPushButton,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
Qt,
|
Qt,
|
||||||
|
QTabWidget,
|
||||||
QTextBrowser,
|
QTextBrowser,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from calibre.ai.config import ConfigureAI
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre.gui2 import Application, error_dialog
|
from calibre.gui2 import Application, error_dialog
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
@ -40,7 +42,7 @@ from calibre.gui2.viewer.config import vprefs
|
|||||||
from calibre.gui2.viewer.highlights import HighlightColorCombo
|
from calibre.gui2.viewer.highlights import HighlightColorCombo
|
||||||
from calibre.gui2.widgets2 import Dialog
|
from calibre.gui2.widgets2 import Dialog
|
||||||
from calibre.utils.icu import primary_sort_key
|
from calibre.utils.icu import primary_sort_key
|
||||||
from polyglot.binary import as_hex_unicode, from_hex_unicode
|
from polyglot.binary import from_hex_unicode
|
||||||
|
|
||||||
# --- Backend Abstraction & Cost Data ---
|
# --- Backend Abstraction & Cost Data ---
|
||||||
MODEL_COSTS = {
|
MODEL_COSTS = {
|
||||||
@ -559,30 +561,17 @@ class ActionEditDialog(QDialog):
|
|||||||
return Action(f'custom-{title}', title, self.prompt_edit.toPlainText().strip())
|
return Action(f'custom-{title}', title, self.prompt_edit.toPlainText().strip())
|
||||||
|
|
||||||
|
|
||||||
class LLMSettingsDialog(Dialog):
|
class LLMSettingsWidget(QWidget):
|
||||||
actions_updated = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(title=_('LLM Settings'), name='llm-settings-dialog', prefs=vprefs, parent=parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
self.setMinimumWidth(550)
|
self.setMinimumWidth(550)
|
||||||
self.layout = QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
api_model_layout = QFormLayout()
|
api_model_layout = QFormLayout()
|
||||||
api_model_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
api_model_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||||
self.api_key_edit = QLineEdit(self)
|
|
||||||
self.api_key_edit.setPlaceholderText(_('Paste your API key here'))
|
|
||||||
self.api_key_edit.setClearButtonEnabled(True)
|
|
||||||
self.model_edit = QLineEdit(self)
|
|
||||||
self.model_edit.setPlaceholderText('google/gemini-flash-1.5')
|
|
||||||
|
|
||||||
self.highlight_color_combo = HighlightColorCombo(self)
|
self.highlight_color_combo = HighlightColorCombo(self)
|
||||||
|
|
||||||
model_label = QLabel(_('&Model (<a href="{}">see list</a>):').format('https://openrouter.ai/models'))
|
|
||||||
model_label.setOpenExternalLinks(True)
|
|
||||||
model_label.setBuddy(self.model_edit)
|
|
||||||
api_model_layout.addRow(_('API &key:'), self.api_key_edit)
|
|
||||||
api_model_layout.addRow(model_label, self.model_edit)
|
|
||||||
api_model_layout.addRow(_('&Highlight style:'), self.highlight_color_combo)
|
api_model_layout.addRow(_('&Highlight style:'), self.highlight_color_combo)
|
||||||
self.layout.addLayout(api_model_layout)
|
self.layout.addLayout(api_model_layout)
|
||||||
self.qa_gb = gb = QGroupBox(_('&Quick actions:'), self)
|
self.qa_gb = gb = QGroupBox(_('&Quick actions:'), self)
|
||||||
@ -600,7 +589,6 @@ class LLMSettingsDialog(Dialog):
|
|||||||
actions_button_layout.addWidget(self.remove_button)
|
actions_button_layout.addWidget(self.remove_button)
|
||||||
actions_button_layout.addStretch(100)
|
actions_button_layout.addStretch(100)
|
||||||
l.addLayout(actions_button_layout)
|
l.addLayout(actions_button_layout)
|
||||||
self.layout.addWidget(self.bb)
|
|
||||||
self.add_button.clicked.connect(self.add_action)
|
self.add_button.clicked.connect(self.add_action)
|
||||||
self.edit_button.clicked.connect(self.edit_action)
|
self.edit_button.clicked.connect(self.edit_action)
|
||||||
self.remove_button.clicked.connect(self.remove_action)
|
self.remove_button.clicked.connect(self.remove_action)
|
||||||
@ -609,8 +597,6 @@ class LLMSettingsDialog(Dialog):
|
|||||||
self.actions_list.setFocus()
|
self.actions_list.setFocus()
|
||||||
|
|
||||||
def load_settings(self):
|
def load_settings(self):
|
||||||
self.api_key_edit.setText(from_hex_unicode(vprefs.get('llm_api_key', '')))
|
|
||||||
self.model_edit.setText(vprefs.get('llm_model_id', 'google/gemini-flash-1.5'))
|
|
||||||
if hsn := vprefs.get('llm_highlight_style'):
|
if hsn := vprefs.get('llm_highlight_style'):
|
||||||
self.highlight_color_combo.highlight_style_name = hsn
|
self.highlight_color_combo.highlight_style_name = hsn
|
||||||
self.load_actions_from_prefs()
|
self.load_actions_from_prefs()
|
||||||
@ -662,13 +648,9 @@ class LLMSettingsDialog(Dialog):
|
|||||||
):
|
):
|
||||||
self.actions_list.takeItem(self.actions_list.row(item))
|
self.actions_list.takeItem(self.actions_list.row(item))
|
||||||
|
|
||||||
def accept(self):
|
def commit(self) -> bool:
|
||||||
vprefs.set('llm_api_key', as_hex_unicode(self.api_key_edit.text().strip()))
|
|
||||||
vprefs.set('llm_model_id', self.model_edit.text().strip() or 'google/gemini-flash-1.5')
|
|
||||||
|
|
||||||
selected_internal_name = self.highlight_color_combo.currentData()
|
selected_internal_name = self.highlight_color_combo.currentData()
|
||||||
vprefs.set('llm_highlight_style', selected_internal_name)
|
vprefs.set('llm_highlight_style', selected_internal_name)
|
||||||
|
|
||||||
disabled_defaults = []
|
disabled_defaults = []
|
||||||
custom_actions = {}
|
custom_actions = {}
|
||||||
for i in range(self.actions_list.count()):
|
for i in range(self.actions_list.count()):
|
||||||
@ -686,6 +668,33 @@ class LLMSettingsDialog(Dialog):
|
|||||||
if custom_actions:
|
if custom_actions:
|
||||||
s['custom_actions'] = custom_actions
|
s['custom_actions'] = custom_actions
|
||||||
vprefs.set('llm_quick_actions', s)
|
vprefs.set('llm_quick_actions', s)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LLMSettingsDialog(Dialog):
|
||||||
|
actions_updated = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(title=_('AI Settings'), name='llm-settings-dialog', prefs=vprefs, parent=parent)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
l = QVBoxLayout(self)
|
||||||
|
self.tabs = tabs = QTabWidget(self)
|
||||||
|
self.ai_config = ai = ConfigureAI(parent=self)
|
||||||
|
tabs.addTab(ai, QIcon.ic('ai.png'), _('AI &Provider'))
|
||||||
|
self.llm_config = llm = LLMSettingsWidget(self)
|
||||||
|
tabs.addTab(llm, QIcon.ic('config.png'), _('Actions and &highlights'))
|
||||||
|
tabs.setCurrentWidget(llm if self.ai_config.is_ready_for_use else ai)
|
||||||
|
l.addWidget(tabs)
|
||||||
|
l.addWidget(self.bb)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
if not self.ai_config.commit():
|
||||||
|
self.tabs.setCurrentWidget(self.ai_config)
|
||||||
|
return
|
||||||
|
if not self.llm_config.commit():
|
||||||
|
self.tabs.setCurrentWidget(self.llm_config)
|
||||||
|
return
|
||||||
self.actions_updated.emit()
|
self.actions_updated.emit()
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user