Files
javis_bot/src/desktop_app/diary_dialog.py
javis-bot c4abf63f38
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Add Discord-native hybrid front-end for Jarvis (bot + bridge)
Transform isair/jarvis into a Discord-controlled voice assistant running on
the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact.

- bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral),
  voice channel join + voice receive/playback, pluggable VNC screen broadcast
  (selfbot live / noVNC / screenshot)
- bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS
  behind a thin localhost HTTP API
- .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite,
  docs/language-comparison.md and docs/vnc-xfce-setup.md

Language decision: hybrid (Python brain + Node/bun Discord layer) because
Discord blocks bot video; native screen broadcast only works via a Node
selfbot library.
2026-06-09 14:51:05 +09:00

229 lines
8.1 KiB
Python

"""Diary update dialog shown during shutdown."""
from __future__ import annotations
from typing import Optional, List
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QTextEdit, QProgressBar, QFrame
)
from PyQt6.QtCore import Qt, pyqtSignal, QObject
from PyQt6.QtGui import QFont
from .themes import JARVIS_THEME_STYLESHEET, COLORS
# IPC protocol prefix - must match daemon.py
DIARY_IPC_PREFIX = "__DIARY__:"
class DiarySignals(QObject):
"""Signals for diary update progress."""
# Emitted when a new token is received from LLM
token_received = pyqtSignal(str)
# Emitted when status changes (e.g., "Analyzing conversations...")
status_changed = pyqtSignal(str)
# Emitted when conversation chunks are available
chunks_received = pyqtSignal(list)
# Emitted when the diary update completes
completed = pyqtSignal(bool) # True = success, False = failed/skipped
class DiaryUpdateDialog(QDialog):
"""
Dialog shown during shutdown diary update.
Shows:
- The conversation chunks being processed
- Live streaming of the diary entry being written
- Progress indication
"""
def __init__(self, parent=None):
super().__init__(parent)
self.signals = DiarySignals()
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
"""Set up the dialog UI."""
self.setWindowTitle("Saving Your Diary")
self.setMinimumSize(550, 450)
self.setWindowFlags(
Qt.WindowType.Dialog |
Qt.WindowType.CustomizeWindowHint |
Qt.WindowType.WindowTitleHint
)
# Apply the shared Jarvis theme
self.setStyleSheet(JARVIS_THEME_STYLESHEET)
layout = QVBoxLayout(self)
layout.setSpacing(16)
layout.setContentsMargins(24, 24, 24, 24)
# Title
title = QLabel("Updating Your Diary")
title.setObjectName("title")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
# Status label
self.status_label = QLabel("Preparing to save...")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setObjectName("subtitle")
layout.addWidget(self.status_label)
# Progress bar (indeterminate)
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0) # Indeterminate
self.progress_bar.setTextVisible(False)
self.progress_bar.setFixedHeight(6)
layout.addWidget(self.progress_bar)
# Conversations section
conv_label = QLabel("Today's Conversations")
conv_label.setObjectName("section_title")
layout.addWidget(conv_label)
self.conversations_text = QTextEdit()
self.conversations_text.setReadOnly(True)
self.conversations_text.setMaximumHeight(100)
self.conversations_text.setPlaceholderText("Loading conversations...")
layout.addWidget(self.conversations_text)
# Diary entry section
diary_label = QLabel("Diary Entry")
diary_label.setObjectName("section_title")
layout.addWidget(diary_label)
self.diary_text = QTextEdit()
self.diary_text.setReadOnly(True)
self.diary_text.setPlaceholderText("Writing diary entry...")
layout.addWidget(self.diary_text, stretch=1)
# Hint at bottom
hint = QLabel("Please wait while Jarvis saves your conversations...")
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
hint.setObjectName("subtitle")
layout.addWidget(hint)
def _connect_signals(self):
"""Connect internal signals."""
self.signals.token_received.connect(self._on_token)
self.signals.status_changed.connect(self._on_status_changed)
self.signals.chunks_received.connect(self._on_chunks_received)
self.signals.completed.connect(self._on_completed)
def _on_chunks_received(self, chunks: list):
"""Handle receiving conversation chunks."""
self.set_conversations(chunks)
def _on_token(self, token: str):
"""Handle receiving a token from the LLM."""
# Append token to diary text
cursor = self.diary_text.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
cursor.insertText(token)
self.diary_text.setTextCursor(cursor)
# Auto-scroll to bottom
scrollbar = self.diary_text.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def _on_status_changed(self, status: str):
"""Handle status change."""
self.status_label.setText(status)
def _on_completed(self, success: bool):
"""Handle completion."""
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(100)
if success:
self.status_label.setText("Diary saved successfully!")
self.status_label.setStyleSheet(f"color: {COLORS['success']};")
else:
self.status_label.setText("No new entries to save")
self.status_label.setStyleSheet(f"color: {COLORS['text_muted']};")
# Clear placeholders if nothing was populated
if not self.conversations_text.toPlainText():
self.conversations_text.setPlainText("(No conversations to save)")
if not self.diary_text.toPlainText():
self.diary_text.setPlainText("(Nothing to write)")
def set_conversations(self, chunks: List[str]):
"""Set the conversation chunks being processed."""
if not chunks:
self.conversations_text.setPlainText("(No conversations to save)")
return
# Format chunks nicely
formatted = []
for i, chunk in enumerate(chunks[-5:], 1): # Show last 5 chunks
# Truncate long chunks
preview = chunk[:200] + "..." if len(chunk) > 200 else chunk
# Clean up whitespace
preview = " ".join(preview.split())
formatted.append(f"{i}. {preview}")
self.conversations_text.setPlainText("\n\n".join(formatted))
def set_diary_content(self, content: str):
"""Set the diary content (for non-streaming updates)."""
self.diary_text.setPlainText(content)
def append_diary_token(self, token: str):
"""Append a token to the diary content (for streaming)."""
self.signals.token_received.emit(token)
def set_status(self, status: str):
"""Update the status message."""
self.signals.status_changed.emit(status)
def mark_completed(self, success: bool = True):
"""Mark the update as completed."""
self.signals.completed.emit(success)
def process_log_line(self, line: str) -> bool:
"""
Process a log line, checking if it contains an IPC event.
Used in subprocess mode where the daemon emits diary events via stdout.
Args:
line: A log line from the daemon
Returns:
True if the line was an IPC event and was processed, False otherwise
"""
line = line.strip()
if not line.startswith(DIARY_IPC_PREFIX):
return False
try:
import json
json_str = line[len(DIARY_IPC_PREFIX):]
event = json.loads(json_str)
event_type = event.get("type")
data = event.get("data")
if event_type == "chunks":
self.signals.chunks_received.emit(data)
elif event_type == "token":
self.signals.token_received.emit(data)
elif event_type == "status":
self.signals.status_changed.emit(data)
elif event_type == "complete":
self.signals.completed.emit(data)
return True
except Exception:
return False
def set_subprocess_mode(self):
"""
Configure dialog for subprocess mode.
In subprocess mode, the daemon emits IPC events via stdout which are
intercepted and forwarded to this dialog via process_log_line().
"""
# Initial state - will be updated when IPC events arrive
self.conversations_text.setPlaceholderText("Waiting for daemon...")
self.diary_text.setPlaceholderText("Waiting for diary generation...")