mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-26 23:00:20 -05:00
Add a copy to clipboard for individual responses
This commit is contained in:
parent
4f522450ce
commit
852acfc21b
@ -39,7 +39,7 @@ class Button(NamedTuple):
|
||||
|
||||
@property
|
||||
def as_html(self) -> str:
|
||||
return f'''<a style="text-decoration: none" href="{escape(self.link)}" tooltip="{escape(self.tooltip)}">{escape(self.text)}</a>'''
|
||||
return f'''<a style="text-decoration: none" href="{escape(self.link)}" title="{escape(self.tooltip)}">{escape(self.text)}</a>'''
|
||||
|
||||
|
||||
class Header(NamedTuple):
|
||||
@ -48,7 +48,7 @@ class Header(NamedTuple):
|
||||
|
||||
@property
|
||||
def as_html(self) -> str:
|
||||
links = '\xa0'.join(b.as_html for b in self.buttons)
|
||||
links = '\xa0\xa0'.join(b.as_html for b in self.buttons)
|
||||
title = '<b><i>' + escape(self.title)
|
||||
if links:
|
||||
return f'''<table width="100%" cellpadding="0" cellspacing="0"><tr><td>{title}\xa0</td>
|
||||
@ -149,7 +149,7 @@ class ChatWidget(QWidget):
|
||||
self.blocks: list[str] = []
|
||||
self.current_message = ''
|
||||
pal = self.palette()
|
||||
self.alternate_color = pal.color(QPalette.ColorRole.Window).name()
|
||||
self.response_color = pal.color(QPalette.ColorRole.Window).name()
|
||||
self.base_color = pal.color(QPalette.ColorRole.Base).name()
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
@ -158,19 +158,19 @@ class ChatWidget(QWidget):
|
||||
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:
|
||||
def add_block(self, body_html: str, header: Header = Header(), is_response: bool = False) -> None:
|
||||
self.current_message = ''
|
||||
html = ''
|
||||
if header.title or header.buttons:
|
||||
html += f'<div>{header.as_html}</div>'
|
||||
html += f'<div>{body_html}</div>'
|
||||
bg = self.alternate_color if is_alternate else self.base_color
|
||||
bg = self.response_color if is_response else self.base_color
|
||||
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:
|
||||
def replace_last_block(self, body_html: str, header: Header = Header(), is_response: bool = False) -> None:
|
||||
if self.blocks:
|
||||
del self.blocks[-1]
|
||||
self.add_block(body_html, header, is_alternate)
|
||||
self.add_block(body_html, header, is_response)
|
||||
|
||||
def show_message(self, msg_html: str, details: str = '', level: int = INFO) -> None:
|
||||
self.blocks = []
|
||||
|
||||
@ -11,6 +11,7 @@ from typing import NamedTuple
|
||||
|
||||
from qt.core import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QDateTime,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
@ -114,6 +115,9 @@ class ConversationHistory:
|
||||
def __iter__(self) -> Iterator[ChatMessage]:
|
||||
return iter(self.items)
|
||||
|
||||
def reverse_iter(self) -> Iterator[ChatMessage]:
|
||||
return reversed(self.items)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.items)
|
||||
|
||||
@ -132,6 +136,11 @@ class ConversationHistory:
|
||||
ans.items = self.items[:upto]
|
||||
return ans
|
||||
|
||||
def only(self, message_index: int) -> 'ConversationHistory':
|
||||
ans = self.copy(message_index + 1)
|
||||
ans.items = [ans.items[-1]]
|
||||
return ans
|
||||
|
||||
def at(self, x: int) -> ChatMessage:
|
||||
return self.items[x]
|
||||
|
||||
@ -160,7 +169,7 @@ def format_llm_note(conversation: ConversationHistory, assistant_name: str) -> s
|
||||
return ''
|
||||
|
||||
main_response = ''
|
||||
for message in reversed(conversation):
|
||||
for message in conversation.reverse_iter():
|
||||
if message.from_assistant:
|
||||
main_response = message.query.strip()
|
||||
break
|
||||
@ -168,8 +177,11 @@ def format_llm_note(conversation: ConversationHistory, assistant_name: str) -> s
|
||||
if not main_response:
|
||||
return ''
|
||||
|
||||
timestamp = QLocale.system().toString(QDateTime.currentDateTime(), QLocale.FormatType.LongFormat)
|
||||
header = f'--- {_("AI Assistant Note")} ({timestamp}) ---'
|
||||
timestamp = QLocale.system().toString(QDateTime.currentDateTime(), QLocale.FormatType.ShortFormat)
|
||||
sep = '―――'
|
||||
header = f'{sep} {_("AI Assistant Note")} ({timestamp}) {sep}'
|
||||
if len(conversation) == 1:
|
||||
return f'{header}\n\n{main_response}'
|
||||
|
||||
record_lines = []
|
||||
for message in conversation:
|
||||
@ -185,11 +197,10 @@ def format_llm_note(conversation: ConversationHistory, assistant_name: str) -> s
|
||||
record_lines.append(entry)
|
||||
|
||||
record_body = '\n\n'.join(record_lines)
|
||||
record_header = f'--- {_("Conversation record")} ---'
|
||||
record_header = f'{sep} {_("Conversation record")} {sep}'
|
||||
|
||||
return (
|
||||
f'{header}\n\n{main_response}\n\n'
|
||||
f'------------------------------------\n\n'
|
||||
f'{record_header}\n\n{record_body}'
|
||||
)
|
||||
|
||||
@ -200,8 +211,10 @@ class LLMPanel(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.save_note_hostname = f'{uuid4().lower()}.calibre'
|
||||
self.configure_ai_hostname = f'{uuid4().lower()}.calibre'
|
||||
hid = uuid4().lower()
|
||||
self.save_note_hostname = f'{hid}.save.calibre'
|
||||
self.configure_ai_hostname = f'{hid}.config.calibre'
|
||||
self.copy_hostname = f'{hid}.copy.calibre'
|
||||
self.counter = count(start=1)
|
||||
|
||||
self.conversation_history = ConversationHistory()
|
||||
@ -360,15 +373,19 @@ class LLMPanel(QWidget):
|
||||
if not content_for_display:
|
||||
continue
|
||||
header = Header()
|
||||
alternate = False
|
||||
is_response = 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, alternate)
|
||||
is_response = True
|
||||
header = Header(assistant, (
|
||||
Button(_('💾'), f'http://{self.save_note_hostname}/{i}', _(
|
||||
'Save this specific response as the note')),
|
||||
Button('📋', f'http://{self.copy_hostname}/{i}', _(
|
||||
'Copy this specific response to the clipboard')),
|
||||
))
|
||||
self.result_display.add_block(content_for_display, header, is_response)
|
||||
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)))
|
||||
self.result_display.add_block(content_for_display, Header(_('{} thinking…').format(assistant)), is_response=True)
|
||||
self.result_display.re_render()
|
||||
|
||||
def scroll_to_bottom(self) -> None:
|
||||
@ -451,23 +468,36 @@ class LLMPanel(QWidget):
|
||||
}
|
||||
self.add_note_requested.emit(payload)
|
||||
|
||||
def save_specific_note(self, message_index):
|
||||
def get_conversation_history_for_specific_response(self, message_index: int) -> ConversationHistory | None:
|
||||
if not (0 <= message_index < len(self.conversation_history)):
|
||||
return
|
||||
if not self.conversation_history.at(message_index).from_assistant:
|
||||
return
|
||||
history_for_record = self.conversation_history.copy(message_index + 1)
|
||||
return None
|
||||
ans = self.conversation_history.at(message_index)
|
||||
if not ans.from_assistant:
|
||||
return None
|
||||
return self.conversation_history.only(message_index)
|
||||
|
||||
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)
|
||||
|
||||
def copy_specific_note(self, message_index: int) -> None:
|
||||
history_for_record = self.get_conversation_history_for_specific_response(message_index)
|
||||
text = format_llm_note(history_for_record, self.assistant_name)
|
||||
if text:
|
||||
QApplication.instance().clipboard().setText(text)
|
||||
|
||||
def on_chat_link_clicked(self, qurl: QUrl):
|
||||
match qurl.host():
|
||||
case self.save_note_hostname:
|
||||
index = int(qurl.path().strip('/'))
|
||||
self.save_specific_note(index)
|
||||
case self.copy_hostname:
|
||||
index = int(qurl.path().strip('/'))
|
||||
self.copy_specific_note(index)
|
||||
case self.configure_ai_hostname:
|
||||
self.show_settings()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user