优化聊天输入和对话气泡交互

This commit is contained in:
2026-05-29 12:35:51 +08:00
parent cc517e149d
commit 27d63965f4
9 changed files with 585 additions and 28 deletions
+2
View File
@@ -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
+9 -11
View File
@@ -1,5 +1,6 @@
#include "ConversationManager.h"
#include <QtGlobal>
#include <utility>
ConversationManager::ConversationManager()
@@ -22,6 +23,11 @@ bool ConversationManager::hasHistory() const
return !m_history.isEmpty();
}
QVector<ChatMessage> ConversationManager::history() const
{
return m_history;
}
bool ConversationManager::setProvider(std::unique_ptr<LLMProvider> 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();
}
}
+2 -2
View File
@@ -17,6 +17,7 @@ public:
bool isBusy() const;
bool hasHistory() const;
QVector<ChatMessage> history() const;
bool setProvider(std::unique_ptr<LLMProvider> 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<LLMProvider> m_provider;
QVector<ChatMessage> m_history;
QString m_systemPrompt;
int m_maxHistoryMessages = 12;
int m_maxRequestHistoryMessages = 12;
};
+81
View File
@@ -1,5 +1,7 @@
#include "ChatBubble.h"
#include <QApplication>
#include <QEvent>
#include <QFontMetrics>
#include <QFrame>
#include <QRect>
@@ -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());
+12
View File
@@ -4,6 +4,8 @@
#include <QTimer>
#include <QWidget>
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;
};
+286
View File
@@ -0,0 +1,286 @@
#include "ChatInputDialog.h"
#include <QApplication>
#include <QEvent>
#include <QFrame>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QTextEdit>
#include <utility>
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<QKeyEvent *>(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<QMouseEvent *>(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<QMouseEvent *>(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;
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <QDialog>
#include <QPoint>
#include <QString>
#include <functional>
class QEvent;
class QLabel;
class QMouseEvent;
class QPushButton;
class QTextEdit;
class ChatInputDialog : public QDialog
{
public:
using SubmitCallback = std::function<bool(const QString &)>;
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;
};
+133 -15
View File
@@ -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 <QCursor>
#include <QDialog>
#include <QGuiApplication>
#include <QInputDialog>
#include <QHideEvent>
#include <QMenu>
#include <QMouseEvent>
#include <QPixmap>
@@ -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<ChatMessage> &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<ChatBubble>())
, m_chatInputDialog(std::make_unique<ChatInputDialog>(MaxUserMessageLength, this))
, m_conversationManager(std::make_unique<ConversationManager>())
, m_petView(new PetView(this))
, m_dragging(false)
@@ -157,6 +197,11 @@ PetWindow::PetWindow(QWidget *parent)
returnToIdleFromBehavior();
});
QPointer<PetWindow> 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<OpenAICompatibleProvider>(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<ChatMessage> history = m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>();
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;
+9
View File
@@ -14,9 +14,11 @@
#include <memory>
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<ChatBubble> m_chatBubble;
std::unique_ptr<ChatInputDialog> m_chatInputDialog;
std::unique_ptr<ConversationManager> m_conversationManager;
std::unique_ptr<LLMProvider> m_aiTestProvider;
PetView *m_petView;