优化聊天输入和对话气泡交互
This commit is contained in:
@@ -41,6 +41,8 @@ qt_add_executable(QtDesktopPet
|
|||||||
src/tray/TrayController.cpp
|
src/tray/TrayController.cpp
|
||||||
src/ui/ChatBubble.h
|
src/ui/ChatBubble.h
|
||||||
src/ui/ChatBubble.cpp
|
src/ui/ChatBubble.cpp
|
||||||
|
src/ui/ChatInputDialog.h
|
||||||
|
src/ui/ChatInputDialog.cpp
|
||||||
src/ui/PetView.h
|
src/ui/PetView.h
|
||||||
src/ui/PetView.cpp
|
src/ui/PetView.cpp
|
||||||
src/ui/SettingsDialog.h
|
src/ui/SettingsDialog.h
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "ConversationManager.h"
|
#include "ConversationManager.h"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
ConversationManager::ConversationManager()
|
ConversationManager::ConversationManager()
|
||||||
@@ -22,6 +23,11 @@ bool ConversationManager::hasHistory() const
|
|||||||
return !m_history.isEmpty();
|
return !m_history.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVector<ChatMessage> ConversationManager::history() const
|
||||||
|
{
|
||||||
|
return m_history;
|
||||||
|
}
|
||||||
|
|
||||||
bool ConversationManager::setProvider(std::unique_ptr<LLMProvider> provider)
|
bool ConversationManager::setProvider(std::unique_ptr<LLMProvider> provider)
|
||||||
{
|
{
|
||||||
if (isBusy())
|
if (isBusy())
|
||||||
@@ -103,9 +109,10 @@ ChatRequest ConversationManager::buildRequest(const ChatMessage &userMessage) co
|
|||||||
request.messages.append({QStringLiteral("system"), m_systemPrompt});
|
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);
|
request.messages.append(userMessage);
|
||||||
return request;
|
return request;
|
||||||
@@ -115,13 +122,4 @@ void ConversationManager::appendExchange(const ChatMessage &userMessage, const C
|
|||||||
{
|
{
|
||||||
m_history.append(userMessage);
|
m_history.append(userMessage);
|
||||||
m_history.append(assistantMessage);
|
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 isBusy() const;
|
||||||
bool hasHistory() const;
|
bool hasHistory() const;
|
||||||
|
QVector<ChatMessage> history() const;
|
||||||
bool setProvider(std::unique_ptr<LLMProvider> provider);
|
bool setProvider(std::unique_ptr<LLMProvider> provider);
|
||||||
void sendUserMessage(const QString &message, ResponseCallback callback);
|
void sendUserMessage(const QString &message, ResponseCallback callback);
|
||||||
void cancel();
|
void cancel();
|
||||||
@@ -25,10 +26,9 @@ public:
|
|||||||
private:
|
private:
|
||||||
ChatRequest buildRequest(const ChatMessage &userMessage) const;
|
ChatRequest buildRequest(const ChatMessage &userMessage) const;
|
||||||
void appendExchange(const ChatMessage &userMessage, const ChatMessage &assistantMessage);
|
void appendExchange(const ChatMessage &userMessage, const ChatMessage &assistantMessage);
|
||||||
void trimHistory();
|
|
||||||
|
|
||||||
std::unique_ptr<LLMProvider> m_provider;
|
std::unique_ptr<LLMProvider> m_provider;
|
||||||
QVector<ChatMessage> m_history;
|
QVector<ChatMessage> m_history;
|
||||||
QString m_systemPrompt;
|
QString m_systemPrompt;
|
||||||
int m_maxHistoryMessages = 12;
|
int m_maxRequestHistoryMessages = 12;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "ChatBubble.h"
|
#include "ChatBubble.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QEvent>
|
||||||
#include <QFontMetrics>
|
#include <QFontMetrics>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
@@ -32,6 +34,10 @@ ChatBubble::ChatBubble(QWidget *parent)
|
|||||||
m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
m_textEdit->setLineWrapMode(QTextEdit::WidgetWidth);
|
m_textEdit->setLineWrapMode(QTextEdit::WidgetWidth);
|
||||||
m_textEdit->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
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(
|
m_textEdit->setStyleSheet(QStringLiteral(
|
||||||
"QTextEdit {"
|
"QTextEdit {"
|
||||||
"background: rgba(255, 255, 255, 232);"
|
"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)
|
void ChatBubble::showMessage(const QString &message, const QPoint &anchorPosition, int durationMs)
|
||||||
{
|
{
|
||||||
|
m_dismissOnExternalInteraction = false;
|
||||||
m_anchorPosition = anchorPosition;
|
m_anchorPosition = anchorPosition;
|
||||||
|
m_autoHideDurationMs = durationMs;
|
||||||
const QString trimmed = message.trimmed();
|
const QString trimmed = message.trimmed();
|
||||||
m_textEdit->setPlainText(trimmed);
|
m_textEdit->setPlainText(trimmed);
|
||||||
m_textEdit->verticalScrollBar()->setValue(0);
|
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
|
QSize ChatBubble::preferredBubbleSize(const QString &message) const
|
||||||
{
|
{
|
||||||
const QFontMetrics metrics(m_textEdit->font());
|
const QFontMetrics metrics(m_textEdit->font());
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QEvent;
|
||||||
|
|
||||||
class ChatBubble : public QWidget
|
class ChatBubble : public QWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -11,12 +13,22 @@ public:
|
|||||||
|
|
||||||
void showMessage(const QString &message, const QPoint &anchorPosition, int durationMs = 10000);
|
void showMessage(const QString &message, const QPoint &anchorPosition, int durationMs = 10000);
|
||||||
void updateAnchorPosition(const QPoint &anchorPosition);
|
void updateAnchorPosition(const QPoint &anchorPosition);
|
||||||
|
void setDismissOnExternalInteraction(bool enabled);
|
||||||
|
void resetAutoHideTimer();
|
||||||
|
void hideBubble();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool isOwnObject(QObject *object) const;
|
||||||
|
bool isUserInteractionEvent(QEvent *event) const;
|
||||||
QSize preferredBubbleSize(const QString &message) const;
|
QSize preferredBubbleSize(const QString &message) const;
|
||||||
void updatePosition();
|
void updatePosition();
|
||||||
|
|
||||||
QTextEdit *m_textEdit = nullptr;
|
QTextEdit *m_textEdit = nullptr;
|
||||||
QTimer m_hideTimer;
|
QTimer m_hideTimer;
|
||||||
QPoint m_anchorPosition;
|
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 "../config/SecretStore.h"
|
||||||
#include "../util/Logger.h"
|
#include "../util/Logger.h"
|
||||||
#include "ChatBubble.h"
|
#include "ChatBubble.h"
|
||||||
|
#include "ChatInputDialog.h"
|
||||||
#include "PetView.h"
|
#include "PetView.h"
|
||||||
#include "SettingsDialog.h"
|
#include "SettingsDialog.h"
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QInputDialog>
|
#include <QHideEvent>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
@@ -41,6 +42,7 @@ QString previewImagePath()
|
|||||||
}
|
}
|
||||||
|
|
||||||
constexpr int MaxUserMessageLength = 4000;
|
constexpr int MaxUserMessageLength = 4000;
|
||||||
|
constexpr int ChatInputLowerOffsetY = 48;
|
||||||
|
|
||||||
bool populateRuntimeApiKey(AIConfig &config, QString *errorMessage)
|
bool populateRuntimeApiKey(AIConfig &config, QString *errorMessage)
|
||||||
{
|
{
|
||||||
@@ -119,11 +121,49 @@ QString userVisibleErrorMessage(const ChatResponse &response)
|
|||||||
|
|
||||||
return message;
|
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)
|
PetWindow::PetWindow(QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_chatBubble(std::make_unique<ChatBubble>())
|
, m_chatBubble(std::make_unique<ChatBubble>())
|
||||||
|
, m_chatInputDialog(std::make_unique<ChatInputDialog>(MaxUserMessageLength, this))
|
||||||
, m_conversationManager(std::make_unique<ConversationManager>())
|
, m_conversationManager(std::make_unique<ConversationManager>())
|
||||||
, m_petView(new PetView(this))
|
, m_petView(new PetView(this))
|
||||||
, m_dragging(false)
|
, m_dragging(false)
|
||||||
@@ -157,6 +197,11 @@ PetWindow::PetWindow(QWidget *parent)
|
|||||||
returnToIdleFromBehavior();
|
returnToIdleFromBehavior();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QPointer<PetWindow> window(this);
|
||||||
|
m_chatInputDialog->setSubmitCallback([window](const QString &message) {
|
||||||
|
return !window.isNull() && window->submitChatMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
loadInitialImage();
|
loadInitialImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +257,8 @@ void PetWindow::showBubbleMessage(const QString &message)
|
|||||||
|
|
||||||
void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
||||||
{
|
{
|
||||||
|
resetBubbleAutoHideTimer();
|
||||||
|
|
||||||
QMenu menu(this);
|
QMenu menu(this);
|
||||||
|
|
||||||
QAction *topAction = menu.addAction(QStringLiteral("取消置顶"));
|
QAction *topAction = menu.addAction(QStringLiteral("取消置顶"));
|
||||||
@@ -228,6 +275,7 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
|||||||
aiTestAction->setEnabled(!aiRequestRunning);
|
aiTestAction->setEnabled(!aiRequestRunning);
|
||||||
QAction *chatAction = menu.addAction(QStringLiteral("聊天"));
|
QAction *chatAction = menu.addAction(QStringLiteral("聊天"));
|
||||||
chatAction->setEnabled(!aiRequestRunning);
|
chatAction->setEnabled(!aiRequestRunning);
|
||||||
|
QAction *showConversationAction = menu.addAction(QStringLiteral("显示对话"));
|
||||||
QAction *cancelAIAction = menu.addAction(QStringLiteral("取消 AI 请求"));
|
QAction *cancelAIAction = menu.addAction(QStringLiteral("取消 AI 请求"));
|
||||||
cancelAIAction->setEnabled(aiRequestRunning);
|
cancelAIAction->setEnabled(aiRequestRunning);
|
||||||
QAction *clearConversationAction = menu.addAction(QStringLiteral("清空对话"));
|
QAction *clearConversationAction = menu.addAction(QStringLiteral("清空对话"));
|
||||||
@@ -264,6 +312,10 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event)
|
|||||||
{
|
{
|
||||||
startChat();
|
startChat();
|
||||||
}
|
}
|
||||||
|
else if (selectedAction == showConversationAction)
|
||||||
|
{
|
||||||
|
showConversationHistory();
|
||||||
|
}
|
||||||
else if (selectedAction == cancelAIAction)
|
else if (selectedAction == cancelAIAction)
|
||||||
{
|
{
|
||||||
cancelActiveAIRequest();
|
cancelActiveAIRequest();
|
||||||
@@ -350,29 +402,39 @@ void PetWindow::startChat()
|
|||||||
return;
|
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())
|
if (!m_conversationManager || m_conversationManager->isBusy())
|
||||||
{
|
{
|
||||||
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
|
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool accepted = false;
|
const QString trimmedMessage = message.trimmed();
|
||||||
const QString message = QInputDialog::getMultiLineText(
|
if (trimmedMessage.isEmpty())
|
||||||
this,
|
|
||||||
QStringLiteral("聊天"),
|
|
||||||
QStringLiteral("输入消息"),
|
|
||||||
{},
|
|
||||||
&accepted).trimmed();
|
|
||||||
if (!accepted || message.isEmpty())
|
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.size() > MaxUserMessageLength)
|
if (trimmedMessage.size() > MaxUserMessageLength)
|
||||||
{
|
{
|
||||||
playState(QStringLiteral("error"), false);
|
playState(QStringLiteral("error"), false);
|
||||||
showBubbleMessage(QStringLiteral("消息太长,请控制在 4000 字以内。"));
|
showBubbleMessage(QStringLiteral("消息太长,请控制在 4000 字以内。"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigManager configManager;
|
ConfigManager configManager;
|
||||||
@@ -382,13 +444,13 @@ void PetWindow::startChat()
|
|||||||
{
|
{
|
||||||
playState(QStringLiteral("error"), false);
|
playState(QStringLiteral("error"), false);
|
||||||
showBubbleMessage(errorMessage);
|
showBubbleMessage(errorMessage);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_conversationManager->setProvider(std::make_unique<OpenAICompatibleProvider>(config)))
|
if (!m_conversationManager->setProvider(std::make_unique<OpenAICompatibleProvider>(config)))
|
||||||
{
|
{
|
||||||
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
|
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
playState(QStringLiteral("think"), false);
|
playState(QStringLiteral("think"), false);
|
||||||
@@ -411,6 +473,8 @@ void PetWindow::startChat()
|
|||||||
window->playState(QStringLiteral("error"), false);
|
window->playState(QStringLiteral("error"), false);
|
||||||
window->showBubbleMessage(QStringLiteral("AI 回复失败:") + userVisibleErrorMessage(response));
|
window->showBubbleMessage(QStringLiteral("AI 回复失败:") + userVisibleErrorMessage(response));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PetWindow::clearConversation()
|
void PetWindow::clearConversation()
|
||||||
@@ -464,10 +528,60 @@ bool PetWindow::hasActiveAIRequest() const
|
|||||||
|| (m_conversationManager && m_conversationManager->isBusy());
|
|| (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)
|
void PetWindow::mouseMoveEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (m_dragging && (event->buttons() & Qt::LeftButton))
|
if (m_dragging && (event->buttons() & Qt::LeftButton))
|
||||||
{
|
{
|
||||||
|
resetBubbleAutoHideTimer();
|
||||||
move(event->globalPosition().toPoint() - m_dragOffset);
|
move(event->globalPosition().toPoint() - m_dragOffset);
|
||||||
updateBubblePosition();
|
updateBubblePosition();
|
||||||
event->accept();
|
event->accept();
|
||||||
@@ -485,6 +599,8 @@ void PetWindow::moveEvent(QMoveEvent *event)
|
|||||||
|
|
||||||
void PetWindow::mousePressEvent(QMouseEvent *event)
|
void PetWindow::mousePressEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
|
resetBubbleAutoHideTimer();
|
||||||
|
|
||||||
if (event->button() == Qt::LeftButton)
|
if (event->button() == Qt::LeftButton)
|
||||||
{
|
{
|
||||||
m_dragging = true;
|
m_dragging = true;
|
||||||
@@ -499,6 +615,8 @@ void PetWindow::mousePressEvent(QMouseEvent *event)
|
|||||||
|
|
||||||
void PetWindow::mouseReleaseEvent(QMouseEvent *event)
|
void PetWindow::mouseReleaseEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
|
resetBubbleAutoHideTimer();
|
||||||
|
|
||||||
if (event->button() == Qt::LeftButton)
|
if (event->button() == Qt::LeftButton)
|
||||||
{
|
{
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class QMenu;
|
class QMenu;
|
||||||
|
class QHideEvent;
|
||||||
class QMoveEvent;
|
class QMoveEvent;
|
||||||
class QPixmap;
|
class QPixmap;
|
||||||
class ChatBubble;
|
class ChatBubble;
|
||||||
|
class ChatInputDialog;
|
||||||
class ConversationManager;
|
class ConversationManager;
|
||||||
class LLMProvider;
|
class LLMProvider;
|
||||||
class PetView;
|
class PetView;
|
||||||
@@ -35,6 +37,8 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
|
void hideEvent(QHideEvent *event) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
@@ -46,9 +50,13 @@ private:
|
|||||||
void addStateTestActions(QMenu *menu);
|
void addStateTestActions(QMenu *menu);
|
||||||
void startAITest();
|
void startAITest();
|
||||||
void startChat();
|
void startChat();
|
||||||
|
bool submitChatMessage(const QString &message);
|
||||||
void clearConversation();
|
void clearConversation();
|
||||||
void cancelActiveAIRequest();
|
void cancelActiveAIRequest();
|
||||||
|
void showConversationHistory();
|
||||||
bool hasActiveAIRequest() const;
|
bool hasActiveAIRequest() const;
|
||||||
|
void resetBubbleAutoHideTimer();
|
||||||
|
QPoint chatInputAnchorPosition() const;
|
||||||
void updateBubblePosition();
|
void updateBubblePosition();
|
||||||
QPoint bubbleAnchorPosition() const;
|
QPoint bubbleAnchorPosition() const;
|
||||||
void playState(const QString &stateName, bool centerWindow);
|
void playState(const QString &stateName, bool centerWindow);
|
||||||
@@ -62,6 +70,7 @@ private:
|
|||||||
void setAlwaysOnTop(bool enabled);
|
void setAlwaysOnTop(bool enabled);
|
||||||
|
|
||||||
std::unique_ptr<ChatBubble> m_chatBubble;
|
std::unique_ptr<ChatBubble> m_chatBubble;
|
||||||
|
std::unique_ptr<ChatInputDialog> m_chatInputDialog;
|
||||||
std::unique_ptr<ConversationManager> m_conversationManager;
|
std::unique_ptr<ConversationManager> m_conversationManager;
|
||||||
std::unique_ptr<LLMProvider> m_aiTestProvider;
|
std::unique_ptr<LLMProvider> m_aiTestProvider;
|
||||||
PetView *m_petView;
|
PetView *m_petView;
|
||||||
|
|||||||
Reference in New Issue
Block a user