diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 17:48:21 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 17:48:21 +0200 |
| commit | 8cb5cc303f1c83d2c63d5cb972a7faee294e7c23 (patch) | |
| tree | cc8e5d23a680f551a9e8b60ac0be0fbc87fd3ea5 | |
| parent | d2459356d648825932137e9c7c5c018233c565a1 (diff) | |
| download | llamachat-8cb5cc303f1c83d2c63d5cb972a7faee294e7c23.tar.gz llamachat-8cb5cc303f1c83d2c63d5cb972a7faee294e7c23.zip | |
feat: move model and prompt pickers to the bottom bar, add button icons
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | llamachat/ui.py | 186 |
2 files changed, 145 insertions, 49 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index cc13970..a79898b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,9 +69,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 moment once used. The text comes from the source markdown, since Qt emits one `<pre>` per line and the block cannot be recovered from the rendered HTML. +- Icons on the chat buttons (New, Attach, Clear files, Stop, Send), drawn + at runtime so there is still no icon file to install. Send is now a square + button beside the entry box with its label under the icon, and New/Attach + sit at the right end of the picker row. ### Fixed +- The model and prompt pickers lived in the top bar, so a narrow window + pushed them into the mode toggle and the context meter. They now live in + the bottom bar, and the top bar keeps only the mode toggle and the + read-outs. - A long line in a code block widened the whole transcript and left it scrolling sideways. Qt's markdown stylesheet wraps `p` and `li` but not `pre`, and it is dropped when only the body fragment is kept, so the block diff --git a/llamachat/ui.py b/llamachat/ui.py index 210c515..c22b3db 100644 --- a/llamachat/ui.py +++ b/llamachat/ui.py @@ -22,10 +22,11 @@ import re from pathlib import Path from PySide6.QtCore import ( - QObject, QRect, QSettings, Qt, QThread, QTimer, Signal, Slot, + QObject, QPointF, QRect, QRectF, QSettings, Qt, QThread, QTimer, Signal, Slot, ) from PySide6.QtGui import ( - QAction, QColor, QDesktopServices, QKeySequence, QPainter, QTextDocument, + QAction, QColor, QDesktopServices, QIcon, QKeySequence, QPainter, + QPainterPath, QPen, QPixmap, QPolygonF, QTextDocument, ) from PySide6.QtWidgets import ( QApplication, QButtonGroup, QComboBox, QDialog, QDialogButtonBox, @@ -53,6 +54,67 @@ SEARCH_SCHEME = "x-llamachat-search:" COPY_SCHEME = "x-llamachat-copy:" +# The buttons get their glyphs drawn at runtime, so there is no icon file +# to install. All the marks share the brand blue, which also makes each +# button an unambiguous control rather than a bare text label. +def _button_icon(kind: str) -> QIcon: + """A small monochrome glyph for one of the chat buttons.""" + pixmap = QPixmap(24, 24) + pixmap.fill(QColor(0, 0, 0, 0)) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.Antialiasing) + colour = QColor("#3F5EFB") + pen = QPen(colour, 2.2) + pen.setCapStyle(Qt.RoundCap) + pen.setJoinStyle(Qt.RoundJoin) + painter.setPen(pen) + + if kind == "new": + painter.drawLine(12, 4, 12, 20) + painter.drawLine(4, 12, 20, 12) + elif kind == "attach": + # A paperclip: one stroke spiralling into the classic double loop. + path = QPainterPath() + path.moveTo(13.5, 16.5) + path.lineTo(13.5, 8.0) + path.cubicTo(13.5, 5.5, 10.5, 5.5, 10.5, 8.0) + path.lineTo(10.5, 15.5) + path.cubicTo(10.5, 18.0, 15.0, 18.0, 15.0, 15.0) + path.lineTo(15.0, 7.0) + path.cubicTo(15.0, 3.5, 7.5, 3.5, 7.5, 7.0) + path.lineTo(7.5, 16.5) + path.cubicTo(7.5, 20.0, 14.0, 20.0, 14.0, 16.5) + painter.drawPath(path) + elif kind == "clear": + # A bin with a lid, for removing attached files. + painter.drawLine(7, 6.5, 17, 6.5) + painter.drawLine(10, 4.5, 14, 4.5) + path = QPainterPath() + path.moveTo(8, 8) + path.lineTo(16, 8) + path.lineTo(14.5, 19.5) + path.lineTo(9.5, 19.5) + path.closeSubpath() + painter.drawPath(path) + elif kind == "stop": + painter.setBrush(colour) + painter.drawRoundedRect(QRectF(6.5, 6.5, 11, 11), 2, 2) + elif kind == "send": + painter.setBrush(colour) + painter.drawPolygon( + QPolygonF( + [ + QPointF(4.0, 12.0), + QPointF(20.0, 5.0), + QPointF(11.5, 13.0), + QPointF(10.0, 20.0), + ] + ) + ) + painter.end() + return QIcon(pixmap) + + class ContextMeter(QWidget): """A bar showing how much of the model's context the next request uses. @@ -501,7 +563,9 @@ class ChatWindow(QMainWindow): central = QWidget() outer = QVBoxLayout(central) - # Top bar: model, mode, search. + # Top bar: the mode toggle plus the context/cost read-outs. The model + # and prompt pickers live in the bottom bar, so a narrow window cannot + # push this row's items into one another. top = QHBoxLayout() self.sidebar_button = QPushButton("☰") self.sidebar_button.setCheckable(True) @@ -511,26 +575,6 @@ class ChatWindow(QMainWindow): self.sidebar_button.toggled.connect(self.set_sidebar_visible) top.addWidget(self.sidebar_button) - self.model_box = QComboBox() - self.model_box.setMinimumWidth(220) - self.model_box.currentTextChanged.connect(self._on_model_changed) - top.addWidget(QLabel("Model:")) - top.addWidget(self.model_box) - - self.model_settings_button = QToolButton() - self.model_settings_button.setText("⚙") - self.model_settings_button.setToolTip( - "Context size, vision and prices for the selected model" - ) - self.model_settings_button.clicked.connect(self.edit_model_settings) - top.addWidget(self.model_settings_button) - - self.reload_button = QPushButton("⟳") - self.reload_button.setToolTip("Reload model list from the router") - self.reload_button.setFixedWidth(32) - self.reload_button.clicked.connect(self.refresh_models) - top.addWidget(self.reload_button) - top.addSpacing(16) self.oneshot_radio = QRadioButton("One-shot") self.chat_radio = QRadioButton("Chat") @@ -542,19 +586,6 @@ class ChatWindow(QMainWindow): top.addWidget(self.oneshot_radio) top.addWidget(self.chat_radio) - top.addSpacing(16) - top.addWidget(QLabel("Prompt:")) - self.prompt_box = QComboBox() - self.prompt_box.setMinimumWidth(150) - self.prompt_box.currentIndexChanged.connect(self._on_prompt_changed) - top.addWidget(self.prompt_box) - - self.prompt_button = QPushButton("…") - self.prompt_button.setToolTip("View and edit system prompts") - self.prompt_button.setFixedWidth(32) - self.prompt_button.clicked.connect(self.edit_prompts) - top.addWidget(self.prompt_button) - top.addStretch() self.meter = ContextMeter() top.addWidget(self.meter) @@ -619,39 +650,96 @@ class ChatWindow(QMainWindow): self.status_row.hide() right_layout.addWidget(self.status_row) + compose = QHBoxLayout() + compose.setSpacing(6) self.input = QPlainTextEdit() self.input.setPlaceholderText( "Type a message. Ctrl+Enter sends, Esc hides the window." ) self.input.setMaximumHeight(140) self.input.textChanged.connect(self.update_meter) - right_layout.addWidget(self.input) + compose.addWidget(self.input, 1) + + # A square send control beside the entry box, like a chat app. + self.send_button = QToolButton() + self.send_button.setIcon(_button_icon("send")) + self.send_button.setText("Send") + self.send_button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + self.send_button.setFixedSize(44, 44) + self.send_button.setToolTip("Send (Ctrl+Enter)") + self.send_button.clicked.connect(self.send) + compose.addWidget(self.send_button, 0, Qt.AlignBottom) + right_layout.addLayout(compose) + # Bottom bar: the model and prompt pickers on the left, the + # conversation actions on the right, so a narrow window cannot push + # the top row together. buttons = QHBoxLayout() - self.new_button = QPushButton("New") - self.new_button.setToolTip("Start a fresh conversation (Ctrl+N)") - self.new_button.clicked.connect(self.new_session) - buttons.addWidget(self.new_button) + buttons.setSpacing(4) - self.attach_button = QPushButton("Attach…") - self.attach_button.clicked.connect(self._on_attach_clicked) - buttons.addWidget(self.attach_button) + self.model_box = QComboBox() + self.model_box.setToolTip("Model to chat with") + self.model_box.setMinimumContentsLength(12) + self.model_box.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon + ) + self.model_box.currentTextChanged.connect(self._on_model_changed) + buttons.addWidget(self.model_box) + + self.model_settings_button = QToolButton() + self.model_settings_button.setText("⚙") + self.model_settings_button.setToolTip( + "Context size, vision and prices for the selected model" + ) + self.model_settings_button.clicked.connect(self.edit_model_settings) + buttons.addWidget(self.model_settings_button) + + self.reload_button = QPushButton("⟳") + self.reload_button.setToolTip("Reload model list from the router") + self.reload_button.setFixedWidth(32) + self.reload_button.clicked.connect(self.refresh_models) + buttons.addWidget(self.reload_button) + + buttons.addSpacing(8) + self.prompt_box = QComboBox() + self.prompt_box.setToolTip("System prompt for this chat") + self.prompt_box.setMinimumContentsLength(10) + self.prompt_box.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon + ) + self.prompt_box.currentIndexChanged.connect(self._on_prompt_changed) + buttons.addWidget(self.prompt_box) + self.prompt_button = QPushButton("…") + self.prompt_button.setToolTip("View and edit system prompts") + self.prompt_button.setFixedWidth(32) + self.prompt_button.clicked.connect(self.edit_prompts) + buttons.addWidget(self.prompt_button) + + buttons.addStretch() self.clear_attach_button = QPushButton("Clear files") + self.clear_attach_button.setIcon(_button_icon("clear")) self.clear_attach_button.clicked.connect(self.clear_attachments) self.clear_attach_button.hide() buttons.addWidget(self.clear_attach_button) - buttons.addStretch() self.stop_button = QPushButton("Stop") + self.stop_button.setIcon(_button_icon("stop")) self.stop_button.clicked.connect(self.stop_stream) self.stop_button.hide() buttons.addWidget(self.stop_button) - self.send_button = QPushButton("Send") - self.send_button.setDefault(True) - self.send_button.clicked.connect(self.send) - buttons.addWidget(self.send_button) + self.new_button = QPushButton("New") + self.new_button.setIcon(_button_icon("new")) + self.new_button.setToolTip("Start a fresh conversation (Ctrl+N)") + self.new_button.clicked.connect(self.new_session) + buttons.addWidget(self.new_button) + + self.attach_button = QPushButton("Attach…") + self.attach_button.setIcon(_button_icon("attach")) + self.attach_button.clicked.connect(self._on_attach_clicked) + buttons.addWidget(self.attach_button) + right_layout.addLayout(buttons) splitter.addWidget(right) |
