diff --git a/CMakeLists.txt b/CMakeLists.txt index 16ac4bb..7e76d47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ set(GXDE_AI_SEE_SOURCES src/chat/markdown.cpp src/chat/chatview.cpp src/ui/portalpanel.cpp + src/ui/aboutdialog.cpp ) add_executable(gxde-ai-see ${GXDE_AI_SEE_SOURCES}) @@ -119,7 +120,8 @@ if (LRELEASE_EXECUTABLE AND TS_FILES) 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(FILES data/gxde-ai-see.desktop data/gxde-ai-see-todo.desktop + data/gxde-ai-see-portal.desktop DESTINATION share/applications) install(DIRECTORY data/icons/hicolor/ DESTINATION share/icons/hicolor) include(GNUInstallDirs) diff --git a/data/gxde-ai-see-portal.desktop b/data/gxde-ai-see-portal.desktop new file mode 100644 index 0000000..b71cc14 --- /dev/null +++ b/data/gxde-ai-see-portal.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=GXDE Portal +Name[zh_CN]=传送门 +Comment=Show or hide the GXDE AI See portal panel +Comment[zh_CN]=显示或隐藏小埃同学的传送门面板 +Exec=gxde-ai-see --show +Icon=gxde-ai-see +Terminal=false +Categories=Utility; +StartupNotify=false diff --git a/data/gxde-ai-see-todo.desktop b/data/gxde-ai-see-todo.desktop index 1aaed5d..848bd69 100644 --- a/data/gxde-ai-see-todo.desktop +++ b/data/gxde-ai-see-todo.desktop @@ -6,7 +6,7 @@ Name[zh_CN]=GXDE 待办 Comment=Floating todo list of GXDE AI See Comment[zh_CN]=小埃同学的待办 Exec=gxde-ai-see --todo -Icon=gxde-ai-see +Icon=gxde-ai-see-todo Terminal=false Categories=Utility; StartupNotify=false diff --git a/data/icons-src/gxde-ai-see-todo.svg b/data/icons-src/gxde-ai-see-todo.svg new file mode 100644 index 0000000..76b3109 --- /dev/null +++ b/data/icons-src/gxde-ai-see-todo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/data/icons/hicolor/128x128/apps/gxde-ai-see-todo.png b/data/icons/hicolor/128x128/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..0f15252 Binary files /dev/null and b/data/icons/hicolor/128x128/apps/gxde-ai-see-todo.png differ diff --git a/data/icons/hicolor/16x16/apps/gxde-ai-see-todo.png b/data/icons/hicolor/16x16/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..a93a2c3 Binary files /dev/null and b/data/icons/hicolor/16x16/apps/gxde-ai-see-todo.png differ diff --git a/data/icons/hicolor/24x24/apps/gxde-ai-see-todo.png b/data/icons/hicolor/24x24/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..af1a8bc Binary files /dev/null and b/data/icons/hicolor/24x24/apps/gxde-ai-see-todo.png differ diff --git a/data/icons/hicolor/256x256/apps/gxde-ai-see-todo.png b/data/icons/hicolor/256x256/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..db14033 Binary files /dev/null and b/data/icons/hicolor/256x256/apps/gxde-ai-see-todo.png differ diff --git a/data/icons/hicolor/32x32/apps/gxde-ai-see-todo.png b/data/icons/hicolor/32x32/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..2dc71be Binary files /dev/null and b/data/icons/hicolor/32x32/apps/gxde-ai-see-todo.png differ diff --git a/data/icons/hicolor/48x48/apps/gxde-ai-see-todo.png b/data/icons/hicolor/48x48/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..053140a Binary files /dev/null and b/data/icons/hicolor/48x48/apps/gxde-ai-see-todo.png differ diff --git a/data/icons/hicolor/64x64/apps/gxde-ai-see-todo.png b/data/icons/hicolor/64x64/apps/gxde-ai-see-todo.png new file mode 100644 index 0000000..02d4bc3 Binary files /dev/null and b/data/icons/hicolor/64x64/apps/gxde-ai-see-todo.png differ diff --git a/src/app.cpp b/src/app.cpp index d8bc94b..b03aaf9 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -52,7 +52,7 @@ bool App::init(bool daemon, bool forceShow, bool showTodo, bool showChat) } else if (data.contains("todo")) { showTodoWindow(); } else if (data.contains("show")) { - showPanel(); + togglePanel(); } }); connect(socket, &QLocalSocket::disconnected, socket, @@ -97,7 +97,7 @@ bool App::init(bool daemon, bool forceShow, bool showTodo, bool showChat) } return; } - QTimer::singleShot(120, this, [this]() { + QTimer::singleShot(80, this, [this]() { if (m_dragging && m_model->isEmpty()) { showPanel(); if (m_panel) { @@ -112,14 +112,19 @@ bool App::init(bool daemon, bool forceShow, bool showTodo, bool showChat) m_panel->stopPortalSuppression(); m_panel->setDragging(false); } - if (m_model->isEmpty() && m_panel && m_panel->isVisible()) { + if (m_model->isEmpty() && !m_panelPinned && m_panel && m_panel->isVisible()) { m_panel->dismiss(); } }); connect(m_model, &StagingModel::countChanged, this, [this]() { - if (!m_daemon && !m_model->isEmpty() && m_panel) { + if (!m_model->isEmpty() && m_panel) { m_panel->popup(); + return; + } + /* cleared: the panel exists for the staging list, hide it again */ + if (!m_dragging && !m_panelPinned && m_panel && m_panel->isVisible()) { + m_panel->dismiss(); } }); @@ -131,6 +136,7 @@ bool App::init(bool daemon, bool forceShow, bool showTodo, bool showChat) } if (!daemon || forceShow || !m_model->isEmpty()) { showPanel(); + m_panelPinned = forceShow; } return true; @@ -144,6 +150,11 @@ void App::ensurePanel() m_panel = new PortalPanel(m_model); connect(m_panel, &PortalPanel::openChatRequested, this, &App::showChatWindow); + connect(m_panel, &PortalPanel::hideRequested, this, [this]() { + m_panelPinned = false; + m_panel->dismiss(); + }); + connect(m_panel, &PortalPanel::todoAdded, this, &App::showTodoWindow); connect(m_panel, &PortalPanel::askWithContent, this, [this](const QString &text, const QStringList &files) { showChatWindow(); @@ -159,6 +170,18 @@ void App::showPanel() m_panel->popup(); } +void App::togglePanel() +{ + ensurePanel(); + if (m_panel->isVisible() && m_panelPinned) { + m_panel->dismiss(); + m_panelPinned = false; + return; + } + m_panelPinned = true; + m_panel->popup(); +} + void App::showChatWindow() { if (!m_chatWindow) { diff --git a/src/app.h b/src/app.h index faa3295..c7fc553 100644 --- a/src/app.h +++ b/src/app.h @@ -32,6 +32,7 @@ public: private: void ensurePanel(); void showPanel(); + void togglePanel(); void showTodoWindow(); void showChatWindow(); @@ -42,4 +43,5 @@ private: QLocalServer *m_server = nullptr; bool m_daemon = false; bool m_dragging = false; + bool m_panelPinned = false; }; diff --git a/src/chat/chatview.cpp b/src/chat/chatview.cpp index 3cef516..03d857d 100644 --- a/src/chat/chatview.cpp +++ b/src/chat/chatview.cpp @@ -182,15 +182,16 @@ public: connect(fork, &QToolButton::clicked, this, [this]() { Q_EMIT forkRequested(m_index); }); actions->addWidget(fork); - if (m_isLast) { - auto *retry = actionButton(tr("重试"), m_actions); - connect(retry, &QToolButton::clicked, this, - [this]() { Q_EMIT retryRequested(); }); - actions->addWidget(retry); - } + auto *edit = actionButton(tr("编辑"), m_actions); + connect(edit, &QToolButton::clicked, this, + [this]() { Q_EMIT editRequested(m_index); }); + actions->addWidget(edit); + auto *retry = actionButton(tr("重试"), m_actions); + connect(retry, &QToolButton::clicked, this, + [this]() { Q_EMIT retryRequested(m_index); }); + actions->addWidget(retry); } actions->addStretch(); - m_actions->hide(); column->addWidget(m_actions); if (m_isUser) { @@ -220,21 +221,10 @@ signals: void copyRequested(int index); void editRequested(int index); void forkRequested(int index); - void retryRequested(); + void retryRequested(int index); void linkActivated(const QUrl &url); protected: - void enterEvent(QEnterEvent *event) override - { - m_actions->show(); - QWidget::enterEvent(event); - } - - void leaveEvent(QEvent *event) override - { - m_actions->hide(); - QWidget::leaveEvent(event); - } private: ChatMessage m_message; diff --git a/src/chat/chatview.h b/src/chat/chatview.h index 967801f..616829f 100644 --- a/src/chat/chatview.h +++ b/src/chat/chatview.h @@ -36,7 +36,7 @@ signals: void copyRequested(int index); void editRequested(int index); void forkRequested(int index); - void retryRequested(); + void retryRequested(int index); void linkActivated(const QUrl &url); protected: diff --git a/src/chat/chatwindow.cpp b/src/chat/chatwindow.cpp index 9f1831d..59bff17 100644 --- a/src/chat/chatwindow.cpp +++ b/src/chat/chatwindow.cpp @@ -5,6 +5,8 @@ #include "config.h" #include "chatview.h" + +#include "../ui/aboutdialog.h" #include "markdown.h" #include "mcpclient.h" @@ -555,7 +557,9 @@ void ChatWindow::setupUi() }); connect(m_messages, &ChatMessageList::editRequested, this, &ChatWindow::editMessage); connect(m_messages, &ChatMessageList::forkRequested, this, &ChatWindow::forkFrom); - connect(m_messages, &ChatMessageList::retryRequested, this, &ChatWindow::retryLast); + connect(m_messages, &ChatMessageList::retryRequested, this, [this](int index) { + retryFrom(index); + }); auto *menu = new QMenu(menuButton); menu->addAction(tr("压缩上下文"), this, [this]() { beginCompression(false); }); @@ -925,9 +929,6 @@ void ChatWindow::editMessage(int index) if (index < 0 || index >= conversation.messages.size()) { return; } - if (conversation.messages.at(index).role != QStringLiteral("user")) { - return; - } QDialog dialog(this); dialog.setWindowTitle(tr("编辑消息")); @@ -959,6 +960,27 @@ void ChatWindow::editMessage(int index) requestCompletion(); } +void ChatWindow::retryFrom(int index) +{ + if (m_client->busy() || m_compressing || m_current < 0) { + return; + } + ChatConversation &conversation = m_conversations[m_current]; + if (index < 0 || index >= conversation.messages.size()) { + return; + } + if (conversation.messages.at(index).role != QStringLiteral("assistant")) { + return; + } + + /* drop everything from the assistant answer on, then regenerate it */ + conversation.messages.resize(index); + conversation.updatedAt = QDateTime::currentMSecsSinceEpoch(); + save(); + renderConversation(); + requestCompletion(); +} + void ChatWindow::retryLast() { if (m_client->busy() || m_compressing || m_current < 0) { @@ -1028,15 +1050,7 @@ void ChatWindow::handleAnchor(const QUrl &url) void ChatWindow::showAbout() { - QMessageBox::about( - this, tr("关于小埃同学"), - tr("

GXDE 小埃同学

" - "

版本 %1

" - "

GXDE 桌面的 AI 助手:传送门、内容工厂、待办与对话。

" - "

许可证:GPL-3.0-or-later
" - "仓库:" - "gitee.com/GXDE-OS/gxde-ai-see

") - .arg(QCoreApplication::applicationVersion())); + showAboutDialog(this, tr("GXDE 桌面的 AI 助手:传送门、内容工厂、待办与对话。")); } void ChatWindow::appendError(const QString &message) diff --git a/src/chat/chatwindow.h b/src/chat/chatwindow.h index 084421a..eda8d7d 100644 --- a/src/chat/chatwindow.h +++ b/src/chat/chatwindow.h @@ -68,6 +68,7 @@ private: void sendPayload(); void editMessage(int index); void retryLast(); + void retryFrom(int index); void forkFrom(int index); void beginCompression(bool thenSend); void handleAnchor(const QUrl &url); diff --git a/src/dialogs/segmentdialog.cpp b/src/dialogs/segmentdialog.cpp index f134387..0d68aa4 100644 --- a/src/dialogs/segmentdialog.cpp +++ b/src/dialogs/segmentdialog.cpp @@ -6,6 +6,7 @@ #include "flowlayout.h" #include +#include #include #include #include @@ -21,12 +22,17 @@ SegmentDialog::SegmentDialog(const QStringList &words, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose); resize(560, 420); + const bool dark = palette().color(QPalette::Window).lightness() < 128; + const QColor capsuleBg = dark ? QColor(255, 255, 255, 20) : QColor(0, 0, 0, 18); + const QColor capsuleBorder = dark ? QColor(255, 255, 255, 30) : QColor(0, 0, 0, 28); + const QColor textColor = palette().color(QPalette::WindowText); + auto *layout = new QVBoxLayout(this); layout->setContentsMargins(16, 14, 16, 14); layout->setSpacing(10); auto *hint = new QLabel(tr("按住并滑过词语可以连续选择,选中后复制。"), this); - hint->setStyleSheet(QStringLiteral("color: rgba(255,255,255,0.55); font-size: 12px;")); + hint->setStyleSheet(QStringLiteral("font-size: 12px;")); layout->addWidget(hint); auto *scroll = new QScrollArea(this); @@ -44,12 +50,20 @@ SegmentDialog::SegmentDialog(const QStringList &words, QWidget *parent) capsule->setCursor(Qt::PointingHandCursor); capsule->setFocusPolicy(Qt::NoFocus); capsule->setStyleSheet(QStringLiteral( - "QPushButton { color: rgba(255,255,255,0.85); background: rgba(255,255,255,0.07);" - " border: 1px solid rgba(255,255,255,0.10); border-radius: 11px;" - " padding: 3px 10px; font-size: 13px; }" - "QPushButton:hover { background: rgba(255,255,255,0.14); }" - "QPushButton:checked { background: rgba(74,144,255,0.55);" - " border-color: rgba(160,205,255,0.9); color: white; }")); + "QPushButton { color: %1; background: %2; border: 1px solid %3;" + " border-radius: 11px; padding: 3px 10px; font-size: 13px; }" + "QPushButton:hover { background: %4; }" + "QPushButton:checked { background: rgba(74,144,255,0.85);" + " border-color: rgba(74,144,255,1.0); color: white; }") + .arg(textColor.name(), + QStringLiteral("rgba(%1,%2,%3,%4)") + .arg(capsuleBg.red()) + .arg(capsuleBg.green()) + .arg(capsuleBg.blue()) + .arg(capsuleBg.alpha()), + capsuleBorder.name(), + dark ? QStringLiteral("rgba(255,255,255,30)") + : QStringLiteral("rgba(0,0,0,28)"))); capsule->installEventFilter(this); connect(capsule, &QPushButton::released, this, [this]() { m_selecting = false; }); flow->addWidget(capsule); @@ -60,7 +74,7 @@ SegmentDialog::SegmentDialog(const QStringList &words, QWidget *parent) auto *all = new QPushButton(tr("全选"), this); auto *none = new QPushButton(tr("清空"), this); m_count = new QLabel(tr("已选 0 个"), this); - m_count->setStyleSheet(QStringLiteral("color: rgba(255,255,255,0.55); font-size: 12px;")); + m_count->setStyleSheet(QStringLiteral("font-size: 12px;")); auto *copy = new QPushButton(tr("复制选中"), this); copy->setStyleSheet(QStringLiteral( "QPushButton { color: white; background: rgba(74,144,255,0.85); border: none;" diff --git a/src/todo/todowindow.cpp b/src/todo/todowindow.cpp index a29b4fd..f7ad43a 100644 --- a/src/todo/todowindow.cpp +++ b/src/todo/todowindow.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -145,6 +146,9 @@ TodoWindow::TodoWindow(TodoModel *model, QWidget *parent) , m_model(model) { setWindowTitle(tr("待办")); + setWindowIcon(QIcon::fromTheme( + QStringLiteral("gxde-ai-see-todo"), + QIcon(QStringLiteral("/usr/share/icons/hicolor/256x256/apps/gxde-ai-see-todo.png")))); setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); setAttribute(Qt::WA_TranslucentBackground); setAttribute(Qt::WA_ShowWithoutActivating); diff --git a/src/ui/aboutdialog.cpp b/src/ui/aboutdialog.cpp new file mode 100644 index 0000000..8d4c756 --- /dev/null +++ b/src/ui/aboutdialog.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2026 GXDE OS Team +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "aboutdialog.h" + +#include +#include +#include + +#include + +void showAboutDialog(QWidget *parent, const QString &description) +{ + auto *dialog = new Dtk::Widget::DAboutDialog(parent); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle(QObject::tr("关于小埃同学")); + dialog->setProductIcon(QApplication::windowIcon()); + dialog->setProductName(QObject::tr("GXDE 小埃同学")); + dialog->setVersion(QCoreApplication::applicationVersion()); + dialog->setDescription(description); + dialog->setLicense(QStringLiteral("GPL-3.0-or-later")); + dialog->setWebsiteName(QStringLiteral("gitee.com/GXDE-OS/gxde-ai-see")); + dialog->setWebsiteLink(QStringLiteral("https://gitee.com/GXDE-OS/gxde-ai-see")); + dialog->show(); +} diff --git a/src/ui/aboutdialog.h b/src/ui/aboutdialog.h new file mode 100644 index 0000000..ed88b4d --- /dev/null +++ b/src/ui/aboutdialog.h @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 GXDE OS Team +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +class QWidget; + +/** DTK native about dialog shared by the panel and the chat window. */ +void showAboutDialog(QWidget *parent, const QString &description); diff --git a/src/ui/portalpanel.cpp b/src/ui/portalpanel.cpp index 34cc994..1bf6894 100644 --- a/src/ui/portalpanel.cpp +++ b/src/ui/portalpanel.cpp @@ -11,6 +11,7 @@ #include "factory/contentclassifier.h" #include "factory/contentfactory.h" +#include "aboutdialog.h" #include "dialogs/segmentdialog.h" #include "dialogs/speechdialog.h" #include "todo/todomodel.h" @@ -580,7 +581,7 @@ public: setMinimumHeight(190); m_portalTimer.setSingleShot(true); - m_portalTimer.setInterval(150); + m_portalTimer.setInterval(80); connect(&m_portalTimer, &QTimer::timeout, this, [this]() { if (m_active && !m_suppressed) { Q_EMIT portalRequested(); @@ -1203,6 +1204,7 @@ void PortalPanel::runFactoryAction(const QString &id, const DropContent &content : content.summary(); TodoModel::instance()->add(text); toast(tr("已添加到待办")); + Q_EMIT todoAdded(); return; } if (id == QLatin1String("save_image")) { @@ -1401,6 +1403,7 @@ void PortalPanel::showContextMenu(const QPoint &globalPos) menu.addSeparator(); QAction *clear = menu.addAction(tr("清空暂存区")); + QAction *hide = menu.addAction(tr("隐藏")); QAction *about = menu.addAction(tr("关于")); QAction *quit = menu.addAction(tr("退出")); @@ -1420,11 +1423,22 @@ void PortalPanel::showContextMenu(const QPoint &globalPos) menuPos.setX(available.right() - menuSize.width()); } } + menuPos.setX(globalPos.x() - menuSize.width() - 10); + if (scr) { + const QRect available = scr->availableGeometry(); + if (menuPos.x() < available.left()) { + menuPos.setX(available.left()); + } + } QAction *chosen = menu.exec(menuPos); if (!chosen) { return; } + if (chosen == hide) { + Q_EMIT hideRequested(); + return; + } if (chosen == fixedPosition) { m_fixedPosition = fixedPosition->isChecked(); saveWindowState(); @@ -1469,13 +1483,7 @@ void PortalPanel::showContextMenu(const QPoint &globalPos) } else if (chosen == clear) { m_model->clearAll(); } else if (chosen == about) { - QMessageBox::about( - this, tr("关于小埃同学"), - tr("

GXDE 小埃同学

版本 %1

" - "

传送门、内容工厂、待办与小埃同学对话。

" - "

GPL-3.0-or-later · " - "gxde-ai-see

") - .arg(QCoreApplication::applicationVersion())); + showAboutDialog(nullptr, tr("传送门、内容工厂、待办与小埃同学对话。")); } else if (chosen == quit) { QCoreApplication::quit(); } diff --git a/src/ui/portalpanel.h b/src/ui/portalpanel.h index dabcd53..37e7edd 100644 --- a/src/ui/portalpanel.h +++ b/src/ui/portalpanel.h @@ -41,6 +41,8 @@ public: signals: void openChatRequested(); void askWithContent(const QString &text, const QStringList &files); + void hideRequested(); + void todoAdded(); protected: void showEvent(QShowEvent *event) override;