appkit-assistant 0.9.0__py3-none-any.whl → 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ """Thread list state management for the assistant.
2
+
3
+ This module contains ThreadListState which manages the thread list sidebar:
4
+ - Loading thread summaries from database
5
+ - Adding new threads to the list (called by ThreadState)
6
+ - Deleting threads from database and list
7
+ - Tracking which thread is currently active/loading
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from collections.abc import AsyncGenerator
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ import reflex as rx
17
+
18
+ from appkit_assistant.backend.models import ThreadModel
19
+ from appkit_assistant.backend.repositories import ThreadRepository
20
+ from appkit_user.authentication.states import UserSession
21
+
22
+ if TYPE_CHECKING:
23
+ from appkit_assistant.state.thread_state import ThreadState
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ThreadListState(rx.State):
29
+ """State for managing the thread list sidebar.
30
+
31
+ Responsibilities:
32
+ - Loading thread summaries from database on initialization
33
+ - Adding new threads to the list (called by ThreadState)
34
+ - Deleting threads from database and list
35
+ - Tracking active/loading thread IDs
36
+
37
+ Does NOT:
38
+ - Create new threads (ThreadState.new_thread does this)
39
+ - Load full thread data (ThreadState.get_thread does this)
40
+ - Persist thread data (ThreadState handles this)
41
+ """
42
+
43
+ # Public state
44
+ threads: list[ThreadModel] = []
45
+ active_thread_id: str = ""
46
+ loading_thread_id: str = ""
47
+ loading: bool = True
48
+
49
+ # Private state
50
+ _initialized: bool = False
51
+ _current_user_id: str = ""
52
+
53
+ # -------------------------------------------------------------------------
54
+ # Computed properties
55
+ # -------------------------------------------------------------------------
56
+
57
+ @rx.var
58
+ def has_threads(self) -> bool:
59
+ """Check if there are any threads."""
60
+ return len(self.threads) > 0
61
+
62
+ # -------------------------------------------------------------------------
63
+ # Initialization
64
+ # -------------------------------------------------------------------------
65
+
66
+ @rx.event(background=True)
67
+ async def initialize(self) -> AsyncGenerator[Any, Any]:
68
+ """Initialize thread list - load summaries from database."""
69
+ async with self:
70
+ if self._initialized:
71
+ return
72
+ self.loading = True
73
+ yield
74
+
75
+ async for _ in self._load_threads():
76
+ yield
77
+
78
+ async def _load_threads(self) -> AsyncGenerator[Any, Any]:
79
+ """Load thread summaries from database (internal)."""
80
+ # Late import to avoid circular dependency
81
+ from appkit_assistant.state.thread_state import ThreadState # noqa: PLC0415
82
+
83
+ async with self:
84
+ user_session: UserSession = await self.get_state(UserSession)
85
+ current_user_id = user_session.user.user_id if user_session.user else ""
86
+ is_authenticated = await user_session.is_authenticated
87
+
88
+ # Handle user change
89
+ if self._current_user_id != current_user_id:
90
+ logger.info(
91
+ "User changed from '%s' to '%s' - resetting state",
92
+ self._current_user_id or "(none)",
93
+ current_user_id or "(none)",
94
+ )
95
+ self._initialized = False
96
+ self._current_user_id = current_user_id
97
+ self._clear_threads()
98
+ yield
99
+
100
+ # Reset ThreadState
101
+ thread_state: ThreadState = await self.get_state(ThreadState)
102
+ thread_state.new_thread()
103
+
104
+ if self._initialized:
105
+ self.loading = False
106
+ yield
107
+ return
108
+
109
+ # Check authentication
110
+ if not is_authenticated:
111
+ self._clear_threads()
112
+ self._current_user_id = ""
113
+ self.loading = False
114
+ yield
115
+ return
116
+
117
+ user_id = user_session.user.user_id if user_session.user else None
118
+
119
+ if not user_id:
120
+ async with self:
121
+ self.loading = False
122
+ yield
123
+ return
124
+
125
+ # Fetch threads from database
126
+ try:
127
+ threads = await ThreadRepository.get_summaries_by_user(user_id)
128
+ async with self:
129
+ self.threads = threads
130
+ self._initialized = True
131
+ logger.debug("Loaded %d threads", len(threads))
132
+ yield
133
+ except Exception as e:
134
+ logger.error("Error loading threads: %s", e)
135
+ async with self:
136
+ self._clear_threads()
137
+ yield
138
+ finally:
139
+ async with self:
140
+ self.loading = False
141
+ yield
142
+
143
+ # -------------------------------------------------------------------------
144
+ # Thread list management
145
+ # -------------------------------------------------------------------------
146
+
147
+ async def add_thread(self, thread: ThreadModel) -> None:
148
+ """Add a new thread to the list.
149
+
150
+ Called by ThreadState via get_state() after first successful response.
151
+ Not an @rx.event so it can be called directly from background tasks.
152
+ Does not persist to DB - ThreadState handles persistence.
153
+
154
+ Args:
155
+ thread: The thread model to add.
156
+ """
157
+ # Check if already in list (idempotent)
158
+ existing = next(
159
+ (t for t in self.threads if t.thread_id == thread.thread_id),
160
+ None,
161
+ )
162
+ if existing:
163
+ logger.debug("Thread already in list: %s", thread.thread_id)
164
+ return
165
+
166
+ # Deactivate other threads
167
+ self.threads = [
168
+ ThreadModel(**{**t.model_dump(), "active": False}) for t in self.threads
169
+ ]
170
+ # Add new thread at beginning (mark as active)
171
+ thread.active = True
172
+ self.threads = [thread, *self.threads]
173
+ self.active_thread_id = thread.thread_id
174
+ logger.debug("Added thread to list: %s", thread.thread_id)
175
+
176
+ @rx.event(background=True)
177
+ async def delete_thread(self, thread_id: str) -> AsyncGenerator[Any, Any]:
178
+ """Delete a thread from database and list.
179
+
180
+ If the deleted thread was the active thread, resets ThreadState
181
+ to show an empty thread.
182
+
183
+ Args:
184
+ thread_id: The ID of the thread to delete.
185
+ """
186
+ # Late import to avoid circular dependency
187
+ from appkit_assistant.state.thread_state import ThreadState # noqa: PLC0415
188
+
189
+ async with self:
190
+ user_session: UserSession = await self.get_state(UserSession)
191
+ is_authenticated = await user_session.is_authenticated
192
+ user_id = user_session.user.user_id if user_session.user else None
193
+
194
+ thread_to_delete = next(
195
+ (t for t in self.threads if t.thread_id == thread_id), None
196
+ )
197
+ was_active = thread_id == self.active_thread_id
198
+
199
+ if not is_authenticated or not user_id:
200
+ return
201
+
202
+ if not thread_to_delete:
203
+ yield rx.toast.error(
204
+ "Chat nicht gefunden.", position="top-right", close_button=True
205
+ )
206
+ logger.warning("Thread %s not found for deletion", thread_id)
207
+ return
208
+
209
+ try:
210
+ # Delete from database
211
+ await ThreadRepository.delete_thread(thread_id, user_id)
212
+
213
+ async with self:
214
+ # Remove from list
215
+ self.threads = [t for t in self.threads if t.thread_id != thread_id]
216
+
217
+ if was_active:
218
+ self.active_thread_id = ""
219
+ # Reset ThreadState to empty thread
220
+ thread_state: ThreadState = await self.get_state(ThreadState)
221
+ thread_state.new_thread()
222
+
223
+ yield rx.toast.info(
224
+ f"Chat '{thread_to_delete.title}' gelöscht.",
225
+ position="top-right",
226
+ close_button=True,
227
+ )
228
+
229
+ except Exception as e:
230
+ logger.error("Error deleting thread %s: %s", thread_id, e)
231
+ yield rx.toast.error(
232
+ "Fehler beim Löschen des Chats.",
233
+ position="top-right",
234
+ close_button=True,
235
+ )
236
+
237
+ # -------------------------------------------------------------------------
238
+ # Logout handling
239
+ # -------------------------------------------------------------------------
240
+
241
+ @rx.event
242
+ async def reset_on_logout(self) -> None:
243
+ """Reset state on user logout to prevent data leakage."""
244
+ # Late import to avoid circular dependency
245
+ from appkit_assistant.state.thread_state import ThreadState # noqa: PLC0415
246
+
247
+ logger.info(
248
+ "Resetting ThreadListState on logout for user: %s",
249
+ self._current_user_id,
250
+ )
251
+
252
+ self._clear_threads()
253
+ self.loading = False
254
+ self._initialized = False
255
+ self._current_user_id = ""
256
+
257
+ # Reset ThreadState
258
+ thread_state: ThreadState = await self.get_state(ThreadState)
259
+ thread_state.new_thread()
260
+
261
+ logger.debug("ThreadListState reset complete")
262
+
263
+ # -------------------------------------------------------------------------
264
+ # Internal helpers
265
+ # -------------------------------------------------------------------------
266
+
267
+ def _clear_threads(self) -> None:
268
+ """Clear thread-related state."""
269
+ self.threads = []
270
+ self.active_thread_id = ""
271
+ self.loading_thread_id = ""