feat chat history

This commit is contained in:
fsylvain committed 2026-09-19 23:02:04 +02:00
1 parent 48c9456223
commit 1648b44189
2 files changed
+250 -2

No files matched your search

+172 -2
View File
@@ -31,7 +31,11 @@ from PyQt6.QtWidgets import (
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMainWindow,
QMenu,
QMessageBox,
QPushButton,
QTextBrowser,
QVBoxLayout,
@@ -113,6 +117,44 @@ QWidget {{
color: {TEXT_PRIMARY};
}}
#sidebar {{
background-color: {BG_APP};
border-right: 1px solid {BORDER};
}}
QPushButton#newChatButton {{
background-color: {BG_ELEVATED};
border: 1px solid {BORDER};
padding: 8px 12px;
color: {TEXT_PRIMARY};
font-weight: 600;
text-align: left;
}}
QPushButton#newChatButton:hover {{
border: 1px solid {ACCENT};
}}
QListWidget#sessionList {{
background-color: transparent;
border: none;
outline: none;
}}
QListWidget#sessionList::item {{
padding: 8px 10px;
color: {TEXT_SECONDARY};
}}
QListWidget#sessionList::item:selected {{
background-color: {BG_ELEVATED};
color: {TEXT_PRIMARY};
}}
QListWidget#sessionList::item:hover {{
background-color: rgba(191, 192, 192, 0.08);
}}
#badge {{
background-color: {BG_ELEVATED};
border: 1px solid {BORDER};
@@ -415,7 +457,11 @@ class ChatWindow(QMainWindow):
self.resize(900, 700)
self._llm = None
self._history: list[tuple[str, str]] = []
self._sessions: list[chatbot.ChatSession] = chatbot.list_sessions()
self._current_session: chatbot.ChatSession = (
self._sessions[0] if self._sessions else chatbot.new_session()
)
self._history: list[tuple[str, str]] = list(self._current_session.history)
self._pending_question = ""
self._ask_thread: AskThread | None = None
# Position in chat_view's document where the in-progress streamed
@@ -442,9 +488,38 @@ class ChatWindow(QMainWindow):
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
root_layout = QHBoxLayout(central)
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.setSpacing(0)
sidebar = QWidget()
sidebar.setObjectName("sidebar")
sidebar.setFixedWidth(220)
sidebar_layout = QVBoxLayout(sidebar)
sidebar_layout.setContentsMargins(12, 16, 12, 16)
sidebar_layout.setSpacing(10)
self.new_chat_button = QPushButton("+ New Chat")
self.new_chat_button.setObjectName("newChatButton")
self.new_chat_button.setMinimumHeight(36)
self.new_chat_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.new_chat_button.clicked.connect(self._on_new_chat)
sidebar_layout.addWidget(self.new_chat_button)
self.session_list = QListWidget()
self.session_list.setObjectName("sessionList")
self.session_list.itemClicked.connect(self._on_session_clicked)
self.session_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.session_list.customContextMenuRequested.connect(self._on_session_context_menu)
sidebar_layout.addWidget(self.session_list, 1)
root_layout.addWidget(sidebar)
content = QWidget()
layout = QVBoxLayout(content)
layout.setContentsMargins(20, 16, 20, 16)
layout.setSpacing(14)
root_layout.addWidget(content, 1)
header = QWidget()
header.setObjectName("header")
@@ -505,6 +580,10 @@ class ChatWindow(QMainWindow):
layout.addLayout(input_row)
self._refresh_session_list()
if self._current_session.history:
self._render_session_history(self._current_session)
self._init_gpu_monitor()
self.statusBar().showMessage("Loading model...")
@@ -555,9 +634,93 @@ class ChatWindow(QMainWindow):
def _set_input_enabled(self, enabled: bool) -> None:
self.input_box.setEnabled(enabled)
self.send_button.setEnabled(enabled)
# Switching/creating/deleting a chat while a generation is streaming
# into chat_view would race the in-progress cursors, so the sidebar
# is only usable while idle, same as the input box.
self.new_chat_button.setEnabled(enabled)
self.session_list.setEnabled(enabled)
if enabled:
self.input_box.setFocus()
def _refresh_session_list(self) -> None:
self.session_list.blockSignals(True)
self.session_list.clear()
for session in self._sessions:
item = QListWidgetItem(session.title)
item.setData(Qt.ItemDataRole.UserRole, session.id)
self.session_list.addItem(item)
if session.id == self._current_session.id:
self.session_list.setCurrentItem(item)
self.session_list.blockSignals(False)
def _render_session_history(self, session: "chatbot.ChatSession") -> None:
self.chat_view.clear()
for question, answer in session.history:
self._append_user(question)
self._append_assistant(answer)
def _on_new_chat(self) -> None:
if self._ask_thread is not None:
return
if not self._current_session.history:
return # already on an empty, unsaved chat
self._current_session = chatbot.new_session()
self._history = []
self.chat_view.clear()
self._append_system("Dev assist chatbot ready. Ask a development question.")
self.session_list.clearSelection()
self.statusBar().showMessage("Ready.")
def _on_session_clicked(self, item: QListWidgetItem) -> None:
if self._ask_thread is not None:
return
session_id = item.data(Qt.ItemDataRole.UserRole)
if session_id == self._current_session.id:
return
session = next((s for s in self._sessions if s.id == session_id), None)
if session is None:
return
self._current_session = session
self._history = list(session.history)
self._render_session_history(session)
self.statusBar().showMessage("Ready.")
def _on_session_context_menu(self, pos) -> None:
item = self.session_list.itemAt(pos)
if item is None or self._ask_thread is not None:
return
session_id = item.data(Qt.ItemDataRole.UserRole)
menu = QMenu(self)
delete_action = menu.addAction("Delete chat")
action = menu.exec(self.session_list.mapToGlobal(pos))
if action == delete_action:
self._delete_session(session_id)
def _delete_session(self, session_id: str) -> None:
reply = QMessageBox.question(
self,
"Delete chat",
"Delete this chat? This can't be undone.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
chatbot.delete_session(session_id)
self._sessions = [s for s in self._sessions if s.id != session_id]
if self._current_session.id == session_id:
if self._sessions:
self._current_session = self._sessions[0]
else:
self._current_session = chatbot.new_session()
self._history = list(self._current_session.history)
self.chat_view.clear()
if self._current_session.history:
self._render_session_history(self._current_session)
else:
self._append_system("Dev assist chatbot ready. Ask a development question.")
self._refresh_session_list()
def _on_model_loaded(self, llm) -> None:
self._llm = llm
self._n_ctx = llm.n_ctx()
@@ -743,6 +906,13 @@ class ChatWindow(QMainWindow):
self._discard_stream_card()
self._append_assistant(answer)
self._history.append((self._pending_question, answer.strip()))
self._current_session.history = self._history
if self._current_session.title == chatbot.DEFAULT_TITLE:
self._current_session.title = chatbot.derive_title(self._pending_question)
chatbot.save_session(self._current_session)
self._sessions = [s for s in self._sessions if s.id != self._current_session.id]
self._sessions.insert(0, self._current_session)
self._refresh_session_list()
self.statusBar().showMessage("Ready.")
self._ask_thread = None
self._set_input_enabled(True)