Download src/clfgui/gui/main_window.py from smlflg/CLFGUI: direct link, hf CLI and curl.
- Browser
- Download file 27.2 kB
-
https://huggingface.co/smlflg/CLFGUI/resolve/main/src/clfgui/gui/main_window.py
- Command line
-
hf download hf://smlflg/CLFGUI/src/clfgui/gui/main_window.py
-
curl -L -o main_window.py https://huggingface.co/smlflg/CLFGUI/resolve/main/src/clfgui/gui/main_window.py
27.2 kB
| from __future__ import annotations | |
| from functools import partial | |
| from PySide6.QtCore import Qt, QTimer | |
| from PySide6.QtWidgets import ( | |
| QApplication, | |
| QFrame, | |
| QHBoxLayout, | |
| QLabel, | |
| QLayout, | |
| QMainWindow, | |
| QMessageBox, | |
| QPushButton, | |
| QScrollArea, | |
| QSizePolicy, | |
| QTextEdit, | |
| QVBoxLayout, | |
| QWidget, | |
| ) | |
| from clfgui.kernel import Job, JobSnapshot, LocalAgentKernel, Phase, SQLiteStatusStore | |
| from .paths import qstandard_db_path | |
| STATUS_LABELS = { | |
| "running": "Läuft", | |
| "waiting_decision": "Human-Gate", | |
| "awaiting_approval": "Ergebnisfreigabe", | |
| "approved": "Freigegeben", | |
| "rejected": "Abgelehnt", | |
| "blocked": "Blockiert", | |
| "pending": "Ausstehend", | |
| "waiting": "Wartet", | |
| "completed": "Fertig", | |
| "failed": "Fehler", | |
| } | |
| PHASE_LABELS = { | |
| "plan": "Plan", | |
| "research": "Research", | |
| "code": "Code", | |
| "test": "Test", | |
| "verify": "Verify", | |
| "result": "Ergebnis", | |
| } | |
| class MainWindow(QMainWindow): | |
| def __init__(self, kernel: LocalAgentKernel | None = None) -> None: | |
| super().__init__() | |
| self.kernel = kernel or LocalAgentKernel(SQLiteStatusStore(qstandard_db_path())) | |
| self.active_job_id: int | None = None | |
| self._selected_decision_id: int | None = None | |
| self._selected_decision_option: str | None = None | |
| self.setWindowTitle("CLFGUI") | |
| self.resize(1360, 820) | |
| self.setMinimumSize(1120, 700) | |
| self._build_ui() | |
| self._apply_style() | |
| self.refresh() | |
| self._timer = QTimer(self) | |
| self._timer.timeout.connect(self.refresh) | |
| self._timer.start(1000) | |
| def _build_ui(self) -> None: | |
| root = QWidget() | |
| root.setObjectName("appRoot") | |
| root_layout = QHBoxLayout(root) | |
| root_layout.setContentsMargins(0, 0, 0, 0) | |
| root_layout.setSpacing(0) | |
| root_layout.addWidget(self._build_sidebar()) | |
| root_layout.addWidget(self._build_workspace(), 1) | |
| self.setCentralWidget(root) | |
| def _build_sidebar(self) -> QWidget: | |
| sidebar = QFrame() | |
| sidebar.setObjectName("sidebar") | |
| sidebar.setFixedWidth(342) | |
| layout = QVBoxLayout(sidebar) | |
| layout.setContentsMargins(22, 22, 22, 22) | |
| layout.setSpacing(18) | |
| title = QLabel("Owner Cockpit") | |
| title.setObjectName("appTitle") | |
| self.status_badge = QLabel("Bereit") | |
| self.status_badge.setObjectName("statusBadge") | |
| self.status_badge.setProperty("state", "idle") | |
| self.status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter) | |
| self.intent_input = QTextEdit() | |
| self.intent_input.setObjectName("intentInput") | |
| self.intent_input.setAcceptRichText(False) | |
| self.intent_input.setPlaceholderText("Ich will ...") | |
| self.intent_input.setFixedHeight(116) | |
| self.submit_button = QPushButton("Auftrag starten") | |
| self.submit_button.setObjectName("primaryButton") | |
| self.submit_button.clicked.connect(self._submit_intent) | |
| jobs_title = QLabel("Aufträge") | |
| jobs_title.setObjectName("sidebarSection") | |
| self.jobs_content = QWidget() | |
| self.jobs_content.setObjectName("jobsContent") | |
| self.jobs_layout = QVBoxLayout(self.jobs_content) | |
| self.jobs_layout.setContentsMargins(0, 0, 0, 0) | |
| self.jobs_layout.setSpacing(8) | |
| jobs_scroll = QScrollArea() | |
| jobs_scroll.setObjectName("jobsScroll") | |
| jobs_scroll.setWidgetResizable(True) | |
| jobs_scroll.setFrameShape(QFrame.Shape.NoFrame) | |
| jobs_scroll.setWidget(self.jobs_content) | |
| layout.addWidget(title) | |
| layout.addWidget(self.status_badge) | |
| layout.addWidget(self.intent_input) | |
| layout.addWidget(self.submit_button) | |
| layout.addSpacing(6) | |
| layout.addWidget(jobs_title) | |
| layout.addWidget(jobs_scroll, 1) | |
| return sidebar | |
| def _build_workspace(self) -> QWidget: | |
| workspace = QWidget() | |
| workspace.setObjectName("workspace") | |
| layout = QVBoxLayout(workspace) | |
| layout.setContentsMargins(24, 22, 24, 22) | |
| layout.setSpacing(18) | |
| top = QHBoxLayout() | |
| top.setSpacing(16) | |
| title_group = QVBoxLayout() | |
| title_group.setSpacing(4) | |
| page_title = QLabel("Aktiver Auftrag") | |
| page_title.setObjectName("pageTitle") | |
| self.header_context = QLabel("Kein Auftrag aktiv.") | |
| self.header_context.setObjectName("headerContext") | |
| self.header_context.setWordWrap(True) | |
| title_group.addWidget(page_title) | |
| title_group.addWidget(self.header_context) | |
| self.plan_status = QLabel("Human-Gates: keine offen") | |
| self.plan_status.setObjectName("topMeta") | |
| top.addLayout(title_group, 1) | |
| top.addWidget(self.plan_status, 0, Qt.AlignmentFlag.AlignTop) | |
| body = QHBoxLayout() | |
| body.setSpacing(18) | |
| body.addWidget(self._build_plan_panel(), 1) | |
| body.addWidget(self._build_action_panel()) | |
| layout.addLayout(top) | |
| layout.addLayout(body, 1) | |
| return workspace | |
| def _build_plan_panel(self) -> QWidget: | |
| panel = self._panel("planPanel") | |
| layout = QVBoxLayout(panel) | |
| layout.setContentsMargins(20, 18, 20, 20) | |
| layout.setSpacing(14) | |
| layout.addWidget(self._section_title("Laufender Plan")) | |
| self.phase_content = QWidget() | |
| self.phase_content.setObjectName("phaseContent") | |
| self.phase_layout = QVBoxLayout(self.phase_content) | |
| self.phase_layout.setContentsMargins(0, 0, 0, 0) | |
| self.phase_layout.setSpacing(10) | |
| phase_scroll = QScrollArea() | |
| phase_scroll.setObjectName("phaseScroll") | |
| phase_scroll.setWidgetResizable(True) | |
| phase_scroll.setFrameShape(QFrame.Shape.NoFrame) | |
| phase_scroll.setWidget(self.phase_content) | |
| layout.addWidget(phase_scroll, 1) | |
| return panel | |
| def _build_action_panel(self) -> QWidget: | |
| panel = QWidget() | |
| panel.setObjectName("actionColumn") | |
| panel.setFixedWidth(402) | |
| layout = QVBoxLayout(panel) | |
| layout.setContentsMargins(0, 0, 0, 0) | |
| layout.setSpacing(18) | |
| layout.addWidget(self._build_decision_panel(), 1) | |
| layout.addWidget(self._build_result_panel(), 1) | |
| return panel | |
| def _build_decision_panel(self) -> QWidget: | |
| panel = self._panel("decisionPanel") | |
| layout = QVBoxLayout(panel) | |
| layout.setContentsMargins(20, 18, 20, 20) | |
| layout.setSpacing(14) | |
| layout.addWidget(self._section_title("Entscheidung")) | |
| self.decision_content = QWidget() | |
| self.decision_content_layout = QVBoxLayout(self.decision_content) | |
| self.decision_content_layout.setContentsMargins(0, 0, 0, 0) | |
| self.decision_content_layout.setSpacing(12) | |
| layout.addWidget(self.decision_content, 1) | |
| return panel | |
| def _build_result_panel(self) -> QWidget: | |
| panel = self._panel("resultPanel") | |
| layout = QVBoxLayout(panel) | |
| layout.setContentsMargins(20, 18, 20, 20) | |
| layout.setSpacing(14) | |
| layout.addWidget(self._section_title("Ergebnis")) | |
| self.result_text = QTextEdit() | |
| self.result_text.setObjectName("resultText") | |
| self.result_text.setReadOnly(True) | |
| self.result_text.setAcceptRichText(False) | |
| self.result_text.setPlaceholderText("Noch kein Ergebnis vorhanden.") | |
| button_row = QHBoxLayout() | |
| button_row.setSpacing(10) | |
| self.approve_button = QPushButton("Freigeben") | |
| self.approve_button.setObjectName("primaryButton") | |
| self.approve_button.clicked.connect(partial(self._approve_result, True)) | |
| self.reject_button = QPushButton("Ablehnen") | |
| self.reject_button.setObjectName("secondaryButton") | |
| self.reject_button.clicked.connect(partial(self._approve_result, False)) | |
| button_row.addWidget(self.approve_button) | |
| button_row.addWidget(self.reject_button) | |
| layout.addWidget(self.result_text, 1) | |
| layout.addLayout(button_row) | |
| return panel | |
| def _panel(name: str) -> QFrame: | |
| panel = QFrame() | |
| panel.setObjectName(name) | |
| panel.setFrameShape(QFrame.Shape.NoFrame) | |
| return panel | |
| def _section_title(text: str) -> QLabel: | |
| label = QLabel(text) | |
| label.setObjectName("sectionTitle") | |
| return label | |
| def _apply_style(self) -> None: | |
| self.setStyleSheet( | |
| """ | |
| #appRoot { | |
| background: #eef1f4; | |
| } | |
| QWidget { | |
| color: #18202a; | |
| font-size: 13px; | |
| } | |
| QLabel { | |
| background: transparent; | |
| } | |
| #sidebar { | |
| background: #151d27; | |
| border-right: 1px solid #25313e; | |
| } | |
| #workspace { | |
| background: #eef1f4; | |
| } | |
| #appTitle { | |
| color: #f8fafc; | |
| font-size: 24px; | |
| font-weight: 800; | |
| } | |
| #sidebarSection { | |
| color: #aeb8c4; | |
| font-size: 12px; | |
| font-weight: 800; | |
| letter-spacing: 0px; | |
| } | |
| #pageTitle { | |
| color: #18202a; | |
| font-size: 24px; | |
| font-weight: 800; | |
| } | |
| #headerContext { | |
| color: #637080; | |
| font-size: 13px; | |
| } | |
| #topMeta { | |
| background: #ffffff; | |
| color: #56616f; | |
| border: 1px solid #d7dde5; | |
| border-radius: 10px; | |
| padding: 8px 12px; | |
| font-weight: 700; | |
| } | |
| #statusBadge { | |
| background: #263241; | |
| color: #dce3ea; | |
| border: 1px solid #334253; | |
| border-radius: 10px; | |
| padding: 9px 12px; | |
| font-weight: 800; | |
| } | |
| #statusBadge[state="waiting_decision"], #statusBadge[state="waiting"] { | |
| background: #382e16; | |
| border-color: #9c7a25; | |
| color: #f7d677; | |
| } | |
| #statusBadge[state="awaiting_approval"], #statusBadge[state="running"] { | |
| background: #173044; | |
| border-color: #34708f; | |
| color: #a6d8ef; | |
| } | |
| #statusBadge[state="approved"] { | |
| background: #173526; | |
| border-color: #2f7b52; | |
| color: #99ddb6; | |
| } | |
| #statusBadge[state="blocked"], #statusBadge[state="rejected"], #statusBadge[state="failed"] { | |
| background: #3e1e1e; | |
| border-color: #9d4a40; | |
| color: #f2b0a7; | |
| } | |
| #intentInput { | |
| background: #0f1721; | |
| color: #f4f7fb; | |
| border: 1px solid #2a3847; | |
| border-radius: 12px; | |
| padding: 10px; | |
| selection-background-color: #2e6b77; | |
| } | |
| #intentInput::placeholder { | |
| color: #758293; | |
| } | |
| #resultText { | |
| background: #f8fafc; | |
| color: #202a35; | |
| border: 1px solid #d8dee6; | |
| border-radius: 12px; | |
| padding: 10px; | |
| selection-background-color: #cde3ec; | |
| } | |
| #primaryButton { | |
| background: #2f7783; | |
| color: #ffffff; | |
| border: 1px solid #286773; | |
| border-radius: 12px; | |
| padding: 10px 14px; | |
| min-height: 34px; | |
| font-weight: 800; | |
| } | |
| #primaryButton:hover:!disabled { | |
| background: #286b76; | |
| } | |
| #secondaryButton { | |
| background: #ffffff; | |
| color: #2e3946; | |
| border: 1px solid #cbd3dc; | |
| border-radius: 12px; | |
| padding: 10px 14px; | |
| min-height: 34px; | |
| font-weight: 800; | |
| } | |
| #secondaryButton:hover:!disabled { | |
| background: #f4f7fa; | |
| } | |
| QPushButton:disabled { | |
| background: #e4e8ed; | |
| border: 1px solid #d3d9e1; | |
| color: #9aa5b2; | |
| } | |
| #jobsScroll, #phaseScroll { | |
| background: transparent; | |
| border: 0; | |
| } | |
| #jobsContent, #phaseContent, #actionColumn { | |
| background: transparent; | |
| } | |
| #jobButton { | |
| background: #1c2734; | |
| color: #c7d0dc; | |
| border: 1px solid #2c3948; | |
| border-radius: 12px; | |
| padding: 11px 12px; | |
| text-align: left; | |
| font-weight: 700; | |
| } | |
| #jobButton:hover { | |
| background: #243244; | |
| } | |
| #jobButton[selected="true"] { | |
| background: #eaf7fa; | |
| color: #14313a; | |
| border-color: #72aeb8; | |
| } | |
| #planPanel, #decisionPanel, #resultPanel { | |
| background: #ffffff; | |
| border: 1px solid #d9dfe7; | |
| border-radius: 18px; | |
| } | |
| #sectionTitle { | |
| color: #18202a; | |
| font-size: 16px; | |
| font-weight: 800; | |
| } | |
| #phaseRow { | |
| background: #f8fafc; | |
| border: 1px solid #e0e5ec; | |
| border-radius: 14px; | |
| } | |
| #phaseRow[active="true"] { | |
| background: #fff8e8; | |
| border-color: #e5be5f; | |
| } | |
| #phaseName { | |
| color: #18202a; | |
| font-weight: 800; | |
| font-size: 14px; | |
| } | |
| #phaseSummary { | |
| color: #5b6775; | |
| font-size: 13px; | |
| } | |
| #statusPill { | |
| border-radius: 10px; | |
| padding: 5px 9px; | |
| font-weight: 800; | |
| font-size: 12px; | |
| } | |
| #statusPill[state="pending"] { | |
| background: #edf1f5; | |
| color: #657181; | |
| } | |
| #statusPill[state="waiting"], #statusPill[state="waiting_decision"] { | |
| background: #fff0c6; | |
| color: #735300; | |
| } | |
| #statusPill[state="completed"], #statusPill[state="approved"] { | |
| background: #daf3e3; | |
| color: #1f6339; | |
| } | |
| #statusPill[state="running"], #statusPill[state="awaiting_approval"] { | |
| background: #dceff7; | |
| color: #22566d; | |
| } | |
| #statusPill[state="blocked"], #statusPill[state="failed"], #statusPill[state="rejected"] { | |
| background: #f9ddd8; | |
| color: #84372c; | |
| } | |
| #decisionTitle { | |
| color: #18202a; | |
| font-size: 16px; | |
| font-weight: 800; | |
| } | |
| #bodyText { | |
| color: #526070; | |
| line-height: 1.35; | |
| } | |
| #optionButton { | |
| background: #f7f9fb; | |
| color: #26313d; | |
| border: 1px solid #d9e0e8; | |
| border-radius: 12px; | |
| padding: 10px 12px; | |
| text-align: left; | |
| font-weight: 700; | |
| } | |
| #optionButton:hover { | |
| background: #eef5f7; | |
| } | |
| #optionButton[selected="true"] { | |
| background: #e5f4f7; | |
| color: #14353c; | |
| border-color: #5ca2ad; | |
| } | |
| #decisionNote { | |
| background: #f8fafc; | |
| color: #202a35; | |
| border: 1px solid #d8dee6; | |
| border-radius: 12px; | |
| padding: 9px; | |
| } | |
| QScrollBar:vertical { | |
| background: transparent; | |
| width: 10px; | |
| margin: 0; | |
| } | |
| QScrollBar::handle:vertical { | |
| background: #c6ced8; | |
| border-radius: 5px; | |
| min-height: 36px; | |
| } | |
| QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { | |
| height: 0; | |
| } | |
| """ | |
| ) | |
| def _submit_intent(self) -> None: | |
| try: | |
| job_id = self.kernel.submit_intent(self.intent_input.toPlainText()) | |
| except ValueError as exc: | |
| QMessageBox.warning(self, "Auftrag fehlt", str(exc)) | |
| return | |
| self.active_job_id = job_id | |
| self.intent_input.clear() | |
| self.refresh() | |
| def _select_job(self, job_id: int) -> None: | |
| self.active_job_id = job_id | |
| self.refresh() | |
| def _select_decision_option(self, decision_id: int, option: str) -> None: | |
| self._selected_decision_id = decision_id | |
| self._selected_decision_option = option | |
| for button in self.decision_content.findChildren(QPushButton): | |
| if button.objectName() != "optionButton": | |
| continue | |
| selected = button.property("decision_id") == decision_id and button.property("option") == option | |
| button.setProperty("selected", "true" if selected else "false") | |
| self._refresh_widget_style(button) | |
| def _answer_decision(self) -> None: | |
| if self._selected_decision_id is None or self._selected_decision_option is None: | |
| QMessageBox.warning(self, "Entscheidung fehlt", "Bitte eine Option auswählen.") | |
| return | |
| self.kernel.answer_decision( | |
| self._selected_decision_id, | |
| self._selected_decision_option, | |
| self.decision_note.toPlainText(), | |
| ) | |
| self._selected_decision_id = None | |
| self._selected_decision_option = None | |
| self.refresh() | |
| def _approve_result(self, approved: bool) -> None: | |
| if self.active_job_id is None: | |
| return | |
| try: | |
| self.kernel.approve_result(self.active_job_id, approved) | |
| except KeyError: | |
| QMessageBox.information(self, "Kein Ergebnis", "Für diesen Auftrag liegt noch kein Ergebnis vor.") | |
| return | |
| self.refresh() | |
| def refresh(self) -> None: | |
| self._refresh_jobs() | |
| snapshot = self._active_snapshot() | |
| self._refresh_header(snapshot) | |
| self._refresh_plan(snapshot) | |
| self._refresh_decisions(snapshot) | |
| self._refresh_results(snapshot) | |
| def _refresh_jobs(self) -> None: | |
| jobs = self.kernel.list_jobs() | |
| if self.active_job_id is None and jobs: | |
| self.active_job_id = jobs[0].id | |
| self._clear_layout(self.jobs_layout) | |
| for job in jobs: | |
| self.jobs_layout.addWidget(self._job_button(job)) | |
| self.jobs_layout.addStretch(1) | |
| def _job_button(self, job: Job) -> QPushButton: | |
| status = STATUS_LABELS.get(job.status, job.status) | |
| intent = job.intent if len(job.intent) <= 48 else f"{job.intent[:45]}..." | |
| button = QPushButton(f"#{job.id} · {status}\n{intent}") | |
| button.setObjectName("jobButton") | |
| button.setProperty("selected", "true" if job.id == self.active_job_id else "false") | |
| button.setCursor(Qt.CursorShape.PointingHandCursor) | |
| button.clicked.connect(partial(self._select_job, job.id)) | |
| button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) | |
| return button | |
| def _active_snapshot(self) -> JobSnapshot | None: | |
| if self.active_job_id is None: | |
| return None | |
| try: | |
| return self.kernel.get_job_status(self.active_job_id) | |
| except KeyError: | |
| self.active_job_id = None | |
| return None | |
| def _refresh_header(self, snapshot: JobSnapshot | None) -> None: | |
| if snapshot is None: | |
| self.header_context.setText("Kein Auftrag aktiv.") | |
| self.status_badge.setText("Bereit") | |
| self._set_status_state("idle") | |
| return | |
| status = STATUS_LABELS.get(snapshot.job.status, snapshot.job.status) | |
| self.status_badge.setText(status) | |
| self._set_status_state(snapshot.job.status) | |
| self.header_context.setText(f"#{snapshot.job.id} · {snapshot.job.intent}") | |
| def _refresh_plan(self, snapshot: JobSnapshot | None) -> None: | |
| self._clear_layout(self.phase_layout) | |
| if snapshot is None: | |
| self.plan_status.setText("Human-Gates: keine offen") | |
| self.phase_layout.addWidget(self._empty_label("Kein Auftrag ausgewählt.")) | |
| self.phase_layout.addStretch(1) | |
| return | |
| open_decisions = [decision for decision in snapshot.decisions if decision.status == "open"] | |
| gate_text = f"{len(open_decisions)} offen" if open_decisions else "keine offen" | |
| self.plan_status.setText(f"Human-Gates: {gate_text}") | |
| for phase in snapshot.phases: | |
| self.phase_layout.addWidget(self._phase_row(phase)) | |
| self.phase_layout.addStretch(1) | |
| def _phase_row(self, phase: Phase) -> QWidget: | |
| row = QFrame() | |
| row.setObjectName("phaseRow") | |
| row.setProperty("active", "true" if phase.status in {"running", "waiting"} else "false") | |
| layout = QHBoxLayout(row) | |
| layout.setContentsMargins(14, 12, 14, 12) | |
| layout.setSpacing(14) | |
| text_group = QVBoxLayout() | |
| text_group.setSpacing(3) | |
| name = QLabel(PHASE_LABELS.get(phase.name, phase.name)) | |
| name.setObjectName("phaseName") | |
| summary = QLabel(phase.summary or "Noch keine Zusammenfassung.") | |
| summary.setObjectName("phaseSummary") | |
| summary.setWordWrap(True) | |
| text_group.addWidget(name) | |
| text_group.addWidget(summary) | |
| status = QLabel(STATUS_LABELS.get(phase.status, phase.status)) | |
| status.setObjectName("statusPill") | |
| status.setProperty("state", phase.status) | |
| status.setAlignment(Qt.AlignmentFlag.AlignCenter) | |
| status.setMinimumWidth(112) | |
| layout.addLayout(text_group, 1) | |
| layout.addWidget(status, 0, Qt.AlignmentFlag.AlignTop) | |
| return row | |
| def _refresh_decisions(self, snapshot: JobSnapshot | None) -> None: | |
| self._clear_layout(self.decision_content_layout) | |
| self._selected_decision_id = None | |
| self._selected_decision_option = None | |
| if snapshot is None: | |
| self.decision_content_layout.addWidget(self._empty_label("Kein Auftrag ausgewählt.")) | |
| self.decision_content_layout.addStretch(1) | |
| return | |
| open_decisions = [decision for decision in snapshot.decisions if decision.status == "open"] | |
| if not open_decisions: | |
| self.decision_content_layout.addWidget(self._empty_label("Keine offenen Human-Gates.")) | |
| self.decision_content_layout.addStretch(1) | |
| return | |
| decision = open_decisions[0] | |
| title = QLabel(decision.title) | |
| title.setObjectName("decisionTitle") | |
| title.setWordWrap(True) | |
| context = QLabel(decision.context) | |
| context.setWordWrap(True) | |
| context.setObjectName("bodyText") | |
| self.decision_content_layout.addWidget(title) | |
| self.decision_content_layout.addWidget(context) | |
| for index, option in enumerate(decision.options): | |
| button = QPushButton(option) | |
| button.setObjectName("optionButton") | |
| button.setProperty("decision_id", decision.id) | |
| button.setProperty("option", option) | |
| button.setProperty("selected", "true" if index == 0 else "false") | |
| button.setCheckable(False) | |
| button.setCursor(Qt.CursorShape.PointingHandCursor) | |
| button.clicked.connect(partial(self._select_decision_option, decision.id, option)) | |
| self.decision_content_layout.addWidget(button) | |
| if index == 0: | |
| self._selected_decision_id = decision.id | |
| self._selected_decision_option = option | |
| self.decision_note = QTextEdit() | |
| self.decision_note.setObjectName("decisionNote") | |
| self.decision_note.setAcceptRichText(False) | |
| self.decision_note.setPlaceholderText("Optionaler Hinweis") | |
| self.decision_note.setFixedHeight(86) | |
| submit = QPushButton("Antwort senden") | |
| submit.setObjectName("primaryButton") | |
| submit.clicked.connect(self._answer_decision) | |
| self.decision_content_layout.addWidget(self.decision_note) | |
| self.decision_content_layout.addWidget(submit) | |
| self.decision_content_layout.addStretch(1) | |
| def _refresh_results(self, snapshot: JobSnapshot | None) -> None: | |
| if snapshot is None or snapshot.result is None: | |
| self.result_text.clear() | |
| self.approve_button.setEnabled(False) | |
| self.reject_button.setEnabled(False) | |
| return | |
| result = snapshot.result | |
| decisions = "\n".join(f"- {item}" for item in result.decisions) or "- Keine" | |
| next_steps = "\n".join(f"- {item}" for item in result.next_steps) or "- Keine" | |
| blocked = "Ja" if result.blocked else "Nein" | |
| approval = ( | |
| "Offen" | |
| if result.approved is None | |
| else "Freigegeben" | |
| if result.approved | |
| else "Abgelehnt" | |
| ) | |
| self.result_text.setPlainText( | |
| "\n\n".join( | |
| [ | |
| f"Was gemacht wurde\n{result.summary}", | |
| f"Was blockiert ist\n{blocked}", | |
| f"Was du entschieden hast\n{decisions}", | |
| f"Was als Nächstes sinnvoll ist\n{next_steps}", | |
| f"Freigabe\n{approval}", | |
| ] | |
| ) | |
| ) | |
| buttons_enabled = result.approved is None and not result.blocked | |
| self.approve_button.setEnabled(buttons_enabled) | |
| self.reject_button.setEnabled(buttons_enabled) | |
| def _empty_label(text: str) -> QLabel: | |
| label = QLabel(text) | |
| label.setObjectName("bodyText") | |
| label.setWordWrap(True) | |
| return label | |
| def _set_status_state(self, state: str) -> None: | |
| self.status_badge.setProperty("state", state) | |
| self._refresh_widget_style(self.status_badge) | |
| def _refresh_widget_style(widget: QWidget) -> None: | |
| widget.style().unpolish(widget) | |
| widget.style().polish(widget) | |
| def _clear_layout(layout: QLayout) -> None: | |
| while layout.count(): | |
| item = layout.takeAt(0) | |
| widget = item.widget() | |
| child_layout = item.layout() | |
| if widget is not None: | |
| widget.setParent(None) | |
| widget.deleteLater() | |
| elif child_layout is not None: | |
| MainWindow._clear_layout(child_layout) | |
| def run_app(argv: list[str]) -> int: | |
| QApplication.setOrganizationName("CLFGUI") | |
| QApplication.setApplicationName("CLFGUI") | |
| QApplication.setStyle("Fusion") | |
| app = QApplication(argv) | |
| window = MainWindow() | |
| window.show() | |
| return app.exec() | |