Skip to content

Commit 61b86ff

Browse files
committed
Add user‑interaction guard to prevent auto‑scroll during generation
- Introduce `m_userInteracted` flag in `ChatEditor` to track whether the user has scrolled, clicked or typed while a generation is in progress. - Install an event filter on the chat’s `QScrollArea` and its vertical scrollbar to set this flag on wheel, mouse‑button and key events. - Guard `scrollToBottom()` so that it only auto‑scrolls when the user has not interacted. This change stops the chat from automatically scrolling to the bottom when the user manually scrolls or interacts during an AI generation, improving the user experience.
1 parent 2662ac2 commit 61b86ff

File tree

4 files changed

+39
-4
lines changed

4 files changed

+39
-4
lines changed

llamachateditor.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ ChatEditor::ChatEditor()
5555
m_scrollArea->setFrameShape(QFrame::NoFrame);
5656
m_scrollArea->setWidgetResizable(true);
5757
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
58+
m_scrollArea->installEventFilter(this);
59+
m_scrollArea->verticalScrollBar()->installEventFilter(this);
5860

5961
m_messageContainer = new QWidget(m_scrollArea);
6062
m_messageLayout = new QVBoxLayout(m_messageContainer);
@@ -246,6 +248,7 @@ void ChatEditor::createFollowUpWidget(const QString &convId,
246248
// capture convId / leafNodeId / question in the lambda
247249
connect(btn, &QPushButton::clicked, this, [this, convId, leafNodeId, q]() {
248250
ChatManager::instance().sendMessage(convId, leafNodeId, q, {}, [this](qint64) {
251+
m_userInteracted = false;
249252
scrollToBottom();
250253
});
251254
});
@@ -425,6 +428,8 @@ void ChatEditor::onSendRequested(const QString &text, const QList<QVariantMap> &
425428
extra,
426429
[this](qint64 leafId) { scrollToBottom(); });
427430
}
431+
432+
m_userInteracted = false;
428433
scrollToBottom();
429434
}
430435

@@ -434,6 +439,7 @@ void ChatEditor::onStopRequested()
434439
ChatManager::instance().stopGenerating(conv.id);
435440

436441
m_input->setIsGenerating(false);
442+
m_userInteracted = false;
437443
}
438444

439445
void ChatEditor::onFileDropped(const QStringList &files)
@@ -465,6 +471,8 @@ void ChatEditor::onRegenerateRequested(const Message &msg)
465471
QString(),
466472
msg.extra,
467473
[this](qint64 leafId) { scrollToBottom(); });
474+
475+
m_userInteracted = false;
468476
}
469477

470478
void ChatEditor::onSiblingChanged(qint64 siblingId)
@@ -514,13 +522,31 @@ void ChatEditor::updateSpeedLabel(const Message &msg)
514522

515523
void ChatEditor::scrollToBottom()
516524
{
525+
if (m_userInteracted)
526+
return;
527+
517528
// Scroll to bottom after the layout has finished
518529
QTimer::singleShot(100, this, [this] {
519530
QScrollBar *sb = m_scrollArea->verticalScrollBar();
520531
sb->setValue(sb->maximum());
521532
});
522533
}
523534

535+
bool ChatEditor::eventFilter(QObject *obj, QEvent *event)
536+
{
537+
if (m_input && m_input->isGenerating()) {
538+
if (obj == m_scrollArea || obj == m_scrollArea->verticalScrollBar()) {
539+
// Mouse wheel, mouse press, key press – any user interaction
540+
if (event->type() == QEvent::Wheel || event->type() == QEvent::MouseButtonPress
541+
|| event->type() == QEvent::MouseButtonRelease
542+
|| event->type() == QEvent::KeyPress) {
543+
m_userInteracted = true;
544+
}
545+
}
546+
}
547+
return QObject::eventFilter(obj, event);
548+
}
549+
524550
class ChatEditorFactory final : public IEditorFactory
525551
{
526552
public:

llamachateditor.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class ChatEditor : public Core::IEditor
3131

3232
bool isDesignModePreferred() const override;
3333

34+
bool eventFilter(QObject *obj, QEvent *event) override;
35+
3436
void refreshMessages(const QVector<Message> &messages, qint64 leafNodeId);
3537
void scrollToBottom();
3638
QWidget *displayServerProps();
@@ -55,15 +57,16 @@ public slots:
5557

5658
private:
5759
TextEditor::TextDocumentPtr m_document;
58-
QScrollArea *m_scrollArea;
59-
QWidget *m_messageContainer;
60-
ChatInput *m_input;
61-
QVBoxLayout *m_messageLayout;
60+
QScrollArea *m_scrollArea{nullptr};
61+
QWidget *m_messageContainer{nullptr};
62+
ChatInput *m_input{nullptr};
63+
QVBoxLayout *m_messageLayout{nullptr};
6264
QVector<ChatMessage *> m_messageWidgets; // keep for cleanup
6365
std::optional<Message> m_editedMessage;
6466
QWidget *m_propsWidget{nullptr};
6567
QWidget *m_followUpWidget{nullptr};
6668
QLabel *m_speedLabel{nullptr};
69+
bool m_userInteracted{false};
6770
};
6871

6972
void setupChatEditor();

llamachatinput.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,11 @@ bool ChatInput::eventFilter(QObject *obj, QEvent *event)
399399
return QWidget::eventFilter(obj, event);
400400
}
401401

402+
bool ChatInput::isGenerating() const
403+
{
404+
return m_isGenerating;
405+
}
406+
402407
void ChatInput::setIsGenerating(bool newIsGenerating)
403408
{
404409
m_isGenerating = newIsGenerating;

llamachatinput.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ChatInput : public QWidget
1616
explicit ChatInput(QWidget *parent = nullptr);
1717

1818
void setIsGenerating(bool newIsGenerating);
19+
bool isGenerating() const;
1920
void setEditingText(const QString &editingText, const QList<QVariantMap> &extra);
2021

2122
void updateMaximumHeight();

0 commit comments

Comments
 (0)