forked from karpathy/reader3
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdatabase.py
More file actions
328 lines (269 loc) · 10.1 KB
/
database.py
File metadata and controls
328 lines (269 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
"""
Database models for storing highlights and AI interactions.
"""
import sqlite3
from datetime import datetime
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class Highlight:
"""User highlight with position info."""
id: Optional[int] = None
book_id: str = ""
chapter_index: int = 0
selected_text: str = ""
context_before: str = ""
context_after: str = ""
created_at: str = ""
@dataclass
class AIAnalysis:
"""AI analysis result (fact-check or discussion)."""
id: Optional[int] = None
highlight_id: int = 0
analysis_type: str = "" # 'fact_check' or 'discussion'
prompt: str = ""
response: str = ""
created_at: str = ""
class Database:
"""Simple SQLite database for storing highlights and AI analyses."""
def __init__(self, db_path: str = "reader_data.db"):
self.db_path = db_path
self.init_db()
def init_db(self):
"""Create tables if they don't exist."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS highlights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT NOT NULL,
chapter_index INTEGER NOT NULL,
selected_text TEXT NOT NULL,
context_before TEXT,
context_after TEXT,
created_at TEXT NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS ai_analyses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
highlight_id INTEGER NOT NULL,
analysis_type TEXT NOT NULL,
prompt TEXT NOT NULL,
response TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (highlight_id) REFERENCES highlights (id)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS reading_progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT NOT NULL UNIQUE,
chapter_index INTEGER NOT NULL,
scroll_position INTEGER DEFAULT 0,
is_completed INTEGER NOT NULL DEFAULT 0,
last_read_at TEXT NOT NULL
)
""")
cursor.execute("PRAGMA table_info(reading_progress)")
progress_columns = {row[1] for row in cursor.fetchall()}
if "is_completed" not in progress_columns:
cursor.execute(
"ALTER TABLE reading_progress ADD COLUMN is_completed INTEGER NOT NULL DEFAULT 0"
)
conn.commit()
conn.close()
def save_highlight(self, highlight: Highlight) -> int:
"""Save a highlight and return its ID."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO highlights (book_id, chapter_index, selected_text,
context_before, context_after, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (
highlight.book_id,
highlight.chapter_index,
highlight.selected_text,
highlight.context_before,
highlight.context_after,
highlight.created_at or datetime.now().isoformat()
))
highlight_id = cursor.lastrowid
conn.commit()
conn.close()
return highlight_id
def save_analysis(self, analysis: AIAnalysis) -> int:
"""Save an AI analysis and return its ID."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO ai_analyses (
highlight_id, analysis_type, prompt, response, created_at
)
VALUES (?, ?, ?, ?, ?)
""", (
analysis.highlight_id,
analysis.analysis_type,
analysis.prompt,
analysis.response,
analysis.created_at or datetime.now().isoformat()
))
analysis_id = cursor.lastrowid
conn.commit()
conn.close()
return analysis_id
def get_highlights_for_chapter(
self, book_id: str, chapter_index: int
) -> List[Dict]:
"""Get all highlights for a specific chapter."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM highlights
WHERE book_id = ? AND chapter_index = ?
ORDER BY created_at DESC
""", (book_id, chapter_index))
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def get_all_highlights_for_book(self, book_id: str) -> List[Dict]:
"""Get all highlights for a book (all chapters)."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM highlights
WHERE book_id = ?
ORDER BY created_at DESC
""", (book_id,))
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def get_analyses_for_highlight(self, highlight_id: int) -> List[Dict]:
"""Get all AI analyses for a highlight."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM ai_analyses
WHERE highlight_id = ?
ORDER BY created_at DESC
""", (highlight_id,))
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def update_analysis(self, analysis_id: int, response: str):
"""Update an existing analysis response (for editing comments)."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
UPDATE ai_analyses
SET response = ?
WHERE id = ?
""", (response, analysis_id))
conn.commit()
conn.close()
def delete_analysis(self, analysis_id: int):
"""Delete an analysis and its highlight if no other analyses exist."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Get the highlight_id before deleting
cursor.execute(
"SELECT highlight_id FROM ai_analyses WHERE id = ?",
(analysis_id,),
)
result = cursor.fetchone()
if result:
highlight_id = result[0]
# Delete the analysis
cursor.execute("DELETE FROM ai_analyses WHERE id = ?", (analysis_id,))
# Check if there are other analyses for this highlight
cursor.execute(
"SELECT COUNT(*) FROM ai_analyses WHERE highlight_id = ?",
(highlight_id,),
)
count = cursor.fetchone()[0]
# If no other analyses, delete the highlight too
if count == 0:
cursor.execute("DELETE FROM highlights WHERE id = ?", (highlight_id,))
conn.commit()
conn.close()
def delete_highlight(self, highlight_id: int):
"""Delete a highlight and any analyses attached to it."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"DELETE FROM ai_analyses WHERE highlight_id = ?",
(highlight_id,),
)
cursor.execute("DELETE FROM highlights WHERE id = ?", (highlight_id,))
conn.commit()
conn.close()
def save_progress(self, book_id: str, chapter_index: int, scroll_position: int = 0):
"""Save or update reading progress for a book."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO reading_progress (
book_id, chapter_index, scroll_position, is_completed, last_read_at
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(book_id) DO UPDATE SET
chapter_index = excluded.chapter_index,
scroll_position = excluded.scroll_position,
is_completed = excluded.is_completed,
last_read_at = excluded.last_read_at
""", (book_id, chapter_index, scroll_position, 0, datetime.now().isoformat()))
conn.commit()
conn.close()
def get_progress(self, book_id: str) -> Optional[Dict]:
"""Get the last read position for a book."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT chapter_index, scroll_position, is_completed FROM reading_progress
WHERE book_id = ?
""", (book_id,))
result = cursor.fetchone()
conn.close()
if not result:
return None
progress = dict(result)
progress["is_completed"] = bool(progress.get("is_completed", 0))
return progress
def set_completed(self, book_id: str, is_completed: bool):
"""Mark a book as completed or not completed."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"SELECT chapter_index, scroll_position FROM reading_progress WHERE book_id = ?",
(book_id,),
)
result = cursor.fetchone()
if result:
chapter_index, scroll_position = result
else:
chapter_index, scroll_position = 0, 0
cursor.execute(
"""
INSERT INTO reading_progress (
book_id, chapter_index, scroll_position, is_completed, last_read_at
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(book_id) DO UPDATE SET
is_completed = excluded.is_completed,
last_read_at = excluded.last_read_at
""",
(
book_id,
chapter_index,
scroll_position,
int(is_completed),
datetime.now().isoformat(),
),
)
conn.commit()
conn.close()