diff --git a/CMakeLists.txt b/CMakeLists.txt index 355570a..97daf8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,8 @@ qt_add_executable(QtDesktopPet src/tray/TrayController.cpp src/ui/ChatBubble.h src/ui/ChatBubble.cpp + src/ui/ChatInputDialog.h + src/ui/ChatInputDialog.cpp src/ui/PetView.h src/ui/PetView.cpp src/ui/SettingsDialog.h diff --git a/src/ai/ConversationManager.cpp b/src/ai/ConversationManager.cpp index 737701a..853d323 100644 --- a/src/ai/ConversationManager.cpp +++ b/src/ai/ConversationManager.cpp @@ -1,5 +1,6 @@ #include "ConversationManager.h" +#include #include ConversationManager::ConversationManager() @@ -22,6 +23,11 @@ bool ConversationManager::hasHistory() const return !m_history.isEmpty(); } +QVector ConversationManager::history() const +{ + return m_history; +} + bool ConversationManager::setProvider(std::unique_ptr provider) { if (isBusy()) @@ -103,9 +109,10 @@ ChatRequest ConversationManager::buildRequest(const ChatMessage &userMessage) co request.messages.append({QStringLiteral("system"), m_systemPrompt}); } - for (const ChatMessage &message : m_history) + const int firstHistoryIndex = qMax(0, m_history.size() - m_maxRequestHistoryMessages); + for (int index = firstHistoryIndex; index < m_history.size(); ++index) { - request.messages.append(message); + request.messages.append(m_history.at(index)); } request.messages.append(userMessage); return request; @@ -115,13 +122,4 @@ void ConversationManager::appendExchange(const ChatMessage &userMessage, const C { m_history.append(userMessage); m_history.append(assistantMessage); - trimHistory(); -} - -void ConversationManager::trimHistory() -{ - while (m_history.size() > m_maxHistoryMessages) - { - m_history.removeFirst(); - } } diff --git a/src/ai/ConversationManager.h b/src/ai/ConversationManager.h index e2f58fc..ebf8a32 100644 --- a/src/ai/ConversationManager.h +++ b/src/ai/ConversationManager.h @@ -17,6 +17,7 @@ public: bool isBusy() const; bool hasHistory() const; + QVector history() const; bool setProvider(std::unique_ptr provider); void sendUserMessage(const QString &message, ResponseCallback callback); void cancel(); @@ -25,10 +26,9 @@ public: private: ChatRequest buildRequest(const ChatMessage &userMessage) const; void appendExchange(const ChatMessage &userMessage, const ChatMessage &assistantMessage); - void trimHistory(); std::unique_ptr m_provider; QVector m_history; QString m_systemPrompt; - int m_maxHistoryMessages = 12; + int m_maxRequestHistoryMessages = 12; }; diff --git a/src/ui/ChatBubble.cpp b/src/ui/ChatBubble.cpp index 457cd48..7776635 100644 --- a/src/ui/ChatBubble.cpp +++ b/src/ui/ChatBubble.cpp @@ -1,5 +1,7 @@ #include "ChatBubble.h" +#include +#include #include #include #include @@ -32,6 +34,10 @@ ChatBubble::ChatBubble(QWidget *parent) m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_textEdit->setLineWrapMode(QTextEdit::WidgetWidth); m_textEdit->setTextInteractionFlags(Qt::TextSelectableByMouse); + m_textEdit->installEventFilter(this); + m_textEdit->viewport()->installEventFilter(this); + m_textEdit->verticalScrollBar()->installEventFilter(this); + qApp->installEventFilter(this); m_textEdit->setStyleSheet(QStringLiteral( "QTextEdit {" "background: rgba(255, 255, 255, 232);" @@ -65,7 +71,9 @@ ChatBubble::ChatBubble(QWidget *parent) void ChatBubble::showMessage(const QString &message, const QPoint &anchorPosition, int durationMs) { + m_dismissOnExternalInteraction = false; m_anchorPosition = anchorPosition; + m_autoHideDurationMs = durationMs; const QString trimmed = message.trimmed(); m_textEdit->setPlainText(trimmed); m_textEdit->verticalScrollBar()->setValue(0); @@ -91,6 +99,79 @@ void ChatBubble::updateAnchorPosition(const QPoint &anchorPosition) } } +void ChatBubble::setDismissOnExternalInteraction(bool enabled) +{ + m_dismissOnExternalInteraction = enabled; +} + +void ChatBubble::resetAutoHideTimer() +{ + if (isVisible() && m_autoHideDurationMs > 0) + { + m_hideTimer.start(m_autoHideDurationMs); + } +} + +void ChatBubble::hideBubble() +{ + m_hideTimer.stop(); + hide(); +} + +bool ChatBubble::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == qApp && m_dismissOnExternalInteraction && event->type() == QEvent::ApplicationDeactivate) + { + hideBubble(); + return false; + } + + if (m_dismissOnExternalInteraction && event->type() == QEvent::MouseButtonPress && !isOwnObject(watched)) + { + hideBubble(); + return false; + } + + if ((watched == m_textEdit || watched == m_textEdit->viewport() || watched == m_textEdit->verticalScrollBar()) + && isUserInteractionEvent(event)) + { + resetAutoHideTimer(); + } + + return QWidget::eventFilter(watched, event); +} + +bool ChatBubble::isOwnObject(QObject *object) const +{ + while (object != nullptr) + { + if (object == this || object == m_textEdit || object == m_textEdit->viewport() || object == m_textEdit->verticalScrollBar()) + { + return true; + } + + object = object->parent(); + } + + return false; +} + +bool ChatBubble::isUserInteractionEvent(QEvent *event) const +{ + switch (event->type()) + { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseButtonDblClick: + case QEvent::MouseMove: + case QEvent::Wheel: + case QEvent::KeyPress: + return true; + default: + return false; + } +} + QSize ChatBubble::preferredBubbleSize(const QString &message) const { const QFontMetrics metrics(m_textEdit->font()); diff --git a/src/ui/ChatBubble.h b/src/ui/ChatBubble.h index 4da8d9b..efc3d7d 100644 --- a/src/ui/ChatBubble.h +++ b/src/ui/ChatBubble.h @@ -4,6 +4,8 @@ #include #include +class QEvent; + class ChatBubble : public QWidget { public: @@ -11,12 +13,22 @@ public: void showMessage(const QString &message, const QPoint &anchorPosition, int durationMs = 10000); void updateAnchorPosition(const QPoint &anchorPosition); + void setDismissOnExternalInteraction(bool enabled); + void resetAutoHideTimer(); + void hideBubble(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; private: + bool isOwnObject(QObject *object) const; + bool isUserInteractionEvent(QEvent *event) const; QSize preferredBubbleSize(const QString &message) const; void updatePosition(); QTextEdit *m_textEdit = nullptr; QTimer m_hideTimer; QPoint m_anchorPosition; + int m_autoHideDurationMs = 0; + bool m_dismissOnExternalInteraction = false; }; diff --git a/src/ui/ChatInputDialog.cpp b/src/ui/ChatInputDialog.cpp new file mode 100644 index 0000000..04545b6 --- /dev/null +++ b/src/ui/ChatInputDialog.cpp @@ -0,0 +1,286 @@ +#include "ChatInputDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +ChatInputDialog::ChatInputDialog(int maxLength, QWidget *parent) + : QDialog(parent) + , m_textEdit(new QTextEdit(this)) + , m_counterLabel(new QLabel(this)) + , m_sendButton(new QPushButton(QStringLiteral("↗"), this)) + , m_maxLength(maxLength) +{ + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_TranslucentBackground); + setModal(false); + resize(560, 76); + setStyleSheet(QStringLiteral( + "QDialog {" + "background: transparent;" + "}" + "QFrame#InputPanel {" + "background: rgba(246, 249, 253, 218);" + "border: 1px solid rgba(32, 33, 36, 38);" + "border-radius: 12px;" + "}" + "QTextEdit {" + "background: transparent;" + "color: #202124;" + "border: none;" + "padding: 8px 10px;" + "selection-background-color: rgba(66, 133, 244, 90);" + "font-size: 20px;" + "}" + "QPushButton {" + "min-width: 34px;" + "max-width: 34px;" + "min-height: 34px;" + "max-height: 34px;" + "border: 1px solid rgba(32, 33, 36, 45);" + "border-radius: 17px;" + "background: rgba(255, 255, 255, 236);" + "color: rgba(32, 33, 36, 190);" + "font-size: 18px;" + "font-weight: 600;" + "padding: 0;" + "}" + "QPushButton:hover {" + "background: rgba(255, 255, 255, 255);" + "}" + "QPushButton:disabled {" + "color: rgba(32, 33, 36, 80);" + "background: rgba(255, 255, 255, 170);" + "}")); + + m_textEdit->setPlaceholderText(QStringLiteral("输入消息,Enter 发送,Shift+Enter 换行")); + m_textEdit->setLineWrapMode(QTextEdit::WidgetWidth); + m_textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_textEdit->setFixedHeight(56); + m_textEdit->installEventFilter(this); + m_textEdit->viewport()->installEventFilter(this); + + m_counterLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_counterLabel->setStyleSheet(QStringLiteral("color: #b3261e; font-size: 12px;")); + m_counterLabel->setVisible(false); + + connect(m_sendButton, &QPushButton::clicked, this, [this]() { + submitIfValid(); + }); + connect(m_textEdit, &QTextEdit::textChanged, this, [this]() { + updateCounter(); + }); + + auto *panel = new QFrame(this); + panel->setObjectName(QStringLiteral("InputPanel")); + panel->installEventFilter(this); + + auto *panelLayout = new QHBoxLayout(panel); + panelLayout->setContentsMargins(16, 6, 8, 6); + panelLayout->setSpacing(8); + panelLayout->addWidget(m_textEdit, 1); + panelLayout->addWidget(m_counterLabel); + panelLayout->addWidget(m_sendButton); + + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(panel); + + updateCounter(); + m_textEdit->setFocus(); +} + +QString ChatInputDialog::message() const +{ + return m_textEdit->toPlainText().trimmed(); +} + +void ChatInputDialog::setSubmitCallback(SubmitCallback callback) +{ + m_submitCallback = std::move(callback); +} + +void ChatInputDialog::showAt(const QPoint &anchorPosition) +{ + move(anchorPosition.x() - width() / 2, anchorPosition.y() - height() / 2); + show(); + raise(); + activateWindow(); + m_textEdit->setFocus(); +} + +bool ChatInputDialog::event(QEvent *event) +{ + if (event->type() == QEvent::WindowDeactivate) + { + hide(); + return true; + } + + return QDialog::event(event); +} + +bool ChatInputDialog::eventFilter(QObject *watched, QEvent *event) +{ + if ((watched == m_textEdit || watched == m_textEdit->viewport()) && event->type() == QEvent::KeyPress) + { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) + { + if (keyEvent->modifiers() & Qt::ShiftModifier) + { + return false; + } + + submitIfValid(); + return true; + } + } + + if ((watched == m_textEdit->viewport() || watched == m_textEdit->parentWidget()) + && event->type() == QEvent::MouseButtonPress) + { + auto *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) + { + beginDrag(mouseEvent->globalPosition().toPoint()); + return false; + } + } + + if ((watched == m_textEdit->viewport() || watched == m_textEdit->parentWidget()) + && event->type() == QEvent::MouseMove) + { + auto *mouseEvent = static_cast(event); + if (mouseEvent->buttons() & Qt::LeftButton) + { + return updateDrag(mouseEvent->globalPosition().toPoint()); + } + } + + if ((watched == m_textEdit->viewport() || watched == m_textEdit->parentWidget()) + && event->type() == QEvent::MouseButtonRelease) + { + endDrag(); + } + + return QDialog::eventFilter(watched, event); +} + +void ChatInputDialog::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + { + beginDrag(event->globalPosition().toPoint()); + } + + QDialog::mousePressEvent(event); +} + +void ChatInputDialog::mouseMoveEvent(QMouseEvent *event) +{ + if ((event->buttons() & Qt::LeftButton) && updateDrag(event->globalPosition().toPoint())) + { + event->accept(); + return; + } + + QDialog::mouseMoveEvent(event); +} + +void ChatInputDialog::mouseReleaseEvent(QMouseEvent *event) +{ + endDrag(); + QDialog::mouseReleaseEvent(event); +} + +bool ChatInputDialog::canSend() const +{ + const int length = message().size(); + return length > 0 && (m_maxLength <= 0 || length <= m_maxLength); +} + +bool ChatInputDialog::submitIfValid() +{ + if (!canSend()) + { + return false; + } + + const QString submittedMessage = message(); + const bool accepted = m_submitCallback ? m_submitCallback(submittedMessage) : true; + if (accepted) + { + clearMessage(); + show(); + raise(); + activateWindow(); + m_textEdit->setFocus(); + } + + return accepted; +} + +void ChatInputDialog::clearMessage() +{ + m_textEdit->clear(); +} + +void ChatInputDialog::updateCounter() +{ + const int length = message().size(); + m_sendButton->setEnabled(canSend()); + + if (m_maxLength > 0) + { + m_counterLabel->setVisible(length > m_maxLength); + if (length > m_maxLength) + { + m_counterLabel->setText(QString::number(length) + QStringLiteral("/") + QString::number(m_maxLength)); + } + return; + } + + m_counterLabel->setVisible(false); +} + +void ChatInputDialog::beginDrag(const QPoint &globalPosition) +{ + m_dragCandidate = true; + m_dragging = false; + m_dragStartPosition = globalPosition; + m_dragWindowPosition = pos(); +} + +bool ChatInputDialog::updateDrag(const QPoint &globalPosition) +{ + if (!m_dragCandidate) + { + return false; + } + + const QPoint delta = globalPosition - m_dragStartPosition; + if (!m_dragging && delta.manhattanLength() < QApplication::startDragDistance()) + { + return false; + } + + m_dragging = true; + move(m_dragWindowPosition + delta); + return true; +} + +void ChatInputDialog::endDrag() +{ + m_dragCandidate = false; + m_dragging = false; +} diff --git a/src/ui/ChatInputDialog.h b/src/ui/ChatInputDialog.h new file mode 100644 index 0000000..919e7d2 --- /dev/null +++ b/src/ui/ChatInputDialog.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +#include + +class QEvent; +class QLabel; +class QMouseEvent; +class QPushButton; +class QTextEdit; + +class ChatInputDialog : public QDialog +{ +public: + using SubmitCallback = std::function; + + explicit ChatInputDialog(int maxLength, QWidget *parent = nullptr); + + QString message() const; + void setSubmitCallback(SubmitCallback callback); + void showAt(const QPoint &anchorPosition); + +protected: + bool event(QEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private: + bool canSend() const; + bool submitIfValid(); + void clearMessage(); + void updateCounter(); + void beginDrag(const QPoint &globalPosition); + bool updateDrag(const QPoint &globalPosition); + void endDrag(); + + QTextEdit *m_textEdit = nullptr; + QLabel *m_counterLabel = nullptr; + QPushButton *m_sendButton = nullptr; + SubmitCallback m_submitCallback; + QPoint m_dragStartPosition; + QPoint m_dragWindowPosition; + int m_maxLength = 0; + bool m_dragging = false; + bool m_dragCandidate = false; +}; diff --git a/src/ui/PetWindow.cpp b/src/ui/PetWindow.cpp index a25e77c..cca7729 100644 --- a/src/ui/PetWindow.cpp +++ b/src/ui/PetWindow.cpp @@ -7,6 +7,7 @@ #include "../config/SecretStore.h" #include "../util/Logger.h" #include "ChatBubble.h" +#include "ChatInputDialog.h" #include "PetView.h" #include "SettingsDialog.h" @@ -15,7 +16,7 @@ #include #include #include -#include +#include #include #include #include @@ -41,6 +42,7 @@ QString previewImagePath() } constexpr int MaxUserMessageLength = 4000; +constexpr int ChatInputLowerOffsetY = 48; bool populateRuntimeApiKey(AIConfig &config, QString *errorMessage) { @@ -119,11 +121,49 @@ QString userVisibleErrorMessage(const ChatResponse &response) return message; } + +QString roleLabel(const QString &role) +{ + if (role == QStringLiteral("user")) + { + return QStringLiteral("你"); + } + + if (role == QStringLiteral("assistant")) + { + return QStringLiteral("桌宠"); + } + + return role; +} + +QString formattedConversationHistory(const QVector &history) +{ + if (history.isEmpty()) + { + return QStringLiteral("暂无对话记录。"); + } + + QStringList lines; + for (const ChatMessage &message : history) + { + const QString content = message.content.trimmed(); + if (content.isEmpty()) + { + continue; + } + + lines.append(roleLabel(message.role) + QStringLiteral(":") + content); + } + + return lines.isEmpty() ? QStringLiteral("暂无对话记录。") : lines.join(QStringLiteral("\n\n")); +} } PetWindow::PetWindow(QWidget *parent) : QWidget(parent) , m_chatBubble(std::make_unique()) + , m_chatInputDialog(std::make_unique(MaxUserMessageLength, this)) , m_conversationManager(std::make_unique()) , m_petView(new PetView(this)) , m_dragging(false) @@ -157,6 +197,11 @@ PetWindow::PetWindow(QWidget *parent) returnToIdleFromBehavior(); }); + QPointer window(this); + m_chatInputDialog->setSubmitCallback([window](const QString &message) { + return !window.isNull() && window->submitChatMessage(message); + }); + loadInitialImage(); } @@ -212,6 +257,8 @@ void PetWindow::showBubbleMessage(const QString &message) void PetWindow::contextMenuEvent(QContextMenuEvent *event) { + resetBubbleAutoHideTimer(); + QMenu menu(this); QAction *topAction = menu.addAction(QStringLiteral("取消置顶")); @@ -228,6 +275,7 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event) aiTestAction->setEnabled(!aiRequestRunning); QAction *chatAction = menu.addAction(QStringLiteral("聊天")); chatAction->setEnabled(!aiRequestRunning); + QAction *showConversationAction = menu.addAction(QStringLiteral("显示对话")); QAction *cancelAIAction = menu.addAction(QStringLiteral("取消 AI 请求")); cancelAIAction->setEnabled(aiRequestRunning); QAction *clearConversationAction = menu.addAction(QStringLiteral("清空对话")); @@ -264,6 +312,10 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event) { startChat(); } + else if (selectedAction == showConversationAction) + { + showConversationHistory(); + } else if (selectedAction == cancelAIAction) { cancelActiveAIRequest(); @@ -350,29 +402,39 @@ void PetWindow::startChat() return; } + if (!m_chatInputDialog) + { + return; + } + + m_chatInputDialog->showAt(chatInputAnchorPosition()); +} + +bool PetWindow::submitChatMessage(const QString &message) +{ + if (m_aiTestProvider && m_aiTestProvider->isBusy()) + { + showBubbleMessage(QStringLiteral("AI 测试请求正在进行。")); + return false; + } + if (!m_conversationManager || m_conversationManager->isBusy()) { showBubbleMessage(QStringLiteral("AI 回复正在进行。")); - return; + return false; } - bool accepted = false; - const QString message = QInputDialog::getMultiLineText( - this, - QStringLiteral("聊天"), - QStringLiteral("输入消息"), - {}, - &accepted).trimmed(); - if (!accepted || message.isEmpty()) + const QString trimmedMessage = message.trimmed(); + if (trimmedMessage.isEmpty()) { - return; + return false; } - if (message.size() > MaxUserMessageLength) + if (trimmedMessage.size() > MaxUserMessageLength) { playState(QStringLiteral("error"), false); showBubbleMessage(QStringLiteral("消息太长,请控制在 4000 字以内。")); - return; + return false; } ConfigManager configManager; @@ -382,13 +444,13 @@ void PetWindow::startChat() { playState(QStringLiteral("error"), false); showBubbleMessage(errorMessage); - return; + return false; } if (!m_conversationManager->setProvider(std::make_unique(config))) { showBubbleMessage(QStringLiteral("AI 回复正在进行。")); - return; + return false; } playState(QStringLiteral("think"), false); @@ -411,6 +473,8 @@ void PetWindow::startChat() window->playState(QStringLiteral("error"), false); window->showBubbleMessage(QStringLiteral("AI 回复失败:") + userVisibleErrorMessage(response)); }); + + return true; } void PetWindow::clearConversation() @@ -464,10 +528,60 @@ bool PetWindow::hasActiveAIRequest() const || (m_conversationManager && m_conversationManager->isBusy()); } +void PetWindow::showConversationHistory() +{ + const QVector history = m_conversationManager ? m_conversationManager->history() : QVector(); + m_chatBubble->showMessage(formattedConversationHistory(history), bubbleAnchorPosition(), 30000); + m_chatBubble->setDismissOnExternalInteraction(true); +} + +void PetWindow::resetBubbleAutoHideTimer() +{ + if (m_chatBubble) + { + m_chatBubble->resetAutoHideTimer(); + } +} + +QPoint PetWindow::chatInputAnchorPosition() const +{ + return frameGeometry().topLeft() + QPoint(width() / 2, height() / 2 + ChatInputLowerOffsetY); +} + +void PetWindow::hideEvent(QHideEvent *event) +{ + if (m_chatBubble) + { + m_chatBubble->hideBubble(); + } + if (m_chatInputDialog) + { + m_chatInputDialog->hide(); + } + + QWidget::hideEvent(event); +} + +void PetWindow::mouseDoubleClickEvent(QMouseEvent *event) +{ + resetBubbleAutoHideTimer(); + if (event->button() == Qt::LeftButton) + { + m_dragging = false; + playResolvedState(m_stateMachine.endDrag(), false); + startChat(); + event->accept(); + return; + } + + QWidget::mouseDoubleClickEvent(event); +} + void PetWindow::mouseMoveEvent(QMouseEvent *event) { if (m_dragging && (event->buttons() & Qt::LeftButton)) { + resetBubbleAutoHideTimer(); move(event->globalPosition().toPoint() - m_dragOffset); updateBubblePosition(); event->accept(); @@ -485,6 +599,8 @@ void PetWindow::moveEvent(QMoveEvent *event) void PetWindow::mousePressEvent(QMouseEvent *event) { + resetBubbleAutoHideTimer(); + if (event->button() == Qt::LeftButton) { m_dragging = true; @@ -499,6 +615,8 @@ void PetWindow::mousePressEvent(QMouseEvent *event) void PetWindow::mouseReleaseEvent(QMouseEvent *event) { + resetBubbleAutoHideTimer(); + if (event->button() == Qt::LeftButton) { m_dragging = false; diff --git a/src/ui/PetWindow.h b/src/ui/PetWindow.h index 17817ff..81aed50 100644 --- a/src/ui/PetWindow.h +++ b/src/ui/PetWindow.h @@ -14,9 +14,11 @@ #include class QMenu; +class QHideEvent; class QMoveEvent; class QPixmap; class ChatBubble; +class ChatInputDialog; class ConversationManager; class LLMProvider; class PetView; @@ -35,6 +37,8 @@ public: protected: void contextMenuEvent(QContextMenuEvent *event) override; + void hideEvent(QHideEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; @@ -46,9 +50,13 @@ private: void addStateTestActions(QMenu *menu); void startAITest(); void startChat(); + bool submitChatMessage(const QString &message); void clearConversation(); void cancelActiveAIRequest(); + void showConversationHistory(); bool hasActiveAIRequest() const; + void resetBubbleAutoHideTimer(); + QPoint chatInputAnchorPosition() const; void updateBubblePosition(); QPoint bubbleAnchorPosition() const; void playState(const QString &stateName, bool centerWindow); @@ -62,6 +70,7 @@ private: void setAlwaysOnTop(bool enabled); std::unique_ptr m_chatBubble; + std::unique_ptr m_chatInputDialog; std::unique_ptr m_conversationManager; std::unique_ptr m_aiTestProvider; PetView *m_petView;