From 32be3f3dbca3d136c8d74c9e735e43e3d5c0ca8c Mon Sep 17 00:00:00 2001 From: Siddhant Sharma Date: Wed, 8 Apr 2026 19:47:03 +0530 Subject: [PATCH 1/2] fix: fix message string representation timestamp format. --- src/memorytext/models/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/memorytext/models/core.py b/src/memorytext/models/core.py index 01bc331..2c78eee 100644 --- a/src/memorytext/models/core.py +++ b/src/memorytext/models/core.py @@ -103,9 +103,9 @@ class Message(Base): conversation: Mapped["Conversation"] = relationship(back_populates="messages") def __str__(self) -> str: - dt_fmt = "%Y-%d-%m %H:%M" + dt_fmt = "%Y-%m-%d %H:%M" return f"{self.timestamp.strftime(dt_fmt)} - {self.sender}: {self.text}" def __repr__(self) -> str: - dt_fmt = "%Y-%d-%m %H:%M" + dt_fmt = "%Y-%m-%d %H:%M" return f"{self.timestamp.strftime(dt_fmt)} - {self.sender}: {self.text}" From b7b1e761c23f148a05d0fd271ee7fe970cea611c Mon Sep 17 00:00:00 2001 From: Siddhant Sharma Date: Wed, 8 Apr 2026 20:48:11 +0530 Subject: [PATCH 2/2] enh: add a jump to date option with toolbar button and shortcut. --- .gitignore | 4 ++ CHANGELOG.md | 3 ++ README.md | 4 +- src/memorytext/models/message_list.py | 25 ++++++++++- src/memorytext/services/message_service.py | 9 ++++ src/memorytext/storage/message_repo.py | 27 ++++++++++++ src/memorytext/views/chat_view.py | 7 +++ src/memorytext/windows/helper_windows.py | 47 +++++++++++++++++++- src/memorytext/windows/main_window.py | 50 +++++++++++++++++++++- 9 files changed, 170 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 56c57f6..ec4c53d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ Thumbs.db # Roadmap / Ideas **/Roadmap.md +# vscode +**/.vscode/ + + # Python cache *.pyc __pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c34701..c73b8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Added 1. Add day separator before the first message of a day. +2. Add a jump / navigate to date floating window triggered with: + - a shortcut: Ctrl+Shift+F. + - a toobar button --- diff --git a/README.md b/README.md index 113ccb5..7b8dadf 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ pip install git+https://gitlab.com/sharmasiddhant/memory.text.git ### Navigation & Structure -- [ ] Day markers -- [ ] Jump to a specific date +- [x] Day markers +- [x] Jump to a specific date - [ ] Timeline-style navigation ### Filter and Search diff --git a/src/memorytext/models/message_list.py b/src/memorytext/models/message_list.py index 9f62621..51e34f5 100644 --- a/src/memorytext/models/message_list.py +++ b/src/memorytext/models/message_list.py @@ -3,11 +3,12 @@ from PySide6.QtCore import QAbstractListModel, QPersistentModelIndex, Qt, QModelIndex from memorytext.util import get_day_string_from_timestamp -from memorytext.services import chat_service +from memorytext.services import chat_service, message_service from memorytext.models.roles import Roles if TYPE_CHECKING: from collections.abc import Callable + import datetime from memorytext.models.core import Message @@ -107,3 +108,25 @@ def fetchMore( self.beginInsertRows(QModelIndex(), offset, offset + count - 1) self._items.extend(new_messages) self.endInsertRows() + + def jump_to_date(self, date: datetime.date): + limit = 10 + target_offset = message_service.get_offset_from_date( + self._conversation_id, date + ) + current_count = self.rowCount() + + if target_offset is not None and target_offset > current_count: + to_fetch = target_offset - current_count + limit + + new_messages, count = self._message_service( + self._conversation_id, to_fetch, current_count + ) + if count: + self.beginInsertRows( + QModelIndex(), current_count, current_count + count - 1 + ) + self._items.extend(new_messages) + self.endInsertRows() + if target_offset is not None: + return self.index(target_offset) diff --git a/src/memorytext/services/message_service.py b/src/memorytext/services/message_service.py index a76420c..7185bfb 100644 --- a/src/memorytext/services/message_service.py +++ b/src/memorytext/services/message_service.py @@ -1,10 +1,19 @@ from __future__ import annotations +from typing import TYPE_CHECKING import memorytext.storage.message_repo as mrepo from memorytext.storage.db import get_db_session +if TYPE_CHECKING: + import datetime + def get_messages( conversation_id: int | None, limit: int = 10, offset: int = 0 ) -> tuple[list, int]: with get_db_session() as session: return mrepo.get_messages(session, conversation_id, limit, offset) + + +def get_offset_from_date(conversation_id: int | None, date: datetime.date): + with get_db_session() as session: + return mrepo.get_offset_from_date(session, conversation_id, date) diff --git a/src/memorytext/storage/message_repo.py b/src/memorytext/storage/message_repo.py index 0b6cee6..94dbdeb 100644 --- a/src/memorytext/storage/message_repo.py +++ b/src/memorytext/storage/message_repo.py @@ -6,6 +6,8 @@ if TYPE_CHECKING: from sqlalchemy.orm import Session + from sqlalchemy import Row + from datetime import date, datetime def get_messages( @@ -25,3 +27,28 @@ def get_messages( message_list = list(session.scalars(stmt_2).fetchall()) session.expunge_all() return message_list, total + + +def get_offset_from_date(session: Session, conversation_id: int | None, date: date): + timestamps = get_first_and_last_ts(session, conversation_id) + date_bdry = date + if timestamps: + first_ts, last_ts = timestamps + if first_ts and date < first_ts.date(): + return 0 + elif last_ts and date > last_ts.date(): + date_bdry = last_ts.date() + + stmt = select(func.count(Message.id)).where( + Message.conversation_id == conversation_id, Message.timestamp < date_bdry + ) + return session.scalar(stmt) + + +def get_first_and_last_ts( + session: Session, conversation_id: int | None +) -> Row[tuple[datetime, datetime]] | None: + stmt = select(func.min(Message.timestamp), func.max(Message.timestamp)).where( + Message.conversation_id == conversation_id + ) + return session.execute(stmt).fetchone() diff --git a/src/memorytext/views/chat_view.py b/src/memorytext/views/chat_view.py index ca6b448..636d9d3 100644 --- a/src/memorytext/views/chat_view.py +++ b/src/memorytext/views/chat_view.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from collections.abc import Callable + import datetime class ChatView(QListView): @@ -42,3 +43,9 @@ def set_messages( def resizeEvent(self, event): super().resizeEvent(event) self.doItemsLayout() + + def jump_to_date(self, date: datetime.date): + target_index = self.message_model.jump_to_date(date) + if target_index: + self.scrollTo(target_index, self.ScrollHint.PositionAtTop) + self.setCurrentIndex(target_index) diff --git a/src/memorytext/windows/helper_windows.py b/src/memorytext/windows/helper_windows.py index 11146e1..d6cf547 100644 --- a/src/memorytext/windows/helper_windows.py +++ b/src/memorytext/windows/helper_windows.py @@ -1,7 +1,8 @@ from __future__ import annotations from typing import TYPE_CHECKING - +import datetime from PySide6.QtWidgets import ( + QDateEdit, QComboBox, QFormLayout, QMainWindow, @@ -12,7 +13,7 @@ QWidget, QLabel, ) -from PySide6.QtCore import Slot +from PySide6.QtCore import Slot, Qt, QDate, Signal from memorytext.services import chat_service if TYPE_CHECKING: @@ -162,3 +163,45 @@ def __init__(self, text: str, ok_action: Callable): container.setLayout(layout) self.setCentralWidget(container) + + +class Floating_Input(QWidget): + selected_date = Signal(datetime.date) + + def __init__( + self, + label: str, + set_date: datetime.date = datetime.date.today(), + chat_view_open: bool = False, + ): + super().__init__() + + self.chat_view_open = chat_view_open + + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint + | Qt.WindowType.WindowStaysOnTopHint + | Qt.WindowType.Tool + ) + layout = QFormLayout(self) + self.input = QDateEdit(self) + self.input.setCalendarPopup(True) + self.input.setDate(QDate(set_date.year, set_date.month, set_date.day)) + self.input.setDisplayFormat("yyyy-MM-dd") + layout.addRow(label, self.input) + + self.input.editingFinished.connect(self.emit_input) + + def toggle_window(self): + if self.isVisible() or not self.chat_view_open: + self.hide() + else: + self.show() + self.activateWindow() + self.input.setFocus() + + def emit_input(self): + if self.input.hasFocus(): + qdate = self.input.date() + self.selected_date.emit(qdate.toPython()) + self.hide() diff --git a/src/memorytext/windows/main_window.py b/src/memorytext/windows/main_window.py index a140372..42291e4 100644 --- a/src/memorytext/windows/main_window.py +++ b/src/memorytext/windows/main_window.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING from PySide6.QtWidgets import ( QMainWindow, QHBoxLayout, @@ -6,13 +9,18 @@ QWidget, ) from PySide6.QtCore import Qt, QModelIndex -from PySide6.QtGui import QAction, QIcon +from PySide6.QtGui import QAction, QIcon, QKeySequence, QShortcut from memorytext.views.chat_view import ChatView from memorytext.views.chat_list_view import ChatListView from memorytext.windows.import_window import ImportWindow +from memorytext.windows.helper_windows import Floating_Input from memorytext.services import message_service +if TYPE_CHECKING: + import datetime + IMPORT_ICON = QIcon.fromTheme("list-add") +JUMP_TO_DATE_ICON = QIcon.fromTheme("go-next") class MainWindow(QMainWindow): @@ -28,8 +36,16 @@ def __init__(self): # Toolbar toolbar = QToolBar("Main Toolbar") self.addToolBar(toolbar) + self.import_action = QAction(IMPORT_ICON, "Import Chat") toolbar.addAction(self.import_action) + + toolbar.addSeparator() + + self.jump_to_date_action = QAction(JUMP_TO_DATE_ICON, "Jump to Date") + self.jump_to_date_action.setDisabled(True) + + toolbar.addAction(self.jump_to_date_action) toolbar.setMovable(False) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) @@ -50,9 +66,29 @@ def __init__(self): self.import_window = None self.import_action.triggered.connect(self.import_new_chat) + # Jump to Date + self.jump_to_date = Floating_Input(label="Jump to date:") + self.jump_to_date.selected_date.connect(self.on_jump_to_date) + + # Shortcut - Jump to Date: Ctrl+Shift+F + self.jump_to_date_shortcut = QShortcut(QKeySequence("Ctrl+Shift+F"), self) + self.jump_to_date_shortcut.activated.connect(self.jump_to_date.toggle_window) + + # Jump to Date Action + self.sidebar.selectionModel().selectionChanged.connect(self.update_toolbar) + self.jump_to_date_action.triggered.connect( + self.jump_to_date_shortcut.activated.emit + ) + self.jump_to_date_close = QShortcut( + QKeySequence(Qt.Key.Key_Escape), self.jump_to_date + ) + self.jump_to_date_close.activated.connect(self.jump_to_date.close) + def load_messages(self, index: QModelIndex): convo_id = self.sidebar.model().get_id(index) # pyright: ignore[reportAttributeAccessIssue] self.chat_area.set_messages(convo_id, message_service.get_messages) + self.chat_area.scrollToTop() + self.jump_to_date.chat_view_open = True def import_new_chat(self): if self.import_window is None: @@ -63,3 +99,15 @@ def handle_chat_deletion(self, deleted_conversation_id: int): self.chat_area.set_messages( deleted_conversation_id, message_service.get_messages ) + + def on_jump_to_date(self, date: datetime.date): + target_index = self.chat_area.message_model.jump_to_date(date) + if target_index and target_index.isValid(): + self.chat_area.scrollTo( + target_index, self.chat_area.ScrollHint.PositionAtTop + ) + self.chat_area.setCurrentIndex(target_index) + + def update_toolbar(self): + has_selection = self.sidebar.selectionModel().hasSelection() + self.jump_to_date_action.setEnabled(has_selection)