diff --git a/CHANGELOG.md b/CHANGELOG.md index 6611de3..fafe89c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index c1602c0..5f77ab3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/data/icons/hicolor/512x512/apps/gxde-ai-see.png b/data/icons/hicolor/512x512/apps/gxde-ai-see.png deleted file mode 100644 index 590cbd1..0000000 Binary files a/data/icons/hicolor/512x512/apps/gxde-ai-see.png and /dev/null differ diff --git a/debian/gxde-ai-see.substvars b/debian/gxde-ai-see.substvars index 910ebe4..723fdc9 100644 --- a/debian/gxde-ai-see.substvars +++ b/debian/gxde-ai-see.substvars @@ -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= diff --git a/src/chat/chatclient.cpp b/src/chat/chatclient.cpp index a44fb01..3f27baa 100644 --- a/src/chat/chatclient.cpp +++ b/src/chat/chatclient.cpp @@ -48,7 +48,7 @@ bool prepareRequest(const QVector &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) { diff --git a/src/chat/chatwindow.cpp b/src/chat/chatwindow.cpp index d228dbb..d06c273 100644 --- a/src/chat/chatwindow.cpp +++ b/src/chat/chatwindow.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -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); diff --git a/src/chat/chatwindow.h b/src/chat/chatwindow.h index e0d628f..e0a29da 100644 --- a/src/chat/chatwindow.h +++ b/src/chat/chatwindow.h @@ -5,6 +5,7 @@ #include #include +#include #include #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 diff --git a/src/factory/contentfactory.cpp b/src/factory/contentfactory.cpp index a1d5981..e50a7b1 100644 --- a/src/factory/contentfactory.cpp +++ b/src/factory/contentfactory.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -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) diff --git a/src/factory/contentfactory.h b/src/factory/contentfactory.h index 17c4b72..48fd40b 100644 --- a/src/factory/contentfactory.h +++ b/src/factory/contentfactory.h @@ -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; diff --git a/src/main.cpp b/src/main.cpp index 8b3a920..822764a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(); diff --git a/src/ui/portalpanel.cpp b/src/ui/portalpanel.cpp index 43c6c36..5af6beb 100644 --- a/src/ui/portalpanel.cpp +++ b/src/ui/portalpanel.cpp @@ -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; } diff --git a/src/ui/portalpanel.h b/src/ui/portalpanel.h index 9a38f09..0cc7e76 100644 --- a/src/ui/portalpanel.h +++ b/src/ui/portalpanel.h @@ -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