From b45a757982489f32f74e477774a223fdb5bae739 Mon Sep 17 00:00:00 2001 From: sorrow404null Date: Fri, 18 Sep 2026 15:14:20 +0800 Subject: [PATCH] fix(ui): repair chat and model settings rendering --- ui/views/main_window.py | 454 +++++++++++++++++++++++++++++++++++----- ui/web/style.css | 14 +- 2 files changed, 406 insertions(+), 62 deletions(-) diff --git a/ui/views/main_window.py b/ui/views/main_window.py index 9365e9e..bbd70c7 100644 --- a/ui/views/main_window.py +++ b/ui/views/main_window.py @@ -1,4 +1,5 @@ import sys +import copy from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtWebEngineWidgets import QWebEngineView # 引入我们刚才写的线程工作类 @@ -422,20 +423,27 @@ class AttachmentPreviewOverlay(QtWidgets.QWidget): class SettingsWindow(QtWidgets.QWidget): - """设置窗口:虚化遮罩居中自适应卡片""" + """设置窗口:可覆盖原生 WebView2 的顶层遮罩。""" close_requested = QtCore.pyqtSignal() + config_changed = QtCore.pyqtSignal(dict) + model_selected = QtCore.pyqtSignal(str, str) def __init__(self, parent=None, config_data=None): super().__init__(parent) - self.config_data = config_data or {} + self._main = parent.window() if parent is not None else None + self.config_data = copy.deepcopy(config_data) if isinstance(config_data, dict) else {} + self._closed = False + self._closing = False self._setup_ui() self._setup_animation() # ------------------------------------------------------------------ def _setup_ui(self): # 全屏透明遮罩(只负责背景展示,不负责关闭) + self.setWindowFlags( + QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.Tool) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) - self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose, True) self.overlay = QtWidgets.QWidget(self) self.overlay.setStyleSheet("background-color: rgba(0, 0, 0, 0.45);") @@ -465,7 +473,8 @@ class SettingsWindow(QtWidgets.QWidget): body_layout.addWidget(self.sidebar) self.content_stack = QtWidgets.QStackedWidget() - self.content_stack.addWidget(self._build_provider_section()) + self.provider_page = self._build_provider_section() + self.content_stack.addWidget(self.provider_page) self.content_stack.addWidget(self._build_placeholder_section()) body_layout.addWidget(self.content_stack, 1) @@ -473,13 +482,24 @@ class SettingsWindow(QtWidgets.QWidget): self.resize_to_parent() self.content_stack.setCurrentWidget(self.content_stack.widget(0)) + if self._main is not None: + self._main.installEventFilter(self) # ------------------------------------------------------------------ def eventFilter(self, obj, event): - """🌟 核心:拦截 main_card 上的鼠标点击,防止冒泡到 overlay 触发关闭""" + """阻止卡片点击穿透,并跟随主窗口几何/生命周期。""" if obj is self.main_card and event.type() == QtCore.QEvent.Type.MouseButtonPress: event.ignore() # 阻止冒泡 return True + if self._main is not None and obj is self._main: + event_type = event.type() + if event_type in ( + QtCore.QEvent.Type.Move, + QtCore.QEvent.Type.Resize, + QtCore.QEvent.Type.WindowStateChange): + self.resize_to_parent() + elif event_type in (QtCore.QEvent.Type.Close, QtCore.QEvent.Type.Hide): + self._close(immediate=True) return super().eventFilter(obj, event) # ------------------------------------------------------------------ @@ -573,6 +593,8 @@ class SettingsWindow(QtWidgets.QWidget): "} " "QPushButton:hover { background-color: #1565c0; }" ) + add_btn.setObjectName("settings_add_provider") + add_btn.clicked.connect(self._prompt_new_provider) header_layout.addWidget(add_btn) layout.addWidget(header) @@ -590,6 +612,8 @@ class SettingsWindow(QtWidgets.QWidget): inner_layout.setSpacing(10) providers = self.config_data.get("providers", {}) + if not isinstance(providers, dict): + providers = {} if not providers: empty = QtWidgets.QLabel("暂无配置") empty.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) @@ -608,7 +632,9 @@ class SettingsWindow(QtWidgets.QWidget): # ------------------------------------------------------------------ def _build_provider_card(self, name: str, config: dict) -> QtWidgets.QWidget: + config = config if isinstance(config, dict) else {} card = QtWidgets.QWidget() + card.setProperty("providerName", name) card.setStyleSheet( "background-color: #fafafa; border-radius: 10px; " "border: 1px solid #e8e8e8;" @@ -635,6 +661,9 @@ class SettingsWindow(QtWidgets.QWidget): "} " "QPushButton:hover { background-color: #e0e0e0; color: #333; }" ) + edit_btn.setProperty("providerName", name) + edit_btn.clicked.connect( + lambda checked=False, provider=name: self._prompt_edit_provider(provider)) row1.addWidget(edit_btn) add_btn = QtWidgets.QPushButton("+模型") @@ -647,11 +676,15 @@ class SettingsWindow(QtWidgets.QWidget): "} " "QPushButton:hover { background-color: #bbdefb; }" ) + add_btn.setObjectName("settings_add_model") + add_btn.setProperty("providerName", name) + add_btn.clicked.connect( + lambda checked=False, provider=name: self._prompt_add_model(provider)) row1.addWidget(add_btn) layout.addLayout(row1) # 🌟 第二行:模型垂直排列,加左缩进,不超出边框 - models = config.get("models", []) + models = self._model_names(config) if models: models_wrap = QtWidgets.QWidget() models_layout = QtWidgets.QVBoxLayout(models_wrap) @@ -659,7 +692,9 @@ class SettingsWindow(QtWidgets.QWidget): models_layout.setSpacing(5) for model_name in models: - mbtn = QtWidgets.QPushButton(model_name) + mbtn = QtWidgets.QPushButton(str(model_name)) + mbtn.setProperty("providerName", name) + mbtn.setProperty("modelName", model_name) mbtn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) mbtn.setFixedHeight(30) # 🌟 明确高度,防止被内容撑开 mbtn.setStyleSheet( @@ -672,7 +707,8 @@ class SettingsWindow(QtWidgets.QWidget): "QPushButton:hover { background-color: #d0ebff; }" ) mbtn.clicked.connect( - lambda checked, m=model_name: print(f"[设置] 选中模型: {m}") + lambda checked=False, provider=name, model=model_name: + self._select_model(provider, model) ) models_layout.addWidget(mbtn) @@ -686,6 +722,192 @@ class SettingsWindow(QtWidgets.QWidget): return card + @staticmethod + def _model_names(config: dict) -> list: + """兼容字符串、对象列表、映射及旧 model_contexts 配置。""" + if not isinstance(config, dict): + return [] + raw = config.get("models", []) + if isinstance(raw, str): + raw = [raw] + elif isinstance(raw, dict): + raw = list(raw.keys()) + elif not isinstance(raw, (list, tuple)): + raw = [] + + names = [] + for entry in raw: + if isinstance(entry, str): + name = entry.strip() + elif isinstance(entry, dict): + name = next((str(entry.get(key, "")).strip() + for key in ("id", "name", "model", "value") + if str(entry.get(key, "")).strip()), "") + else: + name = "" + if name and name not in names: + names.append(name) + + for key in ("model_contexts", "model_max_tokens"): + mapping = config.get(key, {}) + if isinstance(mapping, dict): + for name in mapping: + name = str(name).strip() + if name and name not in names: + names.append(name) + return names + + def _refresh_provider_section(self): + """用当前内存配置重建供应商页,避免旧控件残留。""" + old_page = self.provider_page + index = self.content_stack.indexOf(old_page) + new_page = self._build_provider_section() + self.content_stack.insertWidget(max(0, index), new_page) + self.provider_page = new_page + self.content_stack.setCurrentWidget(new_page) + self.content_stack.removeWidget(old_page) + old_page.deleteLater() + + def _commit_config(self, candidate: dict) -> bool: + """先原子落盘,成功后再更新界面和主窗口。""" + from core.config_paths import save_config + if not save_config(candidate): + return False + self.config_data = copy.deepcopy(candidate) + self._refresh_provider_section() + self.config_changed.emit(copy.deepcopy(self.config_data)) + return True + + def _create_provider(self, name: str, base_url: str = "", api_key: str = "") -> bool: + """创建供应商;供按钮路径与自动化测试共用。""" + provider_name = str(name).strip() + if not provider_name: + return False + candidate = copy.deepcopy(self.config_data) + providers = candidate.get("providers") + if not isinstance(providers, dict): + providers = {} + candidate["providers"] = providers + if provider_name in providers: + return False + providers[provider_name] = { + "api_key": str(api_key).strip(), + "base_url": str(base_url).strip(), + "models": [], + "model_contexts": {}, + } + return self._commit_config(candidate) + + def _update_provider(self, name: str, base_url: str, api_key: str) -> bool: + candidate = copy.deepcopy(self.config_data) + providers = candidate.get("providers") + info = providers.get(name) if isinstance(providers, dict) else None + if not isinstance(info, dict): + return False + info["base_url"] = str(base_url).strip() + info["api_key"] = str(api_key).strip() + return self._commit_config(candidate) + + def _add_model(self, provider: str, model: str) -> bool: + """向供应商添加模型,并立即重建设置列表与主模型弹窗。""" + model_name = str(model).strip() + if not model_name: + return False + candidate = copy.deepcopy(self.config_data) + providers = candidate.get("providers") + info = providers.get(provider) if isinstance(providers, dict) else None + if not isinstance(info, dict) or model_name in self._model_names(info): + return False + + raw = info.get("models") + if isinstance(raw, list): + raw.append(model_name) + elif isinstance(raw, dict): + raw[model_name] = {} + elif isinstance(raw, str) and raw.strip(): + info["models"] = [raw.strip(), model_name] + else: + info["models"] = [model_name] + + contexts = info.get("model_contexts") + if not isinstance(contexts, dict): + contexts = {} + info["model_contexts"] = contexts + contexts.setdefault(model_name, 128000) + if not candidate.get("default_provider") or not candidate.get("default_model"): + candidate["default_provider"] = provider + candidate["default_model"] = model_name + return self._commit_config(candidate) + + def _select_model(self, provider: str, model: str): + providers = self.config_data.get("providers", {}) + info = providers.get(provider) if isinstance(providers, dict) else None + if model not in self._model_names(info): + return + candidate = copy.deepcopy(self.config_data) + candidate["default_provider"] = provider + candidate["default_model"] = model + if self._commit_config(candidate): + self.model_selected.emit(provider, model) + + def _provider_dialog(self, title: str, name: str = "", config=None, editing=False): + config = config if isinstance(config, dict) else {} + dialog = QtWidgets.QDialog(self) + dialog.setWindowTitle(title) + dialog.setModal(True) + dialog.setMinimumWidth(430) + layout = QtWidgets.QFormLayout(dialog) + layout.setContentsMargins(22, 18, 22, 18) + layout.setSpacing(12) + + name_input = QtWidgets.QLineEdit(name) + name_input.setReadOnly(editing) + base_input = QtWidgets.QLineEdit(str(config.get("base_url", ""))) + base_input.setPlaceholderText("https://example.com/v1") + key_input = QtWidgets.QLineEdit(str(config.get("api_key", ""))) + key_input.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) + layout.addRow("名称", name_input) + layout.addRow("Base URL", base_input) + layout.addRow("API Key", key_input) + + buttons = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Save + | QtWidgets.QDialogButtonBox.StandardButton.Cancel) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addRow(buttons) + name_input.setFocus() + + if dialog.exec() != QtWidgets.QDialog.DialogCode.Accepted: + return None + return name_input.text().strip(), base_input.text().strip(), key_input.text().strip() + + def _prompt_new_provider(self): + result = self._provider_dialog("新建模型提供商") + if result is None: + return + if not self._create_provider(*result): + QtWidgets.QMessageBox.warning(self, "无法创建", "提供商名称为空、已存在或配置保存失败。") + + def _prompt_edit_provider(self, provider: str): + providers = self.config_data.get("providers", {}) + info = providers.get(provider) if isinstance(providers, dict) else None + if not isinstance(info, dict): + return + result = self._provider_dialog( + "编辑模型提供商", provider, info, editing=True) + if result is None: + return + _, base_url, api_key = result + if not self._update_provider(provider, base_url, api_key): + QtWidgets.QMessageBox.warning(self, "保存失败", "未能保存提供商配置。") + + def _prompt_add_model(self, provider: str): + model, accepted = QtWidgets.QInputDialog.getText( + self, "添加模型", f"{provider} 的模型名称:") + if accepted and not self._add_model(provider, model): + QtWidgets.QMessageBox.warning(self, "无法添加", "模型名称为空、已存在或配置保存失败。") + # ------------------------------------------------------------------ def _build_placeholder_section(self) -> QtWidgets.QWidget: page = QtWidgets.QWidget() @@ -727,11 +949,14 @@ class SettingsWindow(QtWidgets.QWidget): # ------------------------------------------------------------------ def resize_to_parent(self): - parent = self.parentWidget() - if not parent: + if self._main is None: return - self.setFixedSize(parent.size()) - self.overlay.setFixedSize(self.size()) + + # SettingsWindow 是顶层 Tool 窗口,geometry 必须使用屏幕坐标; + # 主窗口的 rect 则准确表示不含系统标题栏的客户区。 + top_left = self._main.mapToGlobal(self._main.rect().topLeft()) + self.setGeometry(QtCore.QRect(top_left, self._main.rect().size())) + self.overlay.setGeometry(self.rect()) W = int(self.width() * 0.8) H = int(self.height() * 0.88) @@ -743,72 +968,66 @@ class SettingsWindow(QtWidgets.QWidget): # ------------------------------------------------------------------ def _setup_animation(self): - self._anim = QtCore.QPropertyAnimation(self.overlay, b"windowOpacity") - self._anim.setDuration(500) + self._anim = QtCore.QPropertyAnimation(self, b"windowOpacity") + self._anim.setDuration(160) self._anim.setStartValue(0.0) self._anim.setEndValue(1.0) # ------------------------------------------------------------------ def show_with_animation(self): - # 🌟 关键:先 setFixedSize 再 show,否则 show() 时瞬间以极小尺寸闪一下 - self.setFixedSize(self.parentWidget().size()) - self.overlay.setFixedSize(self.size()) - self.overlay.setGraphicsEffect(None) # 重置之前可能的 effect - - # 🌟 初始 opacity=0,再 show 出来,然后做正向动画淡入 + self.resize_to_parent() + self.setWindowOpacity(0.0) self._anim.setStartValue(0.0) self._anim.setEndValue(1.0) self._anim.setDirection(QtCore.QAbstractAnimation.Direction.Forward) self.show() + self.raise_() + self.activateWindow() self._anim.start() # ------------------------------------------------------------------ def _close(self, immediate=False): """关闭窗口。immediate=True 时立即关闭(关闭按钮用),否则播放淡出动画""" + if self._closed: + return if immediate: self._do_close() else: + if self._closing: + return + self._closing = True self._anim.setDirection(QtCore.QAbstractAnimation.Direction.Backward) + try: + self._anim.finished.disconnect(self._on_close_finished) + except TypeError: + pass self._anim.finished.connect(self._on_close_finished) self._anim.start() def _do_close(self): """直接关闭,不播放动画""" + if self._closed: + return + self._closed = True try: self._anim.finished.disconnect(self._on_close_finished) except TypeError: pass + if self._main is not None: + self._main.removeEventFilter(self) self.hide() parent = self.parent() - if parent: - for attr_name in dir(parent): - try: - if getattr(parent, attr_name, None) is self: - setattr(parent, attr_name, None) - break - except Exception: - pass + if parent is not None and getattr(parent, "_settings_win", None) is self: + parent._settings_win = None self.deleteLater() self.close_requested.emit() # ------------------------------------------------------------------ def _on_close_finished(self): - self._anim.finished.disconnect(self._on_close_finished) - self.hide() - parent = self.parent() - if parent: - for attr_name in dir(parent): - try: - if getattr(parent, attr_name, None) is self: - setattr(parent, attr_name, None) - break - except Exception: - pass - self.deleteLater() - self.close_requested.emit() + self._do_close() # ------------------------------------------------------------------ def keyPressEvent(self, event): @@ -1725,6 +1944,7 @@ class ModelSelectPopup(QtWidgets.QWidget): _p.end() self._model_icon = QtGui.QIcon(_pad) self._groups = [] # [{"provider", "header_item", "model_items", "expanded", "chevron"}] + self._empty_item = None self._drawer = None # 进行中的抽屉动画状态 {group, t, timer, row_h, anchor_bottom} self.setup_ui() @@ -1820,13 +2040,62 @@ class ModelSelectPopup(QtWidgets.QWidget): """) + @staticmethod + def _model_names(info): + """兼容字符串、对象列表和以模型名为键的映射,返回去重后的模型名。""" + if not isinstance(info, dict): + return [] + raw_models = info.get("models", []) + if isinstance(raw_models, dict): + raw_models = list(raw_models.keys()) + elif isinstance(raw_models, str): + raw_models = [raw_models] + elif not isinstance(raw_models, (list, tuple)): + raw_models = [] + + names = [] + for raw in raw_models: + name = raw.strip() if isinstance(raw, str) else "" + if isinstance(raw, dict): + for key in ("id", "name", "model", "value"): + value = raw.get(key) + if isinstance(value, str) and value.strip(): + name = value.strip() + break + if name and name not in names: + names.append(name) + + # 某些旧配置只保存模型参数映射,没有显式 models 数组。 + for key in ("model_contexts", "model_max_tokens"): + mapping = info.get(key) + if not isinstance(mapping, dict): + continue + for name in mapping: + if isinstance(name, str) and name.strip() and name.strip() not in names: + names.append(name.strip()) + return names + + def reload_config(self, config_data): + """按最新配置重建列表;配置缺失时仍保留可理解、可绘制的空状态。""" + self._finish_drawer() + self.config_data = config_data if isinstance(config_data, dict) else {} + self.list_widget.clear() + self._groups.clear() + self._empty_item = None + self.populate_data() + self.adjust_popup_height() + def populate_data(self): providers = self.config_data.get("providers", {}) + if not isinstance(providers, dict): + providers = {} # 🆕 图标区宽 = 16 + 34px 透明左边(缩进),高 16(调参工具确认) self.list_widget.setIconSize(QtCore.QSize(self.ICON_SIZE + self.MODEL_ICON_PAD, self.ICON_SIZE)) for provider_name, info in providers.items(): - models = info.get("models", []) + if not isinstance(provider_name, str) or not provider_name.strip(): + continue + models = self._model_names(info) if not models: continue # --- 🆕 供应商分组头(整行可点:展开/收起抽屉) --- @@ -1922,6 +2191,38 @@ class ModelSelectPopup(QtWidgets.QWidget): "chevron": lbl_chevron, }) + if self._groups: + return + + default_provider = self.config_data.get("default_provider") + default_model = self.config_data.get("default_model") + if isinstance(default_provider, str) and default_provider.strip() \ + and isinstance(default_model, str) and default_model.strip(): + self.config_data = dict(self.config_data) + self.config_data["providers"] = { + default_provider.strip(): {"models": [default_model.strip()]} + } + self.populate_data() + return + + self._empty_item = QtWidgets.QListWidgetItem("未配置可用模型") + self._empty_item.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) + self._empty_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._empty_item.setForeground(QtGui.QColor("#8a94a6")) + self._empty_item.setSizeHint(QtCore.QSize(0, 42)) + self.list_widget.addItem(self._empty_item) + + def mark_selected(self, provider, model): + """打开弹窗时高亮当前模型,并确保滚动到可见位置。""" + for group in self._groups: + for item in group["model_items"]: + if item.data(QtCore.Qt.ItemDataRole.UserRole) == (provider, model): + self.list_widget.setCurrentItem(item) + self.list_widget.scrollToItem( + item, QtWidgets.QAbstractItemView.ScrollHint.EnsureVisible) + return + self.list_widget.clearSelection() + # ==================== 🆕 抽屉 v2:fade + 相邻供应商行滑移 ==================== # 收起:A 段(顶部锚定,模型行 27px 不变原地淡出 1→0,下方供应商行整体 # 上滑逐渐盖住 → 两供应商行合并)+ B 段(整体下滑回座按钮锚点) @@ -1946,7 +2247,7 @@ class ModelSelectPopup(QtWidgets.QWidget): return max(self.MIN_POPUP_H, min(self.MAX_POPUP_H, natural)) def toggle_group(self, provider_name: str): - """点击供应商头 → 折叠/展开该供应商的模型组(🆕 抽屉 v2 动画)""" + """点击供应商头后立即应用稳定终态,避免列表子控件动画产生残影。""" g = None for gg in self._groups: if gg["provider"] == provider_name: @@ -1960,12 +2261,11 @@ class ModelSelectPopup(QtWidgets.QWidget): # SVG 箭头切换(展开=朝下 / 收起=朝右) g["chevron"].setPixmap( self._arrow_collapsed if not g["expanded"] else self._arrow_expanded) - if not self.isVisible(): - # 防御:弹窗未显示时直接应用终态(不播动画) - self._apply_group_state(g) - self._apply_height_and_position() - return - self._start_drawer(g) + self._clear_fx() + self._apply_group_state(g) + self.list_widget.doItemsLayout() + self._apply_height_and_position() + self.list_widget.viewport().update() def _apply_group_state(self, g): """应用组终态:展开=可见 27px 行 / 收起=隐藏(行高恒定不变)""" @@ -5446,6 +5746,13 @@ class MainWindow(QtWidgets.QMainWindow): from core.config_paths import load_config as _load_cfg self.config_data = _load_cfg() or {"providers": {}} + default_p = self.config_data.get("default_provider", "GPTBest") + default_m = self.config_data.get("default_model", "gemini-3.1-pro-preview-thinking-high") + # 即使配置缺失,也让弹窗与按钮共享同一默认项,避免按钮有文字而列表为空。 + self.config_data = dict(self.config_data) + self.config_data.setdefault("default_provider", default_p) + self.config_data.setdefault("default_model", default_m) + # 2. 实例化我们写的自定义弹窗 (先不显示) self.model_popup = ModelSelectPopup(self, self.config_data) @@ -5458,19 +5765,34 @@ class MainWindow(QtWidgets.QMainWindow): self.model_selector.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) # 5. 设置默认选中 - default_p = self.config_data.get("default_provider", "GPTBest") - default_m = self.config_data.get("default_model", "gemini-3.1-pro-preview-thinking-high") self.on_model_selected(default_p, default_m) def show_model_popup(self): - """计算位置并执行动画弹出""" + """刷新模型列表,在当前屏幕内计算位置并显示。""" + from core.config_paths import load_config as _load_cfg + latest_config = _load_cfg() or {} + latest_config = dict(latest_config) + latest_config.setdefault("default_provider", self.current_provider or "GPTBest") + latest_config.setdefault( + "default_model", self.current_model or "gemini-3.1-pro-preview-thinking-high") + self.config_data = latest_config + self.model_popup.reload_config(latest_config) + self.model_popup.mark_selected(self.current_provider, self.current_model) + # 获取按钮在屏幕上的全局坐标 btn_pos = self.model_selector.mapToGlobal(QtCore.QPoint(0, 0)) - - # 计算弹窗最终应该停留的目标坐标 + + # 优先放在按钮上方并右对齐;空间不足时放到下方,最终钳制在当前屏幕内。 x = btn_pos.x() + self.model_selector.width() - self.model_popup.width() y = btn_pos.y() - self.model_popup.height() - 5 - + screen = QtGui.QGuiApplication.screenAt(btn_pos) or self.screen() + if screen is not None: + area = screen.availableGeometry() + x = max(area.left(), min(x, area.right() - self.model_popup.width() + 1)) + if y < area.top(): + y = btn_pos.y() + self.model_selector.height() + 5 + y = max(area.top(), min(y, area.bottom() - self.model_popup.height() + 1)) + # 🌟 调用动画显示方法,传入目标坐标 self.model_popup.show_with_animation(QtCore.QPoint(x, y)) @@ -5489,6 +5811,16 @@ class MainWindow(QtWidgets.QMainWindow): #新增:切换模型后刷新上下文显示(因为不同模型上限不同) self.update_context_display() + def _on_settings_config_changed(self, config_data): + """设置页保存后同步刷新主窗口和模型弹窗。""" + if not isinstance(config_data, dict): + return + self.config_data = copy.deepcopy(config_data) + self.model_popup.reload_config(self.config_data) + self.model_popup.mark_selected( + self.current_provider, self.current_model) + self.update_context_display() + def on_reasoning_received(self, session_id, chunk: str): """接收到思考过程""" self._diag_think_n += 1 @@ -6106,14 +6438,20 @@ class MainWindow(QtWidgets.QMainWindow): return widget def show_settings(self): - if hasattr(self, '_settings_win') and self._settings_win is not None: - self._settings_win.deleteLater() - self._settings_win = None + existing = getattr(self, '_settings_win', None) + if existing is not None: + existing._close(immediate=True) + + if hasattr(self, "model_popup"): + self.model_popup.hide() self._settings_win = SettingsWindow( parent=self, config_data=self.config_data ) + self._settings_win.config_changed.connect( + self._on_settings_config_changed) + self._settings_win.model_selected.connect(self.on_model_selected) self._settings_win.show_with_animation() def _on_branch_switch(self, msg_id, direction): """🌟 核心:处理用户点击左右箭头切换分支""" diff --git a/ui/web/style.css b/ui/web/style.css index 608b3a9..5d0a597 100644 --- a/ui/web/style.css +++ b/ui/web/style.css @@ -893,10 +893,16 @@ body, html { } -/* ========== 助手正文透明气泡(流式中/完成后一致) ========== - 🐛 修复:原选择器 .message.assistant 是死代码(JS 只挂 message-wrapper 类),背景从未渲染 */ -/* 🆕 用户决定:助手正文气泡【透明】——不要背景/边框/圆角/内边距(2026-07 像素取证调试后明确: - 原死选择器 P0 修复带来的浅灰底不是想要的效果),正文直接裸排在 85% 定宽列内 */ +/* ========== 助手正文气泡(流式中/完成后一致) ========== */ +.message-wrapper.assistant .reply-content .md-segment { + width: 100%; + box-sizing: border-box; + padding: 10px 14px; + color: #30343b; + background-color: #f6f7f9; + border: 1px solid #eceff3; + border-radius: 8px; +} .message-wrapper.assistant .reply-content .md-segment + .md-segment { margin-top: 6px; }