优化聊天输入和对话气泡交互
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user