feat(chat): DTK 主窗口 + 新模型;fix(panel): 菜单位置/工厂挤压与误隐藏

- ChatWindow 改为 DMainWindow + DTitlebar,图标按 DPR 取图
- DeepSeek 模型改为 deepseek-flash / deepseek-v4-pro,设置里可拉取 /models
- 内容工厂右侧常驻「问问小埃同学」大按钮,移除传送门上的常驻入口
- 工厂出现时面板整体上移预留空间,不再挤压传送门
- 工厂隐藏改为检查指针是否仍在面板内,拖进工厂不再消失
- 右键菜单超出屏幕时向上弹出
- 移除 512 图标(由 293px 源图放大,发糊)
This commit is contained in:
shenmo
2026-10-30 21:03:25 +08:00
parent 798f5aa7ce
commit 4032de318c
12 changed files with 175 additions and 40 deletions
+3
View File
@@ -19,6 +19,9 @@
- MCP:stdio JSON-RPC,自动 initialize / tools-list,工具以 function calling 暴露给模型并在对话中执行
- 合成器 portal(flake-wm / gxde-wlcom):org.gxde.Portal、缩略图坐标映射、
原生 drop 到目标窗口
- 对话窗口改用 DMainWindow/DTitlebar(DTK),消息气泡、会话列表重做
- DeepSeek 模型更新为 deepseek-flash / deepseek-v4-pro,设置里可自动获取 /models
- 内容工厂右侧常驻「问问小埃同学」大按钮;面板为工厂预留空间,不再上挤
- 英文翻译(95 条)与翻译加载;gxde-k9 开机自启与保活开关;不支持环境只提示一次并保持待命
-- shenmo <shenmo@spark-app.store>
+1 -1
View File
@@ -117,6 +117,6 @@ endif()
install(TARGETS gxde-ai-see RUNTIME DESTINATION bin)
install(FILES data/gxde-ai-see.desktop data/gxde-ai-see-todo.desktop DESTINATION share/applications)
install(DIRECTORY data/icons/ DESTINATION share/icons/hicolor)
install(DIRECTORY data/icons/hicolor/ DESTINATION share/icons/hicolor)
include(GNUInstallDirs)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
shlibs:Depends=libc6 (>= 2.34), libdtk2widget6 (>= 6:6.3.8), libdtk6core (>= 6.0.48), libgcc-s1 (>= 3.0), libicu76 (>= 76.1-1~), liblayershellqtinterface6 (>= 6.1.0), libqt6core6t64 (>= 6.8.2), libqt6dbus6 (>= 6.1.2), libqt6gui6 (>= 6.1.2), libqt6network6 (>= 6.1.2), libqt6widgets6 (>= 6.3.0), libstdc++6 (>= 5)
shlibs:Depends=libc6 (>= 2.34), libdtk2widget6 (>= 6:6.3.8), libdtk6core (>= 6.0.48), libgcc-s1 (>= 3.0), libicu76 (>= 76.1-1~), liblayershellqtinterface6 (>= 6.1.0), libqt6core6t64 (>= 6.8.2), libqt6dbus6 (>= 6.1.2), libqt6gui6 (>= 6.8.2), libqt6network6 (>= 6.1.2), libqt6widgets6 (>= 6.3.0), libstdc++6 (>= 5)
misc:Depends=
misc:Pre-Depends=
+1 -1
View File
@@ -48,7 +48,7 @@ bool prepareRequest(const QVector<ChatMessage> &messages, bool stream,
QJsonObject payload;
payload.insert(QStringLiteral("model"),
Config::instance()
->get(QStringLiteral("chat/model"), QStringLiteral("deepseek-chat"))
->get(QStringLiteral("chat/model"), QStringLiteral("deepseek-flash"))
.toString());
QJsonArray allMessages = jsonMessages;
for (const QJsonValue &extra : extraMessages) {
+88 -9
View File
@@ -39,6 +39,7 @@
#include <QMimeData>
#include <QPlainTextEdit>
#include <QPushButton>
#include <DTitlebar>
#include <QRegularExpression>
#include <QSaveFile>
#include <QScreen>
@@ -46,6 +47,10 @@
#include <QShortcut>
#include <QTextCursor>
#include <QTextBrowser>
#include <QIcon>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QUuid>
#include <QVBoxLayout>
@@ -224,10 +229,13 @@ private:
};
ChatWindow::ChatWindow(QWidget *parent)
: QWidget(parent)
: Dtk::Widget::DMainWindow(parent)
, m_client(new ChatClient(this))
{
setWindowTitle(tr("小埃同学"));
titlebar()->setTitle(tr("小埃同学"));
titlebar()->setIcon(QApplication::windowIcon());
titlebar()->setAutoHideOnFullscreen(true);
setMinimumSize(760, 520);
resize(1000, 700);
m_storePath = Config::instance()->dataDir() + QStringLiteral("/chat.json");
@@ -235,6 +243,18 @@ ChatWindow::ChatWindow(QWidget *parent)
setupUi();
McpManager::instance()->reload();
load();
/* DeepSeek renamed the API models; map the old presets once. */
const QString storedModel =
Config::instance()->get(QStringLiteral("chat/model")).toString();
if (storedModel == QStringLiteral("deepseek-chat")) {
Config::instance()->set(QStringLiteral("chat/model"), QStringLiteral("deepseek-flash"));
Config::instance()->sync();
} else if (storedModel == QStringLiteral("deepseek-reasoner")) {
Config::instance()->set(QStringLiteral("chat/model"), QStringLiteral("deepseek-v4-pro"));
Config::instance()->sync();
}
m_current = m_conversations.isEmpty() ? -1 : qBound(0, m_current, m_conversations.size() - 1);
updateSidebar();
renderConversation();
@@ -337,7 +357,9 @@ ChatWindow::ChatWindow(QWidget *parent)
void ChatWindow::setupUi()
{
auto *root = new QHBoxLayout(this);
auto *central = new QWidget(this);
setCentralWidget(central);
auto *root = new QHBoxLayout(central);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
@@ -356,8 +378,11 @@ void ChatWindow::setupUi()
brandRow->setSpacing(10);
auto *logo = new QLabel(sidebarPane);
logo->setFixedSize(34, 34);
logo->setPixmap(QApplication::windowIcon().pixmap(34, 34));
logo->setScaledContents(true);
const qreal ratio = logo->devicePixelRatioF();
QPixmap logoPixmap = QApplication::windowIcon().pixmap(
QSize(qRound(34 * ratio), qRound(34 * ratio)));
logoPixmap.setDevicePixelRatio(ratio);
logo->setPixmap(logoPixmap);
logo->setStyleSheet(QStringLiteral("border-radius: 8px;"));
brandRow->addWidget(logo);
@@ -448,7 +473,7 @@ void ChatWindow::setupUi()
m_titleLabel->setStyleSheet(QStringLiteral("color: rgba(255,255,255,0.92);"));
headerLayout->addWidget(m_titleLabel);
m_modelLabel = new QLabel(QStringLiteral("deepseek-chat"), header);
m_modelLabel = new QLabel(QStringLiteral("deepseek-flash"), header);
m_modelLabel->setStyleSheet(QStringLiteral(
"QLabel { color: rgba(160,200,240,0.75); background: rgba(74,144,255,0.13);"
" border-radius: 8px; padding: 2px 8px; font-size: 10px; }"));
@@ -694,7 +719,7 @@ void ChatWindow::updateHeader()
m_titleLabel->setText(valid ? m_conversations.at(m_current).title : tr("小埃同学"));
m_modelLabel->setText(Config::instance()
->get(QStringLiteral("chat/model"),
QStringLiteral("deepseek-chat"))
QStringLiteral("deepseek-flash"))
.toString());
}
@@ -1046,13 +1071,67 @@ void ChatWindow::openSettings()
auto *modelBox = new QComboBox(&dialog);
modelBox->setEditable(true);
modelBox->addItems({ QStringLiteral("deepseek-chat"),
modelBox->addItems({ QStringLiteral("deepseek-flash"),
QStringLiteral("deepseek-reasoner") });
modelBox->setCurrentText(Config::instance()
->get(QStringLiteral("chat/model"),
QStringLiteral("deepseek-chat"))
QStringLiteral("deepseek-flash"))
.toString());
form->addRow(tr("模型"), modelBox);
auto *modelRow = new QHBoxLayout;
modelRow->setSpacing(6);
modelRow->addWidget(modelBox, 1);
auto *fetchModels = new QPushButton(tr("获取模型列表"), &dialog);
modelRow->addWidget(fetchModels);
form->addRow(tr("模型"), modelRow);
connect(fetchModels, &QPushButton::clicked, &dialog, [=, &dialog]() {
QString base = baseEdit->text().trimmed();
while (base.endsWith(QLatin1Char('/'))) {
base.chop(1);
}
if (base.endsWith(QStringLiteral("/chat/completions"))) {
base.chop(int(qstrlen("/chat/completions")));
}
QNetworkRequest request{QUrl(base + QStringLiteral("/models"))};
request.setRawHeader("Authorization",
"Bearer " + keyEdit->text().trimmed().toUtf8());
auto *manager = new QNetworkAccessManager(&dialog);
QNetworkReply *reply = manager->get(request);
fetchModels->setEnabled(false);
connect(reply, &QNetworkReply::finished, &dialog, [=]() {
fetchModels->setEnabled(true);
const QByteArray body = reply->readAll();
const QJsonDocument document = QJsonDocument::fromJson(body);
const QJsonArray models =
document.object().value(QStringLiteral("data")).toArray();
if (reply->error() != QNetworkReply::NoError || models.isEmpty()) {
fetchModels->setText(tr("获取失败"));
fetchModels->setToolTip(QString::fromUtf8(body).left(200));
reply->deleteLater();
return;
}
const QString current = modelBox->currentText();
modelBox->clear();
for (const QJsonValue &value : models) {
const QJsonObject object = value.toObject();
const QString id = object.value(QStringLiteral("id")).toString();
if (id.isEmpty()) {
continue;
}
modelBox->addItem(id);
const QString name = object.value(QStringLiteral("name")).toString();
if (!name.isEmpty()) {
modelBox->setItemData(modelBox->count() - 1, name, Qt::ToolTipRole);
}
}
const int index = modelBox->findText(current);
if (index >= 0) {
modelBox->setCurrentIndex(index);
}
fetchModels->setText(tr("已更新"));
reply->deleteLater();
});
});
auto *contextSpin = new QSpinBox(&dialog);
contextSpin->setRange(4, 200);
+2 -1
View File
@@ -5,6 +5,7 @@
#include <QJsonArray>
#include <QVector>
#include <DMainWindow>
#include <QWidget>
#include "chatclient.h"
@@ -32,7 +33,7 @@ struct ChatConversation
* conversation, an item targets that conversation).
* Right: conversation view (drop targets the current conversation) and input.
*/
class ChatWindow : public QWidget
class ChatWindow : public Dtk::Widget::DMainWindow
{
Q_OBJECT
+25 -4
View File
@@ -15,6 +15,7 @@
#include <QMimeData>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QStyle>
#include <QToolButton>
#include <QVBoxLayout>
@@ -130,9 +131,13 @@ ContentFactory::ContentFactory(QWidget *parent)
setAcceptDrops(true);
setMinimumHeight(44);
auto *outer = new QVBoxLayout(this);
auto *outer = new QHBoxLayout(this);
outer->setContentsMargins(12, 7, 12, 7);
outer->setSpacing(6);
outer->setSpacing(10);
auto *left = new QVBoxLayout;
left->setContentsMargins(0, 0, 0, 0);
left->setSpacing(6);
auto *header = new QHBoxLayout;
header->setContentsMargins(0, 0, 0, 0);
@@ -155,7 +160,7 @@ ContentFactory::ContentFactory(QWidget *parent)
hint->setStyleSheet(QStringLiteral("color: rgba(255,255,255,0.32); font-size: 11px;"));
header->addWidget(hint);
outer->addLayout(header);
left->addLayout(header);
m_actions = new QWidget(this);
m_grid = new QGridLayout(m_actions);
@@ -163,7 +168,23 @@ ContentFactory::ContentFactory(QWidget *parent)
m_grid->setHorizontalSpacing(6);
m_grid->setVerticalSpacing(6);
m_actions->hide();
outer->addWidget(m_actions);
left->addWidget(m_actions);
outer->addLayout(left, 1);
m_askButton = new QPushButton(tr("问问\n小埃同学"), this);
m_askButton->setCursor(Qt::PointingHandCursor);
m_askButton->setFocusPolicy(Qt::NoFocus);
m_askButton->setFixedWidth(92);
m_askButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
m_askButton->setStyleSheet(QStringLiteral(
"QPushButton { color: white; background: rgba(74,144,255,0.22);"
" border: 1px solid rgba(120,180,255,0.45); border-radius: 10px;"
" font-size: 12px; }"
"QPushButton:hover { background: rgba(74,144,255,0.38);"
" border-color: rgba(160,205,255,0.8); }"));
connect(m_askButton, &QPushButton::clicked, this, &ContentFactory::askRequested);
outer->addWidget(m_askButton);
}
void ContentFactory::setContent(const DropContent &content)
+2
View File
@@ -33,6 +33,7 @@ signals:
void actionRequested(const QString &id, const DropContent &content);
void heightChanged();
void dragActivity();
void askRequested();
protected:
void paintEvent(QPaintEvent *event) override;
@@ -42,6 +43,7 @@ private:
void rebuild();
QLabel *m_summary = nullptr;
class QPushButton *m_askButton = nullptr;
QWidget *m_actions = nullptr;
QGridLayout *m_grid = nullptr;
DropContent m_content;
+5 -1
View File
@@ -20,7 +20,11 @@ int main(int argc, char *argv[])
app.setApplicationName(QStringLiteral("gxde-ai-see"));
app.setApplicationDisplayName(QStringLiteral("GXDE 小埃同学"));
app.setApplicationVersion(QStringLiteral("0.1.0"));
app.setWindowIcon(QIcon::fromTheme(QStringLiteral("gxde-ai-see")));
QIcon appIcon = QIcon::fromTheme(QStringLiteral("gxde-ai-see"));
if (appIcon.isNull()) {
appIcon = QIcon(QStringLiteral("/usr/share/icons/hicolor/256x256/apps/gxde-ai-see.png"));
}
app.setWindowIcon(appIcon);
app.setQuitOnLastWindowClosed(false);
const QString locale = QLocale::system().name();
+46 -22
View File
@@ -584,16 +584,6 @@ public:
}
});
m_chatButton = new QPushButton(tr("问问小埃 →"), this);
m_chatButton->setCursor(Qt::PointingHandCursor);
m_chatButton->setFlat(true);
m_chatButton->setFocusPolicy(Qt::NoFocus);
m_chatButton->setStyleSheet(QStringLiteral(
"QPushButton { color: #9ecbff; background: transparent; border: none;"
" font-size: 13px; }"
"QPushButton:hover { color: #cfe5ff; }"));
connect(m_chatButton, &QPushButton::clicked, this, &PortalZone::chatRequested);
m_hintLabel = new QLabel(this);
m_hintLabel->setAlignment(Qt::AlignCenter);
m_hintLabel->setWordWrap(true);
@@ -627,7 +617,6 @@ protected:
void resizeEvent(QResizeEvent *event) override
{
QWidget::resizeEvent(event);
m_chatButton->setGeometry(12, height() - 42, width() - 24, 30);
m_hintLabel->setGeometry(12, height() - 84, width() - 24, 36);
}
@@ -741,7 +730,6 @@ private:
m_hintTimer.start();
}
QPushButton *m_chatButton = nullptr;
QLabel *m_hintLabel = nullptr;
QTimer m_hintTimer;
QTimer m_portalTimer;
@@ -826,7 +814,8 @@ void PortalPanel::setupWindow()
anchors |= LayerShellQt::Window::AnchorRight;
anchors |= LayerShellQt::Window::AnchorBottom;
layer->setAnchors(anchors);
layer->setMargins(QMargins(0, 0, m_rightMargin, m_bottomMargin));
layer->setMargins(
QMargins(0, 0, m_rightMargin, m_bottomMargin + m_factoryOffset));
layer->setLayer(LayerShellQt::Window::LayerOverlay);
layer->setKeyboardInteractivity(
LayerShellQt::Window::KeyboardInteractivityNone);
@@ -851,7 +840,6 @@ void PortalPanel::setupUi()
m_zone = new PortalZone(this);
m_zone->setFixedWidth(kZoneWidth);
outer->addWidget(m_zone);
connect(m_zone, &PortalZone::chatRequested, this, &PortalPanel::openChatRequested);
connect(m_zone, &PortalZone::portalRequested, this, [this]() {
PortalClient *portal = PortalClient::instance();
if (portal->beginPortal()) {
@@ -936,12 +924,19 @@ void PortalPanel::setupUi()
m_factoryTimer = new QTimer(this);
m_factoryTimer->setSingleShot(true);
m_factoryTimer->setInterval(280);
m_factoryTimer->setInterval(220);
connect(m_factoryTimer, &QTimer::timeout, this, [this]() {
if (m_factory) {
m_factory->clearContent();
m_factory->hide();
if (!m_factory || !m_factory->isVisible()) {
return;
}
/* Dragging from the portal zone into the factory must not hide it:
* only hide once the pointer is really outside the panel. */
if (rect().contains(mapFromGlobal(QCursor::pos()))) {
m_factoryTimer->start();
return;
}
m_factory->clearContent();
m_factory->hide();
refreshLayout();
});
@@ -950,8 +945,13 @@ void PortalPanel::setupUi()
connect(m_view, &StagingView::dragContent, this, &PortalPanel::noteDragContent);
connect(m_view, &StagingView::dragFinished, this, &PortalPanel::noteDragFinished);
connect(m_factory, &ContentFactory::actionRequested, this, &PortalPanel::runFactoryAction);
connect(m_factory, &ContentFactory::askRequested, this, &PortalPanel::openChatRequested);
connect(m_factory, &ContentFactory::heightChanged, this, [this]() { refreshLayout(); });
connect(m_factory, &ContentFactory::dragActivity, this, [this]() { m_factoryTimer->stop(); });
connect(m_factory, &ContentFactory::dragActivity, this, [this]() {
if (m_factoryTimer) {
m_factoryTimer->start();
}
});
}
void PortalPanel::refreshLayout()
@@ -980,6 +980,13 @@ void PortalPanel::updateGeometryForContent()
const int totalHeight =
kMargin * 2 + headerHeight + 6 + selectionHeight + listHeight + factoryHeight;
resize(kPanelWidth, totalHeight);
/* Growing downwards would push the portal/s. Staging area off screen: raise
* the panel by the factory height so the two columns stay where they are. */
if (m_factoryOffset != factoryHeight) {
m_factoryOffset = factoryHeight;
applyMargins();
}
}
void PortalPanel::noteDragContent(const QMimeData *mime)
@@ -1194,7 +1201,8 @@ void PortalPanel::applyMargins()
#ifdef GXDE_HAVE_LAYERSHELLQT
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
if (auto *layer = LayerShellQt::Window::get(windowHandle())) {
layer->setMargins(QMargins(0, 0, m_rightMargin, m_bottomMargin));
layer->setMargins(
QMargins(0, 0, m_rightMargin, m_bottomMargin + m_factoryOffset));
return;
}
}
@@ -1202,7 +1210,7 @@ void PortalPanel::applyMargins()
if (QScreen *scr = screen() ? screen() : QGuiApplication::primaryScreen()) {
const QRect available = scr->availableGeometry();
move(available.right() - width() - m_rightMargin,
available.bottom() - height() - m_bottomMargin);
available.bottom() - height() - m_bottomMargin - m_factoryOffset);
}
}
@@ -1322,7 +1330,23 @@ void PortalPanel::showContextMenu(const QPoint &globalPos)
QAction *about = menu.addAction(tr("关于"));
QAction *quit = menu.addAction(tr("退出"));
QAction *chosen = menu.exec(globalPos);
menu.ensurePolished();
const QSize menuSize = menu.sizeHint();
QPoint menuPos = globalPos;
QScreen *scr = screen() ? screen() : QGuiApplication::primaryScreen();
if (scr) {
const QRect available = scr->availableGeometry();
if (menuPos.y() + menuSize.height() > available.bottom()) {
menuPos.setY(globalPos.y() - menuSize.height());
}
if (menuPos.y() < available.top()) {
menuPos.setY(available.top());
}
if (menuPos.x() + menuSize.width() > available.right()) {
menuPos.setX(available.right() - menuSize.width());
}
}
QAction *chosen = menu.exec(menuPos);
if (!chosen) {
return;
}
+1
View File
@@ -75,6 +75,7 @@ private:
int m_rightMargin = 16;
int m_bottomMargin = 16;
int m_manualListHeight = 0;
int m_factoryOffset = 0;
bool m_fixedPosition = true;
bool m_fixedSize = false;
int m_dragMode = 0; // 0 none, 1 move, 2 resize