diff --git a/src/calibre/gui2/viewer/llm.py b/src/calibre/gui2/viewer/llm.py index 731eecae28..57e27546a9 100644 --- a/src/calibre/gui2/viewer/llm.py +++ b/src/calibre/gui2/viewer/llm.py @@ -212,7 +212,7 @@ def format_llm_note(conversation: ConversationHistory, assistant_name: str) -> s class LLMPanel(QWidget): response_received = pyqtSignal(int, object) - add_note_requested = pyqtSignal(dict) + add_note_requested = pyqtSignal(str, str) def __init__(self, parent=None): super().__init__(parent) @@ -224,7 +224,6 @@ class LLMPanel(QWidget): self.reasoning_hostname = f'{hid}.reasoning.calibre' self.counter = count(start=1) - self.latched_highlight_uuid = None self.latched_conversation_text = None self.current_api_call_number = 0 self.session_cost = 0.0 @@ -317,22 +316,18 @@ class LLMPanel(QWidget): msg = f'{_("First, configure an AI provider")}' self.result_display.show_message(msg) - def update_with_text(self, text, highlight_data=None): + def update_with_text(self, text: str) -> None: self.update_ai_provider_plugin() - new_uuid = (highlight_data or {}).get('uuid') - if not text and not new_uuid: - if self.latched_conversation_text is not None or self.latched_highlight_uuid is not None: + if not text: + if self.latched_conversation_text is not None: self.start_new_conversation() return start_new_convo = False - if new_uuid != self.latched_highlight_uuid: - start_new_convo = True - elif new_uuid is None and text != self.latched_conversation_text: + if text != self.latched_conversation_text: start_new_convo = True if start_new_convo: - self.latched_highlight_uuid = new_uuid self.latched_conversation_text = text self.clear_current_conversation() self.update_ui_state() @@ -343,7 +338,6 @@ class LLMPanel(QWidget): def start_new_conversation(self): self.clear_current_conversation() - self.latched_highlight_uuid = None self.latched_conversation_text = None self.update_ui_state() @@ -467,12 +461,6 @@ class LLMPanel(QWidget): has_responses = self.conversation_history.response_count > 0 for b in self.response_buttons.values(): b.setEnabled(has_responses) - if has_responses: - if self.latched_highlight_uuid: - tt = _("Append this response to the existing highlight's note") - else: - tt = _('Create a new highlight for the selected text and save this response as its note') - self.response_buttons[self.save_as_note].setToolTip(tt) def update_cost(self): h = self.conversation_history @@ -487,11 +475,8 @@ class LLMPanel(QWidget): def save_as_note(self): if self.conversation_history.response_count > 0 and self.latched_conversation_text: - payload = { - 'highlight': self.latched_highlight_uuid, - 'llm_note': format_llm_note(self.conversation_history, self.assistant_name), - } - self.add_note_requested.emit(payload) + self.add_note_requested.emit( + format_llm_note(self.conversation_history, self.assistant_name), vprefs.get('llm_highlight_style', '')) def get_conversation_history_for_specific_response(self, message_index: int) -> ConversationHistory | None: if not (0 <= message_index < len(self.conversation_history)): @@ -503,11 +488,8 @@ class LLMPanel(QWidget): def save_specific_note(self, message_index: int) -> None: history_for_record = self.get_conversation_history_for_specific_response(message_index) - payload = { - 'highlight': self.latched_highlight_uuid, - 'llm_note': format_llm_note(history_for_record, self.assistant_name), - } - self.add_note_requested.emit(payload) + self.add_note_requested.emit( + format_llm_note(history_for_record, self.assistant_name), vprefs.get('llm_highlight_style', '')) def show_reasoning(self, message_index: int) -> None: h = self.get_conversation_history_for_specific_response(message_index) diff --git a/src/calibre/gui2/viewer/lookup.py b/src/calibre/gui2/viewer/lookup.py index 9aa88466a0..938bfc5d16 100644 --- a/src/calibre/gui2/viewer/lookup.py +++ b/src/calibre/gui2/viewer/lookup.py @@ -333,7 +333,7 @@ def blank_html(): class Lookup(QTabWidget): - llm_add_note_requested = pyqtSignal(dict) + add_note_requested = pyqtSignal(str, str) def __init__(self, parent, viewer=None): QTabWidget.__init__(self, parent) @@ -344,7 +344,6 @@ class Lookup(QTabWidget): self.is_visible = False self.selected_text = '' - self.current_highlight_data = None self.current_highlight_cache = None self.current_query = '' self.current_source = '' @@ -424,8 +423,8 @@ class Lookup(QTabWidget): 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) + self.llm_panel.add_note_requested.connect(self.add_note_requested) + self.llm_panel.update_with_text(self.selected_text) def _tab_changed(self, index): vprefs.set('llm_lookup_tab_index', index) @@ -530,7 +529,7 @@ class Lookup(QTabWidget): current_idx = self.currentIndex() if current_idx == self.llm_tab_index: if self.llm_panel: - self.llm_panel.update_with_text(self.selected_text, self.current_highlight_data) + self.llm_panel.update_with_text(self.selected_text) else: query = self.selected_text or self.current_query if self.query_is_up_to_date or not query: @@ -582,7 +581,6 @@ class Lookup(QTabWidget): if processed_annot_data and processed_annot_data.get('uuid'): self.current_highlight_cache = processed_annot_data - self.current_highlight_data = processed_annot_data self.selected_text = text or '' if self.selected_text and self.currentIndex() == self.llm_tab_index: @@ -593,7 +591,7 @@ class Lookup(QTabWidget): self.update_refresh_button_status() if self.llm_panel: - self.llm_panel.update_with_text(self.selected_text, self.current_highlight_data) + self.llm_panel.update_with_text(self.selected_text) def on_forced_show(self): self.update_query() diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 0e52b53778..1b91c832f8 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -98,7 +98,6 @@ class EbookViewer(MainWindow): MainWindow.__init__(self, None) get_boss(self) - self.pending_note_for_next_highlight = None self.annotations_saver = None self.calibre_book_data_for_first_book = calibre_book_data self.shutting_down = self.close_forced = self.shutdown_done = False @@ -223,12 +222,7 @@ class EbookViewer(MainWindow): self.continue_reading() self.setup_mouse_auto_hide() - - try: - self.lookup_widget.llm_add_note_requested.disconnect(self.add_note_to_highlight) - except TypeError: - pass - self.lookup_widget.llm_add_note_requested.connect(self.add_note_to_highlight) + self.lookup_widget.add_note_requested.connect(self.add_notes_or_create_highlight) def create_uuid(self): return uuid.uuid4().hex @@ -858,35 +852,10 @@ class EbookViewer(MainWindow): def highlights_changed(self, changed_annotations: list): try: - if self.pending_note_for_next_highlight: - old_uuids = {h.get('uuid') for h in self.current_book_data.get('annotations_map', {}).get('highlight', []) if h.get('uuid')} - new_master_uuids = {h.get('uuid') for h in changed_annotations if h.get('uuid')} - newly_created_uuids = new_master_uuids - old_uuids - - if newly_created_uuids: - new_uuid = newly_created_uuids.pop() - note_to_add = self.pending_note_for_next_highlight - - js_payload_note = {'uuid': new_uuid, 'notes': note_to_add} - self.web_view.generic_action('set-notes-in-highlight', js_payload_note) - - for h in changed_annotations: - if h.get('uuid') == new_uuid: - h['notes'] = note_to_add - break - - js_payload_focus = {'uuid': new_uuid} - self.web_view.generic_action('show-highlight-selection-bar', js_payload_focus) - - self.pending_note_for_next_highlight = None - else: - self.pending_note_for_next_highlight = None - master_map = self.current_book_data.setdefault('annotations_map', {}) master_map['highlight'] = changed_annotations self.highlights_widget.load(changed_annotations) self.save_annotations() - except Exception: import traceback traceback.print_exc() @@ -902,42 +871,11 @@ class EbookViewer(MainWindow): self.save_annotations() self.web_view.generic_action('set-notes-in-highlight', {'uuid': uuid, 'notes': notes}) - def add_note_to_highlight(self, payload): - highlight_uuid = payload.get('highlight') - new_self_contained_entry = payload.get('llm_note', '') - if not new_self_contained_entry: + def add_notes_or_create_highlight(self, notes: str, style_name_for_new_highlight: str = ''): + if not notes: return - - if highlight_uuid: - found_highlight = False - highlight_list = self.current_book_data.setdefault('annotations_map', {}).get('highlight', []) - for h in highlight_list: - if h.get('uuid') == highlight_uuid: - found_highlight = True - existing_note = h.get('notes', '').strip() - - separator = '\n\n------------------------------------\n\n' - combined_note = f'{existing_note}{separator}{new_self_contained_entry}' if existing_note else new_self_contained_entry - - h['notes'] = combined_note - h['timestamp'] = utcnow().isoformat() - - js_payload = {'uuid': highlight_uuid, 'notes': combined_note} - self.web_view.generic_action('set-notes-in-highlight', js_payload) - - self.save_annotations() - self.statusBar().showMessage('Note appended to highlight', 3000) - break - - if not found_highlight: - # This case should ideally not be hit if the logic is sound, but it is a safe fallback. - pass - - else: - self.pending_note_for_next_highlight = new_self_contained_entry - js_payload = { - 'type': 'apply-highlight', - 'style': style_definition_for_name(vprefs.get('llm_highlight_style', '')), - } - self.web_view.generic_action('annotations', js_payload) - self.statusBar().showMessage('Creating highlight with note...', 3000) + data = { + 'style': style_definition_for_name(style_name_for_new_highlight), + 'notes': notes, + } + self.web_view.generic_action('add-notes-or-create-highlight', data) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 89f06555cc..4bf8b19b7c 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -859,14 +859,14 @@ class IframeBoss: else: end_reference_mode() - def apply_highlight(self, uuid, existing, has_notes, style): + def apply_highlight(self, uuid, existing, has_notes, style_defn): sel = window.getSelection() if not sel.rangeCount: return anchor_before = find_anchor_before_range(sel.getRangeAt(0), self.book.manifest.toc_anchor_map, self.book.manifest.page_list_anchor_map) text = sel.toString() bounds = cfi_for_selection() - style = highlight_style_as_css(style, opts.is_dark_theme, opts.color_scheme.foreground) + style = highlight_style_as_css(style_defn, opts.is_dark_theme, opts.color_scheme.foreground) cls = 'crw-has-dot' if has_notes else None annot_id, intersecting_wrappers = wrap_text_in_range(style, None, cls, self.add_highlight_listeners) removed_highlights = v'[]' @@ -891,7 +891,7 @@ class IframeBoss: 'annotations', type='highlight-applied', uuid=uuid, ok=annot_id is not None, - bounds=bounds, + bounds=bounds, style=style_defn, removed_highlights=removed_highlights, highlighted_text=text, anchor_before=anchor_before diff --git a/src/pyj/read_book/selection_bar.pyj b/src/pyj/read_book/selection_bar.pyj index 0e82e41ab1..8599887e42 100644 --- a/src/pyj/read_book/selection_bar.pyj +++ b/src/pyj/read_book/selection_bar.pyj @@ -989,6 +989,26 @@ class SelectionBar: get_session_data().set('highlight_style', self.current_highlight_style.style) self.focus() + def add_notes_or_create_highlight(self, notes, style): + if self.state is EDITING: + self.hide_editor() + annot_id = self.view.currently_showing.selection.annot_id + if annot_id: + am = self.annotations_manager + existing_notes = am.notes_for_highlight(annot_id) + if existing_notes: + notes = existing_notes + '\n\n------------------------------------\n\n' + notes + q = am.style_for_highlight(annot_id) + if q: + style = q + self.current_notes = notes + self.send_message( + 'apply-highlight', style=style, uuid=short_uuid(), has_notes=v'!!self.current_notes', existing=annot_id, + ) + self.state = WAITING + self.update_position() + self.focus() + def editor_container_clicked(self, ev): ev.stopPropagation(), ev.preventDefault() # }}} @@ -1119,8 +1139,7 @@ class SelectionBar: before = get_toc_nodes_bordering_spine_item()[0] if before: toc_family = family_for_toc_node(before.id) - self.annotations_manager.add_highlight( - msg, self.current_highlight_style.style, notes, toc_family) + self.annotations_manager.add_highlight(msg, msg.style, notes, toc_family) elif msg.type is 'highlight-overlapped': question_dialog( _('Are you sure?'), _('This highlight overlaps existing highlights. Creating it will cause notes' diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index ab1a2fefcc..dcd0e6fb2c 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -1213,6 +1213,11 @@ class View: self.selection_bar.notes_edited(uuid) self.selection_bar.update_position() + def add_notes_or_create_highlight(self, notes, style): + if self.selection_bar.is_visible: + return self.selection_bar.add_notes_or_create_highlight(notes, style) + self.show_error(_('No selection'), _('Cannot create highlight as there is no active selection')) + def show_next_spine_item(self, previous): spine = self.book.manifest.spine idx = spine.indexOf(self.currently_showing.name) diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 318ce6dabb..7aac7986eb 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -267,10 +267,12 @@ def highlight_action(uuid, which): def generic_action(which, data): if which is 'set-notes-in-highlight': view.set_notes_for_highlight(data.uuid, data.notes or '') - if which is 'show-status-message': + elif which is 'show-status-message': view.show_status_message(data.text) - if which is 'remove-recently-opened': + elif which is 'remove-recently-opened': remove_recently_opened(data.path) + elif which is 'add-notes-or-create-highlight': + view.add_notes_or_create_highlight(data.notes, data.style) @from_python