meeting-noter 2.0.0__py3-none-any.whl → 3.0.1__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.

Potentially problematic release.


This version of meeting-noter might be problematic. Click here for more details.

@@ -0,0 +1,262 @@
1
+ """Dashboard screen for Meeting Noter GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from PySide6.QtCore import Qt
12
+ from PySide6.QtGui import QFont
13
+ from PySide6.QtWidgets import (
14
+ QCheckBox,
15
+ QFrame,
16
+ QHBoxLayout,
17
+ QLabel,
18
+ QLineEdit,
19
+ QPushButton,
20
+ QTextEdit,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+ from meeting_noter.config import generate_meeting_name, get_config
26
+ from meeting_noter.daemon import is_process_running, read_pid_file
27
+ from meeting_noter.gui.utils.workers import LiveTranscriptWatcher
28
+ from meeting_noter.gui.widgets.status_indicator import StatusIndicator
29
+
30
+
31
+ DEFAULT_PID_FILE = Path.home() / ".meeting-noter.pid"
32
+ WATCHER_PID_FILE = Path.home() / ".meeting-noter-watcher.pid"
33
+
34
+
35
+ class DashboardScreen(QWidget):
36
+ """Main dashboard screen with recording controls."""
37
+
38
+ def __init__(self, parent=None):
39
+ super().__init__(parent)
40
+ self._setup_ui()
41
+ self._setup_workers()
42
+ self._connect_signals()
43
+
44
+ def _setup_ui(self):
45
+ """Set up the dashboard UI."""
46
+ mono_font = QFont("Menlo, Monaco, Courier New, monospace")
47
+
48
+ layout = QVBoxLayout(self)
49
+ layout.setContentsMargins(30, 30, 30, 30)
50
+ layout.setSpacing(20)
51
+
52
+ # Title
53
+ title = QLabel("Dashboard")
54
+ title.setProperty("class", "heading")
55
+ title_font = QFont("Menlo, Monaco, Courier New, monospace", 22)
56
+ title_font.setBold(True)
57
+ title.setFont(title_font)
58
+ title.setStyleSheet("color: #ffffff;")
59
+ layout.addWidget(title)
60
+
61
+ # Status indicator
62
+ self._status_indicator = StatusIndicator()
63
+ layout.addWidget(self._status_indicator)
64
+
65
+ # Recording controls panel
66
+ controls_panel = QFrame()
67
+ controls_panel.setProperty("class", "panel")
68
+ controls_panel.setStyleSheet(
69
+ "background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 8px; padding: 20px;"
70
+ )
71
+ controls_layout = QVBoxLayout(controls_panel)
72
+ controls_layout.setSpacing(15)
73
+
74
+ # Meeting name input
75
+ name_row = QHBoxLayout()
76
+ name_label = QLabel("Meeting name:")
77
+ name_label.setFont(mono_font)
78
+ name_label.setFixedWidth(140)
79
+ self._name_input = QLineEdit()
80
+ self._name_input.setPlaceholderText(generate_meeting_name())
81
+ self._name_input.setFont(mono_font)
82
+ name_row.addWidget(name_label)
83
+ name_row.addWidget(self._name_input)
84
+ controls_layout.addLayout(name_row)
85
+
86
+ # Live transcription toggle
87
+ toggle_row = QHBoxLayout()
88
+ toggle_label = QLabel("Live transcription:")
89
+ toggle_label.setFont(mono_font)
90
+ toggle_label.setFixedWidth(140)
91
+ self._live_toggle = QCheckBox("Show live transcript")
92
+ self._live_toggle.setFont(mono_font)
93
+ self._live_toggle.setChecked(True)
94
+ toggle_row.addWidget(toggle_label)
95
+ toggle_row.addWidget(self._live_toggle)
96
+ toggle_row.addStretch()
97
+ controls_layout.addLayout(toggle_row)
98
+
99
+ # Buttons
100
+ button_row = QHBoxLayout()
101
+ button_row.setSpacing(12)
102
+
103
+ self._start_btn = QPushButton("▶ Start Recording")
104
+ self._start_btn.setProperty("class", "success")
105
+ self._start_btn.setFont(mono_font)
106
+ self._start_btn.setStyleSheet(
107
+ "background-color: #4ec9b0; color: #0d0d0d; min-width: 160px; padding: 12px 24px; font-weight: 600;"
108
+ )
109
+
110
+ self._stop_btn = QPushButton("■ Stop")
111
+ self._stop_btn.setProperty("class", "danger")
112
+ self._stop_btn.setFont(mono_font)
113
+ self._stop_btn.setStyleSheet(
114
+ "background-color: #f14c4c; color: #ffffff; min-width: 100px; padding: 12px 24px; font-weight: 600;"
115
+ )
116
+
117
+ button_row.addWidget(self._start_btn)
118
+ button_row.addWidget(self._stop_btn)
119
+ button_row.addStretch()
120
+ controls_layout.addLayout(button_row)
121
+
122
+ layout.addWidget(controls_panel)
123
+
124
+ # Live transcription display
125
+ transcript_label = QLabel("LIVE TRANSCRIPTION")
126
+ transcript_label.setFont(mono_font)
127
+ transcript_label.setStyleSheet("font-size: 11px; font-weight: 600; color: #5a5a5a; letter-spacing: 2px;")
128
+ layout.addWidget(transcript_label)
129
+
130
+ self._transcript_display = QTextEdit()
131
+ self._transcript_display.setReadOnly(True)
132
+ self._transcript_display.setPlaceholderText("Waiting for transcription...")
133
+ self._transcript_display.setMinimumHeight(200)
134
+ self._transcript_display.setFont(mono_font)
135
+ self._transcript_display.setStyleSheet(
136
+ "background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 6px; "
137
+ "color: #4ec9b0; padding: 12px; font-size: 13px;"
138
+ )
139
+ layout.addWidget(self._transcript_display, 1) # Stretch factor
140
+
141
+ def _setup_workers(self):
142
+ """Set up background workers."""
143
+ self._transcript_watcher = LiveTranscriptWatcher()
144
+ self._transcript_watcher.new_content.connect(self._on_new_transcript)
145
+ self._transcript_watcher.file_changed.connect(self._on_transcript_file_changed)
146
+ self._transcript_watcher.recording_ended.connect(self._on_recording_ended)
147
+ self._transcript_watcher.start()
148
+
149
+ def _connect_signals(self):
150
+ """Connect button signals."""
151
+ self._start_btn.clicked.connect(self._start_recording)
152
+ self._stop_btn.clicked.connect(self._stop_recording)
153
+ self._live_toggle.toggled.connect(self._on_live_toggle)
154
+
155
+ def _start_recording(self):
156
+ """Start a new recording."""
157
+ # Check if already recording
158
+ daemon_pid = read_pid_file(DEFAULT_PID_FILE)
159
+ if daemon_pid and is_process_running(daemon_pid):
160
+ self._show_message("Already recording. Stop first.")
161
+ return
162
+
163
+ # Get meeting name
164
+ meeting_name = self._name_input.text().strip() or generate_meeting_name()
165
+
166
+ # Start recording daemon
167
+ subprocess.Popen(
168
+ [sys.executable, "-m", "meeting_noter.cli", "daemon", "--name", meeting_name],
169
+ stdout=subprocess.DEVNULL,
170
+ stderr=subprocess.DEVNULL,
171
+ )
172
+
173
+ # Clear input and transcript
174
+ self._name_input.clear()
175
+ self._name_input.setPlaceholderText(generate_meeting_name())
176
+ self._transcript_display.clear()
177
+ self._transcript_display.append(f"Starting recording: {meeting_name}\n")
178
+
179
+ def _stop_recording(self):
180
+ """Stop recording."""
181
+ daemon_pid = read_pid_file(DEFAULT_PID_FILE)
182
+ watcher_pid = None
183
+
184
+ if WATCHER_PID_FILE.exists():
185
+ try:
186
+ watcher_pid = int(WATCHER_PID_FILE.read_text().strip())
187
+ except (ValueError, FileNotFoundError):
188
+ pass
189
+
190
+ stopped = []
191
+
192
+ # Stop daemon gracefully
193
+ if daemon_pid and is_process_running(daemon_pid):
194
+ try:
195
+ os.kill(daemon_pid, signal.SIGTERM)
196
+ stopped.append("recording")
197
+ except ProcessLookupError:
198
+ pass
199
+
200
+ # Stop watcher
201
+ if watcher_pid:
202
+ try:
203
+ os.kill(watcher_pid, signal.SIGTERM)
204
+ stopped.append("watcher")
205
+ except ProcessLookupError:
206
+ pass
207
+ WATCHER_PID_FILE.unlink(missing_ok=True)
208
+
209
+ if not stopped:
210
+ self._show_message("Nothing running")
211
+ else:
212
+ self._show_message(f"Stopped: {', '.join(stopped)}")
213
+
214
+ def _on_live_toggle(self, checked: bool):
215
+ """Handle live transcription toggle."""
216
+ if checked:
217
+ self._transcript_watcher.start()
218
+ else:
219
+ self._transcript_watcher.stop()
220
+
221
+ def _on_new_transcript(self, text: str):
222
+ """Handle new transcript content."""
223
+ if self._live_toggle.isChecked():
224
+ # Format timestamps in cyan
225
+ lines = text.splitlines()
226
+ for line in lines:
227
+ if line.strip() and line.startswith("["):
228
+ self._transcript_display.append(line)
229
+ # Auto-scroll to bottom
230
+ scrollbar = self._transcript_display.verticalScrollBar()
231
+ scrollbar.setValue(scrollbar.maximum())
232
+
233
+ def _on_transcript_file_changed(self, meeting_name: str):
234
+ """Handle new transcript file."""
235
+ self._transcript_display.clear()
236
+ self._transcript_display.append(f"Transcribing: {meeting_name}\n")
237
+
238
+ def _on_recording_ended(self):
239
+ """Handle recording end."""
240
+ self._transcript_display.append("\n--- Recording ended ---")
241
+
242
+ def _show_message(self, message: str):
243
+ """Show a message in the transcript area."""
244
+ self._transcript_display.append(f"\n{message}")
245
+
246
+ def update_status(self, watcher_running: bool, daemon_running: bool, meeting_name: str):
247
+ """Update status indicator."""
248
+ self._status_indicator.update_status(watcher_running, daemon_running, meeting_name)
249
+
250
+ # Update button states
251
+ self._start_btn.setEnabled(not daemon_running)
252
+ self._stop_btn.setEnabled(daemon_running or watcher_running)
253
+
254
+ def refresh(self):
255
+ """Refresh the dashboard."""
256
+ # Update placeholder with new timestamp
257
+ self._name_input.setPlaceholderText(generate_meeting_name())
258
+
259
+ def stop_workers(self):
260
+ """Stop background workers."""
261
+ if hasattr(self, "_transcript_watcher"):
262
+ self._transcript_watcher.stop()
@@ -0,0 +1,184 @@
1
+ """Logs viewer screen for Meeting Noter GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from PySide6.QtGui import QColor, QTextCharFormat, QTextCursor
8
+ from PySide6.QtWidgets import (
9
+ QCheckBox,
10
+ QHBoxLayout,
11
+ QLabel,
12
+ QPushButton,
13
+ QTextEdit,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+ from meeting_noter.gui.utils.workers import LogWatcher
19
+
20
+
21
+ LOG_FILE = Path.home() / ".meeting-noter.log"
22
+
23
+
24
+ class LogsScreen(QWidget):
25
+ """Screen for viewing application logs."""
26
+
27
+ def __init__(self, parent=None):
28
+ super().__init__(parent)
29
+ self._setup_ui()
30
+ self._setup_workers()
31
+ self._connect_signals()
32
+
33
+ def _setup_ui(self):
34
+ """Set up the logs UI."""
35
+ layout = QVBoxLayout(self)
36
+ layout.setContentsMargins(30, 30, 30, 30)
37
+ layout.setSpacing(20)
38
+
39
+ # Title
40
+ title = QLabel("Logs")
41
+ title.setStyleSheet("font-size: 24px; font-weight: 600; color: #ffffff;")
42
+ layout.addWidget(title)
43
+
44
+ # Log path
45
+ path_label = QLabel(f"Log file: {LOG_FILE}")
46
+ path_label.setStyleSheet("color: #858585;")
47
+ layout.addWidget(path_label)
48
+
49
+ # Log content
50
+ self._log_view = QTextEdit()
51
+ self._log_view.setReadOnly(True)
52
+ self._log_view.setStyleSheet(
53
+ "font-family: 'Menlo', 'Monaco', 'Courier New', monospace; font-size: 12px;"
54
+ )
55
+ layout.addWidget(self._log_view, 1)
56
+
57
+ # Controls
58
+ controls_row = QHBoxLayout()
59
+ controls_row.setSpacing(10)
60
+
61
+ self._follow_toggle = QCheckBox("Follow")
62
+ self._follow_toggle.setChecked(True)
63
+ controls_row.addWidget(self._follow_toggle)
64
+
65
+ self._refresh_btn = QPushButton("Refresh")
66
+ self._refresh_btn.setProperty("class", "secondary")
67
+ self._refresh_btn.setStyleSheet("background-color: #3c3c3c;")
68
+ controls_row.addWidget(self._refresh_btn)
69
+
70
+ self._clear_btn = QPushButton("Clear")
71
+ self._clear_btn.setProperty("class", "secondary")
72
+ self._clear_btn.setStyleSheet("background-color: #3c3c3c;")
73
+ controls_row.addWidget(self._clear_btn)
74
+
75
+ controls_row.addStretch()
76
+ layout.addLayout(controls_row)
77
+
78
+ def _setup_workers(self):
79
+ """Set up background workers."""
80
+ self._log_watcher = LogWatcher()
81
+ self._log_watcher.new_content.connect(self._on_new_content)
82
+
83
+ def _connect_signals(self):
84
+ """Connect button signals."""
85
+ self._refresh_btn.clicked.connect(self._refresh_logs)
86
+ self._clear_btn.clicked.connect(self._clear_logs)
87
+ self._follow_toggle.toggled.connect(self._on_follow_toggled)
88
+
89
+ def _on_follow_toggled(self, checked: bool):
90
+ """Handle follow toggle."""
91
+ if checked:
92
+ self._log_watcher.start()
93
+ else:
94
+ self._log_watcher.stop()
95
+
96
+ def _refresh_logs(self):
97
+ """Refresh log content."""
98
+ self._log_view.clear()
99
+ self._load_logs()
100
+
101
+ # Reset watcher position
102
+ if self._follow_toggle.isChecked():
103
+ self._log_watcher.reset()
104
+ if not self._log_watcher.isRunning():
105
+ self._log_watcher.start()
106
+
107
+ def _clear_logs(self):
108
+ """Clear the log display."""
109
+ self._log_view.clear()
110
+
111
+ def _load_logs(self):
112
+ """Load the last 100 lines from the log file."""
113
+ if not LOG_FILE.exists():
114
+ self._log_view.setPlainText("No log file found.")
115
+ return
116
+
117
+ try:
118
+ with open(LOG_FILE, "r") as f:
119
+ lines = f.readlines()[-100:]
120
+
121
+ for line in lines:
122
+ self._append_colored_line(line.rstrip())
123
+
124
+ # Scroll to bottom
125
+ scrollbar = self._log_view.verticalScrollBar()
126
+ scrollbar.setValue(scrollbar.maximum())
127
+
128
+ except Exception as e:
129
+ self._log_view.setPlainText(f"Error reading log file: {e}")
130
+
131
+ def _on_new_content(self, content: str):
132
+ """Handle new log content."""
133
+ if not self._follow_toggle.isChecked():
134
+ return
135
+
136
+ for line in content.splitlines():
137
+ if line.strip():
138
+ self._append_colored_line(line)
139
+
140
+ # Auto-scroll if following
141
+ scrollbar = self._log_view.verticalScrollBar()
142
+ scrollbar.setValue(scrollbar.maximum())
143
+
144
+ def _append_colored_line(self, line: str):
145
+ """Append a line with appropriate color."""
146
+ cursor = self._log_view.textCursor()
147
+ cursor.movePosition(QTextCursor.MoveOperation.End)
148
+
149
+ fmt = QTextCharFormat()
150
+
151
+ # Color based on content
152
+ line_lower = line.lower()
153
+ if "error" in line_lower:
154
+ fmt.setForeground(QColor("#f14c4c")) # Red
155
+ elif "warning" in line_lower:
156
+ fmt.setForeground(QColor("#dcdcaa")) # Yellow
157
+ elif "recording started" in line_lower:
158
+ fmt.setForeground(QColor("#4ec9b0")) # Green
159
+ elif "recording saved" in line_lower or "transcription" in line_lower:
160
+ fmt.setForeground(QColor("#9cdcfe")) # Cyan
161
+ else:
162
+ fmt.setForeground(QColor("#cccccc")) # Default gray
163
+
164
+ cursor.insertText(line + "\n", fmt)
165
+
166
+ def refresh(self):
167
+ """Refresh the logs view."""
168
+ self._refresh_logs()
169
+
170
+ def stop_workers(self):
171
+ """Stop background workers."""
172
+ if hasattr(self, "_log_watcher"):
173
+ self._log_watcher.stop()
174
+
175
+ def showEvent(self, event):
176
+ """Handle show event."""
177
+ super().showEvent(event)
178
+ self._refresh_logs()
179
+
180
+ def hideEvent(self, event):
181
+ """Handle hide event."""
182
+ super().hideEvent(event)
183
+ if hasattr(self, "_log_watcher") and self._log_watcher.isRunning():
184
+ self._log_watcher.stop()