diff --git a/gui.py b/gui.py index fb59115..48bc345 100644 --- a/gui.py +++ b/gui.py @@ -45,23 +45,48 @@ GPU_STATUS_INTERVAL_MS = 1000 # --- Visual theme ------------------------------------------------------ # An explicit dark palette rather than relying on the OS theme, so the app # looks the same everywhere instead of "Qt follows Windows' dark app theme". -BG_APP = "#0b0d12" -BG_PANEL = "#12151c" -BG_ELEVATED = "#181c25" -BORDER = "#262b36" -BORDER_STRONG = "#333949" -TEXT_PRIMARY = "#e6e8ee" -TEXT_SECONDARY = "#8b92a5" -ACCENT = "#6366f1" -ACCENT_HOVER = "#7b7ef4" -ACCENT_PRESSED = "#4f52d6" +# Brand palette: jet black / blue slate / silver / white / coral glow. +JET_BLACK = "#2d3142" +BLUE_SLATE = "#4f5d75" +SILVER = "#bfc0c0" +WHITE = "#ffffff" +CORAL_GLOW = "#ef8354" + +BG_APP = "#242735" # jet black, darkened further for the outer chrome +BG_PANEL = JET_BLACK +BG_ELEVATED = BLUE_SLATE +BORDER = "rgba(191, 192, 192, 0.35)" # silver, translucent +BORDER_STRONG = "rgba(191, 192, 192, 0.55)" +TEXT_PRIMARY = WHITE +TEXT_SECONDARY = SILVER +ACCENT = CORAL_GLOW +ACCENT_HOVER = "#ff9768" +ACCENT_PRESSED = "#cb6f47" + +# Green -> yellow -> red ramp used to color the GPU/VRAM load readout. +LOAD_LOW = (0x22, 0xc5, 0x5e) +LOAD_MID = (0xea, 0xb3, 0x08) +LOAD_HIGH = (0xef, 0x44, 0x44) + + +def _load_color(pct: float) -> str: + """Hex color for a 0-100 load percentage, interpolated low->mid->high.""" + pct = max(0.0, min(100.0, pct)) + if pct <= 50: + c1, c2, t = LOAD_LOW, LOAD_MID, pct / 50.0 + else: + c1, c2, t = LOAD_MID, LOAD_HIGH, (pct - 50) / 50.0 + r = round(c1[0] + (c2[0] - c1[0]) * t) + g = round(c1[1] + (c2[1] - c1[1]) * t) + b = round(c1[2] + (c2[2] - c1[2]) * t) + return f"#{r:02x}{g:02x}{b:02x}" # (accent, background) per chat role, used for the accent stripe and tinted # card background of each message. ROLE_STYLES = { - "user": ("#60a5fa", "#141b2b"), - "assistant": ("#34d399", "#0f211c"), - "system": ("#9aa1b4", "#161922"), + "user": (CORAL_GLOW, "#3a2c26"), + "assistant": (SILVER, "#2f3342"), + "system": (BLUE_SLATE, "#262a36"), } _STYLESHEET = f""" @@ -75,25 +100,15 @@ QWidget {{ font-size: 10.5pt; }} -#header {{ - border-bottom: 1px solid {BORDER}; -}} - #titleLabel {{ font-size: 15pt; font-weight: 600; color: {TEXT_PRIMARY}; }} -#subtitleLabel {{ - font-size: 9pt; - color: {TEXT_SECONDARY}; -}} - #badge {{ background-color: {BG_ELEVATED}; border: 1px solid {BORDER}; - border-radius: 11px; padding: 4px 12px; color: {TEXT_SECONDARY}; font-size: 9pt; @@ -102,14 +117,12 @@ QWidget {{ QTextBrowser#chatView {{ background-color: {BG_PANEL}; border: 1px solid {BORDER}; - border-radius: 14px; padding: 10px; }} QLineEdit#messageInput {{ background-color: {BG_ELEVATED}; border: 1px solid {BORDER}; - border-radius: 19px; padding: 0 16px; color: {TEXT_PRIMARY}; selection-background-color: {ACCENT}; @@ -125,9 +138,8 @@ QLineEdit#messageInput:disabled {{ QPushButton#sendButton {{ background-color: {ACCENT}; - color: #ffffff; + color: {WHITE}; border: none; - border-radius: 19px; padding: 0 22px; font-weight: 600; }} @@ -165,7 +177,6 @@ QScrollBar:vertical {{ QScrollBar::handle:vertical {{ background: {BORDER_STRONG}; - border-radius: 5px; min-height: 24px; }} @@ -205,7 +216,7 @@ def _dark_palette() -> QPalette: palette.setColor(QPalette.ColorRole.Button, QColor(BG_ELEVATED)) palette.setColor(QPalette.ColorRole.ButtonText, QColor(TEXT_PRIMARY)) palette.setColor(QPalette.ColorRole.Highlight, QColor(ACCENT)) - palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff")) + palette.setColor(QPalette.ColorRole.HighlightedText, QColor(WHITE)) return palette @@ -220,14 +231,14 @@ def _make_app_icon() -> QIcon: painter.setRenderHint(QPainter.RenderHint.Antialiasing) gradient = QLinearGradient(0, 0, size, size) - gradient.setColorAt(0.0, QColor(ACCENT_HOVER)) - gradient.setColorAt(1.0, QColor(ACCENT_PRESSED)) + gradient.setColorAt(0.0, QColor(JET_BLACK)) + gradient.setColorAt(1.0, QColor(CORAL_GLOW)) path = QPainterPath() - path.addRoundedRect(2, 2, size - 4, size - 4, 16, 16) + path.addRect(2, 2, size - 4, size - 4) painter.fillPath(path, gradient) - painter.setPen(QColor("#ffffff")) + painter.setPen(QColor(WHITE)) painter.setFont(QFont("Segoe UI", 28, QFont.Weight.Bold)) painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "D") painter.end() @@ -273,8 +284,10 @@ _CODE_FG = "#f8f8f2" _CODE_BLOCK_FORMAT = QTextBlockFormat() _CODE_BLOCK_FORMAT.setBackground(QColor(_CODE_BG)) _CODE_BLOCK_FORMAT.setLeftMargin(10) -_CODE_BLOCK_FORMAT.setTopMargin(1) -_CODE_BLOCK_FORMAT.setBottomMargin(1) +# Each source line is its own QTextBlock (see _append_markdown_block); Qt's +# block background doesn't paint into top/bottom margins, so any non-zero +# margin here left a sliver of the message card's own background showing +# between every line, reading as horizontal gaps through the code block. _CODE_CHAR_FORMAT = QTextCharFormat() _CODE_CHAR_FORMAT.setFontFamilies(["Consolas", "Courier New"]) @@ -418,10 +431,7 @@ class ChatWindow(QMainWindow): title_box.setSpacing(0) title_label = QLabel("Dev Assist") title_label.setObjectName("titleLabel") - subtitle_label = QLabel("Local LLM · tool-augmented") - subtitle_label.setObjectName("subtitleLabel") title_box.addWidget(title_label) - title_box.addWidget(subtitle_label) header_layout.addLayout(title_box) header_layout.addStretch(1) @@ -504,7 +514,13 @@ class ChatWindow(QMainWindow): used_gb = mem.used / (1024**3) total_gb = mem.total / (1024**3) - self.gpu_status_label.setText(f"GPU {util.gpu}% · VRAM {used_gb:.1f}/{total_gb:.1f} GB") + vram_pct = (mem.used / mem.total * 100) if mem.total else 0.0 + gpu_color = _load_color(util.gpu) + vram_color = _load_color(vram_pct) + self.gpu_status_label.setText( + f'GPU {util.gpu}% · ' + f'VRAM {used_gb:.1f}/{total_gb:.1f} GB' + ) def _set_input_enabled(self, enabled: bool) -> None: self.input_box.setEnabled(enabled)