1 from datetime import datetime 2 import math 3 from pathlib import Path 4 5 from PyQt5.QtCore import Qt, QRectF, QTimer 6 from PyQt5.QtGui import ( 7 QBrush, 8 QColor, 9 QFont, 10 QLinearGradient, 11 QPainter, 12 QPainterPath, 13 QPen, 14 QPixmap, 15 QRadialGradient, 16 ) 17 from PyQt5.QtWidgets import ( 18 QFrame, 19 QGridLayout, 20 QHBoxLayout, 21 QLabel, 22 QPushButton, 23 QSizePolicy, 24 QVBoxLayout, 25 QWidget, 26 ) 27 28 from core.voice_privacy import voice_privacy_foundation 29 30 31 GREEN = "#00f58a" 32 PURPLE = "#9b4dff" 33 PURPLE_LIGHT = "#c89cff" 34 CYAN = "#18e6d2" 35 WHITE = "#f4efff" 36 MUTED = "#a89bc4" 37 PANEL = "rgba(7, 7, 22, 225)" 38 PANEL_SOFT = "rgba(10, 7, 28, 190)" 39 BORDER = "rgba(155, 77, 255, 165)" 40 41 42 def _greeting(): 43 hour = datetime.now().hour 44 45 if hour < 12: 46 return "Good morning, Ray." 47 48 if hour < 18: 49 return "Good afternoon, Ray." 50 51 return "Good evening, Ray." 52 53 54 class VoicePrivacyBanner(QFrame): 55 """Live local voice privacy and runtime-state banner.""" 56 57 def __init__(self): 58 super().__init__() 59 60 self.setObjectName("voicePrivacyBanner") 61 self.setFixedHeight(52) 62 self.setMinimumWidth(310) 63 self.setSizePolicy( 64 QSizePolicy.Preferred, 65 QSizePolicy.Fixed, 66 ) 67 68 root = QHBoxLayout(self) 69 root.setContentsMargins(18, 6, 18, 6) 70 root.setSpacing(12) 71 72 self.icon = QLabel("◉") 73 self.icon.setAlignment(Qt.AlignCenter) 74 75 text_column = QVBoxLayout() 76 text_column.setSpacing(0) 77 78 self.top = QLabel() 79 self.bottom = QLabel() 80 81 text_column.addWidget(self.top) 82 text_column.addWidget(self.bottom) 83 84 root.addWidget(self.icon) 85 root.addLayout(text_column) 86 87 self.timer = QTimer(self) 88 self.timer.timeout.connect(self.refresh) 89 self.timer.start(400) 90 91 self.refresh() 92 93 def refresh(self): 94 snapshot = voice_privacy_foundation.snapshot() 95 state = str(snapshot.state).upper() 96 97 if state == "ERROR": 98 accent = "#ff5c70" 99 top_text = "VOICE ERROR" 100 bottom_text = snapshot.error or "VOICE POLICY ERROR" 101 102 elif state == "LISTENING": 103 accent = GREEN 104 top_text = "VOICE LISTENING" 105 bottom_text = "MICROPHONE OPEN • LOCAL ONLY" 106 107 elif state == "PROCESSING": 108 accent = CYAN 109 top_text = "VOICE PROCESSING" 110 bottom_text = "AUDIO DISCARDED • LOCAL ONLY" 111 112 elif state == "READY": 113 accent = PURPLE_LIGHT 114 top_text = "VOICE READY" 115 bottom_text = "MANUAL CAPTURE • LOCAL ONLY" 116 117 else: 118 accent = MUTED 119 top_text = "VOICE MODE OFF" 120 bottom_text = "MICROPHONE CLOSED" 121 122 self.setStyleSheet(f""" 123 QFrame#voicePrivacyBanner {{ 124 background: rgba(4, 7, 18, 220); 125 border: 1px solid {accent}; 126 border-radius: 13px; 127 }} 128 QLabel {{ 129 background: transparent; 130 border: none; 131 }} 132 """) 133 134 self.icon.setStyleSheet( 135 f"color:{accent}; font-size:24px; font-weight:900;" 136 ) 137 138 self.top.setText(top_text) 139 self.top.setStyleSheet( 140 f"color:{accent}; font-size:14px; font-weight:900;" 141 ) 142 143 self.bottom.setText(bottom_text) 144 self.bottom.setStyleSheet( 145 "color:#aa9dc6; font-size:9px; font-weight:800;" 146 ) 147 148 149 class LaptopRuneButton(QPushButton): 150 """Compact Presence Console navigation control.""" 151 152 def __init__( 153 self, 154 icon, 155 title, 156 sub, 157 target_index, 158 stack, 159 compact=False, 160 ): 161 super().__init__() 162 163 self.target_index = target_index 164 self.stack = stack 165 166 if compact: 167 self.setText(f"{icon} {title}") 168 self.setMinimumHeight(48) 169 else: 170 self.setText(f"{icon}\n{title}\n{sub}") 171 self.setMinimumHeight(74) 172 173 self.setCursor(Qt.PointingHandCursor) 174 self.setSizePolicy( 175 QSizePolicy.Expanding, 176 QSizePolicy.Fixed, 177 ) 178 179 self.setStyleSheet(""" 180 QPushButton { 181 background: rgba(8, 5, 25, 225); 182 color: #c69cff; 183 border: 1px solid rgba(155, 77, 255, 185); 184 border-radius: 13px; 185 font-family: "DejaVu Sans Mono", monospace; 186 font-size: 13px; 187 font-weight: 900; 188 padding: 8px 12px; 189 } 190 QPushButton:hover { 191 background: rgba(34, 9, 66, 235); 192 color: #ffffff; 193 border: 1px solid #b76cff; 194 } 195 QPushButton:pressed { 196 background: rgba(0, 245, 138, 65); 197 color: #00f58a; 198 border: 1px solid #00f58a; 199 } 200 """) 201 202 self.clicked.connect(self.open_target) 203 204 def open_target(self): 205 if self.stack is not None: 206 self.stack.setCurrentIndex(self.target_index) 207 208 209 class PresenceCard(QFrame): 210 def __init__(self, title, accent=PURPLE): 211 super().__init__() 212 213 self.setObjectName("presenceCard") 214 self.setStyleSheet(f""" 215 QFrame#presenceCard {{ 216 background: {PANEL}; 217 border: 1px solid {BORDER}; 218 border-radius: 18px; 219 }} 220 QLabel {{ 221 background: transparent; 222 border: none; 223 }} 224 """) 225 226 self.column = QVBoxLayout(self) 227 self.column.setContentsMargins(22, 18, 22, 18) 228 self.column.setSpacing(10) 229 230 heading = QLabel(title) 231 heading.setStyleSheet( 232 f"color:{accent}; font-size:12px; font-weight:900;" 233 ) 234 235 self.column.addWidget(heading) 236 237 238 class ShireCenterCore(QWidget): 239 """Face-first SHIRE presence with truthful expression states.""" 240 241 EXPRESSION_FILES = { 242 "neutral": "shire_neutral.png", 243 "blink_half": "shire_blink_half.png", 244 "blink_closed": "shire_blink_closed.png", 245 "smile": "shire_smile.png", 246 "thinking": "shire_thinking.png", 247 "listening": "shire_listening.png", 248 "focused": "shire_focused.png", 249 "acknowledge": "shire_acknowledge.png", 250 "concerned": "shire_concerned.png", 251 "surprised": "shire_surprised.png", 252 "sentinel_standby": "shire_sentinel_standby.png", 253 "sentinel_active": "shire_sentinel_active.png", 254 "sentinel_lockdown": "shire_sentinel_lockdown.png", 255 } 256 257 NORMAL_BLINK_STATES = { 258 "neutral", 259 "smile", 260 "focused", 261 "acknowledge", 262 "concerned", 263 } 264 265 def __init__(self): 266 super().__init__() 267 268 self.root = Path(__file__).resolve().parent.parent 269 self.expression_dir = ( 270 self.root 271 / "assets" 272 / "shire" 273 / "expressions" 274 ) 275 276 self.fallback_face_path = ( 277 self.root 278 / "assets" 279 / "shire" 280 / "shire_face_main.png" 281 ) 282 283 self.fallback_face = ( 284 QPixmap(str(self.fallback_face_path)) 285 if self.fallback_face_path.exists() 286 else QPixmap() 287 ) 288 289 self.expression_faces = {} 290 291 for state, filename in self.EXPRESSION_FILES.items(): 292 path = self.expression_dir / filename 293 portrait = ( 294 QPixmap(str(path)) 295 if path.exists() 296 else QPixmap() 297 ) 298 299 if portrait.isNull(): 300 portrait = self.fallback_face 301 302 self.expression_faces[state] = portrait 303 304 self.face_path = ( 305 self.expression_dir 306 / self.EXPRESSION_FILES["neutral"] 307 ) 308 309 self.face = self.expression_faces["neutral"] 310 self.expression_state = "neutral" 311 312 self.tick = 0 313 self.animation_interval_ms = 125 314 self._face_cache = {} 315 316 self._blink_intervals = ( 317 38, 318 52, 319 44, 320 60, 321 34, 322 49, 323 ) 324 325 self._blink_interval_index = 0 326 self._blink_ticks_remaining = ( 327 self._blink_intervals[0] 328 ) 329 self._blink_phase = -1 330 self._blink_duration_ms = 90 331 332 self.blink_timer = QTimer(self) 333 self.blink_timer.setSingleShot(True) 334 self.blink_timer.setTimerType(Qt.PreciseTimer) 335 self.blink_timer.timeout.connect( 336 self._finish_blink 337 ) 338 339 self.setMinimumSize(390, 470) 340 self.setSizePolicy( 341 QSizePolicy.Expanding, 342 QSizePolicy.Expanding, 343 ) 344 345 self.timer = QTimer(self) 346 self.timer.setTimerType(Qt.CoarseTimer) 347 self.timer.timeout.connect(self.pulse) 348 self.timer.start(self.animation_interval_ms) 349 350 def pulse(self): 351 self.tick = (self.tick + 1) % 28800 352 353 if self.expression_state in self.NORMAL_BLINK_STATES: 354 if self._blink_phase < 0: 355 self._blink_ticks_remaining -= 1 356 357 if self._blink_ticks_remaining <= 0: 358 self._blink_phase = 0 359 self.blink_timer.start( 360 self._blink_duration_ms 361 ) 362 else: 363 if self.blink_timer.isActive(): 364 self.blink_timer.stop() 365 366 self._blink_phase = -1 367 368 self.update() 369 370 def _finish_blink(self): 371 self._blink_phase = -1 372 self._schedule_next_blink() 373 self.update() 374 375 def _schedule_next_blink(self): 376 self._blink_interval_index = ( 377 self._blink_interval_index + 1 378 ) % len(self._blink_intervals) 379 380 self._blink_ticks_remaining = ( 381 self._blink_intervals[ 382 self._blink_interval_index 383 ] 384 ) 385 386 def set_expression_state(self, state): 387 """Select one approved expression without inventing state.""" 388 389 state = str(state).strip().lower() 390 391 if state not in self.expression_faces: 392 return False 393 394 self.expression_state = state 395 self.face = self.expression_faces[state] 396 397 if self.blink_timer.isActive(): 398 self.blink_timer.stop() 399 400 self._blink_phase = -1 401 self._schedule_next_blink() 402 self.update() 403 404 return True 405 406 def current_expression_state(self): 407 """Return the portrait state currently being rendered.""" 408 409 if ( 410 self.expression_state in self.NORMAL_BLINK_STATES 411 and self._blink_phase == 0 412 ): 413 return "blink_closed" 414 415 return self.expression_state 416 417 def _current_face(self): 418 state = self.current_expression_state() 419 420 return self.expression_faces.get( 421 state, 422 self.fallback_face, 423 ) 424 425 def showEvent(self, event): 426 super().showEvent(event) 427 428 if not self.timer.isActive(): 429 self.timer.start( 430 self.animation_interval_ms 431 ) 432 433 self.update() 434 435 def hideEvent(self, event): 436 if self.timer.isActive(): 437 self.timer.stop() 438 439 if self.blink_timer.isActive(): 440 self.blink_timer.stop() 441 442 self._blink_phase = -1 443 self._schedule_next_blink() 444 445 super().hideEvent(event) 446 447 def resizeEvent(self, event): 448 self._face_cache.clear() 449 super().resizeEvent(event) 450 451 def _scaled_face_for( 452 self, 453 state, 454 portrait_size, 455 ): 456 face = self.expression_faces.get( 457 state, 458 self.fallback_face, 459 ) 460 461 if face.isNull(): 462 return QPixmap() 463 464 target_size = max( 465 1, 466 int( 467 math.ceil( 468 float(portrait_size) * 1.025 469 ) 470 ), 471 ) 472 473 cache_key = ( 474 state, 475 int(face.cacheKey()), 476 target_size, 477 ) 478 479 cached = self._face_cache.get( 480 cache_key 481 ) 482 483 if cached is not None and not cached.isNull(): 484 return cached 485 486 scaled = face.scaled( 487 target_size, 488 target_size, 489 Qt.KeepAspectRatioByExpanding, 490 Qt.SmoothTransformation, 491 ) 492 493 if scaled.isNull(): 494 return face 495 496 if len(self._face_cache) >= 18: 497 self._face_cache.clear() 498 499 self._face_cache[cache_key] = scaled 500 501 return scaled 502 503 def paintEvent(self, event): 504 del event 505 506 painter = QPainter(self) 507 painter.setRenderHint( 508 QPainter.Antialiasing, 509 True, 510 ) 511 painter.setRenderHint( 512 QPainter.SmoothPixmapTransform, 513 True, 514 ) 515 516 w = self.width() 517 h = self.height() 518 cx = w / 2 519 cy = h * 0.47 520 521 seconds = ( 522 self.tick 523 * self.animation_interval_ms 524 / 1000.0 525 ) 526 527 aura = QRadialGradient( 528 cx, 529 cy, 530 min(w, h) * 0.48, 531 ) 532 aura.setColorAt( 533 0.0, 534 QColor(80, 25, 135, 115), 535 ) 536 aura.setColorAt( 537 0.55, 538 QColor(37, 7, 82, 60), 539 ) 540 aura.setColorAt( 541 1.0, 542 QColor(0, 0, 0, 0), 543 ) 544 545 painter.setPen(Qt.NoPen) 546 painter.setBrush(QBrush(aura)) 547 painter.drawEllipse( 548 QRectF( 549 cx - min(w, h) * 0.48, 550 cy - min(w, h) * 0.48, 551 min(w, h) * 0.96, 552 min(w, h) * 0.96, 553 ) 554 ) 555 556 pulse_offset = ( 557 math.sin(seconds * 0.75) + 1.0 558 ) / 2.0 559 560 for index in range(5): 561 radius = ( 562 min(w, h) 563 * (0.34 + index * 0.065) 564 + pulse_offset * 4.0 565 ) 566 567 alpha = max( 568 35, 569 145 - index * 22, 570 ) 571 572 painter.setPen( 573 QPen( 574 QColor( 575 155, 576 77, 577 255, 578 alpha, 579 ), 580 1.4 if index else 2.2, 581 ) 582 ) 583 584 painter.setBrush(Qt.NoBrush) 585 painter.drawEllipse( 586 QRectF( 587 cx - radius, 588 cy - radius, 589 radius * 2, 590 radius * 2, 591 ) 592 ) 593 594 base_portrait_size = min( 595 w * 0.72, 596 h * 0.72, 597 ) 598 599 breath = math.sin( 600 (2.0 * math.pi * seconds) / 6.6 601 ) 602 603 drift_x = ( 604 2.5 605 * math.sin( 606 ((2.0 * math.pi * seconds) / 11.2) 607 + 0.35 608 ) 609 ) 610 611 drift_y = ( 612 2.8 613 * math.sin( 614 (2.0 * math.pi * seconds) / 8.3 615 ) 616 ) 617 618 breathing_scale = 1.0 + ( 619 breath * 0.008 620 ) 621 622 portrait_size = ( 623 base_portrait_size 624 * breathing_scale 625 ) 626 627 portrait_rect = QRectF( 628 cx - portrait_size / 2 + drift_x, 629 cy - portrait_size / 2 + drift_y, 630 portrait_size, 631 portrait_size, 632 ) 633 634 clip_path = QPainterPath() 635 clip_path.addEllipse(portrait_rect) 636 637 painter.save() 638 painter.setClipPath(clip_path) 639 640 rendered_state = ( 641 self.current_expression_state() 642 ) 643 644 scaled = self._scaled_face_for( 645 rendered_state, 646 base_portrait_size, 647 ) 648 649 if not scaled.isNull(): 650 source_size = min( 651 float(scaled.width()), 652 float(scaled.height()), 653 ) 654 655 source_x = max( 656 0.0, 657 ( 658 scaled.width() 659 - source_size 660 ) / 2.0, 661 ) 662 663 source_y = max( 664 0.0, 665 ( 666 scaled.height() 667 - source_size 668 ) / 2.0, 669 ) 670 671 painter.drawPixmap( 672 portrait_rect, 673 scaled, 674 QRectF( 675 source_x, 676 source_y, 677 source_size, 678 source_size, 679 ), 680 ) 681 else: 682 painter.fillRect( 683 portrait_rect, 684 QColor(7, 5, 20), 685 ) 686 687 painter.setPen( 688 QPen( 689 QColor(155, 77, 255), 690 2, 691 ) 692 ) 693 694 painter.setFont( 695 QFont( 696 "DejaVu Sans Mono", 697 25, 698 QFont.Bold, 699 ) 700 ) 701 702 painter.drawText( 703 portrait_rect, 704 Qt.AlignCenter, 705 "SHIRE\nPRESENCE", 706 ) 707 708 painter.restore() 709 710 painter.setPen( 711 QPen( 712 QColor(190, 118, 255, 230), 713 3, 714 ) 715 ) 716 painter.setBrush(Qt.NoBrush) 717 painter.drawEllipse(portrait_rect) 718 719 lower_gradient = QLinearGradient( 720 0, 721 h * 0.62, 722 0, 723 h, 724 ) 725 lower_gradient.setColorAt( 726 0.0, 727 QColor(0, 0, 0, 0), 728 ) 729 lower_gradient.setColorAt( 730 1.0, 731 QColor(1, 1, 8, 220), 732 ) 733 734 painter.setPen(Qt.NoPen) 735 painter.setBrush( 736 QBrush(lower_gradient) 737 ) 738 painter.drawRect( 739 QRectF( 740 0, 741 h * 0.60, 742 w, 743 h * 0.40, 744 ) 745 ) 746 747 painter.setPen( 748 QColor(PURPLE_LIGHT) 749 ) 750 painter.setFont( 751 QFont( 752 "DejaVu Sans Mono", 753 22, 754 QFont.Bold, 755 ) 756 ) 757 painter.drawText( 758 QRectF( 759 0, 760 h - 72, 761 w, 762 30, 763 ), 764 Qt.AlignCenter, 765 "SHIRE", 766 ) 767 768 painter.setPen(QColor(GREEN)) 769 painter.setFont( 770 QFont( 771 "DejaVu Sans Mono", 772 10, 773 QFont.Bold, 774 ) 775 ) 776 painter.drawText( 777 QRectF( 778 0, 779 h - 40, 780 w, 781 22, 782 ), 783 Qt.AlignCenter, 784 "PRESENCE ONLINE • APPROVAL GATE ACTIVE", 785 ) 786 787 788 class VoiceOrb(QWidget): 789 """Visual mirror of the approved manual voice state.""" 790 791 def __init__(self): 792 super().__init__() 793 794 self.setMinimumHeight(104) 795 self.setMaximumHeight(116) 796 self.setSizePolicy( 797 QSizePolicy.Expanding, 798 QSizePolicy.Fixed, 799 ) 800 801 self.timer = QTimer(self) 802 self.timer.timeout.connect(self.update) 803 self.timer.start(300) 804 805 def paintEvent(self, event): 806 del event 807 808 snapshot = voice_privacy_foundation.snapshot() 809 state = str(snapshot.state).upper() 810 811 listening = state == "LISTENING" 812 processing = state == "PROCESSING" 813 ready = state == "READY" 814 error = state == "ERROR" 815 816 painter = QPainter(self) 817 painter.setRenderHint(QPainter.Antialiasing, True) 818 819 w = self.width() 820 h = self.height() 821 cx = w / 2 822 cy = 42 823 824 if error: 825 orb_color = QColor("#ff5c70") 826 elif listening: 827 orb_color = QColor(GREEN) 828 elif processing: 829 orb_color = QColor(CYAN) 830 elif ready: 831 orb_color = QColor(PURPLE_LIGHT) 832 else: 833 orb_color = QColor(PURPLE) 834 835 for index, width_ratio in enumerate( 836 (0.42, 0.32, 0.23) 837 ): 838 rect_width = w * width_ratio 839 840 painter.setPen( 841 QPen( 842 QColor( 843 orb_color.red(), 844 orb_color.green(), 845 orb_color.blue(), 846 105 - index * 20, 847 ), 848 1.4, 849 ) 850 ) 851 852 painter.setBrush(Qt.NoBrush) 853 painter.drawEllipse( 854 QRectF( 855 cx - rect_width / 2, 856 cy - 25 - index * 3, 857 rect_width, 858 50 + index * 6, 859 ) 860 ) 861 862 painter.setPen(QPen(orb_color, 2)) 863 painter.setBrush(QColor(18, 7, 42, 240)) 864 painter.drawEllipse( 865 QRectF(cx - 31, cy - 31, 62, 62) 866 ) 867 868 painter.setPen(QPen(orb_color, 4)) 869 painter.setBrush(Qt.NoBrush) 870 painter.drawRoundedRect( 871 QRectF(cx - 8, cy - 16, 16, 29), 872 8, 873 8, 874 ) 875 painter.drawLine( 876 int(cx), 877 int(cy + 14), 878 int(cx), 879 int(cy + 24), 880 ) 881 painter.drawLine( 882 int(cx - 9), 883 int(cy + 24), 884 int(cx + 9), 885 int(cy + 24), 886 ) 887 888 if listening: 889 state_text = "LISTENING" 890 detail_text = "Speech is filling the top prompt" 891 892 elif processing: 893 state_text = "PROCESSING" 894 detail_text = "Finalising local transcript" 895 896 elif ready: 897 state_text = "VOICE READY" 898 detail_text = "Use MIC in the top prompt bar" 899 900 elif error: 901 state_text = "VOICE ERROR" 902 detail_text = snapshot.error or "Check voice configuration" 903 904 else: 905 state_text = "MICROPHONE OFF" 906 detail_text = "Voice capture is unavailable" 907 908 painter.setPen(orb_color) 909 painter.setFont( 910 QFont("DejaVu Sans Mono", 11, QFont.Bold) 911 ) 912 painter.drawText( 913 QRectF(0, 76, w, 18), 914 Qt.AlignCenter, 915 state_text, 916 ) 917 918 painter.setPen(QColor(MUTED)) 919 painter.setFont( 920 QFont("DejaVu Sans Mono", 8, QFont.Bold) 921 ) 922 painter.drawText( 923 QRectF(0, 94, w, 16), 924 Qt.AlignCenter, 925 detail_text, 926 ) 927 928 929 class LaptopDashboard(QWidget): 930 """SHIRE AIOS face-first Presence Console.""" 931 932 def __init__(self, stack): 933 super().__init__() 934 935 self.stack = stack 936 self.station_drawer = None 937 self.build_ui() 938 939 def _open_station(self, target_index): 940 self.stack.setCurrentIndex(target_index) 941 942 if self.station_drawer is not None: 943 self.station_drawer.setVisible(False) 944 945 def _make_nav_button( 946 self, 947 icon, 948 label, 949 target_index, 950 compact=True, 951 ): 952 button = LaptopRuneButton( 953 icon, 954 label, 955 "", 956 target_index, 957 self.stack, 958 compact=compact, 959 ) 960 961 button.clicked.disconnect() 962 button.clicked.connect( 963 lambda checked=False, index=target_index: 964 self._open_station(index) 965 ) 966 967 return button 968 969 def _toggle_stations(self): 970 visible = not self.station_drawer.isVisible() 971 self.station_drawer.setVisible(visible) 972 973 def _build_greeting_card(self): 974 card = PresenceCard("SHIRE PRESENCE", PURPLE_LIGHT) 975 976 greeting = QLabel(_greeting()) 977 greeting.setStyleSheet( 978 f"color:{PURPLE_LIGHT}; " 979 "font-size:23px; font-weight:900;" 980 ) 981 982 message = QLabel( 983 "The Workshop is ready.\n" 984 "No proposal is active.\n\n" 985 "How can I help you today?" 986 ) 987 988 message.setWordWrap(True) 989 message.setStyleSheet( 990 "color:#d7c8f4; " 991 "font-size:14px; " 992 "font-weight:700; " 993 "line-height:1.35;" 994 ) 995 996 card.column.addWidget(greeting) 997 card.column.addWidget(message) 998 card.column.addStretch(1) 999 1000 return card 1001 1002 def _build_status_card(self): 1003 card = PresenceCard("SYSTEM STATUS", CYAN) 1004 1005 rows = ( 1006 ("CORE", "ONLINE", GREEN), 1007 ("SESSION", "LOCAL X11", CYAN), 1008 ("MICROPHONE", "CLOSED", PURPLE_LIGHT), 1009 ("AUTHORITY", "RAY APPROVAL", GREEN), 1010 ) 1011 1012 for title, value, accent in rows: 1013 row = QHBoxLayout() 1014 1015 left = QLabel(title) 1016 left.setStyleSheet( 1017 "color:#8e82aa; " 1018 "font-size:9px; " 1019 "font-weight:900;" 1020 ) 1021 1022 right = QLabel(value) 1023 right.setAlignment(Qt.AlignRight) 1024 right.setStyleSheet( 1025 f"color:{accent}; " 1026 "font-size:11px; " 1027 "font-weight:900;" 1028 ) 1029 1030 row.addWidget(left) 1031 row.addStretch(1) 1032 row.addWidget(right) 1033 1034 card.column.addLayout(row) 1035 1036 return card 1037 1038 def _build_thought_card(self): 1039 card = PresenceCard("CURRENT THOUGHT", PURPLE_LIGHT) 1040 1041 concept = QLabel("◌") 1042 concept.setAlignment(Qt.AlignCenter) 1043 concept.setMinimumHeight(88) 1044 concept.setStyleSheet( 1045 "color:#b66cff; " 1046 "font-size:64px; " 1047 "font-weight:500;" 1048 ) 1049 1050 title = QLabel("NO ACTIVE DESIGN PREVIEW") 1051 title.setAlignment(Qt.AlignCenter) 1052 title.setStyleSheet( 1053 f"color:{PURPLE_LIGHT}; " 1054 "font-size:12px; " 1055 "font-weight:900;" 1056 ) 1057 1058 note = QLabel( 1059 "Approved STL concepts and Workshop ideas " 1060 "will appear here." 1061 ) 1062 1063 note.setWordWrap(True) 1064 note.setAlignment(Qt.AlignCenter) 1065 note.setStyleSheet( 1066 "color:#9e91ba; " 1067 "font-size:10px; " 1068 "font-weight:700;" 1069 ) 1070 1071 card.column.addWidget(concept) 1072 card.column.addWidget(title) 1073 card.column.addWidget(note) 1074 card.column.addStretch(1) 1075 1076 return card 1077 1078 def _build_next_card(self): 1079 card = PresenceCard("NEXT UP", CYAN) 1080 1081 items = ( 1082 "Open Workshop", 1083 "Review Sentinel", 1084 "Continue Education", 1085 ) 1086 1087 for item in items: 1088 label = QLabel(f"› {item}") 1089 label.setStyleSheet( 1090 "color:#c8afff; " 1091 "font-size:12px; " 1092 "font-weight:800; " 1093 "padding:5px 0;" 1094 ) 1095 card.column.addWidget(label) 1096 1097 card.column.addStretch(1) 1098 1099 return card 1100 1101 def _build_station_drawer(self): 1102 drawer = QFrame() 1103 drawer.setObjectName("stationDrawer") 1104 drawer.setVisible(False) 1105 1106 drawer.setStyleSheet(""" 1107 QFrame#stationDrawer { 1108 background: rgba(5, 4, 18, 240); 1109 border: 1px solid rgba(155, 77, 255, 180); 1110 border-radius: 16px; 1111 } 1112 """) 1113 1114 grid = QGridLayout(drawer) 1115 grid.setContentsMargins(12, 12, 12, 12) 1116 grid.setHorizontalSpacing(10) 1117 grid.setVerticalSpacing(10) 1118 1119 stations = ( 1120 ("▤", "MEMORY", 3), 1121 ("◇", "ATLAS", 4), 1122 ("✦", "EDUCATION", 11), 1123 ("♟", "AGENTS", 7), 1124 ("☷", "TASKS", 8), 1125 ("⚙", "SYSTEM", 6), 1126 ("⌁", "SETTINGS", 9), 1127 ("⌨", "COMMAND", 1), 1128 ) 1129 1130 for index, (icon, label, target) in enumerate(stations): 1131 button = self._make_nav_button( 1132 icon, 1133 label, 1134 target, 1135 compact=True, 1136 ) 1137 1138 grid.addWidget( 1139 button, 1140 index // 4, 1141 index % 4, 1142 ) 1143 1144 return drawer 1145 1146 def build_ui(self): 1147 self.setObjectName("presenceConsole") 1148 1149 self.setStyleSheet(""" 1150 QWidget#presenceConsole { 1151 background: #020208; 1152 color: #f4efff; 1153 font-family: "DejaVu Sans Mono", monospace; 1154 } 1155 QLabel { 1156 background: transparent; 1157 } 1158 """) 1159 1160 root = QVBoxLayout(self) 1161 root.setContentsMargins(26, 20, 26, 16) 1162 root.setSpacing(12) 1163 1164 header = QHBoxLayout() 1165 header.setSpacing(18) 1166 1167 brand_column = QVBoxLayout() 1168 brand_column.setSpacing(0) 1169 1170 brand = QLabel("SHIRE AIOS") 1171 brand.setStyleSheet( 1172 f"color:{PURPLE}; " 1173 "font-size:27px; " 1174 "font-weight:900; " 1175 "letter-spacing:2px;" 1176 ) 1177 1178 core_state = QLabel("CORE ONLINE") 1179 core_state.setStyleSheet( 1180 f"color:{GREEN}; " 1181 "font-size:10px; " 1182 "font-weight:900;" 1183 ) 1184 1185 brand_column.addWidget(brand) 1186 brand_column.addWidget(core_state) 1187 1188 header.addLayout(brand_column) 1189 header.addStretch(1) 1190 header.addWidget(VoicePrivacyBanner()) 1191 1192 root.addLayout(header) 1193 1194 stage = QHBoxLayout() 1195 stage.setSpacing(14) 1196 1197 left_column = QVBoxLayout() 1198 left_column.setSpacing(12) 1199 left_column.addWidget(self._build_greeting_card(), 3) 1200 left_column.addWidget(self._build_status_card(), 2) 1201 1202 center_column = QVBoxLayout() 1203 center_column.setSpacing(2) 1204 1205 center_title = QLabel("SHIRE PRESENCE") 1206 center_title.setAlignment(Qt.AlignCenter) 1207 center_title.setStyleSheet( 1208 f"color:{PURPLE_LIGHT}; " 1209 "font-size:12px; " 1210 "font-weight:900; " 1211 "letter-spacing:2px;" 1212 ) 1213 1214 center_column.addWidget(center_title) 1215 center_column.addWidget(ShireCenterCore(), 1) 1216 center_column.addWidget(VoiceOrb()) 1217 1218 right_column = QVBoxLayout() 1219 right_column.setSpacing(12) 1220 right_column.addWidget(self._build_thought_card(), 3) 1221 right_column.addWidget(self._build_next_card(), 2) 1222 1223 stage.addLayout(left_column, 5) 1224 stage.addLayout(center_column, 8) 1225 stage.addLayout(right_column, 5) 1226 1227 root.addLayout(stage, 1) 1228 1229 dock = QHBoxLayout() 1230 dock.setSpacing(10) 1231 1232 dock.addWidget( 1233 self._make_nav_button("●", "SHIRE", 1) 1234 ) 1235 1236 dock.addWidget( 1237 self._make_nav_button("⚒", "FORGE", 2) 1238 ) 1239 1240 dock.addWidget( 1241 self._make_nav_button("♜", "SENTINEL", 10) 1242 ) 1243 1244 stations_button = QPushButton("☰ STATIONS") 1245 stations_button.setCursor(Qt.PointingHandCursor) 1246 stations_button.setMinimumHeight(48) 1247 1248 stations_button.setStyleSheet(""" 1249 QPushButton { 1250 background: rgba(8, 5, 25, 225); 1251 color: #00f58a; 1252 border: 1px solid rgba(0, 245, 138, 185); 1253 border-radius: 13px; 1254 font-family: "DejaVu Sans Mono", monospace; 1255 font-size: 13px; 1256 font-weight: 900; 1257 padding: 8px 12px; 1258 } 1259 QPushButton:hover { 1260 background: rgba(0, 55, 39, 230); 1261 color: #ffffff; 1262 border: 1px solid #00f58a; 1263 } 1264 QPushButton:pressed { 1265 background: rgba(155, 77, 255, 75); 1266 } 1267 """) 1268 1269 stations_button.clicked.connect( 1270 self._toggle_stations 1271 ) 1272 1273 dock.addWidget(stations_button) 1274 1275 root.addLayout(dock) 1276 1277 self.station_drawer = self._build_station_drawer() 1278 root.addWidget(self.station_drawer) 1279 1280 hint = QLabel( 1281 "VOICE PREVIEW READY • " 1282 "Use MIC in the top prompt bar • " 1283 "Audio is processed locally and discarded" 1284 ) 1285 1286 hint.setAlignment(Qt.AlignCenter) 1287 hint.setStyleSheet( 1288 "color:#8f82a8; " 1289 "font-size:9px; " 1290 "font-weight:800; " 1291 "padding:1px;" 1292 ) 1293 1294 root.addWidget(hint) 1295 1296 def paintEvent(self, event): 1297 del event 1298 1299 painter = QPainter(self) 1300 painter.setRenderHint(QPainter.Antialiasing, True) 1301 1302 gradient = QLinearGradient( 1303 0, 1304 0, 1305 self.width(), 1306 self.height(), 1307 ) 1308 1309 gradient.setColorAt(0.0, QColor(2, 2, 9)) 1310 gradient.setColorAt(0.48, QColor(7, 3, 18)) 1311 gradient.setColorAt(1.0, QColor(2, 2, 8)) 1312 1313 painter.fillRect( 1314 self.rect(), 1315 QBrush(gradient), 1316 ) 1317 1318 painter.setPen( 1319 QPen(QColor(113, 50, 190, 22), 1) 1320 ) 1321 1322 spacing = 54 1323 1324 for x in range(0, self.width(), spacing): 1325 painter.drawLine(x, 0, x, self.height()) 1326 1327 for y in range(0, self.height(), spacing): 1328 painter.drawLine(0, y, self.width(), y) 1329 1330 center_glow = QRadialGradient( 1331 self.width() / 2, 1332 self.height() / 2, 1333 self.width() * 0.42, 1334 ) 1335 1336 center_glow.setColorAt( 1337 0.0, 1338 QColor(106, 25, 180, 50), 1339 ) 1340 1341 center_glow.setColorAt( 1342 1.0, 1343 QColor(0, 0, 0, 0), 1344 ) 1345 1346 painter.setPen(Qt.NoPen) 1347 painter.setBrush(QBrush(center_glow)) 1348 painter.drawEllipse( 1349 QRectF( 1350 self.width() * 0.14, 1351 -self.height() * 0.10, 1352 self.width() * 0.72, 1353 self.height() * 1.20, 1354 ) 1355 )