Get adding llm as note to highlight working

This commit is contained in:
Kovid Goyal 2025-09-07 06:00:54 +05:30
parent 1eef04aaec
commit dbb4a71231
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 55 additions and 111 deletions

View File

@ -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'<a href="http://{self.configure_ai_hostname}">{_("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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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