Skip to content

Commit 4c6d872

Browse files
authored
Merge pull request #57 from rmusser01/dev
Sync
2 parents 730e664 + f173dfa commit 4c6d872

27 files changed

+2133
-653
lines changed

Tests/Chat/test_chat_features.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
import pytest
22
import pytest_asyncio
3-
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
3+
from unittest.mock import AsyncMock, MagicMock, patch
44

5-
from textual.app import App
65
from textual.widgets import Button, TextArea, Static, Select, Checkbox, Input, Label
76
from textual.containers import VerticalScroll
87
from rich.text import Text
98

109
# Modules to be tested
1110
from tldw_chatbook.Widgets.chat_message import ChatMessage
12-
from tldw_chatbook.Event_Handlers.chat_events import (
11+
from tldw_chatbook.Event_Handlers.Chat_Events.chat_events import (
1312
handle_continue_response_button_pressed,
1413
handle_respond_for_me_button_pressed
1514
)
1615
# Mocked app class (simplified)
1716
from tldw_chatbook.app import TldwCli
1817
from tldw_chatbook.Character_Chat import Character_Chat_Lib as ccl
19-
from tldw_chatbook.Utils.Emoji_Handling import get_char, EMOJI_THINKING, FALLBACK_THINKING
2018

2119

2220
# Test Case 1: Thumbs Up/Down Icon Visibility

Tests/Chat/test_chat_sidebar_media_search.py

Lines changed: 336 additions & 0 deletions
Large diffs are not rendered by default.

tldw_chatbook/Constants.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
########################################################################################################################
1111
#
1212
# Functions:
13+
from tldw_chatbook.Third_Party.textual_fspicker import Filters
1314

1415
# --- Constants ---
1516
TAB_CHAT = "chat"
@@ -27,6 +28,21 @@
2728
ALL_TABS = [TAB_CHAT, TAB_CCP, TAB_NOTES, TAB_MEDIA, TAB_SEARCH, TAB_INGEST,
2829
TAB_TOOLS_SETTINGS, TAB_LLM, TAB_LOGS, TAB_STATS, TAB_EVALS, TAB_CODING]
2930

31+
# --- TLDW API Form Specific Option Containers (IDs) ---
32+
TLDW_API_VIDEO_OPTIONS_ID = "tldw-api-video-options"
33+
TLDW_API_AUDIO_OPTIONS_ID = "tldw-api-audio-options"
34+
TLDW_API_PDF_OPTIONS_ID = "tldw-api-pdf-options"
35+
TLDW_API_EBOOK_OPTIONS_ID = "tldw-api-ebook-options"
36+
TLDW_API_DOCUMENT_OPTIONS_ID = "tldw-api-document-options"
37+
TLDW_API_XML_OPTIONS_ID = "tldw-api-xml-options"
38+
TLDW_API_MEDIAWIKI_OPTIONS_ID = "tldw-api-mediawiki-options"
39+
40+
ALL_TLDW_API_OPTION_CONTAINERS = [
41+
TLDW_API_VIDEO_OPTIONS_ID, TLDW_API_AUDIO_OPTIONS_ID, TLDW_API_PDF_OPTIONS_ID,
42+
TLDW_API_EBOOK_OPTIONS_ID, TLDW_API_DOCUMENT_OPTIONS_ID, TLDW_API_XML_OPTIONS_ID,
43+
TLDW_API_MEDIAWIKI_OPTIONS_ID
44+
]
45+
3046

3147
# --- CSS definition ---
3248
# (Keep your CSS content here, make sure IDs match widgets)

tldw_chatbook/Event_Handlers/chat_events.py renamed to tldw_chatbook/Event_Handlers/Chat_Events/chat_events.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,22 @@
1818
from textual.containers import VerticalScroll
1919
from textual.css.query import QueryError
2020

21-
from ..Utils.Utils import safe_float, safe_int
21+
from tldw_chatbook.Utils.Utils import safe_float, safe_int
2222
#
2323
# Local Imports
24-
from ..Widgets.chat_message import ChatMessage
25-
from ..Widgets.titlebar import TitleBar
26-
from ..Utils.Emoji_Handling import (
24+
from tldw_chatbook.Widgets.chat_message import ChatMessage
25+
from tldw_chatbook.Widgets.titlebar import TitleBar
26+
from tldw_chatbook.Utils.Emoji_Handling import (
2727
get_char, EMOJI_THINKING, FALLBACK_THINKING, EMOJI_EDIT, FALLBACK_EDIT,
2828
EMOJI_SAVE_EDIT, FALLBACK_SAVE_EDIT, EMOJI_COPIED, FALLBACK_COPIED, EMOJI_COPY, FALLBACK_COPY
2929
)
30-
from ..Character_Chat import Character_Chat_Lib as ccl
31-
from ..Character_Chat.Character_Chat_Lib import load_character_and_image
32-
from ..DB.ChaChaNotes_DB import ConflictError, CharactersRAGDBError, InputError
33-
from ..Prompt_Management import Prompts_Interop as prompts_interop
30+
from tldw_chatbook.Character_Chat import Character_Chat_Lib as ccl
31+
from tldw_chatbook.Character_Chat.Character_Chat_Lib import load_character_and_image
32+
from tldw_chatbook.DB.ChaChaNotes_DB import ConflictError, CharactersRAGDBError, InputError
33+
from tldw_chatbook.Prompt_Management import Prompts_Interop as prompts_interop
3434
#
3535
if TYPE_CHECKING:
36-
from ..app import TldwCli
36+
from tldw_chatbook.app import TldwCli
3737

3838

3939
#
@@ -2633,6 +2633,38 @@ async def populate_chat_conversation_character_filter_select(app: 'TldwCli') ->
26332633
except Exception as e_unexp:
26342634
logging.error(f"Unexpected error populating char filter select (Chat Tab): {e_unexp}", exc_info=True)
26352635

2636+
2637+
# --- Button Handler Map ---
2638+
# This maps button IDs to their async handler functions.
2639+
CHAT_BUTTON_HANDLERS = {
2640+
"send-chat": lambda app: handle_chat_send_button_pressed(app, "chat"),
2641+
"respond-for-me-button": handle_respond_for_me_button_pressed,
2642+
"stop-chat-generation": handle_stop_chat_generation_pressed,
2643+
"chat-new-conversation-button": handle_chat_new_conversation_button_pressed,
2644+
"chat-new-temp-chat-button": handle_chat_new_conversation_button_pressed, # Reuses handler
2645+
"chat-save-current-chat-button": handle_chat_save_current_chat_button_pressed,
2646+
"chat-save-conversation-details-button": handle_chat_save_details_button_pressed,
2647+
"chat-conversation-load-selected-button": handle_chat_load_selected_button_pressed,
2648+
"chat-prompt-load-selected-button": handle_chat_view_selected_prompt_button_pressed,
2649+
"chat-prompt-copy-system-button": handle_chat_copy_system_prompt_button_pressed,
2650+
"chat-prompt-copy-user-button": handle_chat_copy_user_prompt_button_pressed,
2651+
"chat-load-character-button": handle_chat_load_character_button_pressed,
2652+
"chat-clear-active-character-button": handle_chat_clear_active_character_button_pressed,
2653+
2654+
# --- Sidebar Toggles ---
2655+
"toggle-chat-left-sidebar": lambda app: handle_chat_tab_sidebar_toggle(app, "toggle-chat-left-sidebar"),
2656+
"toggle-chat-right-sidebar": lambda app: handle_chat_tab_sidebar_toggle(app, "toggle-chat-right-sidebar"),
2657+
2658+
# --- Sidebar Media Buttons (from chat_events_sidebar.py) ---
2659+
"chat-media-load-selected-button": 'chat_events_sidebar.handle_chat_media_load_selected_button_pressed',
2660+
"chat-media-copy-title-button": 'chat_events_sidebar.handle_chat_media_copy_title_button_pressed',
2661+
"chat-media-copy-content-button": 'chat_events_sidebar.handle_chat_media_copy_content_button_pressed',
2662+
"chat-media-copy-author-button": 'chat_events_sidebar.handle_chat_media_copy_author_button_pressed',
2663+
"chat-media-copy-url-button": 'chat_events_sidebar.handle_chat_media_copy_url_button_pressed',
2664+
2665+
# --- Note: ChatMessage action buttons (like edit, copy, delete) are handled separately by _get_chat_message_widget_from_button ---
2666+
}
2667+
26362668
#
26372669
# End of chat_events.py
26382670
########################################################################################################################

tldw_chatbook/Event_Handlers/Chat_Events/chat_events_sidebar.py

Lines changed: 229 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,241 @@
22
#
33
# Imports
44
#
5-
# # 3rd-party Libraries
5+
# Standard Library Imports
6+
from typing import TYPE_CHECKING, Dict, Any, Optional
7+
import logging
8+
9+
# 3rd-party Libraries
10+
from textual.widgets import ListItem, Input, ListView, TextArea, Button, Label
11+
from textual.css.query import QueryError
12+
from textual.app import App
613
#
714
# Local Imports
15+
from tldw_chatbook.DB.Client_Media_DB_v2 import MediaDatabase
16+
if TYPE_CHECKING:
17+
from tldw_chatbook.app import TldwCli # Assuming your app class is TldwCli
818
#
919
###########################################################################################################################
1020
#
11-
## Functions:
12-
13-
14-
15-
16-
17-
18-
19-
20-
21-
22-
23-
24-
21+
# Globals
22+
#
23+
logger = logging.getLogger(__name__) # Standard practice for logging
24+
#
25+
###########################################################################################################################
26+
#
27+
# Functions:
28+
#
2529

30+
def _disable_media_copy_buttons(app: 'TldwCli'):
31+
"""Helper to disable all media copy buttons and clear current sidebar media item."""
32+
app.current_sidebar_media_item = None
33+
try:
34+
app.query_one("#chat-media-copy-title-button", Button).disabled = True
35+
app.query_one("#chat-media-copy-content-button", Button).disabled = True
36+
app.query_one("#chat-media-copy-author-button", Button).disabled = True
37+
app.query_one("#chat-media-copy-url-button", Button).disabled = True
38+
except QueryError as e:
39+
logger.warning(f"Could not find a media copy button to disable: {e}")
40+
41+
42+
async def perform_media_sidebar_search(app: 'TldwCli', search_term: str):
43+
"""
44+
Performs a search for media items and populates the results in the sidebar.
45+
This is now an async function.
46+
"""
47+
logger.debug(f"Performing media sidebar search for term: '{search_term}'")
48+
try:
49+
results_list_view = app.query_one("#chat-media-search-results-listview", ListView)
50+
# FIX: Query for the correct ID for the content display area.
51+
review_display = app.query_one("#chat-media-content-display", TextArea)
52+
except QueryError as e:
53+
logger.error(f"Error querying media search UI elements: {e}")
54+
app.notify(f"Error accessing media search UI: {e}", severity="error")
55+
return
56+
57+
await results_list_view.clear()
58+
review_display.clear()
59+
_disable_media_copy_buttons(app)
60+
61+
if not search_term:
62+
logger.debug("Search term is empty, clearing results.")
63+
return
64+
65+
try:
66+
if not app.media_db:
67+
logger.error("app.media_db is not available.")
68+
app.notify("Media database service not initialized.", severity="error")
69+
return
70+
71+
db_instance = app.media_db
72+
73+
search_fields = ['title', 'content', 'author', 'keywords', 'notes']
74+
media_types_filter = None
75+
76+
logger.debug(f"Searching media DB with term: '{search_term}', fields: {search_fields}, types: {media_types_filter}")
77+
78+
media_items = db_instance.search_media_db(
79+
search_query=search_term,
80+
search_fields=search_fields,
81+
media_types=media_types_filter,
82+
date_range=None, # No date range filtering
83+
must_have_keywords=None,
84+
must_not_have_keywords=None,
85+
sort_by="last_modified_desc", # Default sort order
86+
media_ids_filter=None, # No specific media IDs to filter
87+
page=1, # Default to first page
88+
results_per_page=100, # Limit results to 100 for performance
89+
include_trash=False,
90+
include_deleted=False,
91+
)
92+
logger.debug(f"Found {len(media_items)} media items.")
93+
94+
if not media_items:
95+
# FIX: Await the async append method.
96+
await results_list_view.append(ListItem(Label("No media found.")))
97+
else:
98+
for item_dict in media_items:
99+
if isinstance(item_dict, dict):
100+
title = item_dict.get('title', 'Untitled')
101+
media_id = item_dict.get('media_id', 'Unknown ID')
102+
display_label = f"{title} (ID: {media_id[:8]}...)"
103+
list_item = ListItem(Label(display_label))
104+
setattr(list_item, 'media_data', item_dict)
105+
await results_list_view.append(list_item)
106+
else:
107+
logger.warning(f"Skipping non-dictionary item from DB search results: {item_dict}")
108+
109+
except Exception as e:
110+
logger.error(f"Exception during media search: {e}", exc_info=True)
111+
app.notify(f"Error during media search: {e}", severity="error")
112+
await results_list_view.clear()
113+
# FIX: Await the async append method.
114+
await results_list_view.append(ListItem(Label(f"Search error.")))
115+
116+
117+
async def handle_chat_media_search_input_changed(app: 'TldwCli', input_widget: Input):
118+
"""
119+
Handles changes in the media search input, debouncing the search.
120+
"""
121+
search_term = input_widget.value.strip()
122+
logger.debug(f"Media search input changed. Current value: '{search_term}'")
123+
124+
if hasattr(app, '_media_sidebar_search_timer') and app._media_sidebar_search_timer:
125+
app._media_sidebar_search_timer.stop()
126+
logger.debug("Stopped existing media search timer.")
127+
128+
app._media_sidebar_search_timer = app.set_timer(
129+
0.5,
130+
lambda: app.run_worker(perform_media_sidebar_search(app, search_term), exclusive=True)
131+
)
132+
logger.debug(f"Set new media search timer for term: '{search_term}'")
133+
134+
135+
async def handle_chat_media_load_selected_button_pressed(app: 'TldwCli'):
136+
"""
137+
Loads the selected media item's details into the review display.
138+
"""
139+
logger.debug("Load Selected Media button pressed.")
140+
try:
141+
results_list_view = app.query_one("#chat-media-search-results-listview", ListView)
142+
# FIX: Query for the correct ID.
143+
review_display = app.query_one("#chat-media-content-display", TextArea)
144+
except QueryError as e:
145+
logger.error(f"Error querying media UI elements for load: {e}")
146+
_disable_media_copy_buttons(app)
147+
return
148+
149+
highlighted_item = results_list_view.highlighted_child
150+
if highlighted_item is None or not hasattr(highlighted_item, 'media_data'):
151+
app.notify("No media item selected.", severity="warning")
152+
review_display.clear()
153+
_disable_media_copy_buttons(app)
154+
return
155+
156+
media_data: Dict[str, Any] = getattr(highlighted_item, 'media_data')
157+
app.current_sidebar_media_item = media_data
158+
159+
# Format details for display
160+
details_parts = []
161+
if media_data.get('title'): details_parts.append(f"Title: {media_data['title']}")
162+
if media_data.get('author'): details_parts.append(f"Author: {media_data['author']}")
163+
if media_data.get('media_type'): details_parts.append(f"Type: {media_data['media_type']}")
164+
if media_data.get('url'): details_parts.append(f"URL: {media_data['url']}")
165+
details_parts.append("\n--- Content Snippet ---\n")
166+
content_preview = (media_data['content'][:500] + '...') if len(media_data.get('content', '')) > 500 else media_data.get('content')
167+
details_parts.append(content_preview or "No content available.")
168+
169+
review_display.load_text("\n".join(details_parts))
170+
logger.info(f"Successfully loaded media ID {media_data.get('media_id')} into review display.")
171+
172+
# Enable copy buttons
173+
app.query_one("#chat-media-copy-title-button", Button).disabled = not bool(media_data.get('title'))
174+
app.query_one("#chat-media-copy-content-button", Button).disabled = not bool(media_data.get('content'))
175+
app.query_one("#chat-media-copy-author-button", Button).disabled = not bool(media_data.get('author'))
176+
app.query_one("#chat-media-copy-url-button", Button).disabled = not bool(media_data.get('url'))
177+
logger.debug("Media copy buttons state updated.")
178+
179+
180+
async def handle_chat_media_copy_title_button_pressed(app: 'TldwCli'):
181+
"""Copies the title of the currently loaded sidebar media to clipboard."""
182+
logger.debug("Copy Title button pressed.")
183+
if app.current_sidebar_media_item and 'title' in app.current_sidebar_media_item:
184+
title = str(app.current_sidebar_media_item['title'])
185+
app.copy_to_clipboard(title)
186+
app.notify("Title copied to clipboard.")
187+
logger.info(f"Copied title: '{title}'")
188+
else:
189+
app.notify("No media title to copy.", severity="warning")
190+
logger.warning("No media title available to copy.")
191+
192+
193+
async def handle_chat_media_copy_content_button_pressed(app: 'TldwCli'):
194+
"""Copies the content of the currently loaded sidebar media to clipboard."""
195+
logger.debug("Copy Content button pressed.")
196+
if app.current_sidebar_media_item and 'content' in app.current_sidebar_media_item:
197+
content = str(app.current_sidebar_media_item['content'])
198+
app.copy_to_clipboard(content)
199+
app.notify("Content copied to clipboard.")
200+
logger.info("Copied content (length: %s)", len(content))
201+
else:
202+
app.notify("No media content to copy.", severity="warning")
203+
logger.warning("No media content available to copy.")
204+
205+
206+
async def handle_chat_media_copy_author_button_pressed(app: 'TldwCli'):
207+
"""Copies the author of the currently loaded sidebar media to clipboard."""
208+
logger.debug("Copy Author button pressed.")
209+
if app.current_sidebar_media_item and 'author' in app.current_sidebar_media_item:
210+
author = str(app.current_sidebar_media_item['author'])
211+
app.copy_to_clipboard(author)
212+
app.notify("Author copied to clipboard.")
213+
logger.info(f"Copied author: '{author}'")
214+
else:
215+
app.notify("No media author to copy.", severity="warning")
216+
logger.warning("No media author available to copy.")
217+
218+
219+
async def handle_chat_media_copy_url_button_pressed(app: 'TldwCli'):
220+
"""Copies the URL of the currently loaded sidebar media to clipboard."""
221+
logger.debug("Copy URL button pressed.")
222+
if app.current_sidebar_media_item and 'url' in app.current_sidebar_media_item and app.current_sidebar_media_item['url']:
223+
url = str(app.current_sidebar_media_item['url'])
224+
app.copy_to_clipboard(url)
225+
app.notify("URL copied to clipboard.")
226+
logger.info(f"Copied URL: '{url}'")
227+
else:
228+
app.notify("No media URL to copy.", severity="warning")
229+
logger.warning("No media URL available to copy.")
230+
231+
232+
# --- Button Handler Map ---
233+
CHAT_SIDEBAR_BUTTON_HANDLERS = {
234+
"chat-media-load-selected-button": handle_chat_media_load_selected_button_pressed,
235+
"chat-media-copy-title-button": handle_chat_media_copy_title_button_pressed,
236+
"chat-media-copy-content-button": handle_chat_media_copy_content_button_pressed,
237+
"chat-media-copy-author-button": handle_chat_media_copy_author_button_pressed,
238+
"chat-media-copy-url-button": handle_chat_media_copy_url_button_pressed,
239+
}
26240

27241

28242
#

0 commit comments

Comments
 (0)